PL/SQL Triggers: Compound Triggers & Mutating Tables
Triggers are powerful, invisible, and incredibly dangerous if misunderstood. Learn the architecture of database triggers, how to capture row-level changes, and how to definitively solve the infamous 'Mutating Table' error using 11g Compound Triggers.
1. Statement vs. Row-Level Triggers
A DML trigger executes automatically when an INSERT, UPDATE, or DELETE occurs on a table.
Statement-Level Triggers (The Default)
A statement-level trigger fires exactly once per SQL statement, regardless of how many rows are affected. Even if a massive UPDATE affects 10 million rows, the trigger fires once.
- Use case: Security checks (e.g., preventing DML outside business hours), logging global batch events.
- Limitation: Cannot access the specific data being changed (no
:NEWor:OLDvariables).
Row-Level Triggers (FOR EACH ROW)
A row-level trigger fires once for every single row affected by the statement.
- Use case: Data validation, generating primary keys via sequences (pre-12c), audit logging.
- Cost: If you update a million rows, this trigger fires a million times. Context switching can severely degrade performance.
2. The Mutating Table Error (ORA-04091)
This is the most famous error in PL/SQL programming.
ORA-04091: table is mutating, trigger/function may not see it
This occurs when a row-level trigger attempts to query or modify the very table that the triggering statement is currently modifying.
Why does Oracle prevent this?
Imagine you write a BEFORE UPDATE FOR EACH ROW trigger on the EMPLOYEES table. Inside that trigger, you run SELECT MAX(salary) FROM employees.
Since the UPDATE is currently mid-flight (maybe it's updating 50 rows, and it's currently on row 3), the database is in an inconsistent state. The MAX(salary) is ambiguous. To protect data integrity, Oracle hard-stops you.
3. The Modern Fix: Compound Triggers
Before Oracle 11g, fixing a mutating table required creating a package to hold PL/SQL collections, a statement-level BEFORE trigger to clear the collection, a row-level trigger to capture :NEW values into the collection, and a statement-level AFTER trigger to process the collection. It was a nightmare.
Compound Triggers (Oracle 11g+) consolidate all these timing points into a single, elegant object that shares a global state.
By capturing the row data during the EACH ROW phase and deferring the complex queries until the AFTER STATEMENT phase (when the table is no longer mutating), the compound trigger elegantly solves ORA-04091.

