skip to content

How do you tell whether a Couchbase GSI fully covers a SQL++ query?

level: seniorimportance: should knowfreq 44%

answer

  1. The index must carry more than the predicate
  2. One plan operator disappears when it works
  3. Projection and sort fields count too
  4. Leading key eligibility is a separate question
  5. Documents missing that key are simply absent

basics

~20 s

A GSI covers a query when every field the query references appears among the index keys, so the Query service answers from the index alone. Run EXPLAIN: a covered plan shows an index scan with a covers list and no Fetch operator.

solid answer

~50 s

Covering means the index carries everything the statement needs — predicate fields, projected fields, and any `ORDER BY` or `GROUP BY` fields — so the Query service never has to fetch the documents themselves from the Data service. That removes a whole network round trip and a cache lookup per qualifying key, and it is usually the single biggest win available on a hot query. You confirm it with `EXPLAIN`: the plan contains an `IndexScan` operator carrying a `covers` list and, crucially, **no `Fetch` operator**. If a `Fetch` is present, some referenced field is missing from the index. Two caveats: the index is only eligible in the first place if the query constrains its leading key, and a GSI omits documents where the leading key is MISSING, so a covered query can legitimately return fewer documents than a primary scan.

code

sql · 5 lines
sql
-- not covered: country is not indexed, so each key is fetched
CREATE INDEX idx_name ON `app`.inv.airline(name);

-- covered: every referenced field is an index key
CREATE INDEX idx_name_country ON `app`.inv.airline(name, country);

go deeper

for a junior

Recall that an index can sometimes answer a query on its own, without reading the documents, and that EXPLAIN shows the plan.

for a middle

Explain which fields must be index keys for covering — predicate, projection and sort — and identify the Fetch operator in an EXPLAIN plan as the marker of a non-covered query.

for a senior

Demonstrate the diagnosis end to end: check eligibility via the leading key, confirm covering via the absence of Fetch, and weigh the extra index width against the mutation cost before committing.

for a principal

Own the indexing budget: which statements deserve covering indexes, how many indexes a collection can carry before write amplification and index-service memory dominate, and how MISSING-key exclusion is handled as a correctness policy.

## What covering means A Couchbase SQL++ query normally executes in two steps. The Index service scans a global secondary index and returns the qualifying document keys; the Query service then **fetches** those documents from the Data service to evaluate any remaining predicates and build the projection. A *covering* index eliminates the second step: if the index keys already contain every field the statement mentions, the answer can be assembled from index entries alone. The fields that must be present are all of them — not just the ones in `WHERE`. Projected fields, `ORDER BY` fields and `GROUP BY` fields all count. `META().id` is available from the index entry itself, so projecting the document key does not break covering. ## Reading the plan `EXPLAIN` in front of the statement returns the plan without running it. Two markers matter: - an `IndexScan` operator that lists a `covers` array — the expressions the scan can supply directly; - the **absence** of a `Fetch` operator. A plan that shows `IndexScan` followed by `Fetch` is using the index for the predicate but still going to the Data service for the projection. A plan showing `PrimaryScan` means no secondary index was selected at all, which is a bigger problem than not covering. ## The before/after shape ```sql -- not covered: country is not an index key, so every key is fetched CREATE INDEX idx_name ON `app`.inv.airline(name); SELECT name, country FROM `app`.inv.airline WHERE name LIKE "A%"; -- covered: both referenced fields are index keys CREATE INDEX idx_name_country ON `app`.inv.airline(name, country); ``` After the second index exists, the same statement plans without a `Fetch`. Note the field *order* still matters for eligibility even though covering does not care about order: `name` must lead because that is what the predicate constrains. ## Eligibility comes first Covering is irrelevant if the index is not chosen. A GSI is only considered when the query constrains its **leading** key. A classic symptom: an index exists on `orders(created_at)` and a query does `SELECT ... ORDER BY created_at LIMIT 10` with no `WHERE` clause — the planner will not use the index, because nothing constrains the leading key, and the statement falls back to a primary scan or fails outright. The idiomatic fix is to add `WHERE created_at IS NOT MISSING`, which constrains the leading key without changing the intended result set, making the index eligible and the ordered scan cheap. ## The MISSING semantics you must not gloss over A secondary GSI contains an entry only for documents where the leading index key is present. Documents that lack the field are simply absent from the index. That is not a bug; it is what makes partial-shaped document collections indexable cheaply. But it means a query served by that index can return a smaller set than the logically equivalent query served by a primary index. In a schema-flexible collection where some documents genuinely lack the field, this is a correctness question, not a performance one — decide deliberately whether those documents should be in the answer, and if they must be, index an expression that is always present or add the field on write. ## Costs of chasing covering Every extra field you add to an index key widens the index entry and increases index-service memory and disk, and every index must be maintained on every mutation of a covered field. Adding four projection fields to a hot index to make it covering is often the right call; adding them to five indexes to cover five variant queries is usually the wrong one. The disciplined approach is: identify the few statements that dominate the workload, cover those, and let the rest fetch. `ADVISE` will suggest covering indexes per statement, so the consolidation judgment stays with you. ## What interviewers listen for They want to hear that you verify with `EXPLAIN` rather than assert; that you know the distinction between *eligible* (leading key constrained) and *covering* (all referenced fields present); that you can name the `Fetch` operator as the thing whose absence proves covering; and that you can articulate the write-side cost so covering does not become a reflex applied to every index in the cluster.

  • A Couchbase GSI on orders(created_at) is ignored by a query that only does ORDER BY created_at. Why, and what is the fix?
    The planner only considers a GSI when the query constrains its leading key, and an `ORDER BY` alone constrains nothing. Adding `WHERE created_at IS NOT MISSING` supplies that constraint without changing the intended result, making the index eligible and letting the ordered scan run from the index instead of a primary scan.
  • Can a covered Couchbase query return fewer documents than the same query answered by a primary index?
    Yes. A secondary GSI holds entries only for documents whose leading index key is present, so documents missing that field are absent from the index and from the answer. In a collection with genuinely heterogeneous documents that is a correctness decision: either accept the exclusion deliberately, index an always-present expression, or backfill the field.
  • What is the cost of adding projection fields to an index purely to make a query covering?
    Wider index entries, so more index-service memory and disk, and additional maintenance work on every mutation that touches a covered field. It is usually worth it for the handful of statements that dominate the workload, and rarely worth it applied across many variant queries, where consolidating onto fewer composite indexes serves better.

saying these in an interview costs you the question

  • Claims any index used by the query is covering
  • Forgets projected and ORDER BY fields must be indexed
  • Assumes covering also guarantees the index is chosen
  • Ignores that documents missing the leading key are excluded
  • Adds projection fields to every index by reflex

context