Stop Disabling Foreign Keys: Use a Topological Sort Instead

Split scene: on the left a chaotic red warning switch for disabling constraints, on the right a woman DBA confidently standing next to an orderly directed graph on a whiteboard

You inherit a PostgreSQL cleanup script. The very first line is:

It works. Every table gets wiped clean, no FK errors, no complaints. Then one day somebody adds a new table to the schema, forgets to update the delete script, and you end up with orphan rows. No error. No warning. Just silent data corruption that you won’t find until something downstream breaks.

The “just turn off constraints” approach is a ticking time bomb.

What SESSION_REPLICATION_ROLE Actually Does

This setting was designed for logical replication. It tells PostgreSQL: “treat this session as a replica.” The practical side effects are:

  1. Foreign key constraint checks are disabled
  2. User-defined triggers are suppressed
  3. Row-level security policies are bypassed

People use it as a shortcut when they can’t (or don’t want to) figure out the correct deletion order for a set of FK-related tables. It feels clever. It is not. You’re trading a 30-minute ordering exercise for an invisible, accumulating debt.

The Real Problem Is a Dependency Graph

Think about what foreign keys actually represent: directed edges in a graph. If orders has a FK pointing to customers, that’s an edge from orders to customers. You must delete orders before customers.

When you have 136 tables with 229 FK relationships, figuring out the right order by hand is painful. But this is a solved problem in computer science. It’s called topological sorting, and the most intuitive version is Kahn’s algorithm[1]:

  1. Find all tables with no incoming FK edges (the “leaves” – tables nothing else depends on)
  2. Delete those first. Remove them from the graph.
  3. Some tables that previously had incoming edges now have zero. They become the new leaves.
  4. Repeat until the graph is empty.

If the graph is never empty (you can’t find a node with zero incoming edges but there are still nodes left), you have a circular FK dependency, and you know about it immediately instead of discovering it at 2 AM.

Extracting the FK Graph

We had the DDL files in a Git repo (as you should). The FK definitions looked like this:

A regex pass over the DDL repo pulled out all 229 FK edges as (child, parent) pairs. We then partitioned them:

  • Internal edges: both tables are in our delete script (these affect ordering)
  • External edges: the parent table is outside our scope (these don’t affect ordering since we’re not deleting the parent)

Only the internal edges matter for the topological sort.

Running the Sort

Here’s a simplified version of Kahn’s algorithm in Python. The real script read edges from a file; this version hardcodes a small example:

The output is a flat list: delete in this order, top to bottom, and every FK constraint is satisfied at every step.

The Safety Checklist

Before removing SESSION_REPLICATION_ROLE, we verified that a clean topological sort was actually sufficient. Three things can break the “just reorder the DELETEs” approach:

1. Self-referencing FKs – a table with a FK pointing to itself (e.g., employee.manager_id referencing employee.employee_id). You can’t delete a parent row before its child rows when they’re in the same table. We checked: none in our schema.

2. DELETE triggers – a trigger that fires on DELETE could cascade, block, or insert into other tables. We checked all triggers in the schema: five total, all BEFORE INSERT or AFTER UPDATE. None fired on DELETE.

3. DEFERRABLE constraints – constraints marked DEFERRABLE INITIALLY DEFERRED are checked at COMMIT, not at statement execution time. If you’re running the entire script in a transaction, these could let you delete in any order and only fail at the end. More importantly, if you’re relying on that behavior, reordering alone might surface errors you weren’t expecting. We checked: no deferrable constraints defined.

All clear. Safe to remove the crutch.

The Coverage Gap Bonus

Here’s the real payoff. While mapping the FK graph, we found 5 tables that were missing from the delete script entirely. They had FK relationships to tables we were deleting, but nobody had added DELETE statements for them.

Under SESSION_REPLICATION_ROLE TO REPLICA, this was invisible. The FK checks that would have screamed “you can’t delete from customers because customer_preferences still has rows pointing to it” were silenced. The script completed successfully, leaving orphan rows in those 5 tables.

This is the real argument against disabling constraints. Active constraints are a safety net, not a nuisance. They catch exactly the kind of mistake that humans make: forgetting to update a script when the schema changes.

The Takeaway

Constraints exist to catch mistakes. Disabling them to avoid thinking about delete order is trading a small upfront cost for an invisible, accumulating debt. A topological sort took an afternoon; finding orphan rows in production takes much longer.

If you have a script that starts with SET SESSION_REPLICATION_ROLE TO REPLICA and it’s not actually doing replication, it’s worth spending the time to figure out the right delete order. Your future self will thank you.


[1] Kahn, A. B. (1962). “Topological sorting of large networks.” Communications of the ACM, 5(11), 558-562. doi:10.1145/368996.369025
Have you untangled a similar FK mess? I’d love to hear about it. Find me on Bluesky or LinkedIn.