Triggers are code the database runs by itself, automatically, whenever a specific event happens to a table — a row gets inserted, updated, or deleted. Nothing in the application has to call a trigger; that is the entire point of one. You define it once against the table, and from then on it fires no matter which application, script, or human at a SQL console causes the triggering change. That property makes triggers powerful for enforcing rules no application can accidentally skip, and dangerous for hiding logic nobody remembers is there.
What changed in 2026
- Postgres event triggers matured for schema-level auditing — beyond row-level triggers, teams increasingly log DDL changes (new tables, altered columns) automatically for compliance trails.
- ORMs added louder warnings about triggers. Prisma and Drizzle documentation now explicitly flag that a trigger-modified row can differ from what the ORM thinks it just wrote, prompting more teams to reload the row after a write that has triggers attached.
- Change Data Capture (CDC) tools took over use cases that used to need triggers. Debezium and similar tools now read the write-ahead log directly for replication and syncing, which is more efficient than a trigger-based audit table for high-volume changes.
How a trigger fires
A trigger is defined against a table and an event, with timing relative to that event:
CREATE OR REPLACE FUNCTION set_updated_at()
RETURNS TRIGGER AS $
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$ LANGUAGE plpgsql;
CREATE TRIGGER trg_orders_updated_at
BEFORE UPDATE ON orders
FOR EACH ROW
EXECUTE FUNCTION set_updated_at();
Every UPDATE on orders now stamps updated_at automatically, with zero chance an application forgets to set it, because the application is not the one setting it.
Trigger types
| Timing |
Can modify the row? |
Can block the operation? |
Typical use |
| BEFORE |
Yes |
Yes (via exception or NULL return) |
Validation, auto-filled columns |
| AFTER |
No (row is already committed to the statement) |
No, only by raising an exception |
Audit logging, cascading updates |
| INSTEAD OF |
Replaces the operation entirely |
Yes, by definition |
Making a view writable |
Row-level triggers (FOR EACH ROW) fire once per affected row — an UPDATE touching 10,000 rows fires the trigger 10,000 times. Statement-level triggers fire exactly once regardless of how many rows the statement touched, which matters enormously for performance on bulk operations.
What triggers are good for
Triggers earn their place for invariants that must hold no matter what wrote the data: keeping an updated_at column honest, maintaining a denormalized count in sync with its source rows, enforcing a business rule that a plain CHECK constraint cannot express, or writing an audit-log row for every change to a sensitive table. The common thread is that these are database-level guarantees — they should hold whether the write came from your API, a one-off script, or a teammate at psql. If the rule only needs to apply from your own application's code path, a plain function called by that code, or a stored procedure, is usually easier to find and debug later.
Why triggers get a bad reputation
The complaint is almost never that triggers do not work; it is that they are invisible. An UPDATE statement that looks simple in application code can silently cascade into several more writes, and a developer reading the app's code has no way to know that without also checking the schema. Debugging "why did this row change when I did not touch it" is a uniquely frustrating exercise when the answer is a trigger three files away from anywhere your search would look. Triggers that call other triggers compound this further, and a trigger that fails partway through can leave a database transaction rolled back for a reason that appears nowhere in the application's own logs.
FAQ
Do triggers run inside the same transaction as the statement that fired them?
Yes. If a trigger raises an exception, the whole transaction — the original statement and anything already done inside it — rolls back together.
Are triggers slower than doing the same logic in application code?
For row-level triggers on large bulk operations, yes, noticeably — the per-row overhead adds up. For single-row operations the difference is usually negligible.
Can a trigger call a stored procedure?
Yes, and in Postgres a trigger function is itself effectively a small stored procedure written specifically to be called by the trigger mechanism.
What is the difference between a trigger and a foreign key constraint?
A foreign key constraint enforces one specific, well-understood rule (referential integrity) using engine-optimized internals. A trigger can enforce arbitrary custom logic, but is not specialized or optimized the way a constraint is.
Where to go next