Chapter 11 · Document Databases and Aggregate-Oriented Modeling
Querying Nested Structures, Multikey Indexing, Aggregation Pipelines, and Denormalized Reads
Reason about nested predicates, array/multikey index fan-out, aggregation, projections, and denormalized read models with explicit drift detection and repair.
Learning outcomes
Denormalized documents reduce some join work only if the engine can still locate the right documents efficiently. Chapter 11 now moves from shape to access paths: nested predicates, array indexing, projections, aggregation pipelines, and the consistency cost of duplicated read-friendly fields.
Reason about nested-field and array predicates from the document paths they touch.
Explain why array indexes can create multiple index entries per document.
Use projections/aggregations without treating them as free computation.
Detect and repair stale denormalized fields.
1. Query shape should drive index shape
A nested predicate such as “category.id is lighting and price is below 50” still needs an access path. A projection returns only needed fields. An aggregation pipeline is a staged transformation such as filter → group → sort. These abstractions can reduce application-side work, but the database still reads index/data structures, allocates memory, may spill, and—in a distributed deployment—may route to one or many partitions.
For arrays, a multikey-style index conceptually contributes entries for array elements. MongoDB automatically makes an index multikey when the indexed field contains arrays; a document can therefore contribute multiple index entries. Large arrays can make the index much larger than the document count suggests.
2. Make index fan-out and denormalization drift observable
from collections import defaultdict
products = [
{"sku":"A", "category":{"id":"lighting","name":"Lighting"}, "tags":["desk","led"], "price":30},
{"sku":"B", "category":{"id":"lighting","name":"Lighting"}, "tags":["floor","led"], "price":70},
{"sku":"C", "category":{"id":"office","name":"Office"}, "tags":["desk","ergonomic"], "price":120},
]
# Multikey-like inverted entries: one index entry per distinct array value per document.
index = defaultdict(set)
for p in products:
for tag in set(p["tags"]):
index[tag].add(p["sku"])
print("tag index entries:", sum(len(v) for v in index.values()))
print("tag=led candidates:", sorted(index["led"]))
# Nested predicate + projection.
result = [{"sku":p["sku"], "price":p["price"]} for p in products
if p["category"]["id"] == "lighting" and p["price"] < 50]
print("nested predicate result:", result)
# Aggregation-style grouping.
revenue = defaultdict(int)
for p in products:
revenue[p["category"]["id"]] += p["price"]
print("aggregate by category:", dict(revenue))
# Denormalized label drift after category rename.
canonical_name = "Home Lighting"
stale = [p["sku"] for p in products if p["category"]["id"]=="lighting" and p["category"]["name"] != canonical_name]
print("stale denormalized labels:", stale)
for p in products:
if p["sku"] in stale:
p["category"]["name"] = canonical_name
print("after repair:", [p["category"]["name"] for p in products if p["category"]["id"]=="lighting"])
The lab’s tiny inverted index is a teaching model: six distinct tag-to-document entries arise from three documents. It also exposes stale copied category labels after the authoritative category name changes, then repairs them. It does not claim that a production engine stores its index exactly like the Python dictionary.
3. Wrong approach: denormalize because joins sound expensive
If AtlasMart copies category name, supplier risk class, inventory quantity, and customer segment into every product/search/order document, reads may become local but writes become fan-out. A missed update creates an inconsistent derived view. Denormalization is precomputed work: you pay on writes, synchronization, storage, and repair. That can be the right trade, but only when the read path justifies it.
| Signal | Measure |
|---|---|
| query efficiency | documents/keys examined, index selectivity, partitions touched |
| array index cost | index entries per document, index bytes, update rate |
| pipeline cost | input rows/docs, memory/spill, stage latency, tail latency |
| denormalization health | projection lag, mismatch count, repair age, replay position |
4. Production judgment
Prefer indexes aligned with real query shapes, not every imaginable field. Each index consumes storage and write bandwidth. Multikey/array indexes require especially careful cardinality limits. Keep derived fields traceable to their source version/event so reconciliation can tell whether a copy is stale rather than merely different.
Check your understanding
- Why can three documents produce more than three array-index entries?
- What does denormalization trade away?
- What makes an aggregation pipeline expensive?
- How should stale derived data be repaired?
Review the answers
1. Each distinct indexed array value may create its own index entry pointing back to the document.
2. It trades read-time joins/lookups for extra storage, write fan-out, synchronization, and repair work.
3. Large inputs, weak selectivity, grouping/sorting state, spills, network fan-out, and high-cardinality intermediate results.
4. From a clearly identified authoritative source or replayable change stream, with measurable divergence and repair progress.
Authoritative references
- MongoDB — Embedded data models — concrete current implementation guidance for aggregate locality and the 16 MiB BSON document limit.
- MongoDB — References — cases where independent lifecycle, many-to-many relationships, or frequent independent access favor references.
- MongoDB — Multikey indexes — array-index behavior and index-entry implications.
- MongoDB — Schema validation — evidence that flexible documents still benefit from enforced structural rules.
- MongoDB — Avoid unbounded arrays — implementation example of bounding growth with subsetting/references.
- MongoDB 8.3 release notes — dated implementation snapshot; 8.3.8 is the latest released patch as of this chapter review.