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 the same order is stored as normed rows and as a single JSON doc, 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

The same data, both ways

query.sql SQLite
Result

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. An honest account of which questions each shape answers well.

What documents make easy

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

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

Schema flexibility. Adding a field to some docs and not others needs no migration. For truly heterogeneous data — product catalogues where a book and a fridge share almost no fields. This is a real edge rather than laziness.

What rows make easy

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

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

Integrity the DB enforces. Foreign keys, uniqueness and check constraints are declared once and can't be bypassed. In a doc store these often become use code. This Means they hold until some other code path forgets.

Ad-hoc analysis. Aggregating across docs 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 just one order and is never queried on its own. That isn't duplication, just co-location.

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

The question is always whether the copies must agree. If they must, storing them apart 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 plain tables.

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

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

The number behind the update argument

"Rewriting each one" is the whole cost of copying a shared fact, and it's worth seeing at scale. Suppose the customer's address is denormalised into each of their order docs. Changing it isn't 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; each order simply keeps pointing at it. The doc shape trades that single write for one per copy, and while the copies are being rewritten the DB briefly disagrees with itself about where the customer lives. That is the exact trade: the doc read is faster because the join was done once at write time, and each 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. It's why the trade exists at all. Assembling one order. The order, its five line items, the customer — from normed 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 doc was written with the join already done. So The read is a single contiguous fetch with no reassembly. That is a real edge 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 doc pays once at write time to make this read cheap, and the relational shape pays a little at read time to keep each fact in one place. Which way that trade should fall is a property of the workload, not of the DB — read-mostly and object-shaped leans doc, write-shared and queried-many-ways leans relational.

Questions people ask about the Document Model against Rows

When is a document database a better fit than rows? When records are read and written whole, vary in shape, and are rarely joined to other records.

What is denormalisation? Storing related data together, even if it repeats, so reads need no joins. It speeds reads and complicates updates.

Can a relational database store JSON? Yes. PostgreSQL, MySQL and others have JSON column types with indexing, so the line between the models has blurred.

Why are updates harder in documents? A repeated value, such as a customer name, must be changed in every document that copies it.

Do document databases support joins? Some offer lookup operations, but they are not as flexible or fast as relational joins. Design documents around the queries you run.

How do I choose between them? Look at your access pattern. Many cross-entity queries and strict integrity favour rows. Self-contained records read as a unit favour documents.

Where it goes wrong with the Document Model against Rows

Choosing docs to avoid schema design. The schema still exists. It has moved into the use, where nothing enforces it.

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

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

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

Check yourself

0 of 3

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

Store the same order as a nested document and as normalised rows, then run the queries each shape handles well and the ones it does not.

DATABASE · vizlearn.in/database/document_model_vs_rows.html

About the author

Ashish Jangra builds and maintains VizLearn. 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.