← Blog/Field Manual Series/Article 04 · Week 5

You Don't Need MongoDB for "Flexible" Data. You Need One Column Type.

Omenabyte Intelligence·Sep 6, 2026·4 min read
Just Use Postgres — The Field Manual for Replacing MongoDB, Redis, Elasticsearch & More

Product catalogs, form submissions, webhook payloads — a lot of real data genuinely doesn't want a fixed schema. The usual response is reaching for MongoDB purely for that flexibility, which means a second connection pool, a second backup strategy, and a second place your data can quietly drift out of sync with the relational tables that still describe your users and orders.

jsonb solves the actual problem — flexible structure — without giving up the database you already trust for everything else.

Store anything

One table, one jsonb column, one GIN index:

setup.sql — flexible products table
CREATE TABLE products (
  id          bigserial PRIMARY KEY,
  name        text NOT NULL,
  attributes  jsonb NOT NULL DEFAULT '{}'
);

CREATE INDEX idx_products_attributes
  ON products USING GIN (attributes);

Use jsonb, not json. The binary form is stored decomposed at write time instead of as text re-parsed on every read — that's the difference between something you can actually index and something you can only display.

Query it like you mean it

The @> containment operator checks whether one JSON document contains all the keys and values of another:

query.sql — GIN-indexed containment
SELECT name FROM products
WHERE attributes @> '{"color": "red", "size": "M"}';

The GIN index makes this fast the same way a book's index works: it maps individual keys and values straight to the rows that contain them, instead of scanning every row to check.

The part MongoDB genuinely can't do as cleanly

A document-style attribute joined directly against relational order history — one statement, one transaction:

hybrid-join.sql — document meets relational
SELECT p.name, o.order_date
FROM products p
JOIN order_items oi ON oi.product_id = p.id
JOIN orders o ON o.id = oi.order_id
WHERE p.attributes->'specs'->>'material' = 'leather';

Try that across two databases and you're writing application-layer stitching code — and hoping nothing changes between the two calls.

Why this beats standing up a document database

  • One transaction, not two systems — the "document" and the relational rows referencing it commit or roll back together.
  • No sync jobs, because there's no second database to keep consistent.
  • You can still join it, which is the thing you actually lose by going full-document.

Where MongoDB still wins

Genuinely schema-less workloads at a write scale where horizontal partitioning across many nodes is a day-one requirement, or built-in multi-region active-active replication out of the box. Most projects reaching for Mongo on day one aren't actually at that scale yet.

Field Manual Series · Every recipe tested

This is one of eight infrastructure swaps.

Just Use Postgres is a 24-page field manual on replacing MongoDB, Redis, Elasticsearch, Pinecone, and more with the database you're probably already running. Every recipe in it — including this one — was run against a live Postgres instance before it went in the book.

One column type vs. a second database to back up, patch, and keep consistent — the math writes itself.

Up next in the series

Replace InfluxDB With Partitioning + BRIN Indexes

See the full series →