Triggers
A trigger is a block of SQL logic that the database executes automatically in response to an INSERT, UPDATE, or DELETE on a table — without the application ever explicitly calling it. Where a stored procedure runs only when something invokes it with CALL, a trigger runs implicitly, as a side effect of a data-changing statement, which makes it well suited for enforcing rules or bookkeeping that should never be skippable, no matter which application or script performs the change.
Timing: BEFORE vs AFTER
A trigger is defined to fire at a specific timing relative to the statement that caused it:
Timing | When it runs | Typical use |
|---|---|---|
BEFORE | Before the row is written | Validate or modify values before they are saved (e.g. normalize a column, block an invalid change) |
AFTER | After the row has been written | React to a change that has already happened (e.g. write an audit log, update a related table) |
Row-level vs statement-level triggers
Scope | Fires | Example |
|---|---|---|
Row-level | Once per affected row | An UPDATE that changes 500 rows fires the trigger 500 times, once per row |
Statement-level | Once per statement, regardless of row count | The same 500-row UPDATE fires the trigger exactly once, for the statement as a whole |
Worked example: auto-updating an updated_at column
One of the most common trigger use cases is automatically stamping a row with the current time whenever it changes, so application code never has to remember to set it manually:
PostgreSQL: a BEFORE UPDATE row-level trigger
CREATE TABLE products ( product_id INT PRIMARY KEY, name VARCHAR(200), price NUMERIC(10, 2), updated_at TIMESTAMP DEFAULT NOW() ); CREATE FUNCTION set_updated_at() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$; CREATE TRIGGER trg_products_updated_at BEFORE UPDATE ON products FOR EACH ROW EXECUTE FUNCTION set_updated_at(); -- No matter what changes, updated_at is stamped automatically UPDATE products SET price = 29.99 WHERE product_id = 1;
Worked example: auditing changes
An AFTER trigger is a natural fit for recording history without the application having to remember to log it:
PostgreSQL: an AFTER UPDATE trigger writing an audit row
CREATE TABLE price_audit (
audit_id SERIAL PRIMARY KEY,
product_id INT,
old_price NUMERIC(10, 2),
new_price NUMERIC(10, 2),
changed_at TIMESTAMP DEFAULT NOW()
);
CREATE FUNCTION log_price_change()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
IF OLD.price IS DISTINCT FROM NEW.price THEN
INSERT INTO price_audit (product_id, old_price, new_price)
VALUES (OLD.product_id, OLD.price, NEW.price);
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_price_audit
AFTER UPDATE ON products
FOR EACH ROW
EXECUTE FUNCTION log_price_change();A trigger runs automatically in response to INSERT, UPDATE, or DELETE, without being called explicitly.
BEFORE triggers can validate or modify a row before it is saved; AFTER triggers react to a change that already happened.
Row-level triggers fire once per affected row; statement-level triggers fire once per statement.
Common uses are auditing and automatically maintaining columns like updated_at.
Triggers add implicit behavior that can make debugging harder — use them deliberately and document them well.