Filter concepts by levelShowing all levels.

MongoDB · Section 12

Multikey and Array Indexes

Level
advanced
Read
30 min
Concepts
5

Goes past the multikey basics from section 10 into real design implications: how MongoDB detects multikey status from data rather than declaration, the hard restriction that a compound index allows at most one array field per document, how array cardinality drives index size independent of document count, the hashed-index and sort limitations that follow from the same "an array has no single position" fact, and designing arrays with bounded growth specifically to keep their indexes cheap.

This section

What is true here

  1. Multikey status is detected from real data at write time — the first array-valued document flips it, and it generally stays flipped.
  2. A compound index allows at most one array field per document; two array fields together on the same document fails the write.
  3. Multikey index size scales with total array-element count across the collection, not document count.
  4. Hashed indexes reject array-holding fields outright; sorting on a multikey field frequently falls back to an in-memory sort.
  5. Bounding an array before indexing it caps document growth and multikey index growth together, since they share the same cause.

What you will be able to do

  • Explain why an index can silently become multikey without any change to its definition
  • Predict when a compound index will reject a write for the two-array-fields restriction
  • Estimate a multikey index's real size from array length, not just document count
  • Recognize when a hashed index or an index-supported sort is not available due to multikey status
  • Design an indexed array with an explicit bound instead of letting it grow unchecked

How multikey status works

Detection from real data, and the restrictions that follow — the compound-index limit, plus hashed-index and sort behavior.

When an index becomes multikey

standardintermediate

An index becomes multikey the moment any document indexed by it stores an array in the indexed field — MongoDB decides this from the actual data at write time, not from a declared schema. It is a property of the index's current data, and it can flip from scalar to multikey the instant one array-valued document is inserted.

Think of it as

Multikey-ness is not a setting you choose when creating the index — it is a fact MongoDB observes and remembers about the index once it sees the first array. This is why a field that has only ever held scalars, on a schema-flexible database, can silently change an index's behavior and restrictions the moment a single document breaks that pattern.

text
// No syntax to set — MongoDB detects multikey status automatically from the data at write time

What we're doing: Show an index quietly becoming multikey after an initial run of scalar-only inserts.

index-turns-multikey.txttext
db.products.createIndex({ tags: 1 })

db.products.insertOne({ _id: 1, tags: "electronics" })   // scalar — index not yet multikey
db.products.insertOne({ _id: 2, tags: "sale" })           // still scalar

db.products.insertOne({ _id: 3, tags: ["electronics", "sale"] })  // array — index is now multikey
1
The index definition itself gives no indication of whether it will ever be multikey — that depends entirely on the data inserted afterward.
5
This single array-valued insert is what flips the index's status — nothing about the createIndex call or the two prior scalar inserts predicted it.

Why this works: Because MongoDB is schema-flexible, "this field is always a scalar" is an assumption about the data, not a guarantee the database enforces on its own — multikey status quietly tracks whatever the data actually turns out to be, which is exactly why a validator is the tool for enforcing the assumption if it needs to hold.

Assuming a field will never hold an array because it never has so far

Wrong

text
// "tags has always been a single string in this collection" — no validator enforcing it

Better

text
// If a field must stay scalar for compound-index or sort reasons, enforce it with a $jsonSchema validator, not an unverified assumption

What you see: A compound index that relied on a field staying scalar starts hitting the compound-multikey restriction (or unexpected sort behavior) the first time someone inserts an array there.

Why: Schema flexibility means nothing at the database level prevents a field from becoming an array unless a validator says otherwise — an index's behavior assumptions about a field's shape need the same explicit enforcement as any other structural guarantee.

Remember: Multikey status is detected from real data, not declared — the first document with an array in the indexed field flips it, and it generally stays flipped. Enforce a field's scalar-ness with a validator if the index design depends on it.

See also: multikey indexes · compound multikey restrictions

Hashed-index and sort limitations on multikey fields

standardadvanced

Two limitations beyond the compound restriction: a hashed index cannot be built on an array field at all, and sorting on a multikey-indexed field frequently forces MongoDB to fall back to an in-memory sort rather than using the index's own order.

Think of it as

Both limitations trace back to the same root cause as the compound-multikey restriction: an array field does not have one clear position in a single sorted order, because each document can contribute several different values. A hash function needs one definite input to hash, and a sort needs one definite position per document — an array field can violate both expectations, so MongoDB restricts or falls back rather than guessing.

text
// A hashed index rejects array-holding data outright; a sort on a multikey field may need explain() to confirm it isn't falling back to memory

What we're doing: Show the hashed-index rejection concretely, and a sort on a multikey field that needs checking via explain().

hashed-and-sort-limitations.txttext
db.products.createIndex({ tags: "hashed" })
db.products.insertOne({ tags: ["sale", "clearance"] })
// -> fails: hashed indexes do not support array values

db.products.find({ category: "electronics" }).sort({ tags: 1 })
// -> check explain(): the sort on a multikey field frequently shows an in-memory SORT stage
1
A hashed index and multikey are fundamentally incompatible — this insert fails regardless of how the index was intended to be used.
5
Even with tags indexed, sorting by it does not reliably get to skip the in-memory sort the way a scalar field would — explain() is the only way to confirm which actually happened for a given query.

Why this works: Both restrictions exist for the same reason: a hash needs exactly one value to hash per document, and an index-supported sort needs exactly one position per document — an array field can supply several values, breaking both assumptions, so MongoDB restricts the hashed case outright and falls back to memory for the sort case rather than producing an ambiguous result.

Assuming a multikey-indexed field always avoids an in-memory sort, the way a scalar-indexed field would

Wrong

text
// "tags is indexed, so sorting by it should be fast" — assumed without checking explain()

Better

text
// Check explain() specifically for a SORT stage when sorting on a multikey field — do not assume indexed automatically means index-supported sort

What you see: A sort on an indexed array field is unexpectedly slow on a large collection, and explain() reveals an in-memory SORT stage the developer did not expect given the field is indexed.

Why: Being indexed and being index-supported-for-sort are different guarantees for a multikey field specifically — a scalar index reliably supports a matching sort, but a multikey index only does under narrower conditions, so this needs to be verified per query rather than assumed from the scalar-field mental model.

Remember: Hashed indexes reject array-holding fields outright. Sorting on a multikey-indexed field often falls back to an in-memory sort even though the field is indexed — verify with explain(), don't assume from the scalar-field case.

See also: compound multikey restrictions · key ordering and query shapes

The compound multikey restriction

coreadvanced

A compound index can have at most one field that is an array, per document. createIndex({ genres: 1, year: 1 }) is fine if only genres is ever an array — but if any single document has both genres and year as arrays, the index creation (or the offending insert) fails outright.

Think of it as

The restriction exists because indexing two arrays together in one compound index would need one index entry per combination of elements — a cross product that can explode in size and lose any clear meaning ("this document has genre X paired with year Y" is not actually true for every combination). MongoDB avoids that ambiguity entirely by simply disallowing more than one array field per document in a compound index, rather than trying to define a cross-product semantics for it.

text
// At most one field per document can be an array, across all fields in a compound index

What we're doing: Show the exact failure when an update would make a second field in the same compound index become an array.

compound-multikey-violation.txttext
db.movies.createIndex({ genres: 1, year: 1 })
db.movies.insertOne({ _id: 1, genres: ["Drama", "Action"], year: 2020 })   // ok — one array field

// Later, an update tries to also make year an array on the same document:
db.movies.updateOne({ _id: 1 }, { $set: { year: [2020, 2021] } })
// -> fails: "cannot index parallel arrays" — genres and year would both be arrays on this document
1
This document starts compliant — only genres is an array, year is a scalar.
5
The update would make year an array too, which the compound index cannot represent — MongoDB rejects the write rather than silently allowing an ambiguous index state.

Why this works: The failure happens at the moment the violation would actually occur — whether that is the initial insert, a later update, or the createIndex call itself if the data already violates the rule — which is why "which of these fields was last modified" is not what determines the error, "would this document end up with two array fields in the index" is.

Designing a compound index over two fields that could realistically both become arrays for the same document

Wrong

text
db.movies.createIndex({ genres: 1, awards: 1 })  // both fields are naturally list-shaped in this domain

Better

text
// Index them separately, or restructure the schema (e.g. subdocuments) if both need efficient array-based lookup on the same document

What you see: The compound index works fine in testing (where sample data happened to keep one field scalar) and then fails in production the first time a real document needs both fields to be arrays.

Why: This restriction is easy to miss at design time because it depends on real, eventual data shapes rather than anything visible in the createIndex call itself — two fields that are each individually "sometimes an array" in the domain are a compound-multikey violation waiting to happen, not a hypothetical edge case.

At most one array field per document, in a compound index

Allowed

genres: array

year: scalar

year: array

genres: scalar

Rejected

genres AND year

both arrays — write fails

  • Allowed
    • genres: array — year: scalar
    • year: array — genres: scalar
  • Rejected
    • genres AND year — both arrays — write fails

Compound index { genres: 1, year: 1 } — allowed vs. rejected

Compound index { genres: 1, year: 1 } — allowed vs. rejected
DocumentResult
{ genres: ["Drama", "Action"], year: 2020 }allowed — only genres is an array
{ genres: "Drama", year: [2020, 2021] }allowed — only year is an array
{ genres: ["Drama"], year: [2020, 2021] }rejected — both fields are arrays

Together

text
db.movies.createIndex({ genres: 1, year: 1 })

db.movies.insertOne({ genres: ["Drama", "Action"], year: 2020 })          // ok
db.movies.insertOne({ genres: ["Drama"], year: [2020, 2021] })            // fails — both arrays

Remember: A compound index allows at most one array field per document — two array-valued fields together in the same compound index fail the write. Anticipate this if both candidate fields could realistically be arrays.

See also: when an index becomes multikey · compound indexes

Advertisement

Sizing and designing arrays for indexing

How array cardinality drives real index cost, and designing arrays with an explicit bound before indexing them.

How array cardinality affects index size

standardintermediate

A multikey index has roughly one entry per array element per document — a document with a 50-element array contributes 50 index entries, not one. Index size, build time, and per-write update cost all scale with total element count across the collection, not document count.

Think of it as

Think of index size for a multikey field as (number of documents) × (average array length), not just (number of documents) — a collection of 1,000 documents with 3-element arrays produces roughly the same index entry count as 3,000 documents with 1-element arrays. Two collections with the same document count can have very different multikey index sizes if their array lengths differ.

text
// Roughly: multikey index entries ≈ sum of array lengths across the collection, not the document count

What we're doing: Contrast index entry counts for the same document count under two different array-length assumptions.

array-length-vs-index-size.txttext
// 100,000 products, each with a 3-element "tags" array:
// -> roughly 300,000 index entries for the tags index

// 100,000 products, each with a 200-element "reviewIds" array (unbounded growth):
// -> roughly 20,000,000 index entries for the reviewIds index — the same document count, vastly larger index
2
A modest, bounded array (a handful of tags) keeps the multikey index proportionally close to document count.
5
An unbounded or large array multiplies index size by its average length — the same 100,000 documents produce a vastly larger index once the array itself is large.

Why this works: This is the concrete mechanism behind why "index this array field" is not automatically a good idea for every array — an unbounded or high-cardinality array turns "one index per field" into an index whose real size depends on data growth, not schema shape.

Indexing a large or unbounded array field without checking its typical and worst-case length

Wrong

text
// db.users.createIndex({ activityLog: 1 }) — on a field known to grow without bound for active accounts

Better

text
// Check typical AND worst-case array length before indexing it; reference unbounded arrays into their own collection (section 6/8) instead of indexing the array in place

What you see: Index size and write latency for a specific field grow disproportionately for the small fraction of documents with unusually long arrays — the same "outlier" pattern that motivates the outlier schema pattern.

Why: A multikey index's cost is driven by the sum of array lengths, not the document count, so the same unbounded-array risk that applies to document size (section 6) applies just as directly, and for the same underlying reason, to any index built on that array field.

Remember: A multikey index has roughly one entry per array element — its size scales with total element count across the collection, not document count. An unbounded or high-cardinality array makes for a correspondingly larger, costlier index.

See also: designing arrays with bounded growth · bounded vs unbounded arrays

Designing arrays with bounded growth, for indexing

standardintermediate

Applied specifically to indexed arrays: enforce a cap ($slice, application logic, or referencing the overflow into its own collection) rather than letting an indexed array grow without limit — the multikey index cost compounds the same unbounded-growth risk that document size already carries.

Think of it as

An unbounded indexed array is a cost that compounds in two places at once: the document itself grows (section 6's risk), and the multikey index built on it grows in lockstep (this section's risk) — every element added is both a bigger document to rewrite and a new index entry to maintain. Bounding the array caps both costs together, since they share the same root cause.

text
$push: { arrayField: { $each: [item], $slice: -N } }  // keeps an indexed array from growing past N elements

What we're doing: Show an indexed array kept bounded by $slice, versus the unbounded alternative and its compounding cost.

bounded-indexed-array.txttext
// Bounded: the index for recentSearches never grows past 20 entries per user, no matter how many searches happen
db.users.createIndex({ recentSearches: 1 })
db.users.updateOne({ _id: u }, { $push: { recentSearches: { $each: [term], $slice: -20 } } })

// Unbounded: every search ever made grows both the document and this index forever
db.users.createIndex({ allSearchesEver: 1 })
db.users.updateOne({ _id: u }, { $push: { allSearchesEver: term } })
2
The $slice: -20 cap means this user's contribution to the multikey index never exceeds 20 entries, regardless of account age.
6
Without a cap, this field's contribution to the multikey index — and the document's own size — both grow for as long as the account exists, compounding the same unbounded-growth risk in two places.

Why this works: Bounding an array before indexing it addresses the document-growth and index-growth costs together, since they share the same cause — this is a case where one design decision (cap the array) resolves two distinct, additive costs at once.

Indexing an unbounded array field because a query on it exists, without first bounding the array itself

Wrong

text
// db.users.createIndex({ allSearchesEver: 1 }) — added to speed up a "recent searches" feature, on a field with no cap

Better

text
// Either cap the array with $slice (if only recent items matter) or reference it into its own collection (if full history matters) before indexing

What you see: The multikey index for this field grows steadily for the life of every account, and both write latency and index memory footprint degrade for the oldest, most active accounts first.

Why: Indexing does not fix an unbounded-growth problem — it adds a second, parallel cost on top of it, so the array needs to be bounded (or referenced out) as its own decision, independent of and prior to whether it gets indexed.

Remember: Bound an array before indexing it — $slice or referencing the overflow into its own collection caps both document growth and multikey index growth together, since they share the same root cause.

See also: array cardinality and index size · bounded vs unbounded arrays

Advertisement