PostgreSQL Triggers PostgreSQL · Triggers · Intro

PL/pgSQL Triggers — Introduction

Master PostgreSQL trigger fundamentals — the BEFORE/AFTER timing axis, the ROW/STATEMENT scope axis, the five trigger events (INSERT, UPDATE, DELETE, TRUNCATE, INSTEAD OF), and the OLD/NEW/TG_* implicit variables. Includes when triggers are the right tool versus declarative constraints, generated columns, and FK cascades.

A trigger is a database object that fires a function automatically in response to an event on a table — an INSERT, UPDATE, DELETE, or TRUNCATE. The function runs as part of the same transaction as the triggering statement, so its effects either commit together with the change or roll back together with it.

Triggers exist for one main reason: to put logic that must run on data changes inside the database, where every application connecting to that database is forced to obey it. The alternative — relying on every application to do the right thing — only works until someone writes a script that bypasses the rule.

Common uses for triggersExample
Audit loggingEvery UPDATE on employees writes the old and new row to employees_audit
ValidationA BEFORE INSERT check refuses to add a row with negative salary
Derived dataAn UPDATE on order line items recomputes the order total
Materializing viewsChanges to base tables propagate to a denormalized reporting table
Cross-table integrityInserting a customer also inserts a default loyalty record

Every trigger is classified along two independent axes. The combination determines when and how often it fires.

Axis 1: Timing — BEFORE vs AFTER
TimingFiresCan doBest for
BEFOREBefore the row is writtenModify the row (NEW), cancel the operation (return NULL)Validation, normalization, computed columns
AFTERAfter the row is writtenRead the final row; trigger other side effectsAudit logging, cascading actions, notifications
Axis 2: Scope — ROW vs STATEMENT
ScopeFiresSeesBest for
FOR EACH ROWOnce per affected rowOLD and NEW for that rowPer-row validation, audit, derived values
FOR EACH STATEMENTOnce per SQL statement, regardless of row countTransition tables (NEW TABLE / OLD TABLE) in PG ≥10Bulk-aware logic, statement-level summary actions
💡 Concrete difference. An UPDATE employees SET salary = salary * 1.05; on a 100-row table fires a row-level trigger 100 times (once per row) and a statement-level trigger once.
EventFired byOLD/NEW available
INSERTINSERT statementsOnly NEW
UPDATEUPDATE statementsOLD and NEW
DELETEDELETE statementsOnly OLD
TRUNCATETRUNCATE statementsNeither (statement-level only)
INSTEAD OFDML on a view (special case)OLD/NEW depending on operation

Inside a row-level trigger function, two implicit record variables let you access the row data:

  • OLD — the row before the change. Available in UPDATE and DELETE triggers.
  • NEW — the row after the change. Available in INSERT and UPDATE triggers.

Plus a set of TG_* variables that describe the trigger context:

VariableHolds
TG_OPThe operation: 'INSERT', 'UPDATE', 'DELETE', or 'TRUNCATE'
TG_WHENTiming: 'BEFORE', 'AFTER', or 'INSTEAD OF'
TG_LEVELScope: 'ROW' or 'STATEMENT'
TG_TABLE_NAMEName of the table the trigger is on
TG_TABLE_SCHEMASchema of that table
TG_NAMEName of the trigger itself

The most important practical difference between BEFORE and AFTER:

  • BEFORE triggers can modify NEW — set defaults, normalize values, fill in derived columns. Whatever you assign to NEW is what gets written to the table.
  • BEFORE triggers can cancel the operation — return NULL from the trigger function and the row is silently skipped.
  • AFTER triggers see the final row — including any defaults and serial values that were filled in by the database itself.
  • AFTER triggers cannot modify NEW — the row's already on disk. Assignment to NEW is silently ignored.
⚠️ Common bug: trying to set a "last_updated" timestamp in an AFTER UPDATE trigger. Doesn't work — the row is already written when AFTER fires. To auto-fill a timestamp column on every update, you must use a BEFORE trigger that sets NEW.last_updated := CURRENT_TIMESTAMP.

PostgreSQL implements the SQL-standard trigger model with several useful extensions:

  • TRUNCATE triggers — fire on TRUNCATE TABLE. Most other databases skip triggers on TRUNCATE entirely.
  • INSTEAD OF triggers on views — let you make read-only views writable by defining custom DML handlers.
  • Triggers must call a separately-defined function — you can't put the trigger body inline in the CREATE TRIGGER statement. The function returns type trigger and is bound to one or more triggers via EXECUTE FUNCTION.
  • Multiple triggers per event are allowed — fire in alphabetical order by trigger name.
  • Conditional firing via WHEN clauses — trigger only fires if a condition holds, e.g. WHEN (NEW.salary <> OLD.salary).

Triggers are powerful, but invisible — they fire automatically and don't show up in the SQL you're reading. That makes them easy to forget about, hard to debug, and dangerous when overused. Some signs that a trigger isn't the right answer:

SituationBetter tool
Computing a value from other columnsGENERATED ALWAYS AS (...) STORED column — visible in the table definition
Constraint enforcementCHECK, UNIQUE, FOREIGN KEY, or exclusion constraints — declarative and indexable
Cross-table cascades on deleteON DELETE CASCADE on the foreign key — same effect, no trigger
Rate-limited side effects (sending emails, calling APIs)Application code or a job queue — triggers run synchronously inside the transaction and slow it down
Logic that varies by application or contextApplication code — a trigger applies uniformly to every connection

Use triggers when the rule must be enforced at the database level and declarative constraints can't express it. Audit logging is the canonical case.

  1. Keep triggers fast. They run in the transaction's hot path; slow triggers slow every write.
  2. Document them visibly. A trigger fires invisibly — at minimum, comment the table to mention which triggers fire on it.
  3. Don't catch errors silently. A WHEN OTHERS THEN return NEW; in a trigger swallows real bugs.
  4. Don't write to the same table the trigger is on from inside a row-level trigger — you'll trigger the trigger again. Statement-level can sometimes work; usually a sign to refactor.
  5. Use BEFORE for changing data and AFTER for reacting to data. The mental model holds in 95% of cases.
  6. Prefer declarative constraints when they suffice. A CHECK constraint is faster, simpler, and visible in the schema; a trigger has to be opened to read.
  • A trigger is a database-level handler that runs a function automatically when an INSERT, UPDATE, DELETE, or TRUNCATE happens.
  • Two independent axes classify triggers: timing (BEFORE / AFTER / INSTEAD OF) and scope (ROW / STATEMENT).
  • Inside a row-level trigger, OLD and NEW hold the before- and after-images. TG_* variables describe the trigger context.
  • BEFORE triggers can change data (modify NEW, return NULL to skip). AFTER triggers can only react to data already written.
  • PostgreSQL adds TRUNCATE triggers and INSTEAD OF triggers on views to the standard model.
  • Use triggers when database-level enforcement matters and declarative constraints don't suffice. Audit logging is the canonical use case.