skip to content

When should a wide analytics table nest child records in an array column instead of one row per child?

level: middleimportance: should knowfreq 42%

answer

  1. what does one row mean?
  2. parent total repeated on each child row
  3. bounded children ride along fine
  4. who queries the child level?
  5. two grains often means two tables

basics

~20 s

Nest children when the table's grain must stay one row per parent and the children are bounded and always read with the parent. Flatten to one row per child when children carry their own measures or filters, and accept that parent measures then repeat.

solid answer

~50 s

The choice is a grain decision, not a storage one. Nesting order lines into an `items` array keeps the grain at one row per order, so `SUM(order_total)` stays correct and no consumer can fan the parent out by accident. Flattening to one row per line changes the grain: `order_total` now repeats on every line and any naive sum inflates it. Nest when children are always consumed with the parent, bounded in count, and not independently filtered or joined at scale. Flatten — or better, keep a second fact table at line grain — when the child level has its own measures, its own dimensions, and its own audience. The cost of nesting is consumer-facing: arrays are awkward for SQL-light analysts and for BI tools that expect flat columns, and evolving a struct's shape is harder than adding a column.

code

text · 11 lines
text
-- nested: grain = one row per order
order_id | order_total | items
1001     | 120.00      | [{sku:'A',qty:2,line_amt:80.00},{sku:'B',qty:1,line_amt:40.00}]
-- SUM(order_total) = 120.00   COUNT(*) = 1 order

-- flattened: grain = one row per order line
order_id | order_total | sku | qty | line_amt
1001     | 120.00      | A   | 2   | 80.00
1001     | 120.00      | B   | 1   | 40.00
-- SUM(order_total) = 240.00  <-- parent measure at child grain
-- SUM(line_amt)    = 120.00  <-- additive at this grain

go deeper

for a junior

Know that nesting keeps one row per parent while flattening produces one row per child, and that in the flattened shape the parent's total is repeated on every child row.

for a middle

Explain why the flattened shape inflates parent-grain measures, and state the conditions — bounded children, always read with the parent — under which nesting is the right call.

for a senior

Show the judgment to publish two facts at two grains when both have audiences, and to reject nesting for unbounded or independently queried child sets.

for a principal

Own the consumer-contract angle: whether the platform exposes nested structures at all is a tooling and literacy decision across every downstream team, not just a modelling preference.

## Two ways to carry a parent-child relationship in one table A wide mart that must hold orders and their line items has two shapes available. **Nested** — one row per order, with the children in a repeated column of structured values: ```text order_id | order_ts | order_total | items 1001 | 2026-07-02 | 120.00 | [ {sku:'A', qty:2, line_amt:80.00}, {sku:'B', qty:1, line_amt:40.00} ] ``` **Flattened** — one row per order line, with the parent's columns repeated: ```text order_id | order_ts | order_total | sku | qty | line_amt 1001 | 2026-07-02 | 120.00 | A | 2 | 80.00 1001 | 2026-07-02 | 120.00 | B | 1 | 40.00 ``` Both hold the same information. They differ in what one row means, and therefore in what a careless aggregate returns. ## Nesting preserves the grain In the nested shape the grain is one row per order. `SUM(order_total)` is 120.00 and cannot be anything else; `COUNT(*)` is the number of orders. The child data rides along, addressable when a consumer asks for it, invisible when they do not. Nobody can accidentally fan the parent out, because there is only ever one row per parent. This is the strongest argument for nesting in a one-big-table design: it lets a single table serve the parent grain honestly while still carrying detail, which is exactly what a flat wide table cannot do. ## Flattening changes the grain and endangers parent measures In the flattened shape the grain is one row per line. `order_total` is now a **parent-grain measure sitting at child grain**, and summing it double-counts — the classic wide-table defect. The measures that are safe at this grain are the line-level ones (`line_amt`, `qty`); the parent's totals must either be dropped, or recomputed from the line measures, or guarded so that they are counted once per parent. If you find yourself writing tricks to count `order_total` once, that is the model telling you the grain is wrong for that measure. Flattening is nevertheless the right answer often: line-level analysis (product mix, discount by SKU, basket composition) is a real business process with its own dimensions and its own audience. The clean expression of that is a second fact table at line grain — the two grains are two facts, not one table forced to be both. ## When to nest - Children are **always** consumed with the parent and rarely analysed on their own. - The child collection is **bounded** — a handful of items, not an unbounded event stream that grows forever inside one row. Unbounded arrays turn a row into an ever-growing object and make rewrites expensive. - The parent grain is what consumers overwhelmingly query, and you want it protected from accidental fan-out. - The children have no dimensions of their own that consumers need to filter on across all parents. ## When to flatten or split - Child-level measures are the point of the table. - Consumers filter or group by child attributes across the whole dataset ("revenue by SKU"), which is clumsy through a nested column and natural at child grain. - The consuming tools cannot express nested access — many BI layers and SQL-light users handle flat columns only. - The child cardinality is unbounded or highly skewed. ## What nesting costs - **Consumer friction.** Reading inside an array requires unnesting, which is exactly the join-shaped complexity an OBT was meant to remove. You have not eliminated the join so much as renamed it — and the payoff is that the un-unnested reader is safe by default. - **Tooling support varies.** Not every downstream consumer models repeated structured columns well; some flatten them for you, badly. - **Evolution is harder.** Adding a field inside a struct is a schema change to a nested type, generally more awkward than adding a top-level column, and older rows carry the old shape. - **Testing is harder.** Grain tests on a flat table are a `GROUP BY … HAVING COUNT(*) > 1`; testing that every array is well-formed and non-duplicated takes more work. ## A rule of thumb Decide the grain of the table first, from the questions it must answer. If that grain is the parent, nesting is how you keep detail without betraying it. If the questions are genuinely about the children, do not nest and do not flatten a parent-grain table either — publish the child grain as its own fact, and let a consumer who needs both join or query them separately. The failure to avoid is the middle: a flattened table that still advertises parent-grain measures as if they were additive.

  • If children are nested, what stops a consumer from double-counting the parent total anyway?
    Nothing, once they unnest. Unnesting expands one parent row into one row per child and repeats the parent's columns, exactly like a flattened table. The protection nesting gives is that the default read — no unnesting — is at parent grain and correct, so the mistake requires a deliberate step rather than happening by accident.
  • Why is an unbounded child collection a poor candidate for nesting?
    Because the row grows without limit. A user's entire event history inside one array makes that row enormous and skewed, every append rewrites the whole thing, and any read of the parent drags the full collection along. Unbounded child sets belong at their own grain in their own table, keyed back to the parent.
  • When would you publish both an order-grain and a line-grain table instead of picking one?
    When both grains have real audiences: finance asks order-level questions such as average order value and margin, merchandising asks line-level ones such as SKU mix and discount depth. Two facts with clearly named grains, each carrying only measures additive at that grain, is safer and cheaper to explain than one table that tries to serve both.

Nesting is a receipt with its line items printed on it — one receipt, one total. Flattening is tearing the receipt into one slip per item and stamping the full total on each slip; add the slips up and you have charged the customer twice.

saying these in an interview costs you the question

  • Treats nesting versus flattening as a storage choice, not a grain choice
  • Sums a parent total on a flattened child-grain table
  • Nests an unbounded, ever-growing child collection in one row
  • Assumes every consumer tool can read nested array columns
  • Forces two genuine grains into a single wide table

context