Skip to Main Content
  • Questions
  • mutating trigger error while generating sequence numbers

Breadcrumb

Question and Answer

Tom Kyte

Thanks for the question, Wally.

Asked: October 19, 2009 - 5:00 pm UTC

Last updated: October 21, 2009 - 12:20 pm UTC

Version: 10.2

Viewed 1000+ times

You Asked

Tom,

I have a mutating trigger error, for which I know the cause, but I am looking for a fix.

--------------------------------------------------------------------------------
My setup:

select * from v$version;

BANNER
----------------------------------------------------------------
Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 - Prod
PL/SQL Release 10.2.0.1.0 - Production
CORE 10.2.0.1.0 Production
TNS for 32-bit Windows: Version 10.2.0.1.0 - Production
NLSRTL Version 10.2.0.1.0 - Production
--------------------------------------------------------------------------------

I have tried to explain the situation as best as I could, please let me know if you need additional info.

I have a table where each set of data (combination of columns A and B) has a counter/sequence (column seq) associated with it, which starts at 1.

In step #1 the tables and data needed for the example are setup.
In step #2 the trigger that calcuates and resets the sequence is created.
In step #3 sample inserts are run to show that the trigger works in one scenario (single record inserts).
In step #4 has the insert statement (insert from select) that is raising the mutating trigger error.
In Step #5 the trigger is recreated as an autonomous transaction which allows the insert from select, although the sequences generated using this trigger are wrong.

Any and all help would be appreciated.

--------------------------------------------------------------------------------
-- 1. Initial setup.
CREATE TABLE TEST1
(
A VARCHAR2(1 BYTE),
B NUMBER,
SEQ NUMBER
);

Insert into TEST1(A, B, SEQ) Values ('A', 1, 1);
Insert into TEST1(A, B, SEQ) Values ('A', 1, 2);
Insert into TEST1(A, B, SEQ) Values ('A', 2, 1);
Insert into TEST1(A, B, SEQ) Values ('B', 1, 1);
Insert into TEST1(A, B, SEQ) Values ('B', 1, 2);
Insert into TEST1(A, B, SEQ) Values ('B', 1, 3);
Insert into TEST1(A, B, SEQ) Values ('C', 4, 1);
Insert into TEST1(A, B, SEQ) Values ('C', 4, 2);
Insert into TEST1(A, B, SEQ) Values ('C', 4, 3);
Insert into TEST1(A, B, SEQ) Values ('C', 5, 1);
Insert into TEST1(A, B, SEQ) Values ('C', 1, 1);
Insert into TEST1(A, B, SEQ) Values ('D', 2, 1);
Insert into TEST1(A, B, SEQ) Values ('F', 3, 1);
Insert into TEST1(A, B, SEQ) Values ('F', 3, 2);
Insert into TEST1(A, B, SEQ) Values ('F', 4, 1);
COMMIT;

CREATE TABLE TEST2
(
A VARCHAR2(1 BYTE),
B NUMBER
);

Insert into TEST2 (A, B) Values ('A', 1);
Insert into TEST2 (A, B) Values ('A', 1);
Insert into TEST2 (A, B) Values ('B', 2);
Insert into TEST2 (A, B) Values ('B', 2);
Insert into TEST2 (A, B) Values ('D', 5);
Insert into TEST2 (A, B) Values ('D', 5);
COMMIT;

select * from test1 order by a,b,seq;
select * from test2 order by a,b;

--------------------------------------------------------------------------------
-- 2. trigger to calculate and reset sequence

CREATE OR REPLACE TRIGGER test1_trig
BEFORE INSERT
ON TEST1 FOR EACH ROW
DECLARE
v_recno INTEGER;
v_count INTEGER;
--PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
IF :NEW.seq IS NULL OR :NEW.seq = 0
THEN
SELECT COUNT (1)
INTO v_count
FROM test1
WHERE a = :NEW.a AND b = :NEW.b;

IF v_count = 0
THEN
v_recno := 1;
ELSE
SELECT MAX (seq) + 1
INTO v_recno
FROM test1
WHERE a = :NEW.a AND b = :NEW.b;
END IF;

:NEW.seq := v_recno;
END IF;
END;
/
--------------------------------------------------------------------------------
-- 3. Sample insert where the trigger works.

SET DEFINE OFF;
Insert into TEST1(A, B) Values ('A', 2);
Insert into TEST1(A, B) Values ('A', 2);
Insert into TEST1(A, B) Values ('A', 2);
Insert into TEST1(A, B) Values ('D', 5);
Insert into TEST1(A, B) Values ('D', 5);
COMMIT;

select * from test1 order by a,b,seq;
--------------------------------------------------------------------------------
-- 4. Insert statement that raises the mutating trigger error.

insert into test1 (a,b) select a,b from test2;

Error at line 1
ORA-04091: table TEST1 is mutating, trigger/function may not see it
ORA-06512: at "TEST1_TRIG", line 8
ORA-04088: error during execution of trigger 'TEST1_TRIG'
--------------------------------------------------------------------------------
--5. Changing the trigger to an autonomous transaction
CREATE OR REPLACE TRIGGER test1_trig
BEFORE INSERT
ON TEST1 FOR EACH ROW
DECLARE
v_recno INTEGER;
v_count INTEGER;
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
IF :NEW.seq IS NULL OR :NEW.seq = 0
THEN
SELECT COUNT (1)
INTO v_count
FROM test1
WHERE a = :NEW.a AND b = :NEW.b;

IF v_count = 0
THEN
v_recno := 1;
ELSE
SELECT MAX (seq) + 1
INTO v_recno
FROM test1
WHERE a = :NEW.a AND b = :NEW.b;
END IF;

:NEW.seq := v_recno;
END IF;
END;
/

-- running the same insert from select statement
insert into test1 (a,b) select a,b from test2;
commit;

-- selecting the data shows that the sequence is repeated
select * from test1 order by a,b,seq;
--------------------------------------------------------------------------------

Thank you again for your help.

Wally

and Tom said...

why would you maintain this derived value?

You do know that your code is 100% "single user". If you even *attempted* to use this with more than one user - it simply falls apart. It does not work. It is not even close to being usable.

It is the reason I wrote:


The Trouble with Triggers
https://www.oracle.com/technetwork/issue-archive/2008/08-sep/o58asktom-101055.html


Look at your logic:


BEGIN
  IF :NEW.seq IS NULL OR :NEW.seq = 0
  THEN
    SELECT COUNT (1)
    INTO v_count
    FROM test1
    WHERE a = :NEW.a AND b = :NEW.b;

    IF v_count = 0
    THEN
      v_recno := 1;
    ELSE
      SELECT MAX (seq) + 1
      INTO v_recno
      FROM test1
      WHERE a = :NEW.a AND b = :NEW.b;
    END IF;

    :NEW.seq := v_recno;
  END IF;
END;
/ 




what happens when two people insert into that table at the same time?

Say the table is empty:

I cannot see your insert
You cannot see my insert

Hence - v_count would be 0.
Hence - we both get 1



Say the table has data already and there exists a record such that the A and B we are inserting already exists:

I cannot see your insert
You cannot see my insert

hence - v_count would be > 0
Hence - you and I select max(seq) + 1 - but that means we select the SAME data.




Your logic is (as many, I would say even MOST triggers) horrible flawed and "thank goodness it failed and you asked a question" - because this is just "a horrible idea"


Also, what happens when you delete a record? where is that (equally flawed) logic?

Since you want the numbers autogenerated - just autogenerate them at retrieval time. Here is what you will do:



ops$tkyte%ORA10GR2> create or replace view v
  2  as
  3  select a, b, seq, row_number() over (partition by a,b order by seq) rn
  4    from t
  5  /

View created.

ops$tkyte%ORA10GR2>
ops$tkyte%ORA10GR2> select * from v;

A          B        SEQ         RN
- ---------- ---------- ----------
A          1      10321          1
A          1      10744          2
A          2      11167          1
B          1      11590          1
B          1      12013          2
B          1      12436          3
C          1      14551          1
C          4      12859          1
C          4      13282          2
C          4      13705          3
C          5      14128          1
D          2      14974          1
F          3      15397          1
F          3      15820          2
F          4      16243          1

15 rows selected.

ops$tkyte%ORA10GR2>
ops$tkyte%ORA10GR2> insert into t values ( 'A', 1, s.nextval );

1 row created.

ops$tkyte%ORA10GR2>
ops$tkyte%ORA10GR2> set autotrace on explain
ops$tkyte%ORA10GR2> select * from v where a = 'A' and b = 1;

A          B        SEQ         RN
- ---------- ---------- ----------
A          1      10321          1
A          1      10744          2
A          1      16666          3


Execution Plan
----------------------------------------------------------
Plan hash value: 2858554846

---------------------------------------------------------------------------------------
| Id  | Operation                     | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |       |    10 |   410 |    10  (10)| 00:00:01 |
|   1 |  VIEW                         | V     |    10 |   410 |    10  (10)| 00:00:01 |
|   2 |   WINDOW SORT                 |       |    10 |   430 |    10  (10)| 00:00:01 |
|   3 |    TABLE ACCESS BY INDEX ROWID| T     |    10 |   430 |     9   (0)| 00:00:01 |
|*  4 |     INDEX RANGE SCAN          | T_IDX |  1000 |       |     1   (0)| 00:00:01 |
---------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   4 - access("A"='A' AND "B"=1)

ops$tkyte%ORA10GR2> set autotrace off
ops$tkyte%ORA10GR2> delete from t where seq = (select min(seq) from t where a = 'A' and b = 1 );

1 row deleted.

ops$tkyte%ORA10GR2> select * from v where a = 'A' and b = 1;

A          B        SEQ         RN
- ---------- ---------- ----------
A          1      10744          1
A          1      16666          2





Otherwise, what you HAVE to do is......

LOCK THE TABLE


Yes, you have to LOCK THE TABLE - for you need to prevent me from touching in any way shape or form the records that have an A,B value you are touching at the same time.


You either need

a) a table with all distinct A,B values and you need to select for update the row with the A,B value you want to modify/play with before touching it

OR

b) if you don't have such a table or cannot select for update from it - you must LOCK the table in order to do this correctly (eg: with a semblance of data integrity) in a multi user environment.



I won't show you the trigger implementation - because I know (not think, know) it is the wrong approach.

These sequential sequence numbers - they are never really needed (eg: change the requirement)

If you believe (incorrectly :) ) that you do need them - synthesize them during retrieval for display purposes - safe in the knowledge that the true SEQ column exists and is there for your ordering purposes. It will have gaps, it will not be sequential - but that, quite frankly, is besides the point.



Rating

  (2 ratings)

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

Comments

Wally, October 21, 2009 - 12:46 pm UTC

Thanks for your response Tom and the great article.

I had a feeling that there was no correct answer to my problem and that the (flawed) requirements needed to be changed. Now I have some proof to go with my argument.

I will go with your suggestion of using a sequence number and then auto generating the row number at retrieval time.

Oleksandr Alesinskyy, October 22, 2009 - 7:33 am UTC

And this very topic was discussed here endless times, you may want to search on gaps, sequences, ... .

And if your sequential number must be persisted (silly, but sometimes required) generate them not on insert, but by the batch updating job nightly.

And one more suggestion - do not try to put a sequence in the NOCACHE mode to avoid or minimize gaps - gaps will be there, but performance will be minimized.

More to Explore

Analytics

Analytic SQL got you confused? Check out Connor McDonald's complete video course.