skip to content

Which queries can the compound index { userId: 1, status: 1, createdAt: -1 } serve?

level: middleimportance: must knowfreq 78%

answer

  1. One index, one lexicographic ordering
  2. Usable from the left only
  3. Values of a later field are scattered across the index
  4. Field order in the query document is irrelevant
  5. Sorts need matching directions or the exact mirror

basics

~20 s

Only queries whose filter includes the leading field. The usable index prefixes are { userId }, { userId, status } and all three fields; a query on status or createdAt alone cannot use it. It also serves sorts matching the declared key directions or their exact inverse.

solid answer

~50 s

A compound index is a single ordered structure keyed on the concatenation of its fields, so it is usable only from the left. The prefixes here are `{ userId }`, `{ userId, status }`, and `{ userId, status, createdAt }` — a filter on `userId` alone, or `userId` plus `status`, is served; a filter on `status` alone or `createdAt` alone is not, because those values are scattered across the whole index. You can also skip a middle field only at a cost: `{ userId, createdAt }` uses the index for `userId` and then has to check `createdAt` less efficiently, since `createdAt` keys are only ordered within a `(userId, status)` group. For sorting, the index directions matter: it serves `sort({ userId: 1, status: 1, createdAt: -1 })` by walking forward and the exact inverse `sort({ userId: -1, status: -1, createdAt: 1 })` by walking backward, but not a mixed variant such as all-ascending.

code

javascript · 9 lines
javascript
db.orders.createIndex({ userId: 1, status: 1, createdAt: -1 })

// served: leading field present
db.orders.find({ userId: 42 })
db.orders.find({ userId: 42, status: "OPEN" }).sort({ createdAt: -1 })

// not served: no leading field
db.orders.find({ status: "OPEN" })
db.orders.find({ createdAt: { $gt: ISODate("2024-01-01") } })

go deeper

for a junior

Recall that a compound index is used from the left: the query must include the first field. Filtering only on a later field will not use it.

for a middle

Explain the single lexicographic ordering behind the prefix rule, list the usable prefixes for a given spec, and state the forward-or-exact-inverse rule for sorts.

for a senior

Show the design reasoning on a real query: equality fields first so the sorted field lands in one contiguous block, and identify redundant prefix indexes you would drop.

for a principal

Own index-set economics across a service: a small number of well-ordered compound indexes covering the real query shapes, with a rule for retiring prefixes rather than accumulating one index per report.

## What a compound index is `db.orders.createIndex({ userId: 1, status: 1, createdAt: -1 })` builds one index whose keys are ordered tuples: first by `userId`, then within equal `userId` by `status`, then within equal `(userId, status)` by `createdAt` descending. There is one index, not three. Everything about which queries it serves follows from that single ordering. ## The prefix rule Because the sort is lexicographic from the left, the keys for a given `status` value are spread all over the index — they sit inside every `userId` group. So the index is usable only when the query pins the leading field. The usable **index prefixes** are: - `{ userId: ... }` - `{ userId: ..., status: ... }` - `{ userId: ..., status: ..., createdAt: ... }` A query on `{ status: "OPEN" }` alone gets no useful seek from this index; the planner will normally choose a collection scan or a different index. A query on `{ createdAt: { $gt: ... } }` alone is in the same position. Note what the rule is *not* about: the order in which you write the fields in the query document is irrelevant. `{ status: "OPEN", userId: 42 }` and `{ userId: 42, status: "OPEN" }` are the same query and both use the index. What matters is which fields are present, not how they are typed. ## Skipping a middle field A filter on `userId` and `createdAt` but not `status` can still use the index, but only partially: MongoDB seeks on `userId`, and then `createdAt` cannot be used as a tight contiguous bound because within one `userId` the `createdAt` keys are grouped by `status` first. The index is still far better than a collection scan, but it is not as good as an index whose key order matches the query. ## Key order in design The order of the keys is the whole design decision. Put fields you match by equality before the field you sort or range-scan on, so that the sorted or ranged values sit in one contiguous run of the index. Given `find({ userId: 42, status: "OPEN" }).sort({ createdAt: -1 })`, this index is ideal: the two equality matches select one contiguous block, and within that block `createdAt` is already in descending order, so no separate sort step is needed. ## Direction and sorting Directions only ever matter relative to each other. This index can produce: - `sort({ userId: 1, status: 1, createdAt: -1 })` — walk the index forward. - `sort({ userId: -1, status: -1, createdAt: 1 })` — walk it backward; every key direction is flipped. It cannot produce `sort({ userId: 1, status: 1, createdAt: 1 })` from the index alone, because that ordering exists nowhere in the structure — no single traversal yields two fields forward and one backward. If a query pins the leading fields with equality, though, direction on those pinned fields stops mattering: with `userId` and `status` fixed to single values, a sort on `createdAt` alone can still be served, in either direction, by scanning that one contiguous block forward or backward. ## Consequences for the index set Because every prefix of a compound index is itself usable, an index on `{ userId: 1, status: 1 }` is redundant when `{ userId: 1, status: 1, createdAt: -1 }` exists — the wider index serves everything the narrower one does, at slightly larger key size. The reverse is not true: `{ status: 1, userId: 1 }` is a genuinely different index serving a different set of queries. Recognising the redundant-prefix case is how index sets get trimmed without losing coverage. ## Answering well List the three prefixes explicitly, say plainly that `status` alone gets nothing, then show the equality-before-sort design reasoning on a concrete query. Finish with the direction rule — forward or exactly inverted — because that is the part most candidates have never thought about.

  • Does writing the query as { status: "OPEN", userId: 42 } instead of { userId: 42, status: "OPEN" } change whether the index is used?
    No. The order of fields in the query document is meaningless to the planner; both are the same query. What matters is which fields the filter contains relative to the index's key order. Only the index's own key order determines which prefixes are usable.
  • With that index in place, is a separate index on { userId: 1, status: 1 } worth keeping?
    No, it is redundant. Every prefix of a compound index is usable on its own, so the three-field index already serves any query the two-field one would, at a slightly larger key. Dropping the narrower index removes write and storage cost without losing any query coverage.
  • Which sorts can that index satisfy without an in-memory sort step?
    sort({ userId: 1, status: 1, createdAt: -1 }) by walking forward, and its exact inverse sort({ userId: -1, status: -1, createdAt: 1 }) by walking backward. A mixed variant such as all three ascending cannot be produced by any single traversal. If userId and status are pinned by equality, a sort on createdAt alone works in either direction.

saying these in an interview costs you the question

  • Says a compound index works like three separate indexes
  • Thinks the field order in the query document matters
  • Claims any subset of the indexed fields can use the index
  • Believes any sort over the indexed fields is free
  • Keeps a two-field index that is a prefix of a three-field one

context