skip to content

In Elasticsearch, how do you choose between nested, a join field, and denormalization?

level: principalimportance: should knowfreq 36%

answer

  1. measure fan-out, churn ratio, query rate
  2. index the thing you want to return
  3. bounded and co-updated favours one model
  4. independent child writes favour another
  5. reversing any of them means a reindex

basics

~20 s

Start from denormalization: one document per entity you want to return. Add nested only when the many-side is small, bounded and updated with its parent. Use a join field only when children churn far more often than parents and query volume can absorb it.

solid answer

~60 s

Three numbers decide it: the fan-out of the many-side, the ratio of child updates to parent updates, and the query rate. **Denormalize by default** — index the thing you want to return, copying parent fields onto it. Every document is independent, queries are plain and fast, and it scales with shard count; the cost is duplicated fields and re-indexing children when a parent field changes. **Nested** is right when the many-side is small and bounded, always changes with the parent, and the query must correlate several child fields while returning the parent — sizes of a garment, addresses of a customer. Its price is document multiplication and rewriting the whole block on any update. **A join field** buys exactly one thing: updating a child without touching the parent. Take it when children vastly outnumber parents and change constantly, and only if you can pay for slower joining queries, eager global ordinals on every refresh, and shard skew from uneven fan-out. If none fits, run two queries and stitch in the application.

code

json · 9 lines
json
// Denormalized: one document per order line
{
  "line_id": "L-991",
  "sku": "ABC-1",
  "quantity": 2,
  "order_id": "O-77",
  "order_date": "2026-03-01",
  "customer_region": "EU"
}

go deeper

for a junior

Know the three options exist and that denormalizing — one document per thing you want to return — is the usual answer in Elasticsearch, unlike in a relational schema.

for a middle

Be able to state what each model costs: document multiplication and block rewrites for nested, routing plus slower queries for a join field, duplicated fields and fan-out updates for denormalization.

for a senior

Argue a specific case with numbers — fan-out, child-to-parent write ratio, query rate — and explain the migration path, since changing the model means a full reindex behind an alias.

for a principal

Own the policy and its enforcement: a documented default, benchmarks on production-shaped data before commitment, aliases as the standard migration path, and a clear disqualifier list so teams don't rediscover these costs in an incident.

## Frame the decision as three measurements Every useful answer here comes from data, not taste. Measure, on real traffic: 1. **Fan-out** — children per parent: the median and, more importantly, the tail. Bounded at a handful, or unbounded and growing? 2. **Churn ratio** — child writes per parent write. Do children change with the parent, or independently and far more often? 3. **Query profile** — reads per second, latency budget, and which entity the result page actually shows. The third question is the one teams skip, and it usually settles the model on its own: **index the thing you want to return**. ## Denormalization is the default One document per entity to be returned, carrying whatever parent context the query needs. A search over order lines indexes one document per line, with the customer's region and the order date copied in. Every document is independent — index, update, delete in isolation. Queries are plain `bool` clauses with no special syntax, no cross-object matching, no expensive-query gate. Throughput scales with shard count. What you pay: storage for duplicated fields, and a fan-out re-index when a parent field that was copied changes. That second cost is the one to size honestly — if the customer's region changes for a customer with a million lines, that is a million updates, typically through `_update_by_query` on a background schedule. If parent fields are stable, the cost is near zero and denormalization wins outright. ## Nested: correlation with a bounded many-side Choose nested when three things are all true: the many-side is small and has a natural ceiling; it always changes at the same time as the parent; and the query must require several child fields to match the *same* entry while returning the parent. Product variants, garment sizes, a person's addresses. What you pay: each entry becomes a Lucene document, so physical document count multiplies, and because blocks are immutable and contiguous, changing one field rewrites the parent and every child. `index.mapping.nested_objects.limit` (default 10000) caps a single document's objects; hitting it is a signal to remodel, not to raise it. Sorting needs the `nested` sort option, aggregating needs a `nested` aggregation, and showing the matched entry needs `inner_hits`. The disqualifier is unbounded growth. An array that appends forever turns every write into a bigger rewrite; that model must change before it reaches production scale. ## Join field: independent children, at a price A `join` field buys precisely one property that neither other option offers: a child can be created, updated or deleted without rewriting its parent or its siblings. That matters when children vastly outnumber parents and change constantly while parents are stable. What you pay is broad. Children must be routed to the parent's shard, so an uneven fan-out concentrates a hot parent's children on one shard and skews the cluster. The join field maintains eagerly built global ordinals, so refreshes get more expensive as the index grows. Joining queries are markedly slower than the equivalent nested query and are classed as expensive queries, so a locked-down cluster may refuse them. An index may declare only one join field, a child has exactly one parent, and the relation cannot span indices. That bundle is affordable at moderate query rates and unaffordable at high ones. Parent-child is a write-optimised model paid for at read time; it is the exception, not the starting point. ## Application-side join Sometimes the honest answer is two queries: search the children, collect parent ids, fetch the parents with a single multi-get or a `terms` filter, and assemble in the service. It costs a round trip and some code, but it keeps both indices simple, works across indices, and never surprises anyone with cross-object matching or a shard-skew incident. For low-cardinality lookups it is frequently the cheapest option overall. ## Deciding, concretely - Bounded fan-out, children change with parent, need correlated matching, return the parent → **nested**. - Any fan-out, results are the child entity, parent fields stable → **denormalize**. - Huge fan-out, child churn far exceeds parent churn, read volume modest → **join field**. - Relation spans indices, or is needed only for display → **application-side join**. ## Governing it Write the rule down and enforce it in review, because these choices are expensive to reverse: every one of them requires a full reindex, and the query, sort, aggregation and highlighting code all change with the model. Two guardrails are cheap and effective: benchmark with production-shaped data before committing, comparing physical document count and documents rewritten per update; and make aliases-plus-reindex the standard migration path so a wrong choice is recoverable without downtime. Treat "we used nested because the JSON was nested" as the defect it is — the shape of the incoming JSON is not an argument for any of these three models.

  • A parent field copied onto ten million denormalized children changes. How do you handle it?
    Run `_update_by_query` scoped to that parent's children, in the background, with throttling so it does not starve live indexing. Size the job before choosing the model: if such changes are frequent and fan-out is large, the update cost may outweigh the query benefit, and a join field or an application-side join becomes the better trade.
  • What makes a wrong choice here expensive to reverse?
    All three models change the physical layout, so switching requires a full reindex — mappings cannot be altered from object to nested, or to add a join field, in place. Queries, sorts, aggregations, highlighting and the client's response handling all change too. Reindexing behind an alias keeps it doable without downtime, which is why aliases should be the default from day one.
  • Why is the shape of the incoming JSON a bad reason to pick nested?
    Source documents arrive nested because that is how an upstream system serialises them, not because queries need per-entry correlation. If no query correlates two child fields, a plain object field is cheaper and simpler. The model should follow the query and update patterns, not the ingestion format.

saying these in an interview costs you the question

  • Picks nested because the source JSON happens to be nested
  • Treats a join field as Elasticsearch's equivalent of a SQL join
  • Rejects denormalization on principle as data duplication
  • Ignores child-to-parent update ratio when choosing
  • Assumes the model can be changed later without a reindex

context