BEFORE, AFTER and INSTEAD OF Triggers
coreintermediateBEFORE triggers fire before the write is attempted and can modify the row or cancel the operation entirely (by returning NULL). AFTER triggers fire once the write has actually happened and cannot change it — they see the final state. INSTEAD OF triggers replace the operation entirely and only exist on views, which have no storage of their own to write to.
Think of it as
The three timings map onto three different jobs. BEFORE is for shaping or vetoing a row before it becomes real — normalizing a value, rejecting an invalid state. AFTER is for reacting to something that has already, definitely happened — writing an audit log entry, sending a notification — precisely because by then nothing can undo it. INSTEAD OF exists because a view has no rows of its own to insert into; the trigger IS the insert, translating it into whatever real writes the view's owner decides.
What we're doing: Show a BEFORE trigger silently rejecting an invalid row (by returning NULL) versus an AFTER trigger, which by the time it runs can only observe, not stop, the same write.
- 3
- The row is inspected before it is ever written — this is only possible in a BEFORE trigger.
- 4
- Returning NULL from a row-level BEFORE trigger tells the executor to skip this row entirely — no error, no row.
- 11
- The row genuinely never existed; an AFTER trigger could never have prevented this, since it fires only once the write already succeeded.
INSERT 0 0
count
-------
0Why this works: A BEFORE trigger sits in the one place in the pipeline where "should this row exist at all" can still be decided — by the time an AFTER trigger runs, that decision has already been made and committed within the transaction, which is exactly why AFTER triggers are reserved for reacting to facts rather than vetoing them.
Trying to cancel a write from an AFTER trigger
Wrong
Better
What you see: The trigger appears to "work" in casual testing (no error is raised), but the invalid row is actually inserted and remains in the table — RETURN NULL from an AFTER trigger is simply ignored by the executor.
Why: An AFTER trigger's return value is discarded entirely, because by the time it fires the write is already a committed part of the current transaction — there is no operation left to skip. This is a common source of confusion precisely because the trigger runs without error, giving no signal that the intended guard silently did nothing.
- BEFORE — can modify NEW or return NULL to skip
- leads to the actual write (may veto or reshape)
- the actual write — row is inserted/updated/deleted
- leads to AFTER (already committed to this transaction)
- AFTER — sees the final state, cannot change it
BEFORE vs AFTER vs INSTEAD OF
Remember: BEFORE can modify NEW or return NULL to veto the write; AFTER only observes a write that already happened and cannot undo it; INSTEAD OF replaces the operation entirely and exists only on views, which have nothing of their own to write to.
See also: row level vs statement level triggers · primary foreign unique not null check

