Skip to Main Content
  • Questions
  • Before Delete Trigger to insert into history table , the record being deleted.

Breadcrumb

Question and Answer

Tom Kyte

Thanks for the question, monica.

Asked: August 24, 2000 - 6:37 pm UTC

Last updated: December 28, 2005 - 5:41 pm UTC

Version: 8.1.6.0.0

Viewed 10K+ times! This question is

You Asked

I have a table called T1.
Now I want to insert a record into the table T1_History whenever a record is updated or deleted in T1.
How to achieve this? I want to write a Before Update or Delete trigger on T1 so that before updating or deleting the record in T1 I can move it into the history table. but, the table gives a mutating error which is right since I'm trying to query the same table. But, then how to achieve this functionality.
Pl. help, this is very urgent.

Thanx
And with warm regards
monica

and Tom said...


You do not have to query the table to get the data. You want to use an AFTER, FOR EACH ROW trigger (it'll be faster and just as correct)...

Just:

create or replace trigger audit_that_table
after update or delete on that_table
for each row
begin
insert into history_table ( c1, c2, .., cn )
values
( :old.c1, :old.c2, ..., :old.cn );
end;
/

That'll do it.

Rating

  (9 ratings)

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

Comments

how to do for all table

Ak, January 18, 2002 - 3:10 am UTC

Hi tom,
1) My problem is ,i have 500 tables in a schema,i need to
capture old and new value for all this table(when ever any field value for any table is getting changed),and i don't
want to write a trigger for all this 500 tables seprately.Is there any way for this other then trigger ??
How about performance in this case ??

2)Is there any way with which I can capture all the update
statement for all the session.

Thanks,
Regards
Ak


Tom Kyte
January 18, 2002 - 4:42 pm UTC

You need to write a trigger / table -- that is quite simply the way it works. Triggers are put onto tables, period.

In Oracle9i, you can get the triggering statement using a builtin function. You cannot in Oracle8i and before.

Follow-up to Follow-up for AK

Steven R. Gerber, January 18, 2002 - 10:55 pm UTC

1. How about SQL to gen. SQL? Something like
SELECT
'CREATE TRIGGER ON ' || t.Table_Name
...
FROM
All_Tab_Cols t
??? I know that the real query would be much better ...

Capturing Deletes

Chris Soza, April 18, 2002 - 9:18 am UTC

Tom,

How can this be used to capture deletes on a certain condition. for eg: old.c1 like abc% then insert record to history table on delete from main table ?

Tom Kyte
April 18, 2002 - 9:55 am UTC



begin
if ( :old.c1 like 'abc%' )
then
insert into history_table ( c1, c2, .., cn )
values
( :old.c1, :old.c2, ..., :old.cn );
end if;
end;
/





Oracle DBA

A reader, May 20, 2003 - 2:43 pm UTC

Good day Tom,

Today, I faced a problem, which I haven't seen or read anything about it before! Here is the issue:

I have the following table:
D820> desc s1_vehicle_processing
Name Null? Type
----------------------------------------------------- -------- ------------------------------------
VEHICLE_DISPOSITION_NBR NOT NULL NUMBER
CONTRACT_NBR NOT NULL NUMBER
RELEASE_NBR NOT NULL NUMBER
QUANTITY_ENTRY_NBR NOT NULL NUMBER
SCALE_QUANTITY NUMBER
REPORT_QUANTITY NUMBER
OFFICIAL_QUANTITY NUMBER
VENDOR_ID NOT NULL VARCHAR2(10)
APPLIED_SCALE_QUANTITY NUMBER
APPLIED_REPORT_QUANTITY NUMBER
SCALE_HOLD_QUANTITY NUMBER
REPORT_HOLD_QUANTITY NUMBER
APPLIED_TYPE NOT NULL CHAR(1)
QUANTITY_ADDED_TO_CONTRACT NUMBER
GRADE_GOVERN_STATUS NOT NULL NUMBER(5)
SETTLEMENT_STATUS NOT NULL CHAR(1)
SCALE_TICKET_INBOUND_OUTBOUND NOT NULL CHAR(1)
INVOICED_STORAGE_FLAG NOT NULL CHAR(1)
DRYING_FLAG NOT NULL CHAR(1)
DRYING_AMOUNT NOT NULL NUMBER(20,5)
ADVANCE_STATUS NOT NULL CHAR(1)
FREIGHT_FLAG NOT NULL CHAR(1)
FREIGHT_AMOUNT NOT NULL NUMBER(20,5)
ADD_DATE NOT NULL DATE
CHANGE_DATE NOT NULL DATE
TERM_CODE VARCHAR2(10)
DUE_DATE DATE
ADVANCE_COUNTER NOT NULL NUMBER
MARKET_ZONE VARCHAR2(20)
MAX_RECEIPT_NBR NOT NULL NUMBER(5)
INVENTORY_DOLLAR_VALUE NOT NULL NUMBER(18,6)
INVENTORY_CONTRACT_EXCH_RATE NOT NULL NUMBER(20,10)

On this table I have a BEFORE UPDATE trigger. When I issue the following command the first one works and the second one failed. Oracle Error #ora-04020

ALTER TABLE s1_vehicle_processing
ADD inventory_dollar_value number(18,6) DEFAULT 0 not null
/

ALTER TABLE s1_vehicle_processing
ADD inventory_contract_exch_rate number(20,10) DEFAULT 1 not null
/

I noticed Update trigger became Invalid. and after I recompiled the trigger, the second statement will work.

I know that because of structure changes, the trigger became invalid, but I suppose that Oracle recompile it automatically.

Could please tell me what's going on?

Thanks for your time,



Tom Kyte
May 20, 2003 - 8:48 pm UTC

no versions, no nothing.  could be a "product issue" in some older release.  I could not reproduce on 9iR2 in any case:

ops$tkyte@ORA920> create table t ( x int );

Table created.

ops$tkyte@ORA920>
ops$tkyte@ORA920> create trigger t_trigger before update on t
  2  begin
  3          if ( 1=0 )
  4          then
  5             raise_application_error( -20000, 'x' );
  6          end if;
  7  end;
  8  /

Trigger created.

ops$tkyte@ORA920>
ops$tkyte@ORA920> alter table t add y number(18,6) default 0 not null
  2  /

Table altered.

ops$tkyte@ORA920>
ops$tkyte@ORA920> @invalid
ops$tkyte@ORA920> break on object_type skip 1
ops$tkyte@ORA920> column status format a10
ops$tkyte@ORA920> select object_type, object_name, status
  2  from user_objects
  3  where status = 'INVALID'
  4  order by object_type, object_name
  5  /

OBJECT_TYPE        OBJECT_NAME                    STATUS
------------------ ------------------------------ ----------
TRIGGER            T_TRIGGER                      INVALID


ops$tkyte@ORA920>
ops$tkyte@ORA920> alter table t add z number(20,10) default 1 not null
  2  /

Table altered.


<b>I can say, it would be more efficient, specially with the defaults, to do that in a single statement:</b>

ops$tkyte@ORA920> alter table t add ( y number(18,6) default 0 not null, z number(20,10) default 1 not null )
  2  /

Table altered.


why make two passes after all... that'll update both columns in a single update instead of two full scans of the table...

 

A reader, May 21, 2003 - 12:32 am UTC

hi tom,

do you think of fine grained auditing when you say that one can get the triggering statement in 9i?

Tom Kyte
May 21, 2003 - 7:58 am UTC

no, there is a function that will give you the sql text of the triggering statement in 9i.

</code> http://docs.oracle.com/docs/cd/B10501_01/appdev.920/a96590/adg14evt.htm#1005274 <code>

A reader, May 21, 2003 - 9:01 am UTC

Hi Tom,

Thanks millions of tons. That's right. It's a version problem. Sorry I forgot to mention that I am using 8.0.5.

Best Regards,
Arash


AFTER vs. BEFORE

A reader, September 12, 2003 - 2:15 pm UTC

Tom,

In your example you used AFTER UPDATE OR DELETE, could I use BEFORE UPDATE OR DELETE?

create or replace trigger audit_that_table
after update or delete on that_table
for each row
begin
insert into history_table ( c1, c2, .., cn )
values
( :old.c1, :old.c2, ..., :old.cn );
end;
/


Tom Kyte
September 12, 2003 - 2:46 pm UTC

only if you want to consume more resources -- and perhaps audit the wrong values....


it is more costly to use a before trigger.

the before trigger might not see the actual "new" values which could be problematic -- maybe not in your case -- but in general.

Capture old values from 2 tables

Abi, December 28, 2005 - 12:17 pm UTC

Tom,

Database version - 9.2.0.6

I need to capture the old values from two base tables cust and trans_tab

create or replace trigger audit_cust_table
after update or delete on cust
for each row
begin
insert into cust_history ( cust_name, cust_address,trans_id )
values
( :old.cust_name, :old.cust_address, (select max(trans_id) from trans_tab );
end;
/


can I do something like that ?.

Thanks for your help

Tom Kyte
December 28, 2005 - 5:41 pm UTC

what use is the select max(trans_id)???

Yes, you "can", but to me the real question is "should you be"

Trigger Generator

marc weinstock, December 29, 2005 - 10:56 am UTC

Here is a code that will create trigger for all tables that have a table of the same name with an "%_a" table of the same structure. "%_a" is an audit table. (_audit post_fix was to long.)Give it a try.

How to:
GRANT CREATE TRIGGER TO $USER;

create table test(
col1 number,
col2 number);

create table test_a(
col1 number,
col2 number.
test_audit_msg varchar2(4000));
--test_audit_msg is optional, this tell in english what happened for the last record.
create table clob_ddl ( id number primary key, data_value clob );
exec p_trigger_generator.main;
select * from triggers; U will see a trigger for all tables with a audit table. These triggers start with trg_%


----------------------

CREATE OR REPLACE PACKAGE p_trigger_generator AS
PROCEDURE main;

PROCEDURE make_trigger(
table_name VARCHAR2);
END;
/

CREATE OR REPLACE PACKAGE BODY p_trigger_generator AS
--create table clob_ddl ( id number primary key, data_value clob )
l_cnt NUMBER DEFAULT 1;
l_clob CLOB;
l_dummy DUAL.dummy%TYPE;
l_temp LONG;
--REMEMBER, ~ on both sides of the table
delete_table_allowed VARCHAR2(4000) := '~TABLE1~TABLE2~';
--REMEMBER, ~ on both sides of the table
delete_table_handled VARCHAR2(4000) := '~~';

---------------------------------------------------------------
PROCEDURE data_append(
p_string IN VARCHAR2) AS
BEGIN
DBMS_LOB.writeappend(l_clob, LENGTH(p_string), p_string);
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('data_append :' || SQLERRM);
DBMS_OUTPUT.PUT_LINE('data_append : length of p_string is : ' || TO_CHAR(LENGTH(p_string)));
-- DBMS_OUTPUT.put_line (SUBSTR (p_string, 1, 254));
-- DBMS_OUTPUT.put_line (SUBSTR (p_string, 255, 254));
-- DBMS_OUTPUT.put_line (SUBSTR (p_string, 510, 254));
RAISE;
END;

---------------------------------------------------------------
FUNCTION trigger_maker_check_nn(
ptable_name VARCHAR2)
RETURN VARCHAR2 IS
CURSOR cur_col(
ptable_name VARCHAR2) IS
SELECT *
FROM (SELECT ROWNUM row_num,
t.*,
TRUNC(ROWNUM / 5) md
FROM (SELECT LOWER(column_name) column_name,
nullable
FROM USER_TAB_COLUMNS
WHERE table_name = ptable_name
AND nullable = 'N'
AND column_name NOT LIKE '%ENTRY_IDV_CODE'
AND column_name NOT LIKE '%ENTRY_TS'
ORDER BY nullable) t);

col_list VARCHAR2(4096);
BEGIN
FOR cur IN cur_col(ptable_name) LOOP
col_list := '';
col_list :=
col_list || 'IF :old.' || cur.column_name || '<> :new.' || cur.column_name || ' THEN' || CHR(10);
col_list := col_list || 'audit_msg:=audit_msg||''' || cur.column_name || ','';' || CHR(10);
col_list := col_list || 'END IF;' || CHR(10);
data_append(NVL(col_list, ' '));
END LOOP;

-- DBMS_OUTPUT.put_line ('LENGTH (TRIM (col_list))= ' || LENGTH (TRIM (col_list)));
IF col_list IS NULL THEN
RETURN ' ';
ELSE
RETURN col_list;
-- RETURN 'IF ' || col_list || ' THEN' || CHR (10) || ' GOTO INSERT_LABEL;' || CHR (10) || 'END IF;' || CHR (10);
END IF;
EXCEPTION
WHEN OTHERS THEN
-- RETURN ptable_name || ' columns to long';
RAISE;
END;

---------------------------------------------------------------
FUNCTION trigger_maker_check_null(
ptable_name VARCHAR2)
RETURN VARCHAR2 IS
CURSOR cur_col(
ptable_name VARCHAR2) IS
SELECT *
FROM (SELECT ROWNUM row_num,
t.*,
TRUNC(ROWNUM / 5) md
FROM (SELECT LOWER(column_name) column_name,
nullable,
data_type
FROM USER_TAB_COLUMNS
WHERE table_name = ptable_name
AND nullable = 'Y'
AND column_name NOT LIKE '%ENTRY_IDV_CODE'
AND column_name NOT LIKE '%ENTRY_TS'
ORDER BY nullable) t);

col_list VARCHAR2(4096);
BEGIN
FOR cur IN cur_col(ptable_name) LOOP
col_list := '';
col_list :=
col_list
|| 'IF NOT p_util.is_same(:old.'
|| cur.column_name
|| ',:new.'
|| cur.column_name
|| ') THEN'
|| CHR(10);
col_list := col_list || 'audit_msg:=audit_msg||''' || cur.column_name || ','';' || CHR(10);
col_list := col_list || 'END IF;' || CHR(10);
data_append(NVL(col_list, ' '));
END LOOP;

-- IF col_list IS NULL THEN
-- RETURN ' ';
-- ELSE
col_list :=
CHR(10)
|| 'IF audit_msg IS NOT NULL THEN '
|| CHR(10)
|| ' GOTO INSERT_LABEL;'
|| CHR(10)
|| 'END IF;'
|| CHR(10);
data_append(NVL(col_list, ' '));
RETURN ' ';
-- END IF;
EXCEPTION
WHEN OTHERS THEN
-- RETURN ptable_name || ' columns to long';
RAISE;
END;

---------------------------------------------------------------
FUNCTION trigger_maker_insert(
ptable_name VARCHAR2,
new_old VARCHAR2)
RETURN VARCHAR2 IS
audit_msg VARCHAR2(100);

CURSOR cur_col(
ptable_name VARCHAR2) IS
SELECT LOWER(column_name) column_name,
ROWNUM row_num
FROM USER_TAB_COLUMNS
WHERE table_name = ptable_name || '_A';

ret VARCHAR2(4000);
col_list VARCHAR2(4000);
val_list VARCHAR2(4000);
BEGIN
ret := ret || 'INSERT INTO ' || ptable_name || '_a (' || CHR(10) || ' ';

FOR cur IN cur_col(ptable_name) LOOP
IF cur.row_num > 1 THEN
col_list := col_list || ',' || CHR(10) || ' ';
val_list := val_list || ',' || CHR(10) || ' ';
END IF;

col_list := col_list || cur.column_name;

IF cur.column_name LIKE '%audit_msg' THEN
val_list := val_list || 'audit_msg';
ELSE
val_list := val_list || ':' || new_old || '.' || cur.column_name;
END IF;
END LOOP;

RETURN ret || col_list || ')' || CHR(10) || ' values (' || val_list || ')';
EXCEPTION
WHEN OTHERS THEN
-- RETURN ptable_name || ' columns to long';
RAISE;
END;

---------------------------------------------------------------
FUNCTION trigger_maker_data(
ptable_name VARCHAR2)
RETURN VARCHAR2 IS
CURSOR cur_col(
ptable_name VARCHAR2) IS
SELECT LOWER(column_name) column_name,
ROWNUM row_num
FROM USER_TAB_COLUMNS
WHERE table_name = ptable_name;

ret VARCHAR2(4000);
col_list VARCHAR2(4000);
val_list VARCHAR2(4000);
BEGIN
ret := CHR(10) || ' ' || CHR(39);

FOR cur IN cur_col(ptable_name) LOOP
IF cur.row_num > 1 THEN
col_list := col_list || '||' || CHR(39) || ', ' || CHR(39) || '||' || CHR(10) || ' ';
END IF;

col_list := col_list || CHR(39) || cur.column_name || ': ' || CHR(39) || '||:new.' || cur.column_name;
END LOOP;

RETURN CHR(10)
|| ' '
|| col_list
|| CHR(10)
|| '|| '
|| CHR(39)
|| 'rowid: '
|| CHR(39)
|| '||:NEW.rowid';
EXCEPTION
WHEN OTHERS THEN
-- RETURN ptable_name || ' columns to long';
RAISE;
END;

-------------------------------------------------
PROCEDURE main AS
CURSOR cur_tab IS
SELECT *
FROM USER_TABLES
WHERE table_name NOT LIKE '%_A'
AND table_name IN(SELECT SUBSTR(table_name, 1, LENGTH(table_name) - 2)
FROM USER_TABLES
WHERE table_name LIKE '%_A')
ORDER BY 1;
BEGIN
-- p_trigger_generator.gt_clob_table_create;
DELETE FROM clob_ddl;

FOR x IN (SELECT *
FROM USER_TRIGGERS
WHERE trigger_name LIKE 'TR_%' AND trigger_name LIKE '%_A') LOOP
EXECUTE IMMEDIATE 'drop trigger ' || x.trigger_name;
END LOOP;

FOR x IN cur_tab LOOP
make_trigger(x.table_name);
END LOOP;
-- gt_clob_table_drop;
EXCEPTION
WHEN OTHERS THEN
-- RETURN ptable_name || ' columns to long';
DBMS_OUTPUT.PUT_LINE('**length of lob is : ' || TO_CHAR(DBMS_LOB.getlength(l_clob)));
RAISE;
END;

-- ---------------------------------------------------------------
-- PROCEDURE gt_clob_table_create IS
-- BEGIN
-- -- EXECUTE IMMEDIATE 'create table clob_ddl ( id number primary key, data_value clob )';
-- NULL;
-- EXCEPTION
-- WHEN OTHERS THEN
-- DBMS_OUTPUT.put_line ('sqlerrm= ' || SQLERRM);
-- -- EXECUTE IMMEDIATE 'truncate table clob_ddl';
-- END gt_clob_table_create;

---------------------------------------------------------------
-- PROCEDURE gt_clob_table_drop IS
-- BEGIN
-- -- EXECUTE IMMEDIATE 'drop table clob_ddl';
-- NULL;
-- END gt_clob_table_drop;

------------------------------------------------------------------
PROCEDURE make_trigger(
table_name VARCHAR2) AS
s1 VARCHAR2(4000);
s2 VARCHAR2(4000);
s2a VARCHAR2(4000);
s2b VARCHAR2(4000);
s3 VARCHAR2(4000);
s4 VARCHAR2(4000);
s5 VARCHAR2(4000);
s6 VARCHAR2(4000);
s7 VARCHAR2(4000);
s7a VARCHAR2(4000);
s7b VARCHAR2(4000);
delete_label BOOLEAN;
BEGIN
delete_label := FALSE;
-- p_trigger_generator.gt_clob_table_create;
DBMS_OUTPUT.PUT_LINE('Audit Trigger Installing..: ' || table_name);
s1 :=
'CREATE OR REPLACE TRIGGER TR_'
|| SUBSTR(table_name, 1, 25)
|| '_A '
|| CHR(10)
|| 'AFTER INSERT OR UPDATE OR DELETE ON '
|| table_name
|| CHR(10)
|| ' FOR EACH ROW '
|| CHR(10);
s2 := 'DECLARE ' || CHR(10) || 'audit_msg VARCHAR2 (4000); ' || CHR(10) || 'BEGIN '
-- || CHR (10);
-- || ' p_log.process('
-- || CHR (39)
-- || 'TR_'
-- || table_name
-- || '_A '
-- || CHR (39)
-- || '); '
|| CHR(10) || CHR(10);

IF INSTR(delete_table_allowed, '~' || table_name || '~') > 0 THEN
s2a :=
CHR(10)
|| CHR(10)
|| '--deletes allowed, audits handled in service...'
|| CHR(10)
-- || 'IF DELETING THEN '
-- || CHR (10)
-- || ' GOTO INSERT_LABEL;'
-- || CHR (10)
-- || 'END IF;'
|| CHR(10)
|| CHR(10);
ELSIF INSTR(delete_table_handled, '~' || table_name || '~') > 0 THEN
delete_label := TRUE;
s2a :=
'IF DELETING THEN '
|| CHR(10)
|| ' audit_msg:=''DELETE'';'
|| CHR(10)
|| ' GOTO DELETE_LABEL;'
|| CHR(10)
|| 'END IF;';
ELSE
s2a :=
'IF DELETING THEN '
|| CHR(10)
|| ' raise_application_error (-20005, '
|| CHR(39)
|| table_name
|| ' DELETE AUDIT TRIGGER : DELETE IS NOT ALLOWED ON '
|| table_name
|| ' TABLE '
|| CHR(39)
|| '); '
|| CHR(10)
|| 'END IF;';
END IF;

s2b := CHR(10) || CHR(10) || 'IF UPDATING THEN ' || CHR(10) || ' audit_msg:=''UPDATE: '';' || CHR(10);
-- s3 := TRIM (trigger_maker_check_nn (table_name));
-- s4 := TRIM (trigger_maker_check_null (table_name));
s5 :=
'end if;'
|| CHR(10)
|| CHR(10)
|| 'IF INSERTING THEN '
|| CHR(10)
|| ' audit_msg:=''INSERT'';'
|| CHR(10)
|| CHR(10)
|| ' GOTO INSERT_LABEL;'
|| CHR(10)
|| 'END IF;'
|| CHR(10)
|| CHR(10)
-- || 'p_log.process_return; '
-- || CHR (10)
|| 'return;'
|| CHR(10)
|| CHR(10)
|| '<<INSERT_LABEL>>'
|| CHR(10);
s6 := trigger_maker_insert(table_name, 'NEW') || ';' || CHR(10) || CHR(10) || 'RETURN;';

IF delete_label THEN
s7 := CHR(10) || CHR(10) || '<<DELETE_LABEL>>' || CHR(10);
s7a := trigger_maker_insert(table_name, 'OLD') || ';' || CHR(10) || CHR(10) || 'RETURN;' || CHR(10);
ELSE
s7 := CHR(10);
s7a := '';
END IF;

s7b :=
CHR(10)
|| 'EXCEPTION'
|| CHR(10)
|| CHR(10)
|| ' WHEN OTHERS THEN'
|| ' raise; '
|| CHR(10)
|| CHR(10)
|| 'END; '
|| CHR(10);

-- DBMS_OUTPUT.put_line (s1);
-- DBMS_OUTPUT.put_line (s2);
FOR y IN (SELECT s1,
s2,
s2a,
s2b,
s3,
s4,
s5,
s6,
s7,
s7a,
s7b
FROM DUAL) LOOP
EXECUTE IMMEDIATE ' begin insert into clob_ddl values ( :sqx, empty_clob() ) returning data_value into :b1; end; '
USING l_cnt, OUT l_clob;

data_append(NVL(y.s1, ' '));
data_append(NVL(y.s2, ' '));
data_append(NVL(y.s2a, ' '));
data_append(NVL(y.s2b, ' '));
s3 := TRIM(trigger_maker_check_nn(table_name));
s4 := TRIM(trigger_maker_check_null(table_name));
data_append(NVL(y.s4, ' '));
data_append(NVL(y.s5, ' '));
data_append(NVL(y.s6, ' '));
data_append(NVL(y.s7, ' '));
data_append(NVL(y.s7a, ' '));
data_append(NVL(y.s7b, ' '));

EXECUTE IMMEDIATE DBMS_LOB.SUBSTR(l_clob, 32765, 1);

l_cnt := l_cnt + 1;
END LOOP;
-- gt_clob_table_drop;
EXCEPTION
WHEN OTHERS THEN
-- RETURN ptable_name || ' columns to long';
DBMS_OUTPUT.PUT_LINE('***length of lob is : ' || TO_CHAR(DBMS_LOB.getlength(l_clob)));
RAISE;
END;
END p_trigger_generator;
/


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