How does BigQuery's Capacitor format store a repeated field such as an ARRAY column?
answer
- a record is shredded, not stored whole
- each leaf path becomes its own column
- two small numbers rebuild the structure
- nulls and empty arrays are encoded, not stored
- selecting one field reads one column
basics
~20 sCapacitor stores each leaf field of the nested schema as its own compressed column, alongside repetition and definition levels that record where each value sat in the record structure. Reading a nested field touches only that column, and the record shape is rebuilt at read time.
solid answer
~50 sBigQuery's columnar format, **Capacitor**, does not store a nested record as a blob. It flattens the schema into leaf fields — `items.sku`, `items.price`, `user.id` — and stores each as an independent, encoded column on Colossus. To preserve structure it also stores, per value, two small integers: a **repetition level** saying at which level of nesting a new repeated element began, and a **definition level** saying how deep the path was actually defined, which is how NULLs and empty arrays are represented without storing placeholder rows. Together those let the engine reconstruct the exact record shape from only the columns a query touched. The practical consequence is that selecting one field of a 30-field `STRUCT`, or one field out of a repeated record, reads and is billed for only that leaf column — nesting costs you nothing on the read path for fields you did not ask for.
code
sql · 6 linesCREATE TABLE analytics.orders (
order_id INT64,
order_date DATE,
customer STRUCT<id INT64, country STRING, segment STRING>,
items ARRAY<STRUCT<sku STRING, qty INT64, price NUMERIC>>
);go deeper
Recall that nested and repeated fields are stored column-wise like everything else, so asking for one field does not drag the rest of the record along with it.
Explain the mechanism: each leaf path becomes its own encoded column, and repetition and definition levels recorded per value let the engine rebuild the record shape and represent nulls and empty arrays.
Connect the format to modelling: denormalising a parent-child relationship into a repeated field trades a shuffle-heavy join for a column read, while UNNEST still expands rows for downstream stages and huge arrays in one row create skew.
Own the schema strategy — decide where nesting genuinely removes join cost versus where it hurts reusability and downstream tool support, and set the conventions that keep array sizes bounded across the platform.
## The problem nesting creates for a column store A columnar format is easy when every row has exactly one value per column: store the column's values in order, compress, done. Nested and repeated data breaks that. A record like `{order_id: 7, items: [{sku:'A', qty:2}, {sku:'B', qty:1}]}` has two values for `items.sku` in one row and possibly zero in another. Storing the record as a serialised blob would work but destroys the whole point of columnar storage, because reading `order_id` would drag every item along with it. The technique BigQuery uses comes from the original Dremel work and is the same idea later adopted by open columnar formats: **shred the record into leaf columns and store enough per-value bookkeeping to put it back together.** ## Repetition and definition levels Each leaf column stores its values plus two small integers alongside each value: - **Repetition level** — at which level of the nested path a new repeated element started. Level 0 means "this value begins a new top-level record"; a higher level means "this value is another element of a repeated field at that depth." That is what tells the reader where one array ends and the next record's array begins. - **Definition level** — how many of the optional or repeated fields along the path were actually present. It encodes NULLs and empty arrays without storing a placeholder value: if the path ran out before reaching the leaf, the definition level says exactly where it stopped. Both levels are tiny integers with a small range, so they compress extremely well — usually to a fraction of the data's size. ## What that buys you at query time **Reading one leaf reads one column.** `SELECT order_id` from a table with a deeply nested `items` array touches only the `order_id` column. `SELECT item.sku FROM t, UNNEST(items) AS item` touches only the `sku` leaf plus its levels. The other 29 fields of the struct are never read, never decompressed, and never billed under on-demand pricing. **Nesting replaces a join.** Because a repeated record can be stored inline and still read column-wise, the classic pattern of a header table joined to a lines table can be collapsed into one table with a repeated field. The engine reads the parent and child fields from the same file with no shuffle, which is exactly the join cost you were trying to avoid. This is why denormalising into nested structures is idiomatic in BigQuery rather than the anti-pattern it would be in a row store. **Reconstruction is lazy.** The record shape is only rebuilt for the columns the query actually selected, and only when a query needs the structure — a filter on a scalar leaf can be evaluated against that column directly. ## The rest of Capacitor Beyond record shredding, Capacitor applies per-column encoding: dictionary encoding for low-cardinality string columns, run-length style encoding for long runs, and general compression on top. Because encoding is chosen per column, a column of a few distinct country codes and a column of unique UUIDs are stored very differently in the same table. BigQuery also performs storage optimisation in the background — reorganising and re-encoding the physical files after writes — which is why you never issue a rewrite or vacuum command yourself, and why a table's storage footprint can change without you doing anything. ## Costs and limits worth naming - **Filtering deep inside an array is not free.** A predicate on a repeated leaf may still require reading that leaf column across all rows unless partitioning or clustering narrows the files first. Nesting removes joins; it does not remove scans. - **`UNNEST` expands rows for downstream operators.** The read is cheap, but if you unnest a large array and then join or aggregate, the row count feeding those stages is the expanded one, and that costs slot time. - **Very large arrays inside single rows concentrate work.** A row whose array holds millions of elements makes that row's processing an outsized unit of work — the nested-data version of skew. - **Selecting the whole struct defeats the benefit.** `SELECT *` on a table with wide nested records reads every leaf column. The saving comes from asking for specific fields. ## How to answer this crisply Say the shape of the mechanism first: *leaf columns plus repetition and definition levels, so structure survives without giving up columnar reads*. Then give the consequence an interviewer cares about: *you can denormalise a parent-child relationship into a repeated field and read one child field without reading the rest, which trades a join for a column read*. That connects a storage-format detail to a modelling decision, which is what the question is really testing.
- Why is denormalising a parent-child relationship into a repeated field idiomatic in BigQuery?Because the child fields are stored as their own columns in the same file as the parent, the engine reads both without a join and without shuffling data between stages. You get the read economy of a column store and the locality of a pre-joined table at once, which is the opposite of the row-store situation where wide denormalised rows are expensive to scan.
- Does storing data in a nested structure reduce the bytes a query is billed for?Only in the sense that you read fewer leaf columns. Under on-demand pricing you pay for the columns the query references, so selecting one field of a nested record bills that field, not the whole record. If you select the entire struct or use SELECT *, every leaf column is read and the nesting saves nothing.
- What goes wrong when a single row holds an extremely large array?Processing concentrates. A row is the unit that must be reconstructed and handled together, so a row with millions of array elements makes one work unit far heavier than its peers, producing skew in downstream stages after UNNEST. Very large collections belong in their own table keyed back to the parent rather than nested inline.
saying these in an interview costs you the question
- Thinks nested records are stored as JSON blobs per row
- Believes selecting one struct field reads the whole struct
- Assumes arrays are stored by expanding rows on disk
- Says nesting removes the need to scan, not just to join
- Confuses the storage encoding with the SQL syntax for nesting