In your program, a résumé is one object. It has a list of positions, each with a company and dates, and a list of qualifications. You can hold the whole thing in one variable and pass it around.
In a relational database it is four tables and three joins.
That gap has a name.
The idea
The object-relational mismatch is the friction between the tree-shaped objects an application works with and the flat, normalised tables a relational database prefers. It is not a bug in either model. They are shaped for different jobs.
Most of an ORM’s code — and most of the frustration with ORMs — is the translation layer this gap requires. The layer is not the problem; the gap is real, and something has to cross it. The problem is when the layer pretends the gap is not there.
What the document model does about it
A document store’s answer is direct: if the application wants a tree, store a tree.
{
"_id": "ada",
"name": "Ada",
"positions": [
{ "company": "Acme", "title": "Engineer", "from": 2021 },
{ "company": "Initech", "title": "Intern", "from": 2019, "to": 2020 }
],
"education": [{ "school": "Bletchley", "degree": "BSc" }]
}
One document. One read. No joins, no translation layer, no impedance.
The property doing the work here is locality: everything you need is stored together, so fetching it is one seek instead of several. On a nested structure you fetch whole, that is a genuine and significant win.
The one-to-many test
The document model fits when the relationship is genuinely one-to-many and owned: the children belong to this parent, only this parent, and nobody else needs to address them independently.
Positions on a résumé pass that test. Line items on an order pass it. Comments on a post usually pass it, until someone asks for “all comments by this user, newest first” and suddenly comments need to be addressable on their own.
The test is not “is it a list?” It is: would anything outside this document ever need to point at one of these children, or query across all of them?
Embed or reference
The moment two documents need the same thing, you have a decision.
Embed a copy in each document: reads are one operation, and every edit to the shared thing must find and rewrite every copy.
Reference by id: one authoritative copy, edits are a single write, and every read pays for an extra lookup.
Put numbers on it and the crossover is easy to find:
Dials
How often something fetches a document that includes the shared entity.
How often the shared entity itself changes — a company renames, a tag is recoloured.
How many documents would embed their own copy of that entity.
read 1.0k · 1 op per readwrite 1.0k · 200 ops per edit
read 2.0k · 2 ops per readwrite 5 · 1 op per edit
Too close to call on cost alone
Costs are comparable. Decide on which direction your queries run and how much you mind stale copies.
The op counts are a deliberate simplification — a real join is not exactly two reads, and a bulk update is cheaper than 200 separate writes. The shape of the trade is what matters: embedding multiplies the cost of change by the number of copies, and referencing multiplies the cost of reading by the number of hops.
Notice what dominates. When the shared entity is effectively immutable — a country name, a historical exchange rate, the price at the time of purchase — embedding is close to free. When it changes and thousands of copies exist, embedding turns a one-row update into a fan-out write, and a partially failed fan-out leaves documents that disagree with each other.
What each model gives you
| Document | Relational | |
|---|---|---|
| Fetching a whole tree | One read | A join per level |
| Schema changes | Write new shape, read old shape | ALTER TABLE, applies to all rows |
| Many-to-many | Awkward; needs references and application joins | The thing it was designed for |
| Ad-hoc queries across children | Weak unless anticipated | Strong, and often just an index away |
| Enforcing a rule across records | Application’s job | Constraints, done once |
Check yourself
An e-commerce order stores a full copy of the shipping address at checkout, rather than referencing the customer's address record. Is this a modelling mistake?
What to take away
The mismatch is real and someone always pays for it. Documents pay on relationships; tables pay on assembly. Choose by asking whether your children are owned by exactly one parent — and when you duplicate data, be sure you are copying a fact rather than caching a value.