7.5 Triggers
Triggers (PostgreSQL edition)
- Introduction to triggers as event-condition-action (ECA) rules.
- Explanation of the three ways triggers differ from constraints: they are awakened by specific events, they test a condition, and they perform an action if the condition is met.
- Types of triggering events: insert, delete, update, or transaction end.
- The role of the condition: if it does not hold, nothing happens.
- The nature of the action: it can modify the effects of the event, abort the transaction, or perform any other database operations.
#1
read: Section 7.5: Triggers (section)
model: pro
rules: bullet definition math presentation short
Present the following. Use definition formal definition when it applies.
- Introduction to triggers as event-condition-action (ECA) rules.
- Explanation of the three ways triggers differ from constraints: they are awakened by specific events, they test a condition, and they perform an action if the condition is met.
- Types of triggering events: insert, delete, update, or transaction end.
- The role of the condition: if it does not hold, nothing happens.
- The nature of the action: it can modify the effects of the event, abort the transaction, or perform any other database operations.
PL/pgSQL
PL/pgSQL is PostgreSQL’s built-in procedural language. It sits on top of SQL and adds variables, control flow, exceptions, loops, and triggers. You use it when plain SQL is insufficient or when logic needs to run inside the database.
What is PL/pgSQL:
A procedural extension of SQL, compiled and stored by PostgreSQL.
Executes inside the server process (no network round-trips).
Common uses: trigger functions, custom validations, data transformations, stored logic, batch jobs.
Basic Structure of a Function
CREATE OR REPLACE FUNCTION <function_name> (<parameter> <type>, ...)
RETURNS <return-type>
LANGUAGE plpgsql
AS $$
DECLARE
<local_varaibles>;
...
BEGIN
<SQL_statements>
...
RETURN <...>;
END;
$$;Variables and Types
Types are any PostgreSQL type. Assignment uses :=.
DECLARE
counter int := 0;
username text;
calculated numeric(10,2);result := x + y;SQL in the function
Simple SQL
SELECT count(*) INTO counter FROM users;Modifying data
UPDATE accounts SET balance = balance - amount WHERE id = account_id;Returning sets
RETURN QUERY SELECT * FROM users WHERE active;This requires the function to declare the return type as:
RETURNS SETOF <row_type>
Control Flow
IF-ELSIF-ELSE-END IF
IF amount < 0 THEN RAISE EXCEPTION 'amount must be non-negative'; ELSIF amount = 0 THEN RAISE NOTICE 'no-op'; ELSE RAISE NOTICE 'OK'; END IF;LOOP-END LOOP
LOOP counter := counter + 1; EXIT WHEN counter > 10; END LOOP;FOR LOOP-END LOOP
Iteration over numeric range.
FOR i IN 1..5 LOOP RAISE NOTICE 'i=%', i; END LOOP;Iteration over query result.
FOR rec IN SELECT id, name FROM users LOOP RAISE NOTICE 'User: %, %', rec.id, rec.name; END LOOP;
Exception Handling
BEGIN
SELECT 1 / 0;
EXCEPTION
WHEN division_by_zero THEN
RAISE NOTICE 'caught error';
END;Raise statements
They are debugging statements
RAISE NOTICE 'x=%', x;
RAISE WARNING 'threshold reached';
RAISE EXCEPTION 'Fatal: %', msg;Triggers Overview
Triggers are defined using the CREATE TRIGGER statement. It instructs the database engine to execute trigger functions based on certain events.
Core Structure of CREATE TRIGGER
The syntax skeleton:
CREATE TRIGGER <trigger_name>
{ BEFORE | AFTER | INSTEAD OF } { INSERT | UPDATE | DELETE | TRUNCATE }
ON <table_or_view>
[ FOR EACH ROW | FOR EACH STATEMENT ]
[ WHEN ( condition ) ]
EXECUTE FUNCTION <function_name>();Every trigger definition answers three questions:
- When does it fire? → BEFORE or AFTER
- What does it fire for? → INSERT, UPDATE, DELETE, TRUNCATE
- How often does it fire? → per-row or per-statement
BEFORE triggers
CREATE TRIGGER t
BEFORE INSERT OR UPDATE ON mytable
FOR EACH ROW
EXECUTE FUNCTION fn();Characteristics
- Fired before the database modifies the row.
- Only meaningful as row-level (though you can attach statement-level; it just can’t modify the row).
- Can:
- modify NEW
- reject the operation (exception)
- skip the row (RETURN NULL)
- Access to OLD and/or NEW depending on operation type.
Typical use
- Validation, normalization, enforcing rules, computed columns.
AFTER triggers
CREATE TRIGGER t
AFTER UPDATE ON mytable
FOR EACH ROW
EXECUTE FUNCTION fn();Characteristics
- Fired after the row is successfully changed.
- Row-level AFTER triggers can inspect OLD/NEW, but cannot modify the outcome.
- Statement-level AFTER triggers fire once per statement.
- Return value is ignored.
Typical use
- Auditing, logging, maintaining summary tables, NOTIFY events.
INSTEAD OF triggers
CREATE TRIGGER t
INSTEAD OF INSERT ON myview
FOR EACH ROW
EXECUTE FUNCTION fn();- Only for views.
- Completely replace default behavior.
- Used to make views writable.
ROW-level triggers
CREATE TRIGGER <trigger>
...
FOR EACH ROW
EXECUTE FUNCTION <function>;Characteristics
- Fires once per affected row.
- Access to:
- NEW (INSERT/UPDATE)
- OLD (UPDATE/DELETE)
- Only kind that can:
- modify NEW
- cancel specific rows
Example
CREATE TRIGGER validate_price
BEFORE INSERT OR UPDATE ON products
FOR EACH ROW
EXECUTE FUNCTION validate_price_fn();STATEMENT-level triggers
CREATE TRIGGER <trigger>
...
FOR EACH STATEMENT
EXECUTE FUNCTION <function>;Characteristics
- Fires once per SQL statement, regardless of number of rows.
- Has no access to OLD or NEW.
- Cannot influence row data.
- Used for global logging or high-level enforcement.
Example
CREATE TRIGGER log_updates
AFTER UPDATE ON users
FOR EACH STATEMENT
EXECUTE FUNCTION log_change_fn();Trigger functions
Trigger functions are the same language but
RETURNS trigger- It has access to special variables:
CREATE FUNCTION example_trigger()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
RAISE NOTICE 'Operation=%', TG_OP;
RAISE NOTICE 'New row=%', row_to_json(NEW);
RETURN NEW; -- or OLD based on trigger type
END;
$$;NEW and OLD are pseudo-records for row-before/after.
OLD and NEW
INSERT
- OLD: not available (null reference)
- NEW: row to be inserted (before insertion)
UPDATE
- OLD: the row before the update
- NEW: the row after the update
DELETE
- OLD: the row to be deleted
- NEW: not available (null reference)
Examples
Enforcing a constraint
CREATE OR REPLACE FUNCTION check_age() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW.age < 18 THEN RAISE EXCEPTION 'age must be >= 18'; END IF; RETURN NEW; END; $$; CREATE TRIGGER users_age_check BEFORE INSERT OR UPDATE ON users FOR EACH ROW EXECUTE FUNCTION check_age();Compute a Derived Value
CREATE OR REPLACE FUNCTION compute_total() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN NEW.total := NEW.qty * NEW.unit_price; RETURN NEW; END; $$;
More special variables
TG_OPOperation type as an uppercase text string.
Possible values:
- ‘INSERT’
- ‘UPDATE’
- ‘DELETE’
Useful when a single trigger function is attached to multiple events:
IF TG_OP = 'INSERT' THEN ... ELSIF TG_OP = 'UPDATE' THEN ... END IF;TG_TABLE_NAMEThe unqualified table name the trigger is firing on.
TG_TABLE_SCHEMAThe schema name
TG_WHENText ‘BEFORE’ or ‘AFTER’. Not often used because trigger timing is decided in the trigger definition.
TG_LEVELEither ‘ROW’ or ‘STATEMENT’.
Defining Triggers
CREATE TRIGGER <trigger_name>
{ BEFORE | AFTER | INSTEAD OF } { INSERT | UPDATE | DELETE | TRUNCATE }
ON <table_or_view>
[ FOR EACH ROW | FOR EACH STATEMENT ]
[ WHEN ( condition ) ]
EXECUTE FUNCTION <function_name>();BEFORE triggers
Executed before PostgreSQL performs the actual INSERT/UPDATE/DELETE on the row.
Key abilities
- Modify NEW (e.g., enforce or compute values)
- Validate and reject changes (raise exception)
- Cancel a row by returning NULL
- Preprocess data before it gets written
- Prevent an operation entirely
Common uses
- Auto-updating timestamps
- Enforcing custom constraints
- Sanitizing data
- Auto-filling computed columns
Behavior rules
- For INSERT/UPDATE: return NEW to continue
- For DELETE: return OLD
- Returning NULL skips the row
Example of BEFORE trigger
CREATE FUNCTION normalize_email()
RETURNS trigger AS $$
BEGIN
NEW.email := lower(NEW.email);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER normalize_email_tg
BEFORE INSERT OR UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION normalize_email();AFTER triggers
Executed after the row has been modified and the data is already written.
Key abilities
- Observe changes but cannot modify them
- Perform side-effects that depend on final committed data
- Write audit logs, update summary tables, notify external systems
Common uses
- Audit tables
- Maintaining denormalized reporting tables
- Sending NOTIFY events
- Enforcing cross-table logic that must see final results
Behavior rules
- Return value is ignored; must return something but it doesn’t matter
- No ability to cancel the insert/update/delete
Example of AFTER trigger
CREATE FUNCTION audit_user_changes()
RETURNS trigger AS $$
BEGIN
INSERT INTO user_audit(data, changed_at)
VALUES (row_to_json(NEW), now());
RETURN NEW; -- ignored
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER audit_users
AFTER UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION audit_user_changes();INSTEAD OF triggers
These only apply to views, not tables. They intercept operations aimed at a view and let you define custom behavior.
What they do
- The underlying table(s) are not automatically modified
- Your trigger decides what to do with the incoming NEW/OLD row
- Typically used to make a view writable when PostgreSQL wouldn’t allow it
Common uses
- Making a complex view appear updatable
- Routing an INSERT on a view into several tables
- Enforcing special rules on virtualized tables
Behavior rules
- RETURN NEW means “the operation succeeded for the view”
- No automatic change occurs unless your code executes SQL
- Work on INSERT, UPDATE, and DELETE on views
Example of INSTEAD OF trigger
CREATE VIEW active_users AS
SELECT * FROM users WHERE active = true;
CREATE FUNCTION active_users_insert()
RETURNS trigger AS $$
BEGIN
-- Insert into real table instead
INSERT INTO users(id, email, active)
VALUES (NEW.id, NEW.email, true);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER active_users_ins
INSTEAD OF INSERT ON active_users
FOR EACH ROW EXECUTE FUNCTION active_users_insert();