Oracle Mutating Table Error – Complete Interview Guide

Oracle's mutating table error is a common interview and real-time development topic related to row-level triggers. It usually occurs when a trigger tries to query or modify the same table that is currently being changed by an INSERT, UPDATE, or DELETE statement.

1. What is a mutating table error?

A mutating table error occurs when a row-level trigger tries to query or modify the same table that is currently being changed by the triggering DML statement.

CREATE OR REPLACE TRIGGER trg_emp
AFTER UPDATE ON employees
FOR EACH ROW
DECLARE
    v_count NUMBER;
BEGIN
    SELECT COUNT(*)
    INTO v_count
    FROM employees;
END;
/

Then:

UPDATE employees
SET salary = salary + 1000;

This can produce:

ORA-04091: table EMPLOYEES is mutating

2. Why does the mutating-table error occur?

If Oracle is still processing rows for an UPDATE, a row-level trigger firing during that operation cannot safely query the same table and see a consistent state. That is why Oracle raises ORA-04091.

UPDATE EMPLOYEES
      ↓
Row 1 → Trigger
Row 2 → Trigger
Row 3 → Trigger
...
Row 100 → Trigger
      ↓
Statement completes

3. What is the classic mutating-table example?

A common example is checking a rule by querying the same table inside a row-level trigger.

CREATE OR REPLACE TRIGGER trg_salary_check
BEFORE UPDATE OF salary ON employees
FOR EACH ROW
DECLARE
    v_avg_salary NUMBER;
BEGIN
    SELECT AVG(salary)
    INTO v_avg_salary
    FROM employees;

    IF :NEW.salary > v_avg_salary * 2 THEN
        RAISE_APPLICATION_ERROR(
            -20001,
            'Salary is too high'
        );
    END IF;
END;
/

Running an UPDATE on employees can result in ORA-04091 because the trigger queries the same table while it is being updated.

4. Which triggers commonly cause mutating-table errors?

The problem is specifically associated with row-level triggers that access their triggering table during the DML operation. Statement-level triggers do not have the same row-by-row mutating-table problem.

5. Does every query on the same table cause a mutating-table error?

The problematic case is a row-level trigger querying its own triggering table during the triggering DML operation.

6. Can a row-level trigger modify the same table?

Trying to modify the triggering table from its own row-level trigger can also cause problems, including mutating-table errors and recursive behavior.

CREATE OR REPLACE TRIGGER trg_emp
AFTER UPDATE ON employees
FOR EACH ROW
BEGIN
    UPDATE employees
    SET last_modified = SYSDATE
    WHERE employee_id = :NEW.employee_id;
END;
/

7. How can we avoid a mutating-table error?

Common solutions are to use a statement-level trigger, use a compound trigger, use a collection to store row information, move processing to the AFTER STATEMENT section, or redesign the business rule.

Solution 1 — Use a Statement-Level Trigger

8. How can a statement-level trigger help?

A statement-level trigger executes once for the complete DML statement. At that point, the row-by-row processing has completed, so the table can be queried safely.

CREATE OR REPLACE TRIGGER trg_emp_check
AFTER UPDATE ON employees
DECLARE
    v_count NUMBER;
BEGIN
    SELECT COUNT(*)
    INTO v_count
    FROM employees;

    DBMS_OUTPUT.PUT_LINE(
        'Employee count: ' || v_count
    );
END;
/

9. What is the limitation of a statement-level trigger?

A statement-level trigger does not have individual row context, so you cannot use :OLD and :NEW in the statement-level section.

Solution 2 — Compound Trigger

10. Why is a compound trigger useful for mutating-table errors?

A compound trigger allows you to capture information during row processing and access it later in the AFTER STATEMENT section.

CREATE OR REPLACE TRIGGER trg_salary_check
FOR UPDATE OF salary ON employees
COMPOUND TRIGGER

    AFTER STATEMENT IS
        v_avg_salary NUMBER;
    BEGIN
        SELECT AVG(salary)
        INTO v_avg_salary
        FROM employees;

        DBMS_OUTPUT.PUT_LINE(
            'Average salary: ' || v_avg_salary
        );
    END AFTER STATEMENT;

END;
/

11. How can a compound trigger collect changed employee IDs?

CREATE OR REPLACE TRIGGER trg_emp_collect
FOR UPDATE OF salary ON employees
COMPOUND TRIGGER

    TYPE emp_id_list IS
        TABLE OF employees.employee_id%TYPE;

    g_emp_ids emp_id_list := emp_id_list();

    AFTER EACH ROW IS
    BEGIN
        g_emp_ids.EXTEND;
        g_emp_ids(g_emp_ids.COUNT) := :NEW.employee_id;
    END AFTER EACH ROW;

    AFTER STATEMENT IS
    BEGIN
        FOR i IN 1 .. g_emp_ids.COUNT LOOP
            DBMS_OUTPUT.PUT_LINE(
                'Updated Employee: ' || g_emp_ids(i)
            );
        END LOOP;
    END AFTER STATEMENT;

END;
/

12. How does this solve the mutating-table problem?

During AFTER EACH ROW, the trigger only collects data and does not query employees. After the update completes, AFTER STATEMENT runs and can safely query the table.

Solution 3 — Use a Collection

13. Why use a collection?

A collection allows the row-level trigger section to store information without querying the mutating table.

CREATE OR REPLACE TRIGGER trg_emp_changes
FOR UPDATE ON employees
COMPOUND TRIGGER

    TYPE emp_list IS
        TABLE OF employees.employee_id%TYPE;

    g_ids emp_list := emp_list();

    AFTER EACH ROW IS
    BEGIN
        g_ids.EXTEND;
        g_ids(g_ids.COUNT) := :NEW.employee_id;
    END AFTER EACH ROW;

    AFTER STATEMENT IS
    BEGIN
        FOR i IN 1 .. g_ids.COUNT LOOP
            DBMS_OUTPUT.PUT_LINE(
                'Employee: ' || g_ids(i)
            );
        END LOOP;
    END AFTER STATEMENT;

END;
/

Solution 4 — Use an Autonomous Transaction?

No. An autonomous transaction should not be treated as a general solution for mutating-table errors. For this problem, prefer compound triggers, statement-level processing, collections, or redesigning the business rule.

14. Can a package help solve a mutating-table problem?

Historically, one solution was to store values in a package collection and process them later in a statement-level trigger. A compound trigger is usually cleaner and easier to maintain.

15. Classic Interview Question

Question: Why does this trigger cause ORA-04091?

CREATE OR REPLACE TRIGGER trg_emp
AFTER UPDATE ON employees
FOR EACH ROW
DECLARE
    v_total NUMBER;
BEGIN
    SELECT SUM(salary)
    INTO v_total
    FROM employees;
END;
/

Answer: Because employees is being modified and the row-level trigger attempts to query the same table while it is mutating.

16. How would you fix that trigger?

Move the query to statement-level processing.

CREATE OR REPLACE TRIGGER trg_emp
AFTER UPDATE ON employees
DECLARE
    v_total NUMBER;
BEGIN
    SELECT SUM(salary)
    INTO v_total
    FROM employees;

    DBMS_OUTPUT.PUT_LINE(
        'Total salary: ' || v_total
    );
END;
/

17. What if I need :NEW values as well?

Use a compound trigger.

CREATE OR REPLACE TRIGGER trg_emp
FOR UPDATE ON employees
COMPOUND TRIGGER

    TYPE id_list IS
        TABLE OF employees.employee_id%TYPE;

    g_ids id_list := id_list();

    AFTER EACH ROW IS
    BEGIN
        g_ids.EXTEND;
        g_ids(g_ids.COUNT) := :NEW.employee_id;
    END AFTER EACH ROW;

    AFTER STATEMENT IS
        v_total NUMBER;
    BEGIN
        SELECT SUM(salary)
        INTO v_total
        FROM employees;

        DBMS_OUTPUT.PUT_LINE(
            'Total salary: ' || v_total
        );

        FOR i IN 1 .. g_ids.COUNT LOOP
            DBMS_OUTPUT.PUT_LINE(
                'Changed employee: ' || g_ids(i)
            );
        END LOOP;
    END AFTER STATEMENT;

END;
/

18. Is a mutating-table error a compilation error?

Usually no. The trigger can compile successfully, but the error occurs at runtime when the triggering DML statement executes.

19. What is ORA-04091?

The key Oracle error is ORA-04091: table string.string is mutating, trigger/function may not see it. It indicates that a row-level trigger or function called from it is attempting to access a table that is currently undergoing the triggering DML operation.

20. What is ORA-04088?

You may also see ORA-04088: error during execution of trigger, which commonly appears along with an underlying error such as ORA-04091.

21. Does a SELECT from another table cause a mutating-table error?

Normally no. The mutating-table issue is specifically about accessing the table currently undergoing the triggering operation.

22. Does a foreign-key relationship automatically cause a mutating-table error?

No. The issue is typically caused by a row-level trigger or function attempting to access the table that is currently being modified.

23. Can a function called by a trigger cause a mutating-table error?

Yes. If the function queries the same table that is being modified, the same mutating-table problem can occur.

24. Can a DELETE statement cause a mutating-table error?

Yes. The issue is not limited to UPDATE statements. A row-level trigger fired by an INSERT, UPDATE, or DELETE can encounter the problem if it tries to access its triggering table.

CREATE OR REPLACE TRIGGER trg_emp_delete
AFTER DELETE ON employees
FOR EACH ROW
DECLARE
    v_count NUMBER;
BEGIN
    SELECT COUNT(*)
    INTO v_count
    FROM employees;
END;
/

25. Can an INSERT statement cause a mutating-table error?

Yes. A row-level INSERT trigger that queries the same table can also encounter a mutating-table error.

CREATE OR REPLACE TRIGGER trg_emp_insert
AFTER INSERT ON employees
FOR EACH ROW
DECLARE
    v_count NUMBER;
BEGIN
    SELECT COUNT(*)
    INTO v_count
    FROM employees;
END;
/

26. What is the difference between row-level and statement-level triggers?

Feature Row-Level Trigger Statement-Level Trigger
Execution Once for each affected row Once for the complete statement
:OLD and :NEW Available Not available
Mutating-table risk Possible No row-by-row mutating-table problem
Best use Row-specific processing Statement-level processing

27. Can a compound trigger contain both row-level and statement-level logic?

Yes. This is one of the major advantages of a compound trigger. It can contain sections such as BEFORE STATEMENT, BEFORE EACH ROW, AFTER EACH ROW, and AFTER STATEMENT.

CREATE OR REPLACE TRIGGER trg_emp
FOR UPDATE ON employees
COMPOUND TRIGGER

    BEFORE STATEMENT IS
    BEGIN
        DBMS_OUTPUT.PUT_LINE('Statement started');
    END BEFORE STATEMENT;

    AFTER EACH ROW IS
    BEGIN
        DBMS_OUTPUT.PUT_LINE(
            'Employee: ' || :NEW.employee_id
        );
    END AFTER EACH ROW;

    AFTER STATEMENT IS
    BEGIN
        DBMS_OUTPUT.PUT_LINE('Statement completed');
    END AFTER STATEMENT;

END;
/

28. What is the main advantage of a compound trigger?

A compound trigger allows shared state between different timing sections of the same trigger and is particularly useful when row-level information must be collected and processed after the complete DML statement.

Quick Interview Table

Question Answer
What is a mutating table? A table currently being modified by the triggering DML statement
Common error? ORA-04091
Usually associated with? Row-level triggers
Can the trigger query another table? Yes
Can the trigger safely query the triggering table row-by-row? Generally no
Can AFTER STATEMENT query the table? Yes
Best modern solution? Compound trigger when row + statement processing is needed
Can collections help? Yes
Does :OLD/:NEW cause the problem? No
Can a called function cause it? Yes
Can INSERT cause it? Yes
Can DELETE cause it? Yes
Can a compound trigger solve it? Yes, when processing is moved appropriately to AFTER STATEMENT

Key Interview Point

A mutating-table error (ORA-04091) occurs when row-level trigger processing attempts to access the table that is currently being modified. The usual solutions are to move the table access to statement-level processing or use a compound trigger to collect row-level information and process it after the statement completes.

Interview Tip:

If an interviewer asks how to solve ORA-04091, a strong answer is: "Avoid querying the mutating table from the row-level trigger. Move the query to AFTER STATEMENT processing, or use a compound trigger to collect row-level information and process it after the DML statement completes."

```html

Absolutely. Here’s a practical Oracle Schema-Level Trigger FAQ with examples, aimed at interviews and real-world PL/SQL.

What is a schema-level trigger?

A schema-level trigger is a trigger associated with a particular schema/user. It is mainly used to monitor or control DDL and database events occurring in that schema.

The key idea is:
User / Schema
       ↓
DDL or database event
       ↓
Schema-level trigger
       ↓
Trigger logic executes

Typical uses include:

  • Auditing CREATE, ALTER, and DROP
  • Preventing certain DDL operations
  • Logging schema changes
  • Auditing user logon/logoff events
  • Enforcing development/production rules

1. What is a schema-level trigger?

A schema-level trigger is created in a schema and responds to events associated with that schema.

For example:

CREATE OR REPLACE TRIGGER trg_schema_ddl
AFTER CREATE OR ALTER OR DROP ON SCHEMA
BEGIN

    DBMS_OUTPUT.PUT_LINE(
        'DDL operation occurred'
    );

END;
/

This trigger belongs to the schema in which it is created.

It can respond to DDL operations such as:

CREATE
ALTER
DROP

2. What is the difference between a schema trigger and a database trigger?

This is a common interview question.

Schema-level trigger

AFTER CREATE ON SCHEMA

It applies to events in the current schema.

Database-level trigger

AFTER CREATE ON DATABASE

It can respond to events across the database, subject to Oracle privileges and event rules.

SCHEMA TRIGGER
      ↓
One schema


DATABASE TRIGGER
      ↓
Entire database

3. What events can a schema trigger handle?

Schema triggers are commonly used with DDL events such as:

  • CREATE
  • ALTER
  • DROP
  • TRUNCATE
  • ANALYZE
  • GRANT
  • REVOKE

The exact events available depend on the trigger/event type and Oracle version.

Database events can also include events such as:

  • LOGON
  • LOGOFF
  • SERVERERROR

where supported by Oracle's trigger syntax.

4. Can a schema trigger be used for INSERT, UPDATE and DELETE?

No, not as a schema-level event trigger.

For normal table DML:

INSERT
UPDATE
DELETE

you use a table trigger:

CREATE OR REPLACE TRIGGER trg_emp
AFTER INSERT OR UPDATE OR DELETE ON employees
FOR EACH ROW
BEGIN

    ...

END;
/

Schema triggers are primarily used for schema/database events, especially DDL.

INSERT / UPDATE / DELETE
        ↓
Table trigger


CREATE / ALTER / DROP
        ↓
Schema / database trigger

5. How do I create a trigger for all DDL in my schema?

Example:

CREATE OR REPLACE TRIGGER trg_schema_ddl
AFTER CREATE OR ALTER OR DROP ON SCHEMA
BEGIN

    DBMS_OUTPUT.PUT_LINE(
        'DDL operation performed'
    );

END;
/

Now if you execute:

CREATE TABLE test_table (
    id NUMBER
);

the trigger fires.

Similarly:

ALTER TABLE test_table
ADD name VARCHAR2(100);

or:

DROP TABLE test_table;

can cause the trigger to fire.

6. Can we determine which DDL operation occurred?

Yes.

Oracle provides event predicates such as SYSEVENT.

CREATE OR REPLACE TRIGGER trg_schema_ddl
AFTER CREATE OR ALTER OR DROP ON SCHEMA
BEGIN

    DBMS_OUTPUT.PUT_LINE(
        'Event = ' || SYSEVENT
    );

END;
/

Depending on the event, you can determine whether the operation was:

CREATE
ALTER
DROP

7. Can a schema trigger determine the object name?

Yes.

Oracle provides event attributes that can be used in DDL/database event triggers.

Common attributes include:

  • ORA_DICT_OBJ_NAME
  • ORA_DICT_OBJ_TYPE
  • ORA_DICT_OBJ_OWNER

For example:

CREATE OR REPLACE TRIGGER trg_schema_ddl
AFTER CREATE OR ALTER OR DROP ON SCHEMA
BEGIN

    DBMS_OUTPUT.PUT_LINE(
        'Event      = ' || SYSEVENT
    );

    DBMS_OUTPUT.PUT_LINE(
        'Object     = ' || ORA_DICT_OBJ_NAME
    );

    DBMS_OUTPUT.PUT_LINE(
        'Object Type = ' || ORA_DICT_OBJ_TYPE
    );

END;
/

If you execute:

CREATE TABLE employees (
    employee_id NUMBER
);

you could get information similar to:

Event       = CREATE
Object      = EMPLOYEES
Object Type = TABLE

8. Can we audit DDL operations using a schema trigger?

Yes. This is one of the most common use cases.

Create an audit table:

CREATE TABLE ddl_audit (
    username       VARCHAR2(100),
    event_name     VARCHAR2(30),
    object_name    VARCHAR2(128),
    object_type    VARCHAR2(30),
    event_time     TIMESTAMP
);

Create the trigger:

CREATE OR REPLACE TRIGGER trg_ddl_audit
AFTER CREATE OR ALTER OR DROP ON SCHEMA
BEGIN

    INSERT INTO ddl_audit
    (
        username,
        event_name,
        object_name,
        object_type,
        event_time
    )
    VALUES
    (
        SYS_CONTEXT('USERENV', 'SESSION_USER'),
        SYSEVENT,
        ORA_DICT_OBJ_NAME,
        ORA_DICT_OBJ_TYPE,
        SYSTIMESTAMP
    );

END;
/

Now:

CREATE TABLE test_table (
    id NUMBER
);

can create an audit record such as:

USERNAME   EVENT   OBJECT_NAME   OBJECT_TYPE   EVENT_TIME
--------------------------------------------------------------
SCOTT      CREATE  TEST_TABLE    TABLE         11-AUG-2026 ...

9. Can a schema trigger prevent DDL?

Yes.

This is another important use case.

Suppose you don't want users to drop tables.

CREATE OR REPLACE TRIGGER trg_prevent_drop
BEFORE DROP ON SCHEMA
BEGIN

    RAISE_APPLICATION_ERROR(
        -20001,
        'DROP operations are not allowed'
    );

END;
/

Now:

DROP TABLE employees;

will be blocked by the trigger.

DROP TABLE
    ↓
BEFORE DROP trigger
    ↓
RAISE_APPLICATION_ERROR
    ↓
DROP prevented

10. What is the difference between BEFORE and AFTER schema triggers?

BEFORE

Runs before the event completes.

Example:

CREATE OR REPLACE TRIGGER trg_before_drop
BEFORE DROP ON SCHEMA
BEGIN

    ...

END;
/

Useful for:

  • Validation
  • Blocking operations
  • Security checks

AFTER

Runs after the event.

Example:

CREATE OR REPLACE TRIGGER trg_after_create
AFTER CREATE ON SCHEMA
BEGIN

    ...

END;
/

Useful for:

  • Auditing
  • Logging
  • Notifications
  • Post-DDL processing

11. Can a schema trigger use FOR EACH ROW?

No.

This is a very important distinction.

Schema/database event triggers are not row-level DML triggers.

You don't write:

FOR EACH ROW

for:

AFTER CREATE ON SCHEMA

Correct:

CREATE OR REPLACE TRIGGER trg_ddl
AFTER CREATE ON SCHEMA
BEGIN

    ...

END;
/

12. Can a schema trigger use :OLD and :NEW?

No.

:OLD and :NEW are associated with row-level DML triggers.

They are used for:

  • INSERT
  • UPDATE
  • DELETE

on table rows.

DDL event triggers instead use event attributes such as:

SYSEVENT
ORA_DICT_OBJ_NAME
ORA_DICT_OBJ_TYPE
ORA_DICT_OBJ_OWNER

13. What are Oracle event attributes?

Oracle provides special functions/attributes for database event triggers.

Attribute Meaning
SYSEVENT Event that caused the trigger
ORA_DICT_OBJ_NAME Object name
ORA_DICT_OBJ_TYPE Object type
ORA_DICT_OBJ_OWNER Object owner
ORA_LOGIN_USER Login user
ORA_CLIENT_IP_ADDRESS Client IP address, where applicable
ORA_SYSEVENT Event information in applicable event-trigger contexts

For example:

CREATE OR REPLACE TRIGGER trg_ddl
AFTER CREATE OR ALTER OR DROP ON SCHEMA
BEGIN

    DBMS_OUTPUT.PUT_LINE(
        'User = ' || ORA_LOGIN_USER
    );

    DBMS_OUTPUT.PUT_LINE(
        'Event = ' || SYSEVENT
    );

    DBMS_OUTPUT.PUT_LINE(
        'Object = ' || ORA_DICT_OBJ_NAME
    );

END;
/

14. Can a schema trigger audit only tables?

Yes.

You can inspect ORA_DICT_OBJ_TYPE and conditionally process the event.

CREATE OR REPLACE TRIGGER trg_table_ddl
AFTER CREATE OR ALTER OR DROP ON SCHEMA
BEGIN

    IF ORA_DICT_OBJ_TYPE = 'TABLE' THEN

        INSERT INTO ddl_audit
        (
            username,
            event_name,
            object_name,
            object_type,
            event_time
        )
        VALUES
        (
            ORA_LOGIN_USER,
            SYSEVENT,
            ORA_DICT_OBJ_NAME,
            ORA_DICT_OBJ_TYPE,
            SYSTIMESTAMP
        );

    END IF;

END;
/

Now the trigger is interested only in table-related DDL.

15. Can a schema trigger audit procedure creation?

Yes.

CREATE OR REPLACE TRIGGER trg_ddl_audit
AFTER CREATE OR ALTER OR DROP ON SCHEMA
BEGIN

    IF ORA_DICT_OBJ_TYPE = 'PROCEDURE' THEN

        INSERT INTO ddl_audit
        (
            username,
            event_name,
            object_name,
            object_type,
            event_time
        )
        VALUES
        (
            ORA_LOGIN_USER,
            SYSEVENT,
            ORA_DICT_OBJ_NAME,
            ORA_DICT_OBJ_TYPE,
            SYSTIMESTAMP
        );

    END IF;

END;
/

You can similarly check for:

TABLE
VIEW
INDEX
PROCEDURE
FUNCTION
PACKAGE
TRIGGER
SEQUENCE

and other supported object types.

16. Can a schema trigger handle LOGON?

Yes. Event triggers can be used for logon-related processing.

CREATE OR REPLACE TRIGGER trg_schema_logon
AFTER LOGON ON SCHEMA
BEGIN

    DBMS_OUTPUT.PUT_LINE(
        'User logged in: ' || ORA_LOGIN_USER
    );

END;
/

A logon trigger can be useful for things such as:

  • Session initialization
  • Security checks
  • Setting application context
  • Auditing

17. Can we set session values using a LOGON trigger?

Yes.

A logon trigger can initialize session/application context or session settings.

Conceptually:

CREATE OR REPLACE TRIGGER trg_schema_logon
AFTER LOGON ON SCHEMA
BEGIN

    DBMS_APPLICATION_INFO.SET_MODULE(
        module_name => 'EMPLOYEE_APP',
        action_name => 'LOGIN'
    );

END;
/

This can help identify application sessions in monitoring and auditing.

18. What is a LOGOFF trigger?

A logoff event trigger can execute when a session logs off.

Example:

CREATE OR REPLACE TRIGGER trg_schema_logoff
AFTER LOGOFF ON SCHEMA
BEGIN

    INSERT INTO session_audit
    (
        username,
        action_time
    )
    VALUES
    (
        ORA_LOGIN_USER,
        SYSTIMESTAMP
    );

END;
/

The exact usefulness depends on the auditing requirements and database configuration.

19. Can a schema trigger handle errors?

Oracle also supports event triggers for server errors.

For example:

CREATE OR REPLACE TRIGGER trg_server_error
AFTER SERVERERROR ON SCHEMA
BEGIN

    INSERT INTO error_audit
    (
        username,
        error_time
    )
    VALUES
    (
        ORA_LOGIN_USER,
        SYSTIMESTAMP
    );

END;
/

This can be used to capture information when relevant database/server errors occur.

For detailed error information, Oracle provides additional event functions such as:

ORA_SERVER_ERROR
ORA_SERVER_ERROR_MSG

depending on the information you need.

20. Can a schema trigger be used to prevent TRUNCATE?

Yes.

CREATE OR REPLACE TRIGGER trg_prevent_truncate
BEFORE TRUNCATE ON SCHEMA
BEGIN

    RAISE_APPLICATION_ERROR(
        -20002,
        'TRUNCATE is not allowed in this schema'
    );

END;
/

Then:

TRUNCATE TABLE employees;

can be prevented.

This can be useful in environments where developers should not accidentally remove all rows.

21. Can a schema trigger prevent DROP TABLE but allow other DDL?

Yes.

CREATE OR REPLACE TRIGGER trg_control_ddl
BEFORE DROP ON SCHEMA
BEGIN

    IF ORA_DICT_OBJ_TYPE = 'TABLE' THEN

        RAISE_APPLICATION_ERROR(
            -20010,
            'Dropping tables is not allowed'
        );

    END IF;

END;
/

The idea is:

DROP TABLE
    ↓
Blocked

While dropping some other supported object can potentially be allowed.

22. Can a schema trigger check the object name?

Yes.

For example, prevent dropping a particular table:

CREATE OR REPLACE TRIGGER trg_protect_emp
BEFORE DROP ON SCHEMA
BEGIN

    IF ORA_DICT_OBJ_TYPE = 'TABLE'
       AND ORA_DICT_OBJ_NAME = 'EMPLOYEES'
    THEN

        RAISE_APPLICATION_ERROR(
            -20011,
            'EMPLOYEES table is protected'
        );

    END IF;

END;
/

Then:

DROP TABLE employees;

is rejected.

23. Can a schema trigger execute DML?

Yes, subject to the normal trigger restrictions.

For example, an audit trigger can execute:

INSERT INTO ddl_audit
...

However, you should be careful about transaction behavior.

The audit DML normally participates in the same transaction as the triggering operation.

24. Can a schema trigger use COMMIT?

No.

For example:

CREATE OR REPLACE TRIGGER trg_ddl
AFTER CREATE ON SCHEMA
BEGIN

    INSERT INTO ddl_audit
    VALUES (...);

    COMMIT;

END;
/

is not allowed.

Triggers cannot normally issue:

COMMIT
ROLLBACK

because the trigger is part of the transaction/event execution.

25. What happens if the schema trigger raises an exception?

Suppose:

CREATE OR REPLACE TRIGGER trg_protect
BEFORE DROP ON SCHEMA
BEGIN

    RAISE_APPLICATION_ERROR(
        -20001,
        'DROP is not permitted'
    );

END;
/

Then:

DROP TABLE employees;

fails.

The trigger effectively acts as a gate:

DROP TABLE
    ↓
BEFORE DROP trigger
    ↓
Exception
    ↓
DDL operation fails

26. What is the difference between a schema trigger and a table trigger?

Feature Table Trigger Schema Trigger
Main purpose DML processing DDL/database events
Object Table Schema/event
INSERT
UPDATE
DELETE
CREATE
ALTER
DROP
:OLD Row-level
:NEW Row-level
FOR EACH ROW Possible
DDL auditing Not appropriate

27. Schema trigger vs database trigger

This is another important interview question.

Schema trigger

CREATE OR REPLACE TRIGGER trg_schema_ddl
AFTER CREATE ON SCHEMA
BEGIN

    ...

END;
/

Scope: Current schema

Database trigger

CREATE OR REPLACE TRIGGER trg_database_ddl
AFTER CREATE ON DATABASE
BEGIN

    ...

END;
/

Scope: Database-wide event

DDL EVENT
    |
    +-------------------+
    |                   |
    ↓                   ↓
SCHEMA              DATABASE
TRIGGER              TRIGGER
    |                   |
    ↓                   ↓
One schema's       Database-wide
events             scope

28. What is a good real-world DDL auditing example?

Create an audit table:

CREATE TABLE schema_ddl_audit (
    username       VARCHAR2(128),
    event_name     VARCHAR2(30),
    object_owner   VARCHAR2(128),
    object_name    VARCHAR2(128),
    object_type    VARCHAR2(30),
    event_time     TIMESTAMP
);

Trigger:

CREATE OR REPLACE TRIGGER trg_schema_ddl_audit
AFTER CREATE OR ALTER OR DROP ON SCHEMA
BEGIN

    INSERT INTO schema_ddl_audit
    (
        username,
        event_name,
        object_owner,
        object_name,
        object_type,
        event_time
    )
    VALUES
    (
        ORA_LOGIN_USER,
        SYSEVENT,
        ORA_DICT_OBJ_OWNER,
        ORA_DICT_OBJ_NAME,
        ORA_DICT_OBJ_TYPE,
        SYSTIMESTAMP
    );

END;
/

Then:

CREATE TABLE test_table (
    id NUMBER
);

followed by:

ALTER TABLE test_table
ADD name VARCHAR2(100);

and:

DROP TABLE test_table;

can produce audit records similar to:

USERNAME   EVENT   OWNER   OBJECT       TYPE
---------------------------------------------------
SCOTT      CREATE  SCOTT   TEST_TABLE   TABLE
SCOTT      ALTER   SCOTT   TEST_TABLE   TABLE
SCOTT      DROP    SCOTT   TEST_TABLE   TABLE

29. Can we use schema triggers for production protection?

Yes, but use them carefully.

For example, you could prevent developers from dropping critical objects:

CREATE OR REPLACE TRIGGER trg_protect_objects
BEFORE DROP ON SCHEMA
BEGIN

    IF ORA_DICT_OBJ_NAME IN
       ('EMPLOYEES', 'DEPARTMENTS', 'PAYROLL')
    THEN

        RAISE_APPLICATION_ERROR(
            -20020,
            'Protected object cannot be dropped'
        );

    END IF;

END;
/

This can provide an additional safety mechanism.

Important:

Permissions and proper deployment controls should remain the primary security mechanism. Triggers shouldn't be treated as a replacement for Oracle privileges, roles, or controlled deployment.

30. What are common schema-trigger interview traps?

Trap 1

Question: Does a schema trigger fire for every inserted row?

Answer: ❌ No.

Schema event triggers are not row-level DML triggers.

Trap 2

Question: Can a schema trigger use :OLD and :NEW?

Answer: ❌ No.

Those are row-level DML correlation variables.

Trap 3

Question: Can a schema trigger use FOR EACH ROW?

Answer: ❌ No.

DDL/event triggers aren't row-level table triggers.

Trap 4

Question: Can a schema trigger audit CREATE TABLE?

Answer: ✅ Yes.

For example:

AFTER CREATE ON SCHEMA

Trap 5

Question: Can a schema trigger prevent DROP TABLE?

Answer: ✅ Yes.

Use:

BEFORE DROP ON SCHEMA

and raise an exception.

Trap 6

Question: Can a schema trigger perform COMMIT?

Answer: ❌ No.

Trap 7

Question: What's the difference between schema and database trigger?

Answer:

Schema trigger
→ Events in a particular schema

Database trigger
→ Database-wide event scope

31. ⭐ Schema-Level Trigger Cheat Sheet

Feature Schema Trigger
Main purpose DDL / database events
CREATE
ALTER
DROP
TRUNCATE
LOGON ✅ Where supported
LOGOFF ✅ Where supported
SERVERERROR ✅ Where supported
INSERT ❌ Not a table DML trigger
UPDATE ❌ Not a table DML trigger
DELETE ❌ Not a table DML trigger
:OLD
:NEW
FOR EACH ROW
BEFORE
AFTER
COMMIT

The Easiest Way to Remember

Table trigger: "A row changed."

Schema trigger: "Something happened to my schema."

For example:

CREATE TABLE
ALTER TABLE
DROP TABLE
TRUNCATE TABLE
      ↓
Schema-level trigger

Schema Trigger vs Database Trigger

SCHEMA                         DATABASE
   ↓                               ↓
One schema's events          Database-wide events
   ↓                               ↓
Schema-level trigger         Database-level trigger
Interview Tip:

If the interviewer asks, "What is the difference between a schema-level trigger and a table trigger?", a strong answer is:

"A table trigger is primarily used for row-level DML operations such as INSERT, UPDATE and DELETE, while a schema-level trigger is used for schema and database events such as CREATE, ALTER, DROP and other supported events. Schema triggers do not use :OLD, :NEW or FOR EACH ROW."
```

Oracle Compound Trigger FAQS With Examples

Absolutely. Here’s a practical Oracle Compound Trigger FAQ with examples, aimed at interviews and real-world PL/SQL.

What is a compound trigger?

A compound trigger is a special Oracle trigger that can contain multiple timing sections in a single trigger. It is especially useful for sharing state between row-level and statement-level processing and for solving many mutating-table scenarios.

Basic Structure

CREATE OR REPLACE TRIGGER trigger_name
FOR INSERT OR UPDATE OR DELETE ON table_name
COMPOUND TRIGGER

    -- Common variables
    -- Shared by all timing sections

    BEFORE STATEMENT IS
    BEGIN
        ...
    END BEFORE STATEMENT;

    BEFORE EACH ROW IS
    BEGIN
        ...
    END BEFORE EACH ROW;

    AFTER EACH ROW IS
    BEGIN
        ...
    END AFTER EACH ROW;

    AFTER STATEMENT IS
    BEGIN
        ...
    END AFTER STATEMENT;

END trigger_name;
/

Important: Not every timing section is mandatory.

1. What is a compound trigger?

A compound trigger combines multiple trigger timing points into one trigger.

For example:

BEFORE STATEMENT
       ↓
BEFORE EACH ROW
       ↓
AFTER EACH ROW
       ↓
AFTER STATEMENT

Instead of creating four separate triggers, related logic can be placed into one compound trigger.

Example:

CREATE OR REPLACE TRIGGER trg_emp
FOR UPDATE ON employees
COMPOUND TRIGGER

    BEFORE STATEMENT IS
    BEGIN
        DBMS_OUTPUT.PUT_LINE('Before statement');
    END BEFORE STATEMENT;

    AFTER EACH ROW IS
    BEGIN
        DBMS_OUTPUT.PUT_LINE('After row');
    END AFTER EACH ROW;

    AFTER STATEMENT IS
    BEGIN
        DBMS_OUTPUT.PUT_LINE('After statement');
    END AFTER STATEMENT;

END trg_emp;
/

2. Why do we need compound triggers?

One of the most important uses of compound triggers is handling mutating-table scenarios.

Consider this row-level trigger:

CREATE OR REPLACE TRIGGER trg_emp
AFTER UPDATE ON employees
FOR EACH ROW
DECLARE
    v_count NUMBER;
BEGIN

    SELECT COUNT(*)
    INTO v_count
    FROM employees;

END;
/

This can result in:

ORA-04091: table EMPLOYEES is mutating

A compound trigger allows you to collect information during row processing and perform the table-level processing after the statement.

AFTER EACH ROW
       ↓
Collect information
       ↓
AFTER STATEMENT
       ↓
Query/process EMPLOYEES

3. What are the timing sections of a compound trigger?

For a DML compound trigger, the common timing sections are:

BEFORE STATEMENT
BEFORE EACH ROW
AFTER EACH ROW
AFTER STATEMENT

Example:

CREATE OR REPLACE TRIGGER trg_emp
FOR UPDATE ON employees
COMPOUND TRIGGER

    BEFORE STATEMENT IS
    BEGIN
        NULL;
    END BEFORE STATEMENT;

    BEFORE EACH ROW IS
    BEGIN
        NULL;
    END BEFORE EACH ROW;

    AFTER EACH ROW IS
    BEGIN
        NULL;
    END AFTER EACH ROW;

    AFTER STATEMENT IS
    BEGIN
        NULL;
    END AFTER STATEMENT;

END trg_emp;
/

You don't have to use all four sections.

4. Is a compound trigger row-level or statement-level?

This is a common interview trap.

A compound trigger can contain both statement-level and row-level timing sections.

Timing Section Type
BEFORE STATEMENT Statement-level
BEFORE EACH ROW Row-level
AFTER EACH ROW Row-level
AFTER STATEMENT Statement-level
Interview answer: A compound trigger is not simply a row-level trigger or a statement-level trigger. It can combine both types of timing sections.

5. Can a compound trigger have only one timing section?

Yes.

For example:

CREATE OR REPLACE TRIGGER trg_emp
FOR UPDATE ON employees
COMPOUND TRIGGER

    AFTER EACH ROW IS
    BEGIN
        DBMS_OUTPUT.PUT_LINE(
            'Employee updated'
        );
    END AFTER EACH ROW;

END trg_emp;
/

This is a valid compound trigger.

6. What is the biggest advantage of a compound trigger?

One of the biggest advantages is the ability to share state between timing sections.

For example:

CREATE OR REPLACE TRIGGER trg_emp
FOR UPDATE ON employees
COMPOUND TRIGGER

    TYPE t_emp_ids IS TABLE OF employees.employee_id%TYPE;

    g_emp_ids t_emp_ids := t_emp_ids();

    AFTER EACH ROW IS
    BEGIN

        g_emp_ids.EXTEND;
        g_emp_ids(g_emp_ids.COUNT) := :NEW.employee_id;

    END AFTER EACH ROW;

    AFTER STATEMENT IS
    BEGIN

        FOR i IN 1 .. g_emp_ids.COUNT LOOP

            DBMS_OUTPUT.PUT_LINE(
                'Employee ID = ' || g_emp_ids(i)
            );

        END LOOP;

    END AFTER STATEMENT;

END trg_emp;
/

The collection g_emp_ids is available to both sections during the triggering statement.

7. How does state sharing work?

Suppose the following statement updates three employees:

UPDATE employees
SET salary = salary + 1000;

The execution is conceptually:

BEFORE STATEMENT
       ↓
Initialize shared state
       ↓
AFTER EACH ROW
       ↓
Employee 1 added
       ↓
AFTER EACH ROW
       ↓
Employee 2 added
       ↓
AFTER EACH ROW
       ↓
Employee 3 added
       ↓
AFTER STATEMENT
       ↓
Process all 3 employees

This shared-state capability is one of the most powerful features of compound triggers.

8. Does the shared state survive for the entire statement?

Yes.

Variables declared in the compound trigger's declarative section can be shared by the timing sections during that triggering statement.

CREATE OR REPLACE TRIGGER trg_emp
FOR UPDATE ON employees
COMPOUND TRIGGER

    g_count NUMBER := 0;

    AFTER EACH ROW IS
    BEGIN
        g_count := g_count + 1;
    END AFTER EACH ROW;

    AFTER STATEMENT IS
    BEGIN
        DBMS_OUTPUT.PUT_LINE(
            'Rows affected = ' || g_count
        );
    END AFTER STATEMENT;

END trg_emp;
/

If 500 rows are updated:

Rows affected = 500

9. What happens to compound-trigger variables after the statement?

They represent statement-duration state.

After the triggering statement completes, that state is not retained for a future statement.

UPDATE statement #1
       ↓
g_count = 100
       ↓
Statement finishes
       ↓
State is discarded

UPDATE statement #2
       ↓
New trigger execution
       ↓
New shared state

This is an important difference when comparing compound-trigger state with package-level global variables.

10. How can a compound trigger solve a mutating-table problem?

Suppose we want to collect employee IDs during an update and then query employees after the update.

Compound trigger:

CREATE OR REPLACE TRIGGER trg_emp_check
FOR UPDATE ON employees
COMPOUND TRIGGER

    TYPE t_emp_ids IS TABLE OF employees.employee_id%TYPE;

    g_emp_ids t_emp_ids := t_emp_ids();

    AFTER EACH ROW IS
    BEGIN

        g_emp_ids.EXTEND;
        g_emp_ids(g_emp_ids.COUNT) := :NEW.employee_id;

    END AFTER EACH ROW;


    AFTER STATEMENT IS
        v_count NUMBER;
    BEGIN

        SELECT COUNT(*)
        INTO v_count
        FROM employees;

        DBMS_OUTPUT.PUT_LINE(
            'Employee count = ' || v_count
        );

    END AFTER STATEMENT;

END trg_emp_check;
/

The important pattern is:

ROW SECTION
    ↓
Collect data

STATEMENT SECTION
    ↓
Query/process table

11. Can we use :OLD and :NEW in every section?

No.

:OLD and :NEW are available in the row-level sections.

For example:

AFTER EACH ROW IS
BEGIN
    DBMS_OUTPUT.PUT_LINE(:NEW.salary);
END AFTER EACH ROW;

But you cannot use :NEW in an AFTER STATEMENT section because that section is not associated with an individual row.

BEFORE EACH ROW → :OLD / :NEW
AFTER EACH ROW  → :OLD / :NEW

BEFORE STATEMENT → No :OLD / :NEW
AFTER STATEMENT  → No :OLD / :NEW

12. Can we modify :NEW in a compound trigger?

Yes, in a BEFORE EACH ROW section.

CREATE OR REPLACE TRIGGER trg_emp_upper
FOR INSERT OR UPDATE ON employees
COMPOUND TRIGGER

    BEFORE EACH ROW IS
    BEGIN

        :NEW.first_name := UPPER(:NEW.first_name);

    END BEFORE EACH ROW;

END trg_emp_upper;
/

You cannot modify :NEW in:

AFTER EACH ROW
AFTER STATEMENT

13. Can a compound trigger handle INSERT, UPDATE and DELETE?

Yes.

CREATE OR REPLACE TRIGGER trg_emp
FOR INSERT OR UPDATE OR DELETE ON employees
COMPOUND TRIGGER

    AFTER EACH ROW IS
    BEGIN

        IF INSERTING THEN
            DBMS_OUTPUT.PUT_LINE('Inserted');

        ELSIF UPDATING THEN
            DBMS_OUTPUT.PUT_LINE('Updated');

        ELSIF DELETING THEN
            DBMS_OUTPUT.PUT_LINE('Deleted');

        END IF;

    END AFTER EACH ROW;

END trg_emp;
/

14. Can we use INSERTING, UPDATING, and DELETING?

Yes.

AFTER EACH ROW IS
BEGIN

    IF INSERTING THEN
        ...
    ELSIF UPDATING THEN
        ...
    ELSIF DELETING THEN
        ...
    END IF;

END AFTER EACH ROW;

These conditions identify which DML operation caused the trigger to fire.

15. Can a compound trigger contain both BEFORE and AFTER logic?

Yes.

CREATE OR REPLACE TRIGGER trg_emp
FOR INSERT OR UPDATE ON employees
COMPOUND TRIGGER

    BEFORE STATEMENT IS
    BEGIN
        DBMS_OUTPUT.PUT_LINE(
            'Starting DML'
        );
    END BEFORE STATEMENT;


    BEFORE EACH ROW IS
    BEGIN
        IF :NEW.salary < 0 THEN
            RAISE_APPLICATION_ERROR(
                -20001,
                'Salary cannot be negative'
            );
        END IF;
    END BEFORE EACH ROW;


    AFTER EACH ROW IS
    BEGIN
        DBMS_OUTPUT.PUT_LINE(
            'Employee processed'
        );
    END AFTER EACH ROW;


    AFTER STATEMENT IS
    BEGIN
        DBMS_OUTPUT.PUT_LINE(
            'DML completed'
        );
    END AFTER STATEMENT;

END trg_emp;
/

16. What is a real-world auditing example?

Suppose you want to collect changed employee IDs and insert audit records after the statement finishes.

Audit table:

CREATE TABLE employee_audit (
    employee_id NUMBER,
    action_type VARCHAR2(20),
    audit_date  TIMESTAMP
);

Compound trigger:

CREATE OR REPLACE TRIGGER trg_emp_audit
FOR INSERT OR UPDATE OR DELETE ON employees
COMPOUND TRIGGER

    TYPE t_emp_ids IS TABLE OF employees.employee_id%TYPE;

    g_emp_ids t_emp_ids := t_emp_ids();


    AFTER EACH ROW IS
    BEGIN

        g_emp_ids.EXTEND;

        IF INSERTING OR UPDATING THEN
            g_emp_ids(g_emp_ids.COUNT) := :NEW.employee_id;
        ELSE
            g_emp_ids(g_emp_ids.COUNT) := :OLD.employee_id;
        END IF;

    END AFTER EACH ROW;


    AFTER STATEMENT IS
    BEGIN

        FOR i IN 1 .. g_emp_ids.COUNT LOOP

            INSERT INTO employee_audit
            (
                employee_id,
                action_type,
                audit_date
            )
            VALUES
            (
                g_emp_ids(i),
                CASE
                    WHEN INSERTING THEN 'INSERT'
                    WHEN UPDATING  THEN 'UPDATE'
                    WHEN DELETING  THEN 'DELETE'
                END,
                SYSTIMESTAMP
            );

        END LOOP;

    END AFTER STATEMENT;

END trg_emp_audit;
/

The basic design is:

DML statement
     ↓
AFTER EACH ROW
     ↓
Collect IDs
     ↓
AFTER STATEMENT
     ↓
Insert audit records

17. Can we use a collection inside a compound trigger?

Yes. This is extremely common.

CREATE OR REPLACE TRIGGER trg_emp
FOR UPDATE ON employees
COMPOUND TRIGGER

    TYPE t_ids IS TABLE OF NUMBER;

    g_ids t_ids := t_ids();

    AFTER EACH ROW IS
    BEGIN
        g_ids.EXTEND;
        g_ids(g_ids.COUNT) := :NEW.employee_id;
    END AFTER EACH ROW;

END trg_emp;
/

Collections can be used to collect information during row processing and then process it later.

18. Why not simply use a package variable?

Before compound triggers, developers sometimes used package variables to maintain state between row-level and statement-level processing.

That approach can be complicated because package variables can have a broader session lifetime and require additional management.

A compound trigger naturally provides statement-duration state:

DML starts
   ↓
Compound trigger state created
   ↓
Rows processed
   ↓
Shared state available
   ↓
Statement completes
   ↓
State discarded

19. Can a compound trigger have a WHEN clause?

A compound trigger does not use the traditional WHEN clause in the same way as a simple row trigger.

Instead, place the condition inside the relevant row-level section.

AFTER EACH ROW IS
BEGIN

    IF :NEW.salary > 100000 THEN
        DBMS_OUTPUT.PUT_LINE(
            'High salary employee'
        );
    END IF;

END AFTER EACH ROW;

20. Can a compound trigger be used on a view?

There are important Oracle restrictions around compound triggers and views, particularly with INSTEAD OF triggers on noneditioning views.

For normal interview purposes, remember:

Compound trigger on table
        ↓
Can combine multiple timing sections

INSTEAD OF trigger on view
        ↓
Has different rules and restrictions

Do not confuse a normal DML compound trigger with an INSTEAD OF trigger.

21. Can a compound trigger perform COMMIT?

No.

For example:

AFTER STATEMENT IS
BEGIN
    COMMIT;  -- ❌ Not allowed
END AFTER STATEMENT;

Triggers execute as part of the transaction that caused them, so normal trigger transaction-control restrictions apply.

22. Can a compound trigger call a procedure?

Yes.

CREATE OR REPLACE TRIGGER trg_emp
FOR UPDATE ON employees
COMPOUND TRIGGER

    AFTER STATEMENT IS
    BEGIN
        process_employee_changes;
    END AFTER STATEMENT;

END trg_emp;
/

This can be useful when the processing logic is substantial. Keeping the trigger focused and moving reusable business logic into procedures can improve maintainability.

23. What happens if the DML statement affects zero rows?

This is an important interview question.

Suppose:

UPDATE employees
SET salary = salary + 1000
WHERE employee_id = -9999;

No rows are affected.

The statement-level sections can still execute:

BEFORE STATEMENT → executes
BEFORE EACH ROW → 0 times
AFTER EACH ROW  → 0 times
AFTER STATEMENT → executes

24. What happens if a row-level section raises an exception?

Suppose:

BEFORE EACH ROW IS
BEGIN

    IF :NEW.salary < 0 THEN
        RAISE_APPLICATION_ERROR(
            -20001,
            'Invalid salary'
        );
    END IF;

END BEFORE EACH ROW;

If the exception propagates, the DML statement fails. It does not simply continue processing the remaining rows as though the failed row had been skipped.

25. What is the execution order of a compound trigger?

For a statement affecting multiple rows, the timing is conceptually:

BEFORE STATEMENT
       ↓
BEFORE EACH ROW
       ↓
AFTER EACH ROW
       ↓
BEFORE EACH ROW
       ↓
AFTER EACH ROW
       ↓
...
       ↓
AFTER STATEMENT

For three rows:

BEFORE STATEMENT

BEFORE EACH ROW  → Row 1
AFTER EACH ROW   → Row 1

BEFORE EACH ROW  → Row 2
AFTER EACH ROW   → Row 2

BEFORE EACH ROW  → Row 3
AFTER EACH ROW   → Row 3

AFTER STATEMENT

26. What is a very common mutating-table interview question?

Requirement:

When an employee's salary changes, check the total salary of the department after the update.

A simple row-level trigger might try to execute:

SELECT SUM(salary)
FROM employees
WHERE department_id = :NEW.department_id;

inside:

AFTER EACH ROW

This can cause a mutating-table problem.

Compound-trigger solution:

CREATE OR REPLACE TRIGGER trg_salary_check
FOR UPDATE OF salary ON employees
COMPOUND TRIGGER

    TYPE t_depts IS TABLE OF employees.department_id%TYPE;

    g_depts t_depts := t_depts();


    AFTER EACH ROW IS
    BEGIN

        g_depts.EXTEND;
        g_depts(g_depts.COUNT) :=
            :NEW.department_id;

    END AFTER EACH ROW;


    AFTER STATEMENT IS
        v_total_salary NUMBER;
    BEGIN

        FOR i IN 1 .. g_depts.COUNT LOOP

            SELECT SUM(salary)
            INTO v_total_salary
            FROM employees
            WHERE department_id = g_depts(i);

            DBMS_OUTPUT.PUT_LINE(
                'Department ' || g_depts(i) ||
                ' Total Salary = ' || v_total_salary
            );

        END LOOP;

    END AFTER STATEMENT;

END trg_salary_check;
/

The important design pattern is:

Don't query EMPLOYEES
during EACH ROW.

Collect what you need.

Then query EMPLOYEES
AFTER STATEMENT.

27. Can we improve the previous example?

Yes. The previous example may process the same department multiple times.

For example:

Employee 1 → Department 10
Employee 2 → Department 10
Employee 3 → Department 20

The collection could contain:

10
10
20

A better design is to maintain unique department IDs using an associative array.

CREATE OR REPLACE TRIGGER trg_salary_check
FOR UPDATE OF salary ON employees
COMPOUND TRIGGER

    TYPE t_dept_set IS TABLE OF BOOLEAN
        INDEX BY employees.department_id%TYPE;

    g_depts t_dept_set;


    AFTER EACH ROW IS
    BEGIN

        g_depts(:NEW.department_id) := TRUE;

    END AFTER EACH ROW;


    AFTER STATEMENT IS
        v_dept_id employees.department_id%TYPE;
        v_total   NUMBER;
    BEGIN

        v_dept_id := g_depts.FIRST;

        WHILE v_dept_id IS NOT NULL LOOP

            SELECT SUM(salary)
            INTO v_total
            FROM employees
            WHERE department_id = v_dept_id;

            DBMS_OUTPUT.PUT_LINE(
                'Department ' || v_dept_id ||
                ' = ' || v_total
            );

            v_dept_id := g_depts.NEXT(v_dept_id);

        END LOOP;

    END AFTER STATEMENT;

END trg_salary_check;
/

Now each department is processed only once.

28. What are the main advantages of compound triggers?

1. Avoid mutating-table problems

AFTER EACH ROW
      ↓
Collect data
      ↓
AFTER STATEMENT
      ↓
Query table

2. Share state

Row section
    ↓
Collection
    ↓
Statement section

3. Reduce the number of related triggers

Instead of maintaining:

BEFORE STATEMENT trigger
BEFORE ROW trigger
AFTER ROW trigger
AFTER STATEMENT trigger

related logic can be combined into:

ONE COMPOUND TRIGGER

4. Improve bulk-processing designs

Instead of performing expensive processing for every row:

1,000 rows
   ↓
1,000 expensive operations

You can collect the information and process it after the statement:

1,000 rows
   ↓
Collect information
   ↓
Process after statement

29. What are common compound-trigger interview traps?

Trap 1: Is a compound trigger only row-level?

Answer: ❌ No.

It can contain both statement-level and row-level sections.

Trap 2: Can :NEW be used in AFTER STATEMENT?

Answer: ❌ No.

Use :NEW and :OLD in row-level sections.

Trap 3: Can we modify :NEW in AFTER EACH ROW?

Answer: ❌ No.

Modification of :NEW is done in a BEFORE EACH ROW section.

Trap 4: Does a compound trigger automatically solve every mutating-table problem?

Answer: ❌ No.

It provides a mechanism to move processing from the row-level phase to the statement-level phase, but the trigger still needs to be designed correctly.

Trap 5: Does compound-trigger state survive between SQL statements?

Answer: ❌ No.

The shared state belongs to the triggering statement.

Trap 6: Can a compound trigger commit?

Answer: ❌ No.

Normal transaction-control restrictions on triggers still apply.

30. Compound trigger vs normal trigger

Feature Normal Trigger Compound Trigger
Multiple timing sections
BEFORE STATEMENT Separate trigger
BEFORE EACH ROW Separate trigger
AFTER EACH ROW Separate trigger
AFTER STATEMENT Separate trigger
Shared statement-duration state Limited
Mutating-table solutions Sometimes ⭐ Excellent use case
:OLD / :NEW Row-level only Row-level sections
COMMIT

31. Compound trigger vs row-level trigger

Row-level trigger:

FOR EACH ROW
     ↓
Process one row
     ↓
Process next row
     ↓
Process next row

Compound trigger:

COMPOUND TRIGGER
        ↓
BEFORE STATEMENT
        ↓
BEFORE EACH ROW
        ↓
AFTER EACH ROW
        ↓
AFTER STATEMENT

The compound trigger gives you shared state and multiple timing points.

32. ⭐ Compound Trigger Interview Cheat Sheet

Concept Answer
Multiple timing sections
BEFORE STATEMENT
BEFORE EACH ROW
AFTER EACH ROW
AFTER STATEMENT
:OLD / :NEW Row sections only
Modify :NEW BEFORE EACH ROW
Shared variables
State duration Triggering statement
Mutating-table solution ✅ Common use
COMMIT

The Most Important Pattern

             COMPOUND TRIGGER
                    |
        +-----------+-----------+
        |                       |
   EACH ROW               STATEMENT
        |                       |
        ↓                       ↓
Collect information       Process information
        |                       |
        +----------→------------+
                   |
             Shared state

The Easiest Way to Remember

Normal row trigger: "Process this row."

Statement trigger: "Process this SQL statement."

Compound trigger: "Collect information while processing rows, then use it when the statement is complete."

🎯 Three Most Important Interview Concepts

  1. Multiple timing sections
  2. Shared statement-duration state
  3. Solving mutating-table problems
Interview Tip:

If the interviewer asks, "Why would you use a compound trigger?", a strong answer is:

"I would use a compound trigger when I need to share state between row-level and statement-level processing, especially when I need to collect row information and then process or query the table after the statement completes to avoid a mutating-table problem."
```

Oracle Instead of Trigger Faqs With Examples

1. What is an INSTEAD OF trigger?

An INSTEAD OF trigger tells Oracle:

"When someone performs this DML operation on the view, don't perform the normal operation. Execute this trigger logic instead."

Example:

CREATE OR REPLACE TRIGGER trg_emp_view
INSTEAD OF INSERT ON emp_dept_v
FOR EACH ROW
BEGIN
    INSERT INTO employees
    (
        employee_id,
        first_name,
        department_id
    )
    VALUES
    (
        :NEW.employee_id,
        :NEW.first_name,
        :NEW.department_id
    );
END;
/

When a user executes:

INSERT INTO emp_dept_v
(
    employee_id,
    first_name,
    department_id
)
VALUES
(
    101,
    'John',
    10
);

Oracle executes the trigger's INSERT on employees instead of trying to insert directly into the view.

2. Why do we need INSTEAD OF triggers?

They are useful when a view cannot naturally be modified.

For example:

CREATE VIEW emp_dept_v AS
SELECT
    e.employee_id,
    e.first_name,
    d.department_name
FROM employees e
JOIN departments d
    ON e.department_id = d.department_id;

This view combines data from two tables:

EMPLOYEES
    +
DEPARTMENTS
    ↓
EMP_DEPT_V

If you try:

INSERT INTO emp_dept_v
VALUES (101, 'John', 'IT');

Oracle may not know how to distribute the values between the underlying tables.

An INSTEAD OF trigger can provide that logic.

3. On what objects can INSTEAD OF triggers be created?

INSTEAD OF triggers are primarily associated with views.

They are especially useful for:

  • Complex views
  • Join views
  • Views involving multiple tables
  • Views containing aggregations
  • Views involving DISTINCT
  • Views involving set operators such as UNION

Example:

CREATE OR REPLACE TRIGGER trg_emp_dept
INSTEAD OF INSERT ON emp_dept_v
FOR EACH ROW
BEGIN
    ...
END;
/

4. Can an INSTEAD OF trigger be created on a table?

No.

You don't create an INSTEAD OF trigger on a normal table.

For tables, use:

BEFORE
AFTER

For example:

BEFORE INSERT ON employees

or:

AFTER UPDATE ON employees
Interview trap:

INSTEAD OF → View
BEFORE / AFTER → Table

5. Is an INSTEAD OF trigger row-level or statement-level?

An INSTEAD OF trigger is always a row-level trigger.

Therefore:

FOR EACH ROW

is required.

Example:

CREATE OR REPLACE TRIGGER trg_emp_view
INSTEAD OF INSERT ON emp_dept_v
FOR EACH ROW
BEGIN
    ...
END;
/

You cannot create:

CREATE OR REPLACE TRIGGER trg_emp_view
INSTEAD OF INSERT ON emp_dept_v
BEGIN
    ...
END;
/

without FOR EACH ROW.

Interview point:

INSTEAD OF trigger

Always row-level

FOR EACH ROW is required

6. Can INSTEAD OF triggers use :OLD and :NEW?

Yes.

Because INSTEAD OF triggers are row-level triggers.

Operation :OLD :NEW
INSERT
UPDATE
DELETE

7. Example of an INSTEAD OF INSERT trigger

Suppose we have:

CREATE TABLE employees (
    employee_id   NUMBER,
    first_name    VARCHAR2(50),
    department_id NUMBER
);

Create a view:

CREATE VIEW emp_v AS
SELECT
    employee_id,
    first_name,
    department_id
FROM employees;

Create the trigger:

CREATE OR REPLACE TRIGGER trg_emp_v_insert
INSTEAD OF INSERT ON emp_v
FOR EACH ROW
BEGIN

    INSERT INTO employees
    (
        employee_id,
        first_name,
        department_id
    )
    VALUES
    (
        :NEW.employee_id,
        :NEW.first_name,
        :NEW.department_id
    );

END;
/

Now:

INSERT INTO emp_v
(
    employee_id,
    first_name,
    department_id
)
VALUES
(
    101,
    'John',
    10
);

The trigger performs:

INSERT INTO employees ...

instead of inserting directly into the view.

8. Example of an INSTEAD OF UPDATE trigger

Create a view:

CREATE VIEW emp_v AS
SELECT
    employee_id,
    first_name,
    department_id
FROM employees;

Trigger:

CREATE OR REPLACE TRIGGER trg_emp_v_update
INSTEAD OF UPDATE ON emp_v
FOR EACH ROW
BEGIN

    UPDATE employees
    SET
        first_name    = :NEW.first_name,
        department_id = :NEW.department_id
    WHERE employee_id = :OLD.employee_id;

END;
/

Now:

UPDATE emp_v
SET first_name = 'Robert'
WHERE employee_id = 101;

The trigger executes:

UPDATE employees
SET first_name = 'Robert'
WHERE employee_id = 101;

9. Example of an INSTEAD OF DELETE trigger

CREATE OR REPLACE TRIGGER trg_emp_v_delete
INSTEAD OF DELETE ON emp_v
FOR EACH ROW
BEGIN

    DELETE FROM employees
    WHERE employee_id = :OLD.employee_id;

END;
/

Now:

DELETE FROM emp_v
WHERE employee_id = 101;

The trigger deletes the corresponding row from employees.

10. Can one INSTEAD OF trigger handle INSERT, UPDATE and DELETE?

Yes.

CREATE OR REPLACE TRIGGER trg_emp_v_dml
INSTEAD OF INSERT OR UPDATE OR DELETE ON emp_v
FOR EACH ROW
BEGIN

    IF INSERTING THEN

        INSERT INTO employees
        (
            employee_id,
            first_name,
            department_id
        )
        VALUES
        (
            :NEW.employee_id,
            :NEW.first_name,
            :NEW.department_id
        );

    ELSIF UPDATING THEN

        UPDATE employees
        SET
            first_name = :NEW.first_name,
            department_id = :NEW.department_id
        WHERE employee_id = :OLD.employee_id;

    ELSIF DELETING THEN

        DELETE FROM employees
        WHERE employee_id = :OLD.employee_id;

    END IF;

END;
/

Oracle provides:

INSERTING
UPDATING
DELETING

to determine which operation caused the trigger to fire.

11. What is the most common use case?

One of the most important use cases is making a complex view appear updatable.

Suppose we have:

CREATE VIEW emp_dept_v AS
SELECT
    e.employee_id,
    e.first_name,
    d.department_name
FROM employees e
JOIN departments d
    ON e.department_id = d.department_id;

The view contains data from:

EMPLOYEES
      +
DEPARTMENTS
      ↓
EMP_DEPT_V

Suppose a user wants to execute:

INSERT INTO emp_dept_v
(
    employee_id,
    first_name,
    department_name
)
VALUES
(
    101,
    'John',
    'IT'
);

The trigger can translate this into operations on the underlying tables.

CREATE OR REPLACE TRIGGER trg_emp_dept_insert
INSTEAD OF INSERT ON emp_dept_v
FOR EACH ROW
DECLARE
    v_department_id departments.department_id%TYPE;
BEGIN

    SELECT department_id
    INTO v_department_id
    FROM departments
    WHERE department_name = :NEW.department_name;

    INSERT INTO employees
    (
        employee_id,
        first_name,
        department_id
    )
    VALUES
    (
        :NEW.employee_id,
        :NEW.first_name,
        v_department_id
    );

END;
/

Now the view provides a convenient interface while the trigger handles the underlying tables.

12. Can an INSTEAD OF trigger modify multiple tables?

Yes.

This is one of its biggest advantages.

Suppose the view represents:

CUSTOMER
    +
ADDRESS
    +
ORDER

A single DML operation against the view can be translated by the trigger into:

INSERT CUSTOMER
       ↓
INSERT ADDRESS
       ↓
INSERT ORDER

Example:

CREATE OR REPLACE TRIGGER trg_order_v
INSTEAD OF INSERT ON order_v
FOR EACH ROW
BEGIN

    INSERT INTO customers (...)
    VALUES (...);

    INSERT INTO addresses (...)
    VALUES (...);

    INSERT INTO orders (...)
    VALUES (...);

END;
/

This is a common reason for using INSTEAD OF triggers.

13. Can an INSTEAD OF trigger use :OLD and :NEW together?

Yes, for an UPDATE.

CREATE OR REPLACE TRIGGER trg_emp_v
INSTEAD OF UPDATE ON emp_v
FOR EACH ROW
BEGIN

    UPDATE employees
    SET
        first_name = :NEW.first_name
    WHERE employee_id = :OLD.employee_id;

END;
/

Here:

:OLD.employee_id
        ↓
identifies the old row

:NEW.first_name
        ↓
contains the new value

14. Can an INSTEAD OF trigger change :NEW?

You should think of INSTEAD OF triggers differently from BEFORE row triggers.

For an INSTEAD OF trigger, the trigger itself determines what happens to the underlying tables.

For example:

CREATE OR REPLACE TRIGGER trg_emp_v
INSTEAD OF INSERT ON emp_v
FOR EACH ROW
BEGIN

    INSERT INTO employees
    (
        employee_id,
        first_name
    )
    VALUES
    (
        :NEW.employee_id,
        UPPER(:NEW.first_name)
    );

END;
/

Rather than changing the view's :NEW value, the trigger can directly control what is inserted into the base table.

15. What happens if the view has multiple underlying tables?

This is where INSTEAD OF triggers become especially useful.

Example view:

CREATE VIEW employee_department_v AS
SELECT
    e.employee_id,
    e.first_name,
    d.department_id,
    d.department_name
FROM employees e
JOIN departments d
    ON e.department_id = d.department_id;

An update like:

UPDATE employee_department_v
SET department_name = 'FINANCE'
WHERE employee_id = 101;

may not be directly updatable in the way you want.

An INSTEAD OF trigger can determine:

department_name
      ↓
find department_id
      ↓
update employees.department_id

Example:

CREATE OR REPLACE TRIGGER trg_emp_dept_update
INSTEAD OF UPDATE ON employee_department_v
FOR EACH ROW
DECLARE
    v_department_id NUMBER;
BEGIN

    SELECT department_id
    INTO v_department_id
    FROM departments
    WHERE department_name = :NEW.department_name;

    UPDATE employees
    SET department_id = v_department_id
    WHERE employee_id = :OLD.employee_id;

END;
/

16. Can an INSTEAD OF trigger be BEFORE or AFTER?

No.

These are different trigger timings:

BEFORE
AFTER
INSTEAD OF

You don't write:

BEFORE INSTEAD OF INSERT

or:

AFTER INSTEAD OF INSERT

Correct syntax:

INSTEAD OF INSERT ON view_name

17. Can an INSTEAD OF trigger be statement-level?

No.

This is a very common interview question.

INSTEAD OF triggers are row-level only.

Correct:

CREATE OR REPLACE TRIGGER trg_emp_v
INSTEAD OF INSERT ON emp_v
FOR EACH ROW
BEGIN
    ...
END;
/

Incorrect:

CREATE OR REPLACE TRIGGER trg_emp_v
INSTEAD OF INSERT ON emp_v
BEGIN
    ...
END;
/

18. Can an INSTEAD OF trigger be created on a table?

No.

For example:

CREATE OR REPLACE TRIGGER trg_emp
INSTEAD OF INSERT ON employees
FOR EACH ROW
BEGIN
    ...
END;
/

This is not valid because employees is a table.

Use:

BEFORE INSERT ON employees

or:

AFTER INSERT ON employees

19. Can an INSTEAD OF trigger perform validation?

Yes.

Example:

CREATE OR REPLACE TRIGGER trg_emp_v
INSTEAD OF INSERT ON emp_v
FOR EACH ROW
BEGIN

    IF :NEW.employee_id IS NULL THEN
        RAISE_APPLICATION_ERROR(
            -20001,
            'Employee ID cannot be NULL'
        );
    END IF;

    INSERT INTO employees
    (
        employee_id,
        first_name
    )
    VALUES
    (
        :NEW.employee_id,
        :NEW.first_name
    );

END;
/

The trigger validates the input before performing the actual DML.

20. Can an INSTEAD OF trigger call a procedure?

Yes.

CREATE OR REPLACE TRIGGER trg_emp_v
INSTEAD OF INSERT ON emp_v
FOR EACH ROW
BEGIN

    create_employee(
        :NEW.employee_id,
        :NEW.first_name
    );

END;
/

Procedure:

CREATE OR REPLACE PROCEDURE create_employee
(
    p_employee_id NUMBER,
    p_first_name  VARCHAR2
)
IS
BEGIN

    INSERT INTO employees
    (
        employee_id,
        first_name
    )
    VALUES
    (
        p_employee_id,
        p_first_name
    );

END;
/

This can keep complex trigger logic manageable.

21. Does the original DML execute after the INSTEAD OF trigger?

No.

That's the whole meaning of INSTEAD OF.

Suppose:

INSERT INTO emp_v
VALUES (101, 'John');

The flow is:

INSERT INTO emp_v
       ↓
INSTEAD OF trigger
       ↓
Trigger code executes
       ↓
INSERT into employees
       ↓
Original INSERT on view is NOT separately executed
Easy way to remember:

INSTEAD OF = replace the requested operation with my trigger logic.

22. What happens if the INSTEAD OF trigger does nothing?

Suppose:

CREATE OR REPLACE TRIGGER trg_emp_v
INSTEAD OF INSERT ON emp_v
FOR EACH ROW
BEGIN
    NULL;
END;
/

Then:

INSERT INTO emp_v
VALUES (101, 'John');

The trigger fires, but no underlying insert happens.

So the requested operation effectively does nothing.

23. Can an INSTEAD OF trigger perform DML on the same view?

Be careful.

For example:

CREATE OR REPLACE TRIGGER trg_emp_v
INSTEAD OF INSERT ON emp_v
FOR EACH ROW
BEGIN

    INSERT INTO emp_v
    VALUES (...);

END;
/

This can cause recursive trigger execution.

Conceptually:

INSERT into view
      ↓
INSTEAD OF trigger
      ↓
INSERT into same view
      ↓
INSTEAD OF trigger again
      ↓
...

This should generally be avoided.

The trigger should normally perform DML on the underlying base tables, not recursively on the same view.

24. Can an INSTEAD OF trigger perform COMMIT?

No.

For example:

CREATE OR REPLACE TRIGGER trg_emp_v
INSTEAD OF INSERT ON emp_v
FOR EACH ROW
BEGIN

    INSERT INTO employees (...);

    COMMIT;

END;
/

This is not allowed.

You generally cannot use transaction control such as:

COMMIT
ROLLBACK

inside a trigger.

25. What is the difference between BEFORE, AFTER, and INSTEAD OF?

Trigger Usually used on Purpose
BEFORE Table Validate/modify before DML
AFTER Table Audit/log after DML
INSTEAD OF View Replace DML on a view

For a table:

Table
  ↓
BEFORE INSERT
  ↓
Insert
  ↓
AFTER INSERT

For a view:

View
 ↓
INSERT
 ↓
INSTEAD OF trigger
 ↓
Your custom DML
 ↓
Base table(s)

26. What is a complete example with INSERT, UPDATE and DELETE?

Let's create two tables.

Departments

CREATE TABLE departments (
    department_id   NUMBER PRIMARY KEY,
    department_name VARCHAR2(100)
);

Employees

CREATE TABLE employees (
    employee_id   NUMBER PRIMARY KEY,
    first_name    VARCHAR2(100),
    department_id NUMBER REFERENCES departments(department_id)
);

Create a view:

CREATE OR REPLACE VIEW emp_dept_v AS
SELECT
    e.employee_id,
    e.first_name,
    d.department_name
FROM employees e
JOIN departments d
    ON e.department_id = d.department_id;

Now create an INSTEAD OF trigger:

CREATE OR REPLACE TRIGGER trg_emp_dept_v
INSTEAD OF INSERT OR UPDATE OR DELETE ON emp_dept_v
FOR EACH ROW
DECLARE
    v_department_id NUMBER;
BEGIN

    IF INSERTING THEN

        SELECT department_id
        INTO v_department_id
        FROM departments
        WHERE department_name = :NEW.department_name;

        INSERT INTO employees
        (
            employee_id,
            first_name,
            department_id
        )
        VALUES
        (
            :NEW.employee_id,
            :NEW.first_name,
            v_department_id
        );

    ELSIF UPDATING THEN

        SELECT department_id
        INTO v_department_id
        FROM departments
        WHERE department_name = :NEW.department_name;

        UPDATE employees
        SET
            first_name = :NEW.first_name,
            department_id = v_department_id
        WHERE employee_id = :OLD.employee_id;

    ELSIF DELETING THEN

        DELETE FROM employees
        WHERE employee_id = :OLD.employee_id;

    END IF;

END;
/

Now users can work with:

INSERT INTO emp_dept_v
(
    employee_id,
    first_name,
    department_name
)
VALUES
(
    101,
    'John',
    'IT'
);

Instead of directly manipulating the underlying tables, the trigger translates the request.

27. What happens if 100 rows are affected?

Because an INSTEAD OF trigger is row-level:

DELETE FROM emp_dept_v;

If the view represents 100 affected rows:

DELETE statement
      ↓
100 rows
      ↓
INSTEAD OF trigger
      ↓
fires 100 times

This is an important distinction.

Trigger type Execution
Statement-level trigger Once per statement
Row-level trigger Once per affected row
INSTEAD OF trigger Always row-level

28. Common INSTEAD OF trigger interview traps

Trap 1

Question: Is an INSTEAD OF trigger row-level or statement-level?

Answer: ✅ Always row-level.

FOR EACH ROW

is required.

Trap 2

Question: Can it be created on a table?

Answer: ❌ No.

It is primarily used for views.

Trap 3

Question: Can it use :OLD and :NEW?

Answer: ✅ Yes.

Because it is row-level.

Trap 4

Question: Can it be BEFORE or AFTER?

Answer: ❌ No.

It is a separate trigger timing:

INSTEAD OF

Trap 5

Question: What does INSTEAD OF mean?

Answer:

The trigger executes instead of the DML operation that was requested on the view.

Trap 6

Question: Can it update multiple base tables?

Answer: ✅ Yes.

This is one of its major use cases.

Trap 7

Question: Can it use :NEW during INSERT?

Answer: ✅ Yes.

Trap 8

Question: Can it use :OLD during DELETE?

Answer: ✅ Yes.

29. INSTEAD OF trigger cheat sheet

Memorize this:

Feature INSTEAD OF
Used on VIEW
Used on table ❌ No
Row-level ✅ Always
FOR EACH ROW ✅ Required
:OLD ✅ Yes
:NEW ✅ Yes
BEFORE ❌ No
AFTER ❌ No
INSERT ✅ Yes
UPDATE ✅ Yes
DELETE ✅ Yes
Multiple base tables ✅ Yes
COMMIT ❌ No

⭐ The easiest way to remember

BEFORE → "Do something before DML."

AFTER → "Do something after DML."

INSTEAD OF → "Don't perform the requested view DML; I'll handle it."

And the key interview statement:

Trigger TABLE VIEW
BEFORE
AFTER
INSTEAD OF
Most important rule:

INSTEAD OF trigger

VIEW

FOR EACH ROW

:OLD / :NEW available

Perform DML on underlying table(s)
If you're preparing for an Oracle interview, the next-level questions are usually around complex view updatability, join views, trigger recursion, trigger restrictions, compound triggers, mutating-table errors, and advanced INSTEAD OF trigger design.
```