ORA-02291 Integrity Constraint — Parent Row Missing
Fix Oracle ORA-02291 by inserting the parent row first, ordering loads, or deferring constraints.
20+ years shipping high-throughput database systems. Notes here come from systems that actually shipped.
- ✓An Oracle database with FK constraints you can inspect
- ✓The failing load's child and parent table names
- ✓Read access to all_constraints and all_cons_columns
- ORA-02291 means your child row points at a parent key that doesn't exist — the message names the constraint, and the dictionary maps that name to the exact parent table and columns
- Translate it in one query: join all_constraints to all_cons_columns on the named constraint to learn the parent table, then anti-join your staged rows to list every orphan
- Load in dependency order (parents before children), or mark the constraint DEFERRABLE INITIALLY DEFERRED so Oracle checks once at COMMIT instead of per row
- Decide each orphan's fate deliberately: insert the missing parent, reassign the child to a real parent, or delete the row — after archiving what you remove
Picture a library where every loan must link to a member card. ORA-02291 is the scanner rejecting slip 9917 — a card never issued. Maybe new members are not registered yet (parents load later), the card was mistyped (bad key), or the member left years ago (deleted parent). The fix is registering members first (insert order), checking all slips together at closing time (deferred constraints), or ruling on ghost slips (orphan cleanup).
ORA-02291: integrity constraint (SHOP.FK_ORDER_CUSTOMER) violated - parent key not found. It arrives during bulk loads, ETL cutovers, and parallelized migrations: thousands of child rows rejected because their parents aren't there (yet). The application worked row-by-row for years; the loader just changed the order, the parallelism, or the source — and referential integrity did exactly what it was built to do.
Foreign keys enforce a simple law: no child without its parent. That law spans insert order (parents first), deletions (no orphaning parents without CASCADE or child cleanup), and concurrent loads (two streams racing). Break any of the three and error 02291 rejects the write — loudly, per row, until the loader gives up.
This guide moves from the constraint name to the parent table, lists orphans with anti-joins, restores dependency order, uses deferred constraints for cyclic loads, cleans up strays, and gates future loads with pre-flight orphan checks that fail fast every time.
Read the Constraint Name, Not Just the Code
The message names SHOP.FK_ORDER_CUSTOMER — owner plus constraint — and the dictionary does the rest. Join all_constraints (constraint type R, status, deferrability, delete rule) to all_cons_columns (the child columns in position order), then resolve r_constraint_name back through all_cons_columns to the parent table and its columns. One query returns the complete relationship: child table, child columns, parent table, parent columns, and whether the constraint even permits deferral.
Read the status columns while you're there: STATUS (ENABLED/DISABLED), VALIDATED/NOVALIDATE, DEFERRABLE/DEFERRED. A DISABLED or NOVALIDATE constraint that 'lets everything through' explains the opposite symptom (bad data present, no errors) and warns that re-enabling will surface every accumulated orphan at once. Know the constraint's state before you change the data's.
Save the translation in the ticket: 'FK_ORDER_CUSTOMER = orders.customer_id -> customers.customer_id, immediate, NO ACTION.' That single line lets anyone reproduce the orphan query without re-deriving the relationship — and trend analysis on constraint names shows whether failures cluster on one relationship (topology bug) or scatter (source drift).
Find the Orphans With an Anti-Join
The anti-join is the definitive orphan census: staged (or live) child rows with no matching parent. NOT EXISTS reads most clearly and stops at the first match per row; NOT IN risks NULL semantics (a single NULL parent key voids the whole result); LEFT JOIN ... IS NULL works but scans awkwardly. Standardize on NOT EXISTS and the team reads every orphan query at sight.
Always count before listing: the COUNT(*) with the identical predicate sizes the response (dozens = surgical fixes, millions = topology or source bug) without flooding your terminal. Then list with the child key plus the offending FK value — the pair is what the cleanup acts on. Timestamp the orphans too: a tight recent cluster means ordering, a wide historical scatter means drift, and the two get different owners.
Run the census against staging before production loads, and against production after any parent-side delete or archive job without exception. Archive purges are the quiet orphan factories: the purge deletes parents its WHERE clause matched while children outside its scope keep pointing at the grave. Every purge job earns a post-flight orphan count run by the job itself, failing the batch when strays appear.
Insert Order: Parents Before Children
Dependency order is the primary fix and it belongs in the loader's DAG, not in tribal knowledge. Phase the job: all parent streams to completion, a barrier, then child streams — with the barrier as a first-class DAG node that fails loudly if a parent phase partial-fails. Hash-partitioning rows across workers is fine within a phase; across phases it recreates the race. The incident's 16 workers were correct parallelism applied to the wrong dimension.
For single-database migrations, order the DML the same way: INSERT parents, then children, then create (or enable) the FK — or load with constraints DISABLED and ENABLE ... VALIDATE after, letting validation census all violations at once instead of failing on the first row. Staging tables make this clean: raw files land unconstrained, validated sets move in dependency order, and the live tables never see an orphan.
Document the dependency graph beside the loader config: which tables are parents of which, kept in load order. The graph is the onboarding doc and the incident map in one — when 02291 names a constraint, the graph shows its position and which phase owns it. Topology you can see is topology you can fix quickly and safely every time.
Deferred Constraints for Cyclic Loads
Some loads can't order cleanly: cyclic references (A points at B, B points at A), single-transaction migrations that must interleave, or windows too tight for phasing. DEFERRABLE INITIALLY DEFERRED constraints move the check to COMMIT — insert in any order inside the transaction, and Oracle validates the whole graph once at commit. Real orphans still fail, with the same 02291, just at COMMIT time instead of row time.
Prefer INITIALLY IMMEDIATE for deferrable constraints you add: day-to-day behavior stays immediate (fail fast per row), and transactions that need freedom call SET CONSTRAINTS ALL DEFERRED explicitly. Blanket INITIALLY DEFERRED hides genuine violations until commit across all sessions, turning quick feedback into end-of-transaction surprises. Deferral is a tool you reach for, not a default you live in.
Convert with care: ALTER TABLE ... MODIFY CONSTRAINT ... DEFERRABLE re-locks briefly, so always convert off-peak. And never defer to mask drift — if the anti-join census shows true orphans (no parent anywhere, at any time), deferral just postpones the failure to COMMIT. Deferral tolerates ordering within a single transaction; only repair fixes the true absence of parents.
Cleanup: Adopt, Create, or Delete Orphans
True orphans need a fate decision per batch, made with the data owner, not the loader. Adopt: the child points at the wrong parent but the right one exists — UPDATE the FK to the correct key (common after source-system merges that renumbered parents). Create: the parent is genuinely missing but reconstructible — INSERT it from the child's distinct key values plus source defaults, marked for review. Delete: the child is junk (test rows, abandoned carts past retention) — archive then DELETE.
Archive before any destructive fate: INSERT the doomed rows into an orphan_archive table with timestamps and reason codes before UPDATE or DELETE touches them. Finance asking about row counts next quarter gets evidence, not recollection — and a mis-fated batch restores from the archive instead of the backup tapes.
Execute set-based with the same anti-join predicate as the census, so cleanup and diagnosis can't disagree. Re-run the census after each fate batch until zero: the loop (census, decide, act, re-census) is the procedure, and 'zero orphans' is the exit criterion every cleanup ticket records before it may close. Partial cleanups stopping at 'few enough' become next quarter's incident report.
Prevention: Validate Before Loading
Make orphan-free loads a gate, not a hope. The pre-flight anti-join count must read zero before the loader inserts anything — wire it as the DAG's first node with a hard fail, not a warning email. Warnings get snoozed through quarters; hard gates get fixed before lunch. Extend the gate per relationship: every FK the load touches gets its own census query, generated from the dictionary so new constraints join the gate automatically.
Monitor the error as a topology signal. Alert on the first ORA-02291 after any ETL deploy — ordering regressions never self-heal, and the first occurrence is the cheapest moment to catch them. Trend rejections per constraint across loads: a step-change names the deploy that reordered streams even when the diff looked innocent.
Keep staging tables deferrable and live tables immediate. Staging absorbs out-of-order arrivals with deferred checks at merge time; live tables fail fast per row so application bugs surface instantly. The two-tier discipline gives bulk loads freedom and OLTP strictness from the same constraint definitions — correct by construction. Generate the per-FK census queries from the dictionary so new relationships join the gate with zero manual wiring.
Parallel ETL Loaded 2M Children Before Their Parents
- Order loads by dependency, not by row hash: parallel streams need phase barriers between parents and children, or the fast stream fails against missing parents.
- Pre-flight orphan counts beat post-failure error tables: one anti-join before loading fails the job in seconds instead of aborting it after 2M rejections.
- Keep deferral as a safety net, not the design: deferred constraints tolerate ordering, but phased topology is what makes ordering correct.
| File | Command / Code | Purpose |
|---|---|---|
| translate_fk_2291.sql | SELECT c.table_name AS child_table, cc.column_name AS child_col, | Read the Constraint Name, Not Just the Code |
| orphan_census.sql | SELECT COUNT(*) AS orphans | Find the Orphans With an Anti-Join |
| ordered_load.sql | INSERT INTO shop.customers (customer_id, name) | Insert Order |
| defer_fk.sql | ALTER TABLE shop.orders | Deferred Constraints for Cyclic Loads |
| orphan_fates.sql | CREATE TABLE orphan_archive AS | Cleanup |
| preload_gate_2291.sh | set -euo pipefail | Prevention |
Key takeaways
Common mistakes to avoid
5 patternsRe-pulling the source when loads reject en masse
Disabling the FK to 'finish the load'
Using NOT IN for orphan hunts
Deferring everything INITIALLY DEFERRED
Deleting orphans without archiving
Interview Questions on This Topic
What does ORA-02291 mean?
Frequently Asked Questions
20+ years shipping high-throughput database systems. Notes here come from systems that actually shipped.
That's Oracle. Mark it forged?
5 min read · try the examples if you haven't