Oracle PL/SQL – Triggers

Subject: Information Technology – Database Systems
Grade: 12th Grade – HTL Computer Science
Prerequisites: Procedures, Functions, Packages, Exceptions
Author: HTL Pinkafeld – IF/IT



1. What are Triggers?

1.1 Definition

A trigger is a stored PL/SQL block that automatically executes when a specific database event occurs. Unlike procedures, a trigger is never explicitly called.

Event (INSERT / UPDATE / DELETE)
    ↓
Oracle detects: trigger is defined
    ↓
Trigger code executes automatically
    ↓
Normal processing continues (or is aborted)

1.2 Trigger Types

Type Fired By Typical Use
DML Trigger INSERT, UPDATE, DELETE Auditing, validation, derivation
INSTEAD OF DML on Views Updatable complex views
DDL Trigger CREATE, ALTER, DROP Schema protection, change log
System Trigger LOGON, LOGOFF, STARTUP Session logging, initialization

1.3 Use Cases


2. Creating DML Triggers

2.1 Syntax

CREATE [OR REPLACE] TRIGGER trigger_name
    {BEFORE | AFTER | INSTEAD OF}
    {INSERT | UPDATE [OF column] | DELETE} [OR {INSERT | UPDATE | DELETE}] ...
    ON table_name
    [FOR EACH ROW]
    [WHEN (condition)]
DECLARE
    -- Variable declarations (optional)
BEGIN
    -- Trigger code
EXCEPTION
    -- Error handling (optional)
END [trigger_name];
/

2.2 Simple AFTER INSERT Trigger

CREATE OR REPLACE TRIGGER trg_emp_insert
AFTER INSERT ON emp
FOR EACH ROW
BEGIN
    INSERT INTO emp_audit (action, empno, ename, sal, ts, usr)
    VALUES ('INSERT', :NEW.empno, :NEW.ename, :NEW.sal, SYSTIMESTAMP, USER);
END trg_emp_insert;
/

-- Test:
INSERT INTO emp (empno, ename, job, deptno, sal, hiredate)
VALUES (9001, 'NEWUSER', 'CLERK', 10, 2000, SYSDATE);
COMMIT;

2.3 BEFORE INSERT – Setting a Value

-- Automatically populate primary key from sequence:
CREATE OR REPLACE TRIGGER trg_emp_pk
BEFORE INSERT ON emp
FOR EACH ROW
WHEN (NEW.empno IS NULL)
BEGIN
    :NEW.empno := emp_seq.NEXTVAL;
END;
/

-- Test (empno is set automatically):
INSERT INTO emp (ename, job, deptno, sal, hiredate)
VALUES ('AUTOUSER', 'ANALYST', 20, 3500, SYSDATE);
COMMIT;

3. Row-Level vs. Statement-Level Triggers

3.1 Statement Trigger (Default)

A statement trigger fires once per DML statement – regardless of how many rows are affected. No FOR EACH ROW:

CREATE OR REPLACE TRIGGER trg_dept_access
BEFORE INSERT OR UPDATE OR DELETE ON dept
BEGIN
    -- Deny changes outside business hours:
    IF TO_NUMBER(TO_CHAR(SYSDATE, 'HH24')) NOT BETWEEN 8 AND 17 THEN
        RAISE_APPLICATION_ERROR(
            -20100,
            'Changes to DEPT only allowed between 08:00 and 17:00!'
        );
    END IF;
    IF TO_CHAR(SYSDATE, 'DY', 'NLS_DATE_LANGUAGE=ENGLISH') IN ('SAT', 'SUN') THEN
        RAISE_APPLICATION_ERROR(-20101, 'No changes on weekends!');
    END IF;
END trg_dept_access;
/

3.2 Row-Level Trigger with FOR EACH ROW

A row-level trigger fires for each affected row. Updating 14 employees triggers 14 executions:

CREATE OR REPLACE TRIGGER trg_sal_check
BEFORE UPDATE OF sal ON emp
FOR EACH ROW
BEGIN
    IF :NEW.sal < :OLD.sal * 0.80 THEN
        RAISE_APPLICATION_ERROR(
            -20200,
            'Pay cut of more than 20% is not allowed! (' ||
            :OLD.sal || ' → ' || :NEW.sal || ')'
        );
    END IF;
END trg_sal_check;
/

3.3 Firing Order

DML statement (e.g., UPDATE on 5 rows)
    ↓
BEFORE Statement trigger  (1×)
    ↓
    For each affected row:
    ├─ BEFORE Row trigger  (5×)
    ├─ Row is changed
    └─ AFTER Row trigger   (5×)
    ↓
AFTER Statement trigger   (1×)

4. :NEW and :OLD Pseudo-Records

4.1 Availability

Timing :OLD :NEW
BEFORE INSERT NULL New value (modifiable)
AFTER INSERT NULL Inserted value
BEFORE UPDATE Old value New value (modifiable)
AFTER UPDATE Old value New value
BEFORE DELETE Old value NULL
AFTER DELETE Old value NULL

Important: :NEW can only be modified in BEFORE triggers!

4.2 Example: Complete Audit

CREATE OR REPLACE TRIGGER trg_emp_audit
AFTER INSERT OR UPDATE OR DELETE ON emp
FOR EACH ROW
DECLARE
    v_action VARCHAR2(10);
BEGIN
    IF    INSERTING THEN v_action := 'INSERT';
    ELSIF UPDATING  THEN v_action := 'UPDATE';
    ELSIF DELETING  THEN v_action := 'DELETE';
    END IF;

    INSERT INTO emp_audit
        (action, empno, old_ename, new_ename, old_sal, new_sal, old_deptno, new_deptno)
    VALUES
        (v_action,
         COALESCE(:OLD.empno, :NEW.empno),
         :OLD.ename,  :NEW.ename,
         :OLD.sal,    :NEW.sal,
         :OLD.deptno, :NEW.deptno);
END trg_emp_audit;
/

4.3 INSERTING, UPDATING, DELETING

When a trigger covers multiple events, these predicates distinguish them:

CREATE OR REPLACE TRIGGER trg_multi
BEFORE INSERT OR UPDATE OR DELETE ON emp
FOR EACH ROW
BEGIN
    IF INSERTING THEN
        :NEW.hiredate := NVL(:NEW.hiredate, SYSDATE);
    ELSIF UPDATING('SAL') THEN
        DBMS_OUTPUT.PUT_LINE('Salary: ' || :OLD.sal || ' → ' || :NEW.sal);
    ELSIF DELETING THEN
        IF :OLD.job = 'PRESIDENT' THEN
            RAISE_APPLICATION_ERROR(-20300, 'President cannot be deleted!');
        END IF;
    END IF;
END;
/

5. WHEN Clause in Triggers

The WHEN clause filters rows for which the trigger body executes. Inside WHEN, :NEW and :OLD are written without a colon:

CREATE OR REPLACE TRIGGER trg_high_salary
AFTER INSERT OR UPDATE OF sal ON emp
FOR EACH ROW
WHEN (NEW.sal > 5000)   -- No : before NEW/OLD in WHEN!
BEGIN
    DBMS_OUTPUT.PUT_LINE(
        'Warning: ' || :NEW.ename || ' has a salary of ' || :NEW.sal
    );
END;
/

6. Compound Triggers

6.1 Motivation

The mutating table problem occurs when a row trigger accesses the same table being modified. Compound triggers solve this:

CREATE OR REPLACE TRIGGER trg_emp_compound
FOR INSERT OR UPDATE ON emp
COMPOUND TRIGGER

    TYPE t_sal_tab IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
    v_salaries t_sal_tab;
    v_idx      PLS_INTEGER := 0;

    BEFORE STATEMENT IS
    BEGIN
        v_idx := 0;
        v_salaries.DELETE;
    END BEFORE STATEMENT;

    AFTER EACH ROW IS
    BEGIN
        v_idx := v_idx + 1;
        v_salaries(v_idx) := :NEW.sal;
    END AFTER EACH ROW;

    AFTER STATEMENT IS
        v_total NUMBER := 0;
    BEGIN
        FOR i IN 1..v_idx LOOP
            v_total := v_total + v_salaries(i);
        END LOOP;
        DBMS_OUTPUT.PUT_LINE('Total salary changed/inserted: ' || v_total);
    END AFTER STATEMENT;

END trg_emp_compound;
/

7. INSTEAD OF Triggers on Views

Complex views (with JOINs, GROUP BY, DISTINCT) are normally not directly updatable. An INSTEAD OF trigger intercepts DML on the view:

CREATE OR REPLACE VIEW vw_emp_dept AS
    SELECT e.empno, e.ename, e.sal, e.deptno, d.dname, d.loc
    FROM   emp e JOIN dept d ON e.deptno = d.deptno;

CREATE OR REPLACE TRIGGER trg_empdet_insert
INSTEAD OF INSERT ON vw_emp_dept
FOR EACH ROW
DECLARE
    v_cnt NUMBER;
BEGIN
    SELECT COUNT(*) INTO v_cnt FROM dept WHERE deptno = :NEW.deptno;
    IF v_cnt = 0 THEN
        RAISE_APPLICATION_ERROR(-20400, 'Department ' || :NEW.deptno || ' does not exist.');
    END IF;

    INSERT INTO emp (empno, ename, sal, deptno, job, hiredate)
    VALUES (:NEW.empno, :NEW.ename, :NEW.sal, :NEW.deptno, 'CLERK', SYSDATE);
END;
/

-- INSERT on the view now works:
INSERT INTO vw_emp_dept (empno, ename, sal, deptno)
VALUES (9100, 'VIEWUSER', 2500, 20);
COMMIT;

8. DDL and System Triggers

8.1 DDL Trigger

CREATE OR REPLACE TRIGGER trg_schema_protect
BEFORE DROP ON SCHEMA
BEGIN
    IF ORA_DICT_OBJ_TYPE = 'TABLE' THEN
        INSERT INTO ddl_log (action, obj, usr, ts)
        VALUES (ORA_SYSEVENT, ORA_DICT_OBJ_NAME, ORA_LOGIN_USER, SYSTIMESTAMP);
        COMMIT;

        IF ORA_DICT_OBJ_NAME IN ('EMP', 'DEPT', 'SALGRADE') THEN
            RAISE_APPLICATION_ERROR(-20500,
                'Protection: Table ' || ORA_DICT_OBJ_NAME || ' cannot be dropped!');
        END IF;
    END IF;
END;
/

8.2 System Trigger (LOGON/LOGOFF)

CREATE OR REPLACE TRIGGER trg_logon_audit
AFTER LOGON ON DATABASE
BEGIN
    INSERT INTO session_log (usr, ts, ip_addr, event)
    VALUES (
        SYS_CONTEXT('USERENV', 'SESSION_USER'),
        SYSTIMESTAMP,
        SYS_CONTEXT('USERENV', 'IP_ADDRESS'),
        'LOGIN'
    );
    COMMIT;
EXCEPTION
    WHEN OTHERS THEN NULL;  -- Never block login due to trigger error!
END;
/

8.3 Event Functions in DDL/System Triggers

Function Meaning
ORA_SYSEVENT Name of triggering event (CREATE, DROP, …)
ORA_DICT_OBJ_TYPE Type of affected object (TABLE, INDEX, …)
ORA_DICT_OBJ_NAME Name of affected object
ORA_DICT_OBJ_OWNER Schema owner
ORA_LOGIN_USER Currently logged-in user

9. Managing Triggers

-- Show all own triggers:
SELECT trigger_name, trigger_type, triggering_event, table_name, status
FROM   user_triggers
ORDER  BY table_name, trigger_name;

-- Disable a single trigger (e.g., for bulk load):
ALTER TRIGGER trg_emp_audit DISABLE;

-- Enable a single trigger:
ALTER TRIGGER trg_emp_audit ENABLE;

-- Disable all triggers on a table:
ALTER TABLE emp DISABLE ALL TRIGGERS;

-- Enable all triggers on a table:
ALTER TABLE emp ENABLE ALL TRIGGERS;

-- Compile a trigger:
ALTER TRIGGER trg_emp_audit COMPILE;

-- Drop a trigger:
DROP TRIGGER trg_emp_audit;

10. Summary and Outlook

10.1 Trigger Types and Timing

Trigger Type Timing FOR EACH ROW :NEW/:OLD Typical Use
BEFORE Statement Before DML No No Access control
BEFORE Row Before each row Yes Yes (modifiable) Setting values
AFTER Row After each row Yes Yes (read-only) Auditing
AFTER Statement After DML No No Statistics
INSTEAD OF Instead of DML Yes Yes Make views updatable
Compound All timings Mixed Yes Mutating table fix
DDL Schema changes No No Schema protection
System DB events No No Login logging

10.2 Best Practices

10.3 Course Completion

This completes the Oracle PL/SQL course. Topics covered:

Chapter Content
1 Introduction Anonymous block, variables, %TYPE, %ROWTYPE, SELECT INTO
2 Control Structures IF, CASE, LOOP, WHILE, FOR, EXIT, CONTINUE
3 Cursors Implicit, explicit, cursor FOR, REF CURSOR, BULK COLLECT
4 Exceptions Predefined, user-defined, RAISE_APPLICATION_ERROR
5 Procedures/Functions Stored subprograms, IN/OUT parameters, overloading
6 Packages Spec/Body, package variables, standard packages
7 Triggers DML, INSTEAD OF, DDL, system triggers

Further reading: Oracle Advanced PL/SQL – Collections, Object Types, Native Dynamic SQL (EXECUTE IMMEDIATE), Fine-Grained Auditing, Virtual Private Database

Previous TopicPackages End of CourseNo further topic