51 Triggers
A trigger is a function that PostgreSQL calls automatically when a specific event happens on a table — INSERT, UPDATE, DELETE, or TRUNCATE, fired BEFORE or AFTER the row change, for each row or for the whole statement. Triggers are the standard tool for enforcing invariants the type system cannot, auditing changes, denormalizing for performance, and keeping updated_at timestamps current.
What you’ll learn
- The
BEFOREvsAFTERdistinction - Row-level (
FOR EACH ROW) vs statement-level (FOR EACH STATEMENT) - The
NEWandOLDrow variables - A canonical example: auto-updating
updated_at - An audit-log trigger that writes to a side table
Example
A trigger is two pieces: a trigger function (returns trigger) and a trigger that ties the function to a table and an event. Inside the function:
NEWis the new row (forINSERTandUPDATE).OLDis the old row (forUPDATEandDELETE).TG_OPis the operation ('INSERT','UPDATE','DELETE','TRUNCATE').TG_TABLE_NAMEetc. are also available for reusable triggers.
For BEFORE ROW INSERT/UPDATE triggers, return NEW (possibly modified); for DELETE, return OLD. Returning NULL skips that row’s operation. For AFTER ROW triggers, the return value is ignored — the operation already happened. Statement-level triggers (FOR EACH STATEMENT) fire once per statement regardless of row count and do not have NEW/OLD.
The updated_at bumper is the most common trigger in the world. Define one trigger function, attach it to every table with an updated_at column.
Run the example
To run:
Common pitfalls
- A
BEFOREtrigger that returnsNULLsilently swallows the row — theINSERT/UPDATEis skipped without an error. ReturnNEWfor INSERT/UPDATE andOLDfor DELETE unless you mean to skip the row. - Triggers fire inside the same transaction as the triggering statement. Heavy logic in a trigger slows every modification of the table.
- Triggers do not fire on
COPY ... FROMby default? They do, actually — butTRUNCATEdoes not fire row-level triggers (only statement-level). Plan accordingly. - Putting business logic in triggers makes it hard to discover from application code. Many teams limit triggers to infrastructure concerns (updated_at, audit) and keep business logic in the application.
Use pg_trigger_depth() inside a trigger to detect recursion. A trigger that updates the same table that fired it can loop — guard with IF pg_trigger_depth() > 1 THEN RETURN NEW; END IF;.
The script drops its demo tables and trigger functions at the end. Recreate the timestamp trigger function from the example before continuing. Add an updated_at column to fruits and attach the trigger from above. Update a row and confirm updated_at changes. Then add a BEFORE INSERT trigger that uppercases name automatically, and see it in action.
Downloads
SQL file: triggers.sql · All lesson files · Setup guide