skip to content

A busy table has accumulated a dozen single-column and multi-column indexes and writes have slowed. How would you decide which multi-column index keys the workload actually needs, and which existing indexes are safe to drop as redundant?

level: principalimportance: should knowfreq 45%

answer

  1. Demand (query shapes) before supply (index list)
  2. Prefix ⊂ longer key = redundant
  3. (b,a) is not a prefix of (a,b)
  4. Unique/PK indexes are constraints, not candidates
  5. Full business cycle of usage counters; drop one at a time

basics

~20 s

Collect the real query shapes, design one key per shape family with equality columns first and the range or sort column last, then drop any index whose key is a leftmost prefix of another. Verify with index usage counters and roll the drops out one at a time behind a quick rollback.

solid answer

~50 s

Start from evidence, not from the index list: capture the recurring query shapes (filter columns, predicate kinds, sort and limit) plus their call rates. For each shape family, derive a key: equality-constrained columns first, then the range or ORDER BY column. Then apply prefix subsumption — any index whose key is a **leftmost prefix** of another index's key is redundant, because every access path it offers is offered by the longer key. `(a)` is subsumed by `(a, b, c)`; `(a, b)` is subsumed too; `(b, a)` is **not**. Merge near-duplicates: `(tenant_id, status)` and `(tenant_id, created_at)` may collapse into `(tenant_id, status, created_at)` if the status-only shape tolerates it — but do not merge blindly, because a longer key is wider and its scans read more pages. Then validate: index-usage counters over a full business cycle (include month-end and batch jobs), and unique/constraint-backing indexes excluded from the candidate list. Drop one at a time, watch latency, keep the DDL to recreate at hand.

code

sql · 9 lines
sql
-- existing
CREATE INDEX i1 ON orders (tenant_id);
CREATE INDEX i2 ON orders (tenant_id, status);
CREATE INDEX i3 ON orders (tenant_id, status, created_at);
CREATE INDEX i4 ON orders (status, tenant_id);

-- i1 and i2 are leftmost prefixes of i3  -> redundant, drop
-- i4 is NOT a prefix of i3 (different leading column) -> keep if a
--    status-only or status-leading shape exists, else drop on evidence

go deeper

for a junior

Know the mechanical part: an index whose columns are the leading columns of another index is redundant, and each extra index slows writes.

for a middle

Add the derivation — one key per query-shape family, equality first — and mention using index usage statistics before dropping anything.

for a senior

Own the rollout: constraint-backing exclusions, full-cycle evidence including replicas and batch jobs, create-before-drop, one drop per window with a rollback statement ready.

for a principal

Frame it as a workload-level budget — read benefit per shape against write amplification and cache footprint — and institutionalize a review rule so index sprawl does not reappear.

## Why this is a judgment problem Every index is paid for on every write — an insert touches every index on the table, an update touches every index whose key columns changed (and in some engines every index, period), and a delete must remove or mark entries everywhere. Indexes also compete for buffer cache and lengthen maintenance operations. So the goal is the *smallest set of keys* that still gives every important query shape a good access path. ## Step 1 — inventory the demand, not the supply List the recurring query shapes from actual traffic: statement digests, slow-query capture, ORM query logs. For each, record the filter columns and predicate kind (equality / range / IN / expression), the sort and whether there is a `LIMIT`, and the call rate. Two shapes that differ only in constants are one family. Do not forget periodic jobs — a monthly reconciliation query that runs once but must finish before a deadline is a real shape. ## Step 2 — derive one key per family Apply the ordering rules: equality-constrained columns leftmost, then the single range column or the `ORDER BY` columns. Choose the order among the equality columns so that useful **sub-prefixes** land on other shapes — this is what turns N indexes into one. A key `(tenant_id, status, created_at)` simultaneously serves `tenant_id = ?`, `tenant_id = ? AND status = ?`, and the tenant+status+time-range shape, and can supply `ORDER BY created_at` inside a pinned tenant+status. ## Step 3 — prefix subsumption The mechanical part. An index whose key is a leftmost prefix of another index's key is redundant for lookup purposes: `(a)` ⊂ `(a, b)` ⊂ `(a, b, c)`. Every seek the shorter key supports, the longer key supports identically, because both descend on the same leading columns. Three honest caveats: 1. **Not a prefix, not redundant.** `(b, a)` and `(a, b)` are different structures serving different single-column shapes. 2. **Width costs something.** The longer key is physically wider, so full scans of it read more pages and it has lower fanout. If a hot query does a large scan of the short index, keeping it can be justified — measure rather than assume. 3. **Constraint-backing indexes are not droppable.** A unique index enforces a constraint; dropping it changes semantics, not just performance. Primary-key and unique indexes come off the candidate list immediately. ## Step 4 — check usage evidence Every mainstream engine exposes per-index usage counters (scans/seeks and last-used information). Reset or snapshot them, then observe across a **full business cycle** — a week is usually not enough if month-end reporting exists. Zero scans over a full cycle is strong evidence, but combine it with the static prefix analysis so you understand *why* something is unused. Also check replicas: an index unused on the primary may be the one carrying the reporting workload on a read replica. ## Step 5 — roll out safely Drop one index per change window, most-clearly-redundant first, and keep the exact `CREATE INDEX` statement ready to restore. Watch p95/p99 latency for the affected shapes and the plan for the queries you predicted would move onto the surviving key. Where the engine supports it, marking an index invisible/disabled before dropping gives a reversible test. Create replacement indexes concurrently/online before dropping the ones they replace, never after. ## Step 6 — set a norm so it does not regrow The dozen indexes appeared because each was added in isolation to fix one query. The durable fix is a review rule: any new index proposal must state the query shape it serves and show that no existing key's prefix already covers it. Keep the index set documented next to the schema so the next engineer extends a key rather than adding a sibling. ## What a strong answer sounds like Evidence first, derive keys from shapes, apply prefix subsumption mechanically, exclude constraint-backing indexes, validate against a full cycle of usage counters, roll out reversibly, and close with the process change that stops the sprawl from returning. Acknowledging the write-cost-versus-read-benefit tradeoff explicitly — including that some redundancy is a deliberate purchase — is what separates this from a checklist.

  • When is it legitimate to keep an index whose key is a prefix of another?
    When a hot query scans a large portion of the short index rather than seeking a few entries, the narrower key means fewer pages read per scan, and that read saving can outweigh its write cost. The same argument applies when the short index is much better cached because of its size. Both cases should be settled by measurement — page counts and observed latency — not by assumption.
  • How do you avoid breaking a query you never observed when you drop an index?
    Sample over a full business cycle rather than a quiet week, and include batch jobs, reporting replicas and admin tooling in the capture. Use the engine's reversible option first — making the index invisible or disabled — so recovery is a metadata change rather than a rebuild. Keep the exact CREATE statement, drop one index per window, and monitor the specific shapes you predicted would migrate to the surviving key.

saying these in an interview costs you the question

  • Treating (b, a) as redundant given (a, b)
  • Dropping a unique index without noticing it enforces a constraint
  • Judging usage from a few days of counters and missing month-end jobs
  • Merging every key into one very wide index without accounting for scan width and write cost
  • Dropping several indexes in one change so a regression cannot be attributed

context