DEV Community

Tyson Cung
Tyson Cung

Posted on

MySQL's 20-Year Bug #11472 Is Finally Fixed: Here's Why It Mattered

After 20 years, 1,600+ Reddit upvotes, birthday celebration threads, and at least one postponed marriage proposal, MySQL Bug #11472 has been fixed.

This isn't just another bug fix story. It's a case study in how one design decision, buried deep in a database engine, can silently corrupt data integrity for two decades. And it's a reminder that the quietest bugs are often the most dangerous.

Let's break down what the bug was, why it survived 20 years, and what the fix means for your applications.


The Bug: When CASCADE Goes Silent

Here's a scenario that looks completely innocent:

-- Parent table: users
CREATE TABLE users (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    status VARCHAR(20)
);

-- Child table: orders, with a CASCADE
CREATE TABLE orders (
    id INT PRIMARY KEY,
    user_id INT,
    amount DECIMAL(10,2),
    FOREIGN KEY (user_id) REFERENCES users(id)
        ON DELETE CASCADE
);

-- Audit trigger: log every deletion
CREATE TRIGGER audit_order_delete
    AFTER DELETE ON orders
    FOR EACH ROW
    INSERT INTO audit_log (table_name, row_id, action, timestamp)
    VALUES ('orders', OLD.id, 'DELETE', NOW());
Enter fullscreen mode Exit fullscreen mode

Now run this:

-- Delete a user; CASCADE should delete their orders
DELETE FROM users WHERE id = 42;
Enter fullscreen mode Exit fullscreen mode

What you expect: The user is deleted. Their orders are cascade-deleted. The audit_order_delete trigger fires for each deleted order.

What actually happened (for 20 years): The orders were deleted. The trigger never fired. No audit log entries. Silent data loss.


The Architecture: Why This Happened

MySQL processes foreign key cascades before trigger execution. The execution flow looked like this:

MySQL CASCADE execution flow showing the bypass

The FK cascade system bypassed the trigger infrastructure entirely. It used an internal row-deletion path that didn't go through the standard DELETE handler, so triggers, which hook into the standard handler, were never invoked.

This is the kind of architecture decision that makes sense in a vacuum (FK cascades are internal operations, right?) but creates a deep violation of the principle of least surprise in practice.


What This Broke in Production

The cascade-trigger gap wasn't theoretical. It silently corrupted three critical patterns:

1. Audit Trails

Every system that relied on triggers for audit logging had invisible holes. When a parent record was deleted, the cascade-deleted children left no audit trail:

Expected audit log:
  [2026-05-27 14:02:01] orders | #8823 | DELETE
  [2026-05-27 14:02:01] orders | #8824 | DELETE
  [2026-05-27 14:02:01] orders | #8825 | DELETE

Actual audit log (pre-fix):
  (empty; triggers never fired)
Enter fullscreen mode Exit fullscreen mode

2. Business Logic in Triggers

Teams that encoded validation or side effects in child-table triggers had them silently skipped:

-- This trigger NEVER fired on cascade deletes
CREATE TRIGGER refund_order
    AFTER DELETE ON orders
    FOR EACH ROW
BEGIN
    IF OLD.status = 'paid' THEN
        INSERT INTO refunds (order_id, amount, reason)
        VALUES (OLD.id, OLD.amount, 'user_deleted');
    END IF;
END;
Enter fullscreen mode Exit fullscreen mode

Users who deleted their accounts would cascade-delete orders, but refunds would never be issued because the trigger never ran.

3. Denormalization and Caching

Triggers used to maintain denormalized fields or cache tables silently fell out of sync:

-- Keeps a running total, silently falls out of sync
CREATE TRIGGER update_user_total
    AFTER DELETE ON orders
    FOR EACH ROW
    UPDATE users SET total_spent = total_spent - OLD.amount
    WHERE id = OLD.user_id;
Enter fullscreen mode Exit fullscreen mode

After a cascade delete, users.total_spent would remain unchanged, even though the orders were gone.


Why It Took 20 Years to Fix

Not because MySQL engineers didn't care. Because fixing it correctly required touching the most sensitive part of the engine:

Three subsystems the fix had to coordinate across

Each subsystem represented a risk. A careless fix could:

  • Break existing applications relying on triggers not firing
  • Cause deadlocks in high-concurrency workloads (FK cascades + triggers = complex locking)
  • Create infinite loops (trigger fires → FK cascade → trigger fires → ...)

The MySQL team's solution in 9.7 is elegant: a new system variable foreign_key_checks_trigger (default: OFF) that explicitly opt-ins to the new behavior. This means existing applications with workarounds won't break, but new applications can get correct behavior:

-- Enable trigger execution on FK cascades (MySQL 9.7+)
SET foreign_key_checks_trigger = ON;

-- Now triggers fire correctly
DELETE FROM users WHERE id = 42;
-- audit_order_delete trigger fires for each cascaded order ✓
Enter fullscreen mode Exit fullscreen mode

The Human Side: Internet Folklore

Bug #11472 became more than a bug. It became a piece of developer culture. Here's the timeline:

Bug #11472 timeline: 2005-2026

The original reporter, who filed the bug in 2005, is still active and commented on the fix in 2026. Two decades of waiting.

The most upvoted Reddit comment on the fix thread?

"WTF? I was relying on this feature! Change it back please." 987 upvotes

Developer humor at its finest.


What This Means for Your Applications

If You're Running MySQL < 9.7

You still have the bug. Your triggers on child tables are silently skipped during FK cascade operations. Check your applications:

-- Find tables with BOTH triggers AND foreign key relationships
SELECT 
    t.TABLE_NAME,
    tr.TRIGGER_NAME,
    tr.EVENT_MANIPULATION,
    k.REFERENCED_TABLE_NAME,
    k.COLUMN_NAME
FROM information_schema.TRIGGERS tr
JOIN information_schema.TABLES t ON tr.EVENT_OBJECT_TABLE = t.TABLE_NAME
JOIN information_schema.KEY_COLUMN_USAGE k 
    ON t.TABLE_NAME = k.TABLE_NAME 
    AND k.REFERENCED_TABLE_NAME IS NOT NULL
WHERE t.TABLE_SCHEMA = 'your_database';
Enter fullscreen mode Exit fullscreen mode

If You're Migrating to MySQL 9.7

The fix is opt-in. Your existing code won't break, but you should:

  1. Audit your triggers: Identify which ones depend on FK cascade behavior
  2. Test with foreign_key_checks_trigger = ON: Make sure your triggers handle cascade events correctly
  3. Watch for performance: FK cascade + triggers can increase locking overhead in high-write workloads
  4. Update your workarounds: If you wrote application-level code to compensate for the bug, you can now remove it

The Migration Path

-- Phase 1: Enable in a staging environment
SET GLOBAL foreign_key_checks_trigger = ON;

-- Phase 2: Monitor for issues
-- Check slow query log for new trigger-related queries
-- Watch for deadlocks in FK-heavy workloads

-- Phase 3: Enable in production (gradual rollout)
-- Start with read replicas, then primary
Enter fullscreen mode Exit fullscreen mode

The Bigger Lesson

Bug #11472 teaches something important about software architecture: silent failures are worse than loud ones.

If MySQL had thrown an error when triggers couldn't fire on cascade operations, every DBA would have noticed immediately. The bug would have been fixed in 2005. But because it failed silently (rows were deleted, triggers were skipped, everything looked normal on the surface), it survived two decades.

This is the same pattern we see in:

  • Distributed systems that silently drop messages
  • API clients that retry without logging failures
  • Database ORMs that swallow constraint violations

Silence is not a feature. It's a time bomb.


Key Takeaways

  1. Bug #11472 caused MySQL triggers to silently skip execution during FK CASCADE operations. Rows were deleted but triggers never fired.

  2. The fix in MySQL 9.7 is opt-in via foreign_key_checks_trigger. Existing applications won't break, but you should audit your trigger dependencies.

  3. The architecture problem was a bypass: FK cascades used an internal deletion path that didn't route through the standard trigger dispatcher.

  4. 20 years of survival shows how deeply buried design decisions can create persistent, invisible bugs.

  5. Audit your audit trails. If you use triggers for logging, make sure they're actually firing for all delete paths, not just direct DELETEs.


The MySQL 9.7 release notes include the full details of this fix. If you're running production MySQL with triggers + foreign keys, this is the upgrade you've been waiting two decades for.

Follow for more deep dives on database internals, software architecture, and the bugs that shaped our industry.

Top comments (0)