In our database we have a table, in which records are referenced by id from about 4 other tables. These ‘child’ tables have a foreign key to the ‘master’ table, with ‘on delete set null’. All tables have a mutating-tables system (ie: package with plsql-table, process the records when procedure gets called from after statement trigger).
However, upon deletion of a record in the master-table, the child-record gives the ‘table is mutating’-error. Which i find kind of odd, since the foreign-key seems to trigger some implicit update-statement, which lands in the plsql-table.
All i’m after is trying to find why this is, i can’t seem to dredge up some relevant info!
Sure, we do have a solution to this by simply setting the referenced id field to null in the associated tables, from the after-statement trigger of the master, but i’d still like to know why this happens.
Code to reproduce the error:
CREATE TABLE master_table (ID NUMBER(5) NOT NULL);
CREATE TABLE child_table (ID NUMBER(5) NOT NULL, master_id NUMBER(5));
alter table master_table add constraint master_pk primary key (ID);
alter table child_table add constraint child_pk primary key (ID);
ALTER TABLE child_table
add constraint on_delete_master foreign key (master_id)
references master_table (ID) on delete set null;
CREATE OR REPLACE PACKAGE pkg_child
IS
PROCEDURE init_temp;
PROCEDURE add_temp(i_action IN VARCHAR2,
i_master_old IN child_table.master_id%TYPE,
i_master_new IN child_table.master_id%TYPE);
PROCEDURE process_temp;
END;
/
CREATE OR REPLACE PACKAGE BODY pkg_child IS
TYPE temp_record IS RECORD(
action VARCHAR2(1),
old_master_id child_table.master_id%TYPE,
new_master_id child_table.master_id%TYPE);
TYPE type_temp IS TABLE OF temp_record INDEX BY BINARY_INTEGER;
tab_temp type_temp;
PROCEDURE init_temp IS
BEGIN
tab_temp.delete;
END;
PROCEDURE add_temp(i_action IN VARCHAR2,
i_master_old IN child_table.master_id%TYPE,
i_master_new IN child_table.master_id%TYPE) IS
v_id BINARY_INTEGER;
BEGIN
v_id := nvl(tab_temp.last, 0) + 1;
tab_temp(v_id).action := i_action;
tab_temp(v_id).old_master_id := i_master_old;
tab_temp(v_id).new_master_id := i_master_new;
END;
PROCEDURE process_temp IS
v_id BINARY_INTEGER;
v_total NUMBER;
BEGIN
v_id := tab_temp.first;
WHILE v_id IS NOT NULL LOOP
IF tab_temp(v_id).action = 'U' THEN
SELECT COUNT(1)
INTO v_total
FROM child_table;
END IF;
v_id := tab_temp.next(v_id);
END LOOP;
END;
END;
/
CREATE OR REPLACE TRIGGER child_table_bs
BEFORE
INSERT OR UPDATE OR DELETE
ON child_table
REFERENCING OLD AS OLD NEW AS NEW
BEGIN
pkg_child.init_temp;
END;
/
CREATE OR REPLACE TRIGGER child_table_ar
AFTER
INSERT OR DELETE OR UPDATE
ON child_table
REFERENCING OLD AS OLD NEW AS NEW
FOR EACH ROW
DECLARE
v_action VARCHAR2(1);
BEGIN
IF inserting THEN
v_action := 'I';
ELSIF updating THEN
v_action := 'U';
ELSIF deleting THEN
v_action := 'D';
END IF;
pkg_child.add_temp(v_action, :old.id, :new.id);
END;
/
CREATE OR REPLACE TRIGGER child_table_as
AFTER
INSERT OR UPDATE OR DELETE
ON child_table
REFERENCING OLD AS OLD NEW AS NEW
BEGIN
pkg_child.process_temp;
END;
/
INSERT ALL
INTO master_table (id) VALUES (1)
INTO master_table (id) VALUES (2)
INTO master_table (id) VALUES (3)
INTO master_table (id) VALUES (4)
SELECT * FROM dual;
INSERT ALL
INTO child_table (id, master_id) VALUES (1, NULL)
INTO child_table (id, master_id) VALUES (2, 1)
INTO child_table (id, master_id) VALUES (3, 2)
INTO child_table (id, master_id) VALUES (4, NULL)
SELECT * FROM dual;
-- error on this delete: mutating tables
-- why?
DELETE FROM master_table
WHERE id = 2;
Clean-up Code:
DROP TRIGGER child_table_bs;
DROP TRIGGER child_table_ar;
DROP TRIGGER child_table_as;
DROP PACKAGE pkg_child;
DROP TABLE child_table;
DROP TABLE master_table;
Thanks
You have one statement, the DELETE from the master table which can affect multiple rows.
Because of the CASCADE constraint, each deleted row will fire off an implicit/recursive UPDATE statement of the CHILD table. That is, you can in theory have multiple UPDATEs of the child table.
Say you did a
DELETE FROM master_table WHERE id in (1, 2)That would generate two UPDATE child_table statements. Each of those would try to execute the AFTER UPDATE trigger so you’d get two executions of
The results of any SELECT under the single DELETE statement should be consistent at a particular point in time. But the SELECT isn’t occurring at the end of the DELETE, but is executed multiple times during the delete, each time potentially with a different result. Oracle can work out which result you want/expect, so throws the mutating table error.
Without knowing the business requirements, it is hard to recommend a solution. Like Oracle, we don’t know what you are trying to do. Possibly the situation can be resolved by an ON-COMMIT MV or a DBMS_JOB that will execute at the end of the transaction.