Share

Assertions Use Cases in Oracle

Assertions are boolean expressions that have the semantics of a constraint. The database must ensure the truth of these boolean expressions while transactions change data and commit these changes.

Prerequisites

Assertions are schema-level objects. If the boolean expression references tables of other schemas, you must have the required object-privileges to access these tables.

When a foreign key constraint references a parent table in another schema, you are required to have the REFERENCES object privilege on the parent table.

For assertions to reference tables in another schema you require the ASSERTION REFERENCES object privilege on those tables. Also note that you must prefix these tables from other schemas with their schema-name (SYNONYMs are unsupported in assertions).

To create a assertion in your own schema, you must have the CREATE ASSERTION system privilege. With the CREATE ASSERTION privilege you can alter and drop assertions in the same schema using ALTER ASSERTION and DROP ASSERTION.

To create assertions in any schema or a specified schema, you must have the CREATE ANY ASSERTION [ON SCHEMA...] system privilege.

The following SQL statement creates an assertion, named NO_TRAINERS_IN_BOSTON, to demand that there are no trainers employed in Boston-based departments. The top-level NOT EXISTS represents a Boolean expression which demands that the result set of the join between the EMP and DEPT tables is to remain empty.

CREATE ASSERTION no_trainers_in_boston CHECK
(NOT EXISTS
   (SELECT 'trainer in Boston'
    FROM   emp e, dept d
    WHERE  e.deptno = d.deptno
      AND  e.job = 'TRAINER'
      AND  d.loc = 'BOSTON'));

Example for Assertions Use Cases :

The Problem: The “Impossible Promotion” in an Event Sourcing Pipeline

The Scenario:

You are building an analytics system for a retail bank. You have a table that tracks the state of customer accounts over time based on an event stream (deposits, withdrawals, interest applied).

The table tracks the account_id, a timestamp (status_timestamp), the current balance, and the current account tier (Standard, Gold, Platinum).

The Business Rule (The Invariant):

A fundamental rule in the banking domain is that an account cannot skip tiers when being promoted. An account must go from Standard -> Gold -> Platinum. It cannot jump directly from Standard to Platinum in a single state update.

The Solution for Oracle Database (Pre-23.26.1)

Since Oracle didn’t support true assertions until version 23.26.1, we need to implement the “no skipping tiers” rule using a combination of features that have been available for decades .

Option 1: The Compound Trigger Approach (Most Elegant for Oracle 11g+)

The Challenge with Simple Triggers:

If we try to write a simple row-level trigger that queries the account_history table, we’ll hit the infamous ORA-04091: mutating table error. This happens because the table is in flux during the DML operation .

The Solution: Compound Triggers (Oracle 11g+)

Compound triggers allow us to collect changes at the row level and validate them after the table stabilizes .

-- Create the table (standard Oracle syntax)

CREATE TABLE account_history (

    account_id        NUMBER(10),

    status_timestamp  TIMESTAMP(3),

    balance           NUMBER(15,2),

    tier              VARCHAR2(10) CHECK (tier IN ('Standard', 'Gold', 'Platinum')),

    CONSTRAINT pk_account_history PRIMARY KEY (account_id, status_timestamp)

)

ORGANIZATION INDEX;  -- Good for time-series data


-- Create a sequence for tie-breaking if needed

CREATE SEQUENCE seq_account_history;

-- The compound trigger that enforces the "no tier skipping" rule

CREATE OR REPLACE TRIGGER trg_no_illegal_promotion

FOR INSERT OR UPDATE ON account_history

COMPOUND TRIGGER

    

    -- Type to hold changed rows in memory

    TYPE t_change_rec IS RECORD (

        account_id      account_history.account_id%TYPE,

        new_tier        account_history.tier%TYPE,

        new_timestamp   account_history.status_timestamp%TYPE

    );

    

    TYPE t_change_tab IS TABLE OF t_change_rec INDEX BY PLS_INTEGER;

    g_changes t_change_tab;

    g_idx PLS_INTEGER := 0;

    

    -- BEFORE EACH ROW: Collect all changes

    BEFORE EACH ROW IS

    BEGIN

        g_idx := g_idx + 1;

        g_changes(g_idx).account_id := :NEW.account_id;

        g_changes(g_idx).new_tier := :NEW.tier;

        g_changes(g_idx).new_timestamp := :NEW.status_timestamp;

    END BEFORE EACH ROW;

    

    -- AFTER STATEMENT: Validate all collected changes

    AFTER STATEMENT IS

        v_previous_tier VARCHAR2(10);

        v_tier_level NUMBER;

        v_prev_level NUMBER;

    BEGIN

        FOR i IN 1 .. g_changes.COUNT LOOP

            -- Find the most recent tier BEFORE this change for the same account

            BEGIN

                SELECT tier INTO v_previous_tier

                FROM account_history

                WHERE account_id = g_changes(i).account_id

                  AND status_timestamp < g_changes(i).new_timestamp

                ORDER BY status_timestamp DESC

                FETCH FIRST 1 ROW ONLY;

                

                -- Map tiers to numeric values for comparison

                v_tier_level := CASE g_changes(i).new_tier

                    WHEN 'Standard' THEN 1

                    WHEN 'Gold' THEN 2

                    WHEN 'Platinum' THEN 3

                END;

                

                v_prev_level := CASE v_previous_tier

                    WHEN 'Standard' THEN 1

                    WHEN 'Gold' THEN 2

                    WHEN 'Platinum' THEN 3

                END;

                

                -- Check for illegal skip

                IF v_tier_level - v_prev_level > 1 THEN

                    RAISE_APPLICATION_ERROR(-20001, 

                        'Illegal tier promotion: Cannot skip from ' || 

                        v_previous_tier || ' to ' || g_changes(i).new_tier);

                END IF;

                

            EXCEPTION

                WHEN NO_DATA_FOUND THEN

                    -- First record for this account, no previous tier to check

                    NULL;

            END;

        END LOOP;

        

        -- Clear the collection

        g_changes.DELETE;

    END AFTER STATEMENT;

    

END trg_no_illegal_promotion;

/

 

How It Works:

  • The BEFORE EACH ROW section captures all changed rows in a PL/SQL collection, avoiding mutating table errors
  • The AFTER STATEMENT section runs when the table is stable and validates every change
  • It checks each account’s history to ensure tiers increase by only one level at a time

Option 2: The Materialized View with Constraint Approach

This approach leverages Oracle’s materialized view capabilities to enforce cross-row constraints .

-- Step 1: Create a materialized view log to support fast refresh

CREATE MATERIALIZED VIEW LOG ON account_history

WITH ROWID, (account_id, tier, status_timestamp)

INCLUDING NEW VALUES;

-- Step 2: Create a materialized view that detects violations

CREATE MATERIALIZED VIEW mv_tier_violation

REFRESH FAST ON COMMIT

AS

-- This view identifies any account that skipped a tier

SELECT DISTINCT curr.account_id

FROM account_history curr, account_history prev

WHERE curr.account_id = prev.account_id

  AND curr.status_timestamp > prev.status_timestamp

  AND NOT EXISTS (

      SELECT 1 FROM account_history mid

      WHERE mid.account_id = curr.account_id

        AND mid.status_timestamp > prev.status_timestamp

        AND mid.status_timestamp < curr.status_timestamp

  )

  AND (

      (prev.tier = 'Standard' AND curr.tier = 'Platinum') OR

      (prev.tier = 'Standard' AND curr.tier = 'Gold' AND 

       EXISTS (SELECT 1 FROM account_history 

               WHERE account_id = curr.account_id 

               AND tier = 'Platinum' 

               AND status_timestamp > curr.status_timestamp)) OR

      (prev.tier = 'Gold' AND curr.tier = 'Standard')

  );

-- Step 3: Add a constraint that the MV must be empty

ALTER TABLE mv_tier_violation

ADD CONSTRAINT ck_no_tier_skip CHECK (1=0) DISABLE;

-- Any transaction that causes a violation will leave a row in the MV

-- and fail on COMMIT with ORA-12048

 

Option 3: The Oracle 23.26.1+ Native Assertion

If you’re on the latest Oracle version, you can use the native CREATE ASSERTION  :

CREATE ASSERTION no_illegal_promotion

CHECK (

    NOT EXISTS (

        SELECT 1

        FROM account_history curr

        WHERE EXISTS (

            SELECT 1

            FROM account_history prev

            WHERE prev.account_id = curr.account_id

              AND prev.status_timestamp < curr.status_timestamp

              AND NOT EXISTS (

                  SELECT 1

                  FROM account_history mid

                  WHERE mid.account_id = curr.account_id

                    AND mid.status_timestamp > prev.status_timestamp

                    AND mid.status_timestamp < curr.status_timestamp

              )

              AND (

                  (prev.tier = 'Standard' AND curr.tier = 'Platinum') OR

                  (prev.tier = 'Gold' AND curr.tier = 'Standard')

              )

        )

    )

);

 

Why This Matters for Oracle

The key insight is that complex temporal business rules can be enforced in any Oracle version, but the approach differs dramatically based on your version :

Oracle Version Recommended Approach Complexity Performance

23.26.1+ Native CREATE ASSERTION Low Best

11g – 19c Compound Triggers Medium Good

Any Version Materialized View + Constraint High Moderate

Legacy (pre-11g) Multiple Triggers + Package Vars Very High Poor

The Real-World Impact

Banks running on Oracle 11g (still common in 2026!) can implement this pattern today. When a trading system bug tries to promote a customer from Silver to Platinum in one step, the compound trigger catches it before corrupted data reaches the reporting warehouse. This prevents incorrect executive decisions based on impossible customer journeys .

The beauty of the compound trigger approach is that it maintains read consistency—the validation sees the table as it was before your transaction, combined with your current changes, ensuring no phantom reads or missed violations .

Loading

You may also like