Skip to Main Content
  • Questions
  • When will a after statement trigger mutate

Breadcrumb

Question and Answer

Tom Kyte

Thanks for the question, Lars Stampe.

Asked: May 27, 2008 - 12:32 pm UTC

Last updated: May 29, 2009 - 8:44 am UTC

Version: 10.2.0.4

Viewed 1000+ times

You Asked

Tom,

I am well aware of your reservation towards triggers - but I have a something that puzzles me.

Assume that you have a table where you want to use an after statement trigger to update one or more rows in the same table as the trigger is placed on.

A lot of litterature on the internet suggests that if you collect 'the changed data' and then do your stuff in an after statement trigger then you are home free - see http://www.oracle-base.com/articles/9i/MutatingTableExceptions.php or http://download.oracle.com/docs/cd/B19306_01/appdev.102/b14251/adfns_triggers.htm#sthref1270

<quote from last>
Trigger Restrictions on Mutating Tables

A mutating table is a table that is being modified by an UPDATE, DELETE, or INSERT statement, or a table that might be updated by the effects of a DELETE CASCADE constraint.

The session that issued the triggering statement cannot query or modify a mutating table. This restriction prevents a trigger from seeing an inconsistent set of data.

This restriction applies to all triggers that use the FOR EACH ROW clause. Views being modified in INSTEAD OF triggers are not considered mutating.
</quote>

The things that puzzles me is that the statement "This restriction applies to all triggers that use the FOR EACH ROW clause.". In the following (Cut down as much as possible) is the table clearly mutating although everything is executed in the AFTER STATEMENT TRIGGER:

CREATE TABLE T
(
  PK       NUMBER                               NOT NULL,
  CONT     VARCHAR2(20 CHAR)                    NOT NULL,
  UPDDATE  DATE
)
/
CREATE OR REPLACE PACKAGE MCSDAT.t_pkg
IS
   PROCEDURE init;

   PROCEDURE COLLECT (v_rowid ROWID);

   PROCEDURE COMPLETE;
END;
/
CREATE OR REPLACE PACKAGE BODY MCSDAT.t_pkg
IS
   TYPE t_collect IS TABLE OF ROWID
      INDEX BY BINARY_INTEGER;

   v_collect      t_collect;

   PROCEDURE dbg (v_caption IN VARCHAR2)
   IS
   BEGIN
      DBMS_OUTPUT.put_line (v_caption || ' @ ' || TO_CHAR (SYSDATE, 'YYYYMMDDHH24MISS'));
   END;

   PROCEDURE init
   IS
   BEGIN
      dbg ('Init Called');
      v_collect.DELETE;
   END;

   PROCEDURE COLLECT (v_rowid ROWID)
   IS
      l_next   NUMBER;
   BEGIN
      dbg ('Collect Called');
      l_next := v_collect.COUNT + 1;
      v_collect (l_next) := v_rowid;
   END;

   PROCEDURE COMPLETE
   IS
      l_now   DATE := SYSDATE;
   BEGIN
      dbg ('Complete Called');

      IF NOT v_inprogress
      THEN
         FORALL i IN 1 .. v_collect.LAST
            UPDATE t
               SET upddate = l_now
             WHERE ROWID = v_collect (i);
      END IF;
   END;
END;
/
CREATE OR REPLACE TRIGGER t_bs before update or insert on t
begin
   t_pkg.Init ;
end ;
/
CREATE OR REPLACE TRIGGER t_ar
   AFTER UPDATE OR INSERT
   ON t
   FOR EACH ROW
BEGIN
   t_pkg.COLLECT (:NEW.ROWID);
END;
/
CREATE OR REPLACE TRIGGER t_as
   after UPDATE OR INSERT
   ON t
BEGIN
   t_pkg.complete;
END;
/

in sqlplus:

set serveroutput on
insert into t(pk,cont) values (1,'Cont 1') ;

The result is (not surprising?):
SQL> insert into t(pk,cont) values (1,'Cont 1') ;
insert into t(pk,cont) values (1,'Cont 1')
*
ERROR at line 1:
ORA-00036: maximum number of recursive SQL levels (50) exceeded
ORA-06512: at "MCSDAT.T_PKG", line 48
....
ORA-06512: at "MCSDAT.T_AS", line 2
ORA-04088: error during execution of trigger 'MCSDAT.T_AS'
ORA-06512: at "MCSDAT.T_PKG"

This is mutating even though the 'work' is in a AFTER STATEMENT - so where did I go wrong?

Thanks,

Lars

and Tom said...

this is not mutating.

You have recursion happening.


think about it:

...
   PROCEDURE COMPLETE
   IS
      l_now   DATE := SYSDATE;
   BEGIN
      dbg ('Complete Called');

      IF NOT v_inprogress
      THEN
         FORALL i IN 1 .. v_collect.LAST
            UPDATE t
               SET upddate = l_now
             WHERE ROWID = v_collect (i);
      END IF;
   END;
.....



you update T in that function, you call that function from a trigger. When you call that function and it updates T, the triggers on T will fire again. And again. And again.


This is not the mutating table constraint here - it is a natural side effect of your logic.


Looking at your logic, you just want:


CREATE OR REPLACE TRIGGER t_ar
before UPDATE OR INSERT
ON t
FOR EACH ROW
BEGIN
:new.upddate := sysdate;

END;
/


YOU DO NOT WANT TO RE-UPDATE THAT TABLE, you just want to modify the :new record.


So, delete the other triggers, remove the package and just do what you need right there.

Rating

  (5 ratings)

Is this answer out of date? If it is, please let us know via a Comment

Comments

This would never be real life code!

Lars Stampe Villadsen, May 28, 2008 - 4:23 pm UTC

Thanks Tom,

I would never do this in any production system - promise - but I was puzzled by the statement that mutating table not happens in after statement triggers. Therefore I tried to cut it down to measurable size in my example - but your statement about recursion instead of 'mutation' makes sense!

Sincerely,

Lars

recursive triggers

A reader, May 30, 2008 - 2:53 am UTC

Actually I've experienced something similar on customer's site. I thought as well it was matter of mutating tables but than I realised it was recursive trigger. The trigger is updating the same table and fires a trigger one more time although the update is made on a column that is not in the column list. For example:
update T
set TODATE = (
select nvl(min(FROMDATE)-step, theend)
from T
where FROMDATE>td_date(i) and
PORIK=td_pk_PORIK(i)
)
where
PORIK=td_pk_PORIK(i)
and FROMDATE =
(
select max(FROMDATE)
from T
where FROMDATE < td_date(i) and
PORIK=td_pk_PORIK(i)
);
Fires trigger

CREATE OR REPLACE TRIGGER T_TSB
BEFORE
delete or
insert or
update of PORIK,FROMDATE
ON T
BEGIN
T_PKG.BeforeStatement
END;

The state of the objects involved is valid. I debugged it by simply putting dbms_output in strategic places and compared with healthy database and I am sure that trigger fired after that statement. How is that possible?

Tom Kyte
May 30, 2008 - 7:10 am UTC

I have no clue what you are trying to say here or ask.

... and compared with healthy database ...
what is a healthy database, or more importantly, what is a sick one?

... How is that possible? ...
how is WHAT possible??!?!?!?

if you have a question - we'll want to see an entire test case from soup to nuts - staring with create table, insert into, create trigger, followed by "see, look at this" (the actual cut and paste from sqlplus clearly demonstrated the issue you clearly document...)

and make it as small, tiny, concise - yet 100% complete (eg: i doubt the package is relevant, I'm sure the update can be small and table T needs maybe 3 columns)

i hate triggers.

Avoiding Recursive Triggers

David Piazza, May 27, 2009 - 9:19 pm UTC

Hi Tom,

The customer is modifying their application, and wanted to rename a column in a table and use that with the new code. But the old code is still using the old column. So, they want to add a new column to the table that the new code will use. They want to keep both columns in sync, i.e., when the old column is updated then the new column will be updated and vice versa, until the old code is phased out, and then the old column will be removed. I thought of using triggers to keep the columns in sync, but this will lead to the dreaded recursive trigger. Do you have any ideas on how to keep the columns in sync?

Tom Kyte
May 28, 2009 - 7:17 am UTC

it would not lead to the 'dreaded recursive trigger' (whatever that is)

however - silly question - why not use a view?

new code uses a view that makes the column appear to have any name they like.
old code uses base table that has old name

both co-exist happily forever.

Using a View instead of a trigger

David Piazza, May 28, 2009 - 4:07 pm UTC

Thanks Tom. Yeah, I should have thought of using a view :^(, and probably should use views more than I do. My first fleeting thought about this was to put an update trigger on each of the columns to update the other column that wasn't being updated, but then realized this would create a loop of triggers firing which I thought was the dreaded recursive trigger. After 8 years of posting questions, you're still the best...
Tom Kyte
May 28, 2009 - 5:20 pm UTC


Help on mutating table

martina, May 29, 2009 - 8:20 am UTC

Hello Tom,

why in all heavens name does the following example work ????

INFO/INFO_PROD> create table trg (trg number);

Table created.

INFO/INFO_PROD> create or replace
2 trigger trg1 before insert or update or delete on trg for each row
3 declare
4 cursor c_trg is select trg,count(*) anz
5 from trg group by trg;
6 begin
7 for r_trg in c_trg loop
8 dbms_output.put_line (r_trg.trg||' '||r_trg.anz);
9 end loop;
10 end;
11 .
INFO/INFO_PROD> /

Trigger created.

INFO/INFO_PROD> insert into trg values (1);

1 row created.

INFO/INFO_PROD> insert into trg values (1);
1 1

1 row created.

INFO/INFO_PROD> insert into trg values (2);
1 2

1 row created.

INFO/INFO_PROD> insert into trg values (2);
1 2
2 1

1 row created.

INFO/INFO_PROD> insert into trg values (3);
1 2
2 2

1 row created.

and could You or someone give me some (mental) assistance how to tell students that one just does not do that ? i almost freaked out yesterday, the assignment was to have some statistics (in a table of course) and maintain that via a trigger ... i might argue that the result is wrong next week ...

thanks and best regards
martina
Tom Kyte
May 29, 2009 - 8:44 am UTC

insert with values is KNOWN to insert exactly 0 or 1 rows - hence the mutating table constraint doesn't apply.

ops$tkyte%ORA9IR2> create or replace
  2  trigger trg1 before insert or update or delete on trg for each row
  3  declare
  4  cursor c_trg is select trg,count(*) anz
  5         from trg group by trg;
  6  begin
  7  for r_trg in c_trg loop
  8      dbms_output.put_line (r_trg.trg||' '||r_trg.anz);
  9  end loop;
 10  end;
 11  /

Trigger created.

ops$tkyte%ORA9IR2>
ops$tkyte%ORA9IR2> insert into trg values ( 1 );

1 row created.

ops$tkyte%ORA9IR2> insert into trg select 1 from dual;
insert into trg select 1 from dual
            *
ERROR at line 1:
ORA-04091: table OPS$TKYTE.TRG is mutating, trigger/function may not see it
ORA-06512: at "OPS$TKYTE.TRG1", line 2
ORA-06512: at "OPS$TKYTE.TRG1", line 5
ORA-04088: error during execution of trigger 'OPS$TKYTE.TRG1'




however, that said, it would NOT be the mutating table constraint at all that would make:

"the assignment
was to have some statistics (in a table of course) and maintain that via a
trigger ..."

really hard - it would be a MULTI-USER environment that would.

I cannot see your work until you commit, you cannot see my work until I commit. If we run at the same time and create some summaries - we'll create summaries that do not include each others work - hence they will be wrong.

It isn't the mutating table constraint that is the issue in getting the wrong answer, it is "thinking serially, one at a time, in a linear procedural fashion" that'll kill th em. They have to think about what happens when 1,000 people do modifications at the same time.


More to Explore

PL/SQL demos

Check out more PL/SQL tutorials on our LiveSQL tool.

PL/SQL docs

PL/SQL reference manual from the Oracle documentation library