The Document Model against Rows

The same data as normalised tables and as nested documents, and what each shape makes easy.

Overview

The same order, two shapes

Run the query above and then the variants. The identical order is stored as normalised rows and as a single JSON document, and both are queried in the same session — because modern SQL engines support both.

Relational: an orders row, several order_items rows referring back to it, a customers row. Each fact recorded once, joined when needed.

Document: one record containing the order, its items nested inside it, and the customer details copied in.

The Document Model against Rows

query.sqlReady

Worth knowing

A document stores related data together, nested. A relational schema stores it apart and joins on demand.
Documents win when the access pattern is 'give me this whole thing' — one read, no joins.
Rows win when the data is queried from several directions, or when the same fact appears in many documents and has to stay consistent.
SQLite, PostgreSQL and MySQL all have JSON columns, so the choice is per-column now rather than per-database.

The Document Model against Rows

One order, stored two ways, and an honest account of which questions each shape answers well.

What documents make easy

One read for one thing. Fetching an order means retrieving one document. No joins, no round trips, and the data is contiguous on disk. For a read-this-whole-object access pattern, this is genuinely faster and the gap widens as the object gets more parts.

A shape that matches the code. The document deserialises straight into an object. No object-relational mapping layer reassembling a graph from five result sets.

Schema flexibility. Adding a field to some documents and not others requires no migration. For genuinely heterogeneous data — product catalogues where a book and a fridge share almost no attributes — this is a real advantage rather than laziness.

What rows make easy

Querying from any direction. "Which customers bought product 902" is straightforward relationally and awkward in a document store, because the data is organised around orders, not products. Documents optimise one access path and make the others harder.

Updating a shared fact once. If the customer's address is copied into every order document, changing it means finding and rewriting every one. Relationally it is a single UPDATE. This is the classic normalisation argument and it has not stopped being true.

Integrity the database enforces. Foreign keys, uniqueness and check constraints are declared once and cannot be bypassed. In a document store these usually become application code, which means they hold until some other code path forgets.

Ad-hoc analysis. Aggregating across documents means either a scan or a purpose-built index, and the queries are harder to write.

Denormalisation is the actual trade

Nesting the items inside the order is fine — an order item belongs to exactly one order and is never queried independently. That is not duplication, just co-location.

Copying the customer's name and address into every order is duplication, and it buys read speed at the cost of update cost and the risk of divergence. Sometimes that is right: an invoice arguably *should* record the address as it was at the time, in which case it is not duplication at all but a historical fact.

The question is always whether the copies must agree. If they must, storing them separately means keeping them in sync forever.

The distinction has mostly dissolved

PostgreSQL's jsonb is indexable, queryable and transactional. MySQL and SQLite have JSON functions — the queries on this page use SQLite's, in the same session as ordinary tables.

So the modern answer is usually neither purely one nor the other: relational tables for the entities that are queried from several directions and must stay consistent, and a JSON column for the parts that are genuinely variable and only ever read alongside their parent.

What a dedicated document database still offers is horizontal scaling and operational tooling built around that model from the start. What it gives up is joins and multi-document transactions — though MongoDB has had the latter since 4.0, which narrowed the gap considerably.

The number behind the update argument

"Rewriting every one" is the whole cost of copying a shared fact, and it is worth seeing at scale. Suppose the customer's address is denormalised into each of their order documents. Changing it is not one write:

orders holding the copydocument store: rewritesrelational: rows changed
1001001
10,00010,0001
1,000,0001,000,0001

Relationally the address lives in one customers row and one UPDATE changes it; every order simply keeps pointing at it. The document shape trades that single write for one per copy, and while the copies are being rewritten the database briefly disagrees with itself about where the customer lives. That is the exact trade: the document read is faster because the join was done once at write time, and every later change to a shared fact pays it back in full.

The read the document shape wins

The mirror image of the update cost is the read, and it is why the trade exists at all. Assembling one order — the order, its five line items, the customer — from normalised tables means touching three tables, and an ORM that loads them naively issues one query for the order and then one per related set:

shapereads to assemble one order
document1 (the whole object, contiguous)
relational, one join query3 tables joined
relational, naive ORM1 + N round trips (the N+1 problem)

The document was written with the join already done, so the read is a single contiguous fetch with no reassembly. That is a real advantage for a read-this-whole-object access pattern, and it grows as the object gains parts. It is the same coin as the update cost: the document pays once at write time to make this read cheap, and the relational shape pays a little at read time to keep every fact in one place. Which way that trade should fall is a property of the workload, not of the database — read-mostly and object-shaped leans document, write-shared and queried-many-ways leans relational.

Where it goes wrong

Choosing documents to avoid schema design. The schema still exists; it has moved into the application, where nothing enforces it.

Unbounded arrays. A document that grows without limit — every event appended to one record — eventually exceeds the size limit and rewrites the whole thing on every append.

Duplicating a fact that must stay consistent. Fine for a historical snapshot, a slow disaster for live data.

Using JSON columns for well-structured data. If every row has the same fields, they are columns. Putting them in JSON gives up type checking and constraints for nothing.

Check yourself

0 of …

Answer without scrolling back up.

  1. When does the document model genuinely beat normalised rows?

  2. What is the real cost of copying a customer's address into every order document?

  3. Why has the relational/document distinction largely dissolved?

Cheat sheet

The Document Model against Rows

Run the query above and then the variants. The identical order is stored as normalised rows and as a single JSON document, and both are queried in the same session — because modern SQL engines support both.

DATABASE · AI.Cruz.in/database/document_model_vs_rows.html

About the author

AI.Cruz builds and maintains AI.Cruz. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.