Engines automatically index the referenced key on the parent side of a foreign key but usually leave the referencing columns on the child side unindexed. What goes wrong in production because of that, and how do you decide which of those columns to index?
answer
- parent side indexed by definition, child side not
- reverse search = per parent row, per child table
- MySQL/InnoDB auto-creates it; PG/Oracle/SQL Server do not
- leading columns of a composite index or it doesn't count
- also helps WHERE parent_id = ? and nested loops
basics
~20 sParent DELETEs and referenced-key UPDATEs must search the child table for referencing rows; with no index that is a full scan per parent row, and in engines that lock the scanned children it also causes lock escalation and deadlocks. Index child referencing columns on any table whose parent gets deleted, updated, or joined on.
solid answer
~60 sThe parent side is indexed for free because the referenced columns must be a primary or unique key. The child side has no such requirement, so the reverse existence search — "are there children of this parent?" — often has no access path. Symptoms: a DELETE or key UPDATE on the parent that should touch one row scans the entire child table, once per parent row and once per referencing table. Purge and archive jobs mysteriously take hours; a small parent delete blocks behind a huge child scan. In engines that take locks on the rows examined by the referential action, the scan can also lock far more of the child than expected, producing timeouts and deadlocks. The same missing index usually hurts the everyday `WHERE parent_id = ?` lookup and nested-loop joins from parent to child. Decision rule: index the referencing columns whenever parents are deleted or their keys change, whenever a referential action such as CASCADE or SET NULL exists, or whenever you query children by parent. Skip it only for immutable, never-purged parents — and weigh the write and space cost.
go deeper
Know the fact: the child-side referencing columns usually need an index you create yourself, or deletes on the parent get slow.
Explain the mechanism — the per-parent-row reverse search, leading-column requirement — and that the index also serves ordinary lookups by parent.
Diagnose it from plans and lock waits, describe the deadlock pattern from widened lock footprints, and give a per-relationship rule rather than a blanket one.
Trade write amplification and cache footprint against purge/archival latency; consider partitioning by parent key or soft deletes as structural alternatives, and treat dropping the constraint as an explicit correctness trade.
## Why the asymmetry exists A foreign key's referenced columns must be a primary key or unique key, so the parent always has a supporting index and the child-side existence probe is fast by construction. Nothing in the standard requires an index on the *referencing* columns, and most engines do not create one. MySQL/InnoDB is the notable exception — it requires and will auto-create an index on the child columns — while Postgres, Oracle and SQL Server leave it entirely to you. ## What breaks **Parent deletes and key updates become table scans.** The check "do children exist for this key?" is executed per affected parent row, per referencing table. With no index it is a full scan. Deleting 10,000 parents from a table referenced by two 100-million-row children is 20,000 full scans; the statement appears hung. The same applies to `ON DELETE CASCADE`, which must *find* and then delete the children. **Locking blowups.** Where the engine locks rows it examines while enforcing a referential action, an unindexed scan widens the lock footprint far beyond the logically affected rows. Two transactions deleting different parents can then contend on the same child pages and deadlock. Deadlock graphs that show two parent DELETEs fighting over an unrelated child table are the classic fingerprint. **Everyday query plans suffer too.** `SELECT * FROM order_items WHERE order_id = ?` and nested-loop joins driven from the parent both want exactly that index. So the index usually pays for itself twice. ## Diagnosing it Start from the catalog: enumerate foreign keys and check whether any index has the referencing columns as its *leading* columns. A composite index on `(status, order_id)` does not serve the reverse search for `order_id`; leading-column order matters. Then confirm with an execution plan for a single-parent DELETE — a sequential/full scan of the child under the delete node is the smoking gun. Watching for long lock waits attributed to a child table during parent maintenance is the operational signal. ## Choosing which ones to index An index on every referencing column is a defensible default, but it is not free: each one adds write amplification on child INSERT/UPDATE/DELETE, more space, and more pages to keep cached. Decide per relationship: - **Index it** when parent rows are ever deleted or their keys change; when a referential action (CASCADE, SET NULL, SET DEFAULT) is declared; when the application queries children by parent; when the child table is large. - **Consider skipping** when the parent is append-only reference data that is never deleted and whose key never changes (currencies, country codes), when the child table is small enough that a scan is trivial, or when the referencing column is so low-cardinality that the index would rarely be chosen anyway. - **Watch the leading column.** If a composite index already starts with the referencing columns, you are covered; if it merely contains them, you are not. Also note the difference between the *existence* search and the *action* search: even a plain `NO ACTION`/`RESTRICT` foreign key needs the search, so "we don't use CASCADE" is not a reason to skip the index. ## Alternatives and mitigations Where adding the index is genuinely undesirable, the workable alternatives are structural: delete children explicitly in the application before the parent (using whatever access path exists), partition the child by the parent key so parent removal becomes a partition drop, or soft-delete parents so the referential search never fires. Dropping the foreign key to make the delete fast is a trade of correctness for latency and should be an explicit, argued decision rather than an accident.
- How would you find the missing indexes across a whole schema?Query the catalog for every foreign key constraint and its referencing columns, then check whether any index on that table has those columns as its leading columns, in order. Anything without a match is a candidate. Confirm each candidate against actual workload evidence — parent delete or update frequency and plans showing full scans of the child — before adding indexes everywhere.
- Does an index on (status, customer_id) satisfy a foreign key on customer_id?No. The reverse existence search filters on customer_id alone, and a B-tree can only seek efficiently on a leading-column prefix. With status leading, the engine would have to scan or skip-scan the whole index. You need an index whose leading column is customer_id.
saying these in an interview costs you the question
- Assuming every engine indexes both sides of a foreign key automatically
- Thinking the index only matters if you use ON DELETE CASCADE
- Believing any index that mentions the column works, regardless of column order
- Blaming the parent table's index when a purge job is slow
- Proposing to drop the foreign key as the default fix for a slow delete