Close

MERGE, Triggers, and Statement Restart

The MERGE command in Oracle is a great tool for modifying data in a table; efficiently solving the old problem of insert collisions and updates of non-existent rows. Most of the time we use it becaue it “just works.” But, what really goes on when you issue a merge? In particular, which triggers fire? How does it work with interaction of other sessions, and how does statement restart impact it?

To examine these I’ll set up a small test table, a bunch of triggers, a logging table, and some code to populate the log as a statement executes.

CREATE TABLE mergetest
(
    id     INTEGER PRIMARY KEY,
    val    VARCHAR2(50)
);

CREATE TABLE mergetest_log
(
    logtime       TIMESTAMP,
    logmessage    VARCHAR2(100)
);

CREATE OR REPLACE PROCEDURE write_mergetest_log(p_message IN VARCHAR2)
IS
    PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
    INSERT INTO mergetest_log(logtime, logmessage)
         VALUES (SYSTIMESTAMP, SYS_CONTEXT('userenv', 'client_info') || ' ' || p_message);

    COMMIT;
END;

CREATE OR REPLACE FUNCTION trigger_action
    RETURN VARCHAR2
IS
BEGIN
 RETURN CASE
            WHEN INSERTING
            THEN
                'INSERT'
            WHEN UPDATING
            THEN
                'UPDATE'
            WHEN DELETING
            THEN
                'DELETE'
            ELSE
                'UNKNOWN'
        END;
END;

CREATE OR REPLACE TRIGGER trg_before_delete
    BEFORE DELETE
    ON mergetest
BEGIN
    write_mergetest_log($$plsql_unit || ' ' || trigger_action);
END;

CREATE OR REPLACE TRIGGER trg_before_insert
    BEFORE INSERT
    ON mergetest
BEGIN
    write_mergetest_log($$plsql_unit || ' ' || trigger_action);
END;

CREATE OR REPLACE TRIGGER trg_before_update
    BEFORE UPDATE
    ON mergetest
BEGIN
    write_mergetest_log($$plsql_unit || ' ' || trigger_action);
END;

CREATE OR REPLACE TRIGGER trg_after_delete
    AFTER DELETE
    ON mergetest
BEGIN
    write_mergetest_log($$plsql_unit || ' ' || trigger_action);
END;

CREATE OR REPLACE TRIGGER trg_after_insert
    AFTER INSERT
    ON mergetest
BEGIN
    write_mergetest_log($$plsql_unit || ' ' || trigger_action);
END;

CREATE OR REPLACE TRIGGER trg_after_update
    AFTER UPDATE
    ON mergetest
BEGIN
    write_mergetest_log($$plsql_unit || ' ' || trigger_action);
END;

CREATE OR REPLACE TRIGGER trg_before_row_delete
    BEFORE DELETE
    ON mergetest
    FOR EACH ROW
BEGIN
    write_mergetest_log($$plsql_unit || ' ' || trigger_action);
END;

CREATE OR REPLACE TRIGGER trg_before_row_insert
    BEFORE INSERT
    ON mergetest
    FOR EACH ROW
BEGIN
    write_mergetest_log($$plsql_unit || ' ' || trigger_action);
END;

CREATE OR REPLACE TRIGGER trg_before_row_update
    BEFORE UPDATE
    ON mergetest
    FOR EACH ROW
BEGIN
    write_mergetest_log($$plsql_unit || ' ' || trigger_action);
END;

CREATE OR REPLACE TRIGGER trg_after_rowdelete
    AFTER DELETE
    ON mergetest
    FOR EACH ROW
BEGIN
    write_mergetest_log($$plsql_unit || ' ' || trigger_action);
END;

CREATE OR REPLACE TRIGGER trg_after_rowinsert
    AFTER INSERT
    ON mergetest
    FOR EACH ROW
BEGIN
    write_mergetest_log($$plsql_unit || ' ' || trigger_action);
END;

CREATE OR REPLACE TRIGGER trg_after_rowupdate
    AFTER UPDATE
    ON mergetest
    FOR EACH ROW
BEGIN
    write_mergetest_log($$plsql_unit || ' ' || trigger_action);
END;

So, that’s simple enough. Two tables, all the triggers do the same thing, simply logging who they are and what they are doing and the logging procedure uses an autonomous transaction so the progress can be captured even if the calling statements are rolled back.

You might wonder why I used so many triggers instead of a single composite trigger, or at least combining INSERT/UPDATE/DELETE of each trigger type. In fact, my early tests when writing this article did use a single trigger. The problem with that was I lost some granularity of information when statement restart occurred and some actions became undefined. I’ll highlight this in later example code and output.

Now, we’ll execute 3 merge statements, the first into the empty table, then a merge update of the row we just inserted, and then another merge that should create another new row. Then we’ll check the log table and see what happened.

clear screen
TRUNCATE TABLE mergetest;
TRUNCATE TABLE mergetest_log;
exec dbms_application_info.set_client_info('SESSION ONE');
exec write_mergetest_log('MERGE 1');
MERGE INTO mergetest t
     USING (SELECT 1 id, 'mergeupdate' updateval, 'mergeinsert' insertval FROM DUAL) s
        ON (t.id = s.id)
WHEN MATCHED
THEN
    UPDATE SET t.val = s.updateval
WHEN NOT MATCHED
THEN
    INSERT     VALUES (s.id, s.insertval);
exec write_mergetest_log('COMMIT');
COMMIT;
exec write_mergetest_log('MERGE 2');
MERGE INTO mergetest t
     USING (SELECT 1 id, 'mergeupdate' updateval, 'mergeinsert' insertval FROM DUAL) s
        ON (t.id = s.id)
WHEN MATCHED
THEN
    UPDATE SET t.val = s.updateval
WHEN NOT MATCHED
THEN
    INSERT     VALUES (s.id, s.insertval);
exec write_mergetest_log('COMMIT');
COMMIT;
exec write_mergetest_log('MERGE 3');
MERGE INTO mergetest t
     USING (SELECT 2 id, 'mergeupdate' updateval, 'mergeinsert' insertval FROM DUAL) s
        ON (t.id = s.id)
WHEN MATCHED
THEN
    UPDATE SET t.val = s.updateval
WHEN NOT MATCHED
THEN
    INSERT     VALUES (s.id, s.insertval);
exec write_mergetest_log('COMMIT');
COMMIT;

select * from mergetest_log order by logtime;

Note, both insert and update statement-level triggers fire; but for the row-level triggers, only the operation actually performed are fired.

LOGTIME                      LOGMESSAGE
---------------------------- -----------------------------------------
2018-01-07 15:41:43.337157   SESSION ONE MERGE 1
2018-01-07 15:41:43.386852   SESSION ONE TRG_BEFORE_INSERT INSERT
2018-01-07 15:41:43.387073   SESSION ONE TRG_BEFORE_UPDATE UPDATE
2018-01-07 15:41:43.404012   SESSION ONE TRG_BEFORE_ROW_INSERT INSERT
2018-01-07 15:41:43.437701   SESSION ONE TRG_AFTER_ROW_INSERT INSERT
2018-01-07 15:41:43.437873   SESSION ONE TRG_AFTER_INSERT INSERT
2018-01-07 15:41:43.438004   SESSION ONE TRG_AFTER_UPDATE UPDATE
2018-01-07 15:41:43.491317   SESSION ONE COMMIT
2018-01-07 15:41:43.524916   SESSION ONE MERGE 2
2018-01-07 15:41:43.552057   SESSION ONE TRG_BEFORE_INSERT INSERT
2018-01-07 15:41:43.552230   SESSION ONE TRG_BEFORE_UPDATE UPDATE
2018-01-07 15:41:43.552452   SESSION ONE TRG_BEFORE_ROW_UPDATE UPDATE
2018-01-07 15:41:43.552599   SESSION ONE TRG_AFTER_ROW_UPDATE UPDATE
2018-01-07 15:41:43.552715   SESSION ONE TRG_AFTER_INSERT INSERT
2018-01-07 15:41:43.552827   SESSION ONE TRG_AFTER_UPDATE UPDATE
2018-01-07 15:41:43.609417   SESSION ONE COMMIT
2018-01-07 15:41:43.641952   SESSION ONE MERGE 3
2018-01-07 15:41:43.669958   SESSION ONE TRG_BEFORE_INSERT INSERT
2018-01-07 15:41:43.670092   SESSION ONE TRG_BEFORE_UPDATE UPDATE
2018-01-07 15:41:43.670259   SESSION ONE TRG_BEFORE_ROW_INSERT INSERT
2018-01-07 15:41:43.670442   SESSION ONE TRG_AFTER_ROW_INSERT INSERT
2018-01-07 15:41:43.670551   SESSION ONE TRG_AFTER_INSERT INSERT
2018-01-07 15:41:43.670643   SESSION ONE TRG_AFTER_UPDATE UPDATE
2018-01-07 15:41:43.726528   SESSION ONE COMMIT

24 rows selected.

Next, let’s try it with a second session involved and see how the locking impacts the merge. I’ll color code the sessions to help indicate which session is producing which output. Green background for session 1, red background for session 2.


clear screen
exec dbms_application_info.set_client_info('SESSION ONE');
TRUNCATE TABLE mergetest;
TRUNCATE TABLE mergetest_log;
INSERT INTO mergetest VALUES (1, 'start');
COMMIT;
exec write_mergetest_log('UPDATE');
UPDATE mergetest set val = 'update' WHERE id = 1;
pause "Run merge in other session"



clear screen
exec dbms_application_info.set_client_info('SESSION TWO');
SELECT * FROM mergetest;
exec write_mergetest_log('MERGE');
MERGE INTO mergetest t
     USING (SELECT 1 id, 'mergeupdate' updateval, 'mergeinsert' insertval FROM DUAL) s
        ON (t.id = s.id)
WHEN MATCHED
THEN
    UPDATE SET t.val = s.updateval
WHEN NOT MATCHED
THEN
    INSERT     VALUES (s.id, s.insertval);
exec write_mergetest_log('COMMIT');
COMMIT;



exec write_mergetest_log('COMMIT');
COMMIT;
exec write_mergetest_log('INSERT');
INSERT INTO mergetest VALUES (1, 'insert');

When run, the output looked like this…


15:54:31 SQL> exec dbms_application_info.set_client_info('SESSION ONE');

PL/SQL procedure successfully completed.

15:54:31 SQL> TRUNCATE TABLE mergetest;

Table truncated.

15:54:32 SQL> TRUNCATE TABLE mergetest_log;

Table truncated.

15:54:32 SQL> INSERT INTO mergetest VALUES (1, 'start');

1 row created.

15:54:32 SQL> COMMIT;

Commit complete.

15:54:32 SQL> exec write_mergetest_log('UPDATE');

PL/SQL procedure successfully completed.

15:54:32 SQL> UPDATE mergetest set val = 'update' WHERE id = 1;

1 row updated.

15:54:32 SQL> pause "Run merge in other session"
"Run merge in other session"



15:54:41 SQL> exec dbms_application_info.set_client_info('SESSION TWO');

PL/SQL procedure successfully completed.

15:54:41 SQL> SELECT * FROM mergetest;

        ID VAL
---------- --------------------------------------------------
         1 start

15:54:41 SQL> exec write_mergetest_log('MERGE');

PL/SQL procedure successfully completed.

15:54:41 SQL> MERGE INTO mergetest t
15:54:41   2       USING (SELECT 1 id, 'mergeupdate' updateval, 'mergeinsert' insertval FROM DUAL) s
15:54:41   3          ON (t.id = s.id)
15:54:41   4  WHEN MATCHED
15:54:41   5  THEN
15:54:41   6      UPDATE SET t.val = s.updateval
15:54:41   7  WHEN NOT MATCHED
15:54:41   8  THEN
15:54:41   9      INSERT     VALUES (s.id, s.insertval);

1 row merged.

15:54:54 SQL> exec write_mergetest_log('COMMIT');

PL/SQL procedure successfully completed.

15:54:54 SQL> COMMIT;

Commit complete.



exec write_mergetest_log('COMMIT');
15:54:54 SQL> COMMIT;

Commit complete.

15:54:54 SQL> exec write_mergetest_log('INSERT');

PL/SQL procedure successfully completed.

15:54:54 SQL> INSERT INTO mergetest VALUES (1, 'insert');
INSERT INTO mergetest VALUES (1, 'insert')
*
ERROR at line 1:
ORA-00001: unique constraint (SDS.SYS_C0048605) violated

The logs then again show the statement-level triggers fire for both insert and update, regardless of what the merge actually does; but look at the merge in session two.
The before triggers fire a second time due to statement restart, but the activity is actually “UNKNOWN”, which I’ve highlighted in purple. Using before-triggers is always tricky with statement restart, but this adds an extra layer of complication. If you have a trigger that uses the “INSERTING/UPDATING/DELETING” activity functions to determine the action, on statement restart those triggers will fail. This is what happened to me when I used a single consolidated trigger in my initial tests. By using individual triggers for each action I could track exactly which activity is causing the trigger to fire even if the activity itself is undefined.

15:54:54 SQL> select * from mergetest_log order by logtime;

LOGTIME                       LOGMESSAGE
----------------------------- --------------------------------------------
2018-01-07 15:54:31.449622    SESSION ONE TRG_BEFORE_INSERT INSERT
2018-01-07 15:54:31.450485    SESSION ONE TRG_BEFORE_ROW_INSERT INSERT
2018-01-07 15:54:31.544272    SESSION ONE TRG_AFTER_ROW_INSERT INSERT
2018-01-07 15:54:31.544476    SESSION ONE TRG_AFTER_INSERT INSERT
2018-01-07 15:54:31.581790    SESSION ONE UPDATE
2018-01-07 15:54:31.599431    SESSION ONE TRG_BEFORE_UPDATE UPDATE
2018-01-07 15:54:31.599642    SESSION ONE TRG_BEFORE_ROW_UPDATE UPDATE
2018-01-07 15:54:31.599823    SESSION ONE TRG_AFTER_ROW_UPDATE UPDATE
2018-01-07 15:54:31.599954    SESSION ONE TRG_AFTER_UPDATE UPDATE
2018-01-07 15:54:40.794142    SESSION TWO MERGE
2018-01-07 15:54:40.820151    SESSION TWO TRG_BEFORE_INSERT INSERT
2018-01-07 15:54:40.820316    SESSION TWO TRG_BEFORE_UPDATE UPDATE
2018-01-07 15:54:53.233075    SESSION TWO TRG_BEFORE_ROW_UPDATE UPDATE
2018-01-07 15:54:53.233366    SESSION TWO TRG_BEFORE_UPDATE UNKNOWN
2018-01-07 15:54:53.233495    SESSION TWO TRG_BEFORE_INSERT UNKNOWN
2018-01-07 15:54:53.233703    SESSION TWO TRG_BEFORE_ROW_UPDATE UPDATE
2018-01-07 15:54:53.233857    SESSION TWO TRG_AFTER_ROW_UPDATE UPDATE
2018-01-07 15:54:53.233978    SESSION TWO TRG_AFTER_INSERT INSERT
2018-01-07 15:54:53.234079    SESSION TWO TRG_AFTER_UPDATE UPDATE
2018-01-07 15:54:53.262496    SESSION ONE INSERT
2018-01-07 15:54:53.296106    SESSION TWO COMMIT
2018-01-07 15:54:59.362455    SESSION ONE TRG_BEFORE_INSERT INSERT
2018-01-07 15:54:59.362682    SESSION ONE TRG_BEFORE_ROW_INSERT INSERT
23 rows selected.

While I’ve used merge many times I’ll admit I hadn’t really thought about the trigger implications as much as I should have. In particular I hadn’t considered the possibility of an UNKNOWN action inside of trigger and what I’d do to accommodate one when encountered. I hope you’ve found this useful as well.