An Oracle mutating table error occurs when a row-level trigger tries to read from or modify the same table that is currently being modified by the triggering statement.
The classic error is:
ORA-04091: table <TABLE_NAME> is mutating, trigger/function may not see it
ORA-06512: at "<TRIGGER_NAME>", line ...
The key idea is:
UPDATE employees
↓
Row-level trigger
↓
SELECT ... FROM employees
↓
❌ ORA-04091: table is mutating
1. What is a mutating table?
A mutating table is a table that is currently being modified by a DML statement.
For example:
UPDATE employees
SET salary = salary + 1000;
While Oracle is processing this statement, the employees table is considered mutating.
If a row-level trigger on employees tries to query employees, Oracle can raise:
ORA-04091
2. What is the classic example?
Suppose we have:
CREATE TABLE employees (
employee_id NUMBER,
department_id NUMBER,
salary NUMBER
);
Create a row-level trigger:
CREATE OR REPLACE TRIGGER trg_emp_check
AFTER UPDATE ON employees
FOR EACH ROW
DECLARE
v_count NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM employees
WHERE department_id = :NEW.department_id;
END;
/
Now execute:
UPDATE employees
SET salary = salary + 1000
WHERE department_id = 10;
Oracle may raise:
ORA-04091: table EMPLOYEES is mutating,
trigger/function may not see it
Why?
EMPLOYEES is being updated
↓
Row-level trigger fires
↓
Trigger queries EMPLOYEES
↓
EMPLOYEES is mutating
↓
ORA-04091
3. Why does Oracle raise ORA-04091?
Oracle prevents a row-level trigger from seeing an inconsistent intermediate state of the table.
Consider:
UPDATE employees
SET salary = salary * 1.10;
Suppose 1,000 rows are being updated:
Rows 1–99 → already processed
Row 100 → currently processing
Rows 101–1000 → not processed yet
If the trigger queries the table, what should Oracle return?
- Old data?
- New data?
- Partially updated data?
To avoid this ambiguity, Oracle restricts this access and raises:
ORA-04091
4. Does the mutating-table problem occur with every trigger?
No. The classic mutating-table problem is associated with row-level triggers.
For example:
CREATE OR REPLACE TRIGGER trg_emp
AFTER UPDATE ON employees
FOR EACH ROW
BEGIN
...
END;
/
Potential mutating-table problem:
UPDATE employees
↓
FOR EACH ROW
↓
Query employees
↓
❌ ORA-04091
5. What about a statement-level trigger?
A statement-level trigger generally does not have this mutating-table problem.
CREATE OR REPLACE TRIGGER trg_emp
AFTER UPDATE ON employees
DECLARE
v_count NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM employees;
DBMS_OUTPUT.PUT_LINE(
'Employee count = ' || v_count
);
END;
/
There is no FOR EACH ROW. Therefore, Oracle executes it once after the statement completes, when the table is no longer in the row-by-row mutating state.
6. What is the difference between row-level and statement-level behavior?
Suppose:
UPDATE employees
SET salary = salary + 1000;
affects 500 rows.
Row-level trigger:
UPDATE
↓
Row 1 → trigger
Row 2 → trigger
Row 3 → trigger
...
Row 500 → trigger
If each trigger execution queries employees:
❌ ORA-04091
Statement-level trigger:
UPDATE
↓
500 rows processed
↓
Statement trigger
↓
SELECT FROM employees
↓
✅ Generally allowed
7. Can a row-level trigger query another table?
Yes. The problem is generally querying the triggering table, not simply any table.
CREATE OR REPLACE TRIGGER trg_emp
AFTER UPDATE ON employees
FOR EACH ROW
DECLARE
v_dept_name VARCHAR2(100);
BEGIN
SELECT department_name
INTO v_dept_name
FROM departments
WHERE department_id = :NEW.department_id;
END;
/
If departments is not the table currently being modified, this is generally fine.
employees trigger
↓
query employees ❌ Potential mutating-table error
employees trigger
↓
query departments ✅ Generally okay
8. Can a row-level trigger modify another table?
Yes. For example:
CREATE OR REPLACE TRIGGER trg_emp_audit
AFTER UPDATE ON employees
FOR EACH ROW
BEGIN
INSERT INTO employee_audit
(
employee_id,
old_salary,
new_salary
)
VALUES
(
:OLD.employee_id,
:OLD.salary,
:NEW.salary
);
END;
/
Here:
EMPLOYEES
↓
Row trigger
↓
INSERT into EMPLOYEE_AUDIT
This is a normal and common use case.
9. Is modifying another table always safe?
Not necessarily.
You can have other problems if modifying another table causes another trigger to fire and creates a recursive or circular dependency.
employees trigger
↓
UPDATE departments
↓
departments trigger
↓
UPDATE employees
↓
employees trigger
↓
...
This can lead to recursion or other errors.
The classic ORA-04091 mutating-table restriction concerns accessing the table currently being modified from its row-level trigger.
10. Can a row-level trigger query the triggering table using :NEW?
Using :NEW itself is fine.
CREATE OR REPLACE TRIGGER trg_emp
BEFORE UPDATE ON employees
FOR EACH ROW
BEGIN
IF :NEW.salary < 0 THEN
RAISE_APPLICATION_ERROR(
-20001,
'Salary cannot be negative'
);
END IF;
END;
/
This does not query the table. It simply uses the current row's values:
:OLD.salary
:NEW.salary
That's perfectly normal.
11. What is a bad trigger design?
Consider:
CREATE OR REPLACE TRIGGER trg_emp
AFTER INSERT ON employees
FOR EACH ROW
DECLARE
v_count NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM employees;
END;
/
This is the classic bad design.
Then:
INSERT INTO employees
VALUES (101, 10, 50000);
can result in:
ORA-04091
because:
INSERT employees
↓
Row-level trigger
↓
SELECT employees
↓
Mutating table
12. How do I fix a mutating-table error?
There are several approaches. The correct solution depends on why you need to query the table.
Common solutions are:
- Avoid querying the triggering table.
- Use
:OLDand:NEW. - Use a statement-level trigger.
- Use a compound trigger.
- Use a package + statement-level processing.
- Use a constraint instead of a trigger.
- Redesign the business logic.
13. Solution 1: Use :OLD and :NEW
Sometimes you don't actually need to query the table.
Bad:
SELECT salary
FROM employees
WHERE employee_id = :NEW.employee_id;
If you're already processing that row, simply use:
:NEW.salary
For example:
CREATE OR REPLACE TRIGGER trg_salary_check
BEFORE UPDATE ON employees
FOR EACH ROW
BEGIN
IF :NEW.salary > 100000 THEN
DBMS_OUTPUT.PUT_LINE(
'High salary: ' || :NEW.salary
);
END IF;
END;
/
This is much simpler.
14. Solution 2: Use a statement-level trigger
Suppose your requirement is: after an update, count all employees whose salary exceeds 100,000.
Don't query the table from the row trigger. Instead:
CREATE OR REPLACE TRIGGER trg_emp_check
AFTER UPDATE ON employees
DECLARE
v_count NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM employees
WHERE salary > 100000;
DBMS_OUTPUT.PUT_LINE(
'High salary employees = ' || v_count
);
END;
/
Then:
UPDATE employees
SET salary = salary + 500;
The statement completes first, then the statement-level trigger runs.
15. Solution 3: Use a compound trigger
A compound trigger is one of the best solutions when you need both:
- Row-level information
- Statement-level processing
CREATE OR REPLACE TRIGGER trg_emp_compound
FOR UPDATE ON employees
COMPOUND TRIGGER
AFTER EACH ROW IS
BEGIN
DBMS_OUTPUT.PUT_LINE(
'Processing employee ' || :NEW.employee_id
);
END AFTER EACH ROW;
AFTER STATEMENT IS
v_count NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM employees
WHERE salary > 100000;
DBMS_OUTPUT.PUT_LINE(
'High salary employees = ' || v_count
);
END AFTER STATEMENT;
END;
/
The important concept is:
UPDATE employees
↓
AFTER EACH ROW
↓
Collect information
↓
All rows processed
↓
AFTER STATEMENT
↓
Query employees
This avoids the classic mutating-table problem.
16. Why is a compound trigger useful?
Suppose 100 rows are updated.
You need:
For each row:
collect employee_id
After the statement:
query employees
perform validation
A normal row-level trigger cannot safely query the mutating table.
A compound trigger can separate the processing:
COMPOUND TRIGGER
|
+----------------------+----------------------+
| |
AFTER EACH ROW AFTER STATEMENT
| |
Collect data Query table
:NEW / :OLD Table is stable
This is one of the most important Oracle solutions to ORA-04091.
17. Example: Enforcing a department salary rule
Suppose the business rule is:
The total salary of employees in a department must not exceed a certain amount.
A naïve trigger might do this:
CREATE OR REPLACE TRIGGER trg_salary_check
AFTER UPDATE OF salary ON employees
FOR EACH ROW
DECLARE
v_total NUMBER;
BEGIN
SELECT SUM(salary)
INTO v_total
FROM employees
WHERE department_id = :NEW.department_id;
IF v_total > 1000000 THEN
RAISE_APPLICATION_ERROR(
-20001,
'Department salary limit exceeded'
);
END IF;
END;
/
This can cause:
ORA-04091
because:
employees
↓
row-level trigger
↓
SELECT SUM(salary)
FROM employees
↓
❌ Mutating table
18. How would a compound trigger solve that?
Conceptually:
CREATE OR REPLACE TRIGGER trg_salary_check
FOR UPDATE OF salary ON employees
COMPOUND TRIGGER
AFTER STATEMENT IS
v_total NUMBER;
BEGIN
SELECT SUM(salary)
INTO v_total
FROM employees;
IF v_total > 1000000 THEN
RAISE_APPLICATION_ERROR(
-20001,
'Salary limit exceeded'
);
END IF;
END AFTER STATEMENT;
END;
/
The important idea is not the exact business rule, but the timing:
Rows are updated
↓
Statement completes
↓
AFTER STATEMENT
↓
Query employees
↓
Validate final state
19. Can a BEFORE EACH ROW trigger query the table?
Generally, no if it is the same table.
CREATE OR REPLACE TRIGGER trg_emp
BEFORE UPDATE ON employees
FOR EACH ROW
DECLARE
v_count NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM employees;
END;
/
This is still a row-level trigger.
The fact that it is BEFORE rather than AFTER doesn't solve the mutating-table problem.
20. Can an AFTER EACH ROW trigger query the table?
Generally, no.
CREATE OR REPLACE TRIGGER trg_emp
AFTER UPDATE ON employees
FOR EACH ROW
DECLARE
v_count NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM employees;
END;
/
Still:
FOR EACH ROW
↓
same table query
↓
ORA-04091
The problem is row-level timing, not simply BEFORE versus AFTER.
21. Does an AFTER STATEMENT trigger have the mutating-table problem?
Normally, no.
CREATE OR REPLACE TRIGGER trg_emp
AFTER UPDATE ON employees
DECLARE
v_count NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM employees;
END;
/
This executes at statement level. Therefore, the table is no longer in the row-by-row mutating state.
22. Can a DELETE trigger cause ORA-04091?
Yes.
CREATE OR REPLACE TRIGGER trg_emp_delete
AFTER DELETE ON employees
FOR EACH ROW
DECLARE
v_count NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM employees;
END;
/
Then:
DELETE FROM employees
WHERE department_id = 10;
can result in:
ORA-04091: table EMPLOYEES is mutating
The same principle applies to:
INSERT
UPDATE
DELETE
23. Can an INSERT trigger cause ORA-04091?
Yes.
CREATE OR REPLACE TRIGGER trg_emp_insert
AFTER INSERT ON employees
FOR EACH ROW
DECLARE
v_count NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM employees;
END;
/
Then:
INSERT INTO employees
VALUES (101, 10, 50000);
can cause:
ORA-04091
24. Can a trigger call a function that queries the same table?
Yes, and this is a very common hidden cause.
Suppose:
CREATE OR REPLACE FUNCTION get_employee_count
RETURN NUMBER
IS
v_count NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM employees;
RETURN v_count;
END;
/
Now:
CREATE OR REPLACE TRIGGER trg_emp
AFTER UPDATE ON employees
FOR EACH ROW
BEGIN
DBMS_OUTPUT.PUT_LINE(
get_employee_count()
);
END;
/
The trigger itself doesn't contain:
SELECT ... FROM employees
but the function does.
The execution path is:
UPDATE employees
↓
row trigger
↓
get_employee_count()
↓
SELECT employees
↓
ORA-04091
This is a very common interview scenario.
25. What does ORA-06512 mean in this situation?
You may see:
ORA-04091: table EMPLOYEES is mutating,
trigger/function may not see it
ORA-06512: at "SCOTT.TRG_EMP", line 8
The important error is:
ORA-04091
ORA-06512 generally tells you the PL/SQL location where the error propagated.
So when debugging:
ORA-04091
↓
Find trigger/function
↓
Find SELECT/operation against triggering table
26. How do I troubleshoot ORA-04091?
Step 1: Identify the table
Look at:
ORA-04091: table EMPLOYEES is mutating
So:
EMPLOYEES
is the triggering table.
Step 2: Find the trigger
ORA-06512: at "SCOTT.TRG_EMP", line 10
Step 3: Inspect the trigger
SELECT trigger_name,
trigger_type,
triggering_event,
status
FROM user_triggers
WHERE trigger_name = 'TRG_EMP';
Step 4: Look for:
SELECT ...
FROM employees
or a function/procedure that eventually queries employees.
Step 5: Decide whether you actually need that query.
Often the solution is:
:NEW / :OLD
or
AFTER STATEMENT
or
COMPOUND TRIGGER
27. Can we use a package to solve a mutating-table problem?
Yes.
Historically, a common workaround was:
Row-level trigger
↓
Store information in package variables
↓
Statement-level trigger
↓
Query table
However, compound triggers are generally cleaner for this type of requirement.
Instead of managing package state manually:
Package variables
+
Multiple triggers
you can often use:
Compound trigger
with shared state across the timing sections.
28. Can constraints be used instead of a trigger?
Sometimes, and this is often preferable.
If the rule is a simple data integrity rule, consider:
- PRIMARY KEY
- FOREIGN KEY
- UNIQUE
- CHECK
- NOT NULL
For example, instead of:
CREATE OR REPLACE TRIGGER trg_salary
BEFORE INSERT OR UPDATE ON employees
FOR EACH ROW
BEGIN
IF :NEW.salary < 0 THEN
RAISE_APPLICATION_ERROR(
-20001,
'Salary cannot be negative'
);
END IF;
END;
/
You can use:
ALTER TABLE employees
ADD CONSTRAINT chk_salary
CHECK (salary >= 0);
This is simpler and avoids unnecessary trigger logic.
29. Is a mutating-table error the same as a recursive trigger error?
No. They are different concepts.
Mutating table:
Row trigger
↓
Access triggering table
↓
ORA-04091
Recursive/circular trigger behavior:
Trigger A
↓
modifies table B
↓
Trigger B
↓
modifies table A
↓
Trigger A
↓
...
They can sometimes occur together in badly designed systems, but they are not the same error.
30. Is ORA-04091 caused by a deadlock?
No. These are different problems.
Mutating table:
ORA-04091
means the row-level trigger is trying to access a table that is currently being modified.
Deadlock:
ORA-00060
typically involves transactions waiting on each other's locks.
Mutating table ≠ Deadlock
31. What is the most common interview example?
This is worth memorizing:
CREATE OR REPLACE TRIGGER trg_emp
AFTER UPDATE ON employees
FOR EACH ROW
DECLARE
v_count NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM employees;
END;
/
Then:
UPDATE employees
SET salary = salary + 1000;
Result:
ORA-04091: table EMPLOYEES is mutating
Why?
UPDATE employees
↓
FOR EACH ROW trigger
↓
SELECT FROM employees
↓
❌ Mutating table
32. What is the best solution to the classic example?
If you don't need per-row processing, make it a statement-level trigger:
CREATE OR REPLACE TRIGGER trg_emp
AFTER UPDATE ON employees
DECLARE
v_count NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM employees;
DBMS_OUTPUT.PUT_LINE(
'Employee count = ' || v_count
);
END;
/
Now:
UPDATE employees
SET salary = salary + 1000;
runs the trigger once after the statement.
33. What if I need both row-level and statement-level logic?
Use a compound trigger.
CREATE OR REPLACE TRIGGER trg_emp
FOR UPDATE ON employees
COMPOUND TRIGGER
AFTER EACH ROW IS
BEGIN
DBMS_OUTPUT.PUT_LINE(
'Employee changed: ' ||
:NEW.employee_id
);
END AFTER EACH ROW;
AFTER STATEMENT IS
v_count NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM employees;
DBMS_OUTPUT.PUT_LINE(
'Total employees = ' || v_count
);
END AFTER STATEMENT;
END;
/
This gives you:
Per-row information
+
Statement-level table access
without querying the mutating table during
the row-level phase.
34. What is the difference between :NEW/:OLD and querying the table?
Suppose you're updating:
UPDATE employees
SET salary = salary + 1000
WHERE employee_id = 101;
Inside a row trigger, you already have:
:OLD.salary
:NEW.salary
So don't do this unnecessarily:
SELECT salary
FROM employees
WHERE employee_id = :NEW.employee_id;
Instead:
DBMS_OUTPUT.PUT_LINE(
'Old salary = ' || :OLD.salary
);
DBMS_OUTPUT.PUT_LINE(
'New salary = ' || :NEW.salary
);
Think:
Current row data
↓
:OLD / :NEW
Table-wide information
↓
Statement-level processing
35. Mutating Table Cheat Sheet
| Item | Answer |
|---|---|
| ORA-04091 | Table is mutating |
| Main cause | Row-level trigger accesses the triggering table |
| Common operations | INSERT / UPDATE / DELETE |
| :OLD / :NEW | ✅ Available in row trigger |
| SELECT same table | ❌ Can cause ORA-04091 |
| Query other table | ✅ Generally okay |
| Statement trigger | ✅ Generally okay |
| Compound trigger | ✅ Common solution |
| Package workaround | ✅ Possible |
| Constraints | ✅ Prefer when appropriate |
The Golden Rule
Instead:
Need current row?
↓
Use :OLD / :NEW
Need final table state?
↓
Use AFTER STATEMENT
Need both?
↓
Use a COMPOUND TRIGGER
Can the rule be a constraint?
↓
Prefer a CONSTRAINT
36. Final Interview Summary
If an interviewer asks:
"What is a mutating table error?"
A strong answer is:
ORA-04091 occurs when a row-level trigger attempts to read from or otherwise access the table that is currently being modified. Oracle prevents this because the table is in an intermediate state while the statement is processing rows. Common solutions are to use :OLD/:NEW, move the logic to statement-level processing, use a compound trigger, or replace the trigger with an appropriate constraint.
Remember this flow:
UPDATE employees
↓
Row-level trigger
↓
SELECT FROM employees
↓
❌ ORA-04091
Correct approaches:
Current row data
↓
:OLD / :NEW
Final table state
↓
AFTER STATEMENT
Need both
↓
COMPOUND TRIGGER
Simple integrity rule
↓
CONSTRAINT
Remember: Row-level trigger + access to the triggering table = potential ORA-04091.
No comments:
Post a Comment