We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Trigger Basics
A trigger is a function that PostgreSQL calls automatically when a specific event happens on a table -- an INSERT, UPDATE, DELETE, or TRUNCATE. Triggers let you enforce complex business rules, maintain derived data, and record audit history without relying on application code.
How Triggers Work
The trigger system has two parts:
- Trigger function -- a special function with
RETURNS TRIGGERthat contains the logic. - Trigger definition -- a
CREATE TRIGGERstatement that binds the function to a table and event.
When the event occurs, PostgreSQL invokes the trigger function automatically.
Creating a Trigger Function
A trigger function looks like a normal PL/pgSQL function, except:
- It takes no parameters (data comes through special variables).
- It returns the special type
TRIGGER.
CREATE FUNCTION my_trigger_fn()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
-- trigger logic here
RETURN NEW; -- for BEFORE triggers on INSERT/UPDATE
END;
$$;
Creating a Trigger
CREATE TRIGGER trigger_name
{BEFORE | AFTER | INSTEAD OF} {INSERT | UPDATE | DELETE | TRUNCATE}
ON table_name
[FOR EACH {ROW | STATEMENT}]
[WHEN (condition)]
EXECUTE FUNCTION trigger_function_name();
Complete Example
-- Step 1: Create the trigger function
CREATE FUNCTION set_updated_at()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at := NOW();
RETURN NEW;
END;
$$;
-- Step 2: Attach it to a table
CREATE TRIGGER trg_employees_updated
BEFORE UPDATE ON employees
FOR EACH ROW
EXECUTE FUNCTION set_updated_at();
Now every time a row in employees is updated, the updated_at column is automatically set to the current timestamp.
BEFORE vs. AFTER
BEFORE Triggers
- Fire before the actual data change.
- Can modify the row being inserted or updated (by changing
NEW). - Can cancel the operation by returning
NULL. - Use for: validation, setting default values, modifying data before it is written.
CREATE FUNCTION validate_salary()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.salary < 0 THEN
RAISE EXCEPTION 'Salary cannot be negative: %', NEW.salary;
END IF;
IF NEW.salary > 500000 THEN
RAISE NOTICE 'Capping salary at 500000 for %', NEW.name;
NEW.salary := 500000;
END IF;
RETURN NEW; -- must return NEW to proceed
END;
$$;
CREATE TRIGGER trg_validate_salary
BEFORE INSERT OR UPDATE ON employees
FOR EACH ROW
EXECUTE FUNCTION validate_salary();
AFTER Triggers
- Fire after the data change is complete.
- Cannot modify the row (the write has already happened).
- Can see the final state of the row.
- Use for: logging, notifications, updating related tables.
CREATE FUNCTION log_new_employee()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO employee_log(employee_id, action, logged_at)
VALUES (NEW.id, 'HIRED', NOW());
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_log_hire
AFTER INSERT ON employees
FOR EACH ROW
EXECUTE FUNCTION log_new_employee();
FOR EACH ROW vs. FOR EACH STATEMENT
FOR EACH ROW
The trigger function runs once per affected row. If an UPDATE changes 100 rows, the function executes 100 times. The special variables NEW and OLD are available.
FOR EACH STATEMENT
The trigger function runs once per SQL statement, regardless of how many rows are affected. NEW and OLD are not available (there is no single row to reference).
CREATE FUNCTION log_bulk_update()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO audit_log(table_name, action, logged_at)
VALUES (TG_TABLE_NAME, TG_OP, NOW());
RETURN NULL; -- return value is ignored for AFTER triggers
END;
$$;
CREATE TRIGGER trg_log_bulk
AFTER UPDATE ON employees
FOR EACH STATEMENT
EXECUTE FUNCTION log_bulk_update();
Special Trigger Variables
Inside a trigger function, PostgreSQL provides several special variables:
| Variable | Description |
|---|---|
NEW |
The new row for INSERT/UPDATE (NULL for DELETE) |
OLD |
The old row for UPDATE/DELETE (NULL for INSERT) |
TG_NAME |
The name of the trigger |
TG_TABLE_NAME |
The name of the table the trigger is on |
TG_TABLE_SCHEMA |
The schema of the table |
TG_OP |
The operation: 'INSERT', 'UPDATE', 'DELETE', or 'TRUNCATE' |
TG_WHEN |
'BEFORE', 'AFTER', or 'INSTEAD OF' |
TG_LEVEL |
'ROW' or 'STATEMENT' |
TG_NARGS |
Number of arguments passed to the trigger function |
TG_ARGV |
Array of string arguments passed to the trigger |
Using TG_OP for Multi-Event Triggers
A single trigger function can handle multiple events:
CREATE FUNCTION employee_audit()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
INSERT INTO audit_log(table_name, action, new_data, logged_at)
VALUES (TG_TABLE_NAME, 'INSERT', ROW_TO_JSON(NEW), NOW());
RETURN NEW;
ELSIF TG_OP = 'UPDATE' THEN
INSERT INTO audit_log(table_name, action, old_data, new_data, logged_at)
VALUES (TG_TABLE_NAME, 'UPDATE', ROW_TO_JSON(OLD), ROW_TO_JSON(NEW), NOW());
RETURN NEW;
ELSIF TG_OP = 'DELETE' THEN
INSERT INTO audit_log(table_name, action, old_data, logged_at)
VALUES (TG_TABLE_NAME, 'DELETE', ROW_TO_JSON(OLD), NOW());
RETURN OLD;
END IF;
RETURN NULL;
END;
$$;
CREATE TRIGGER trg_employee_audit
AFTER INSERT OR UPDATE OR DELETE ON employees
FOR EACH ROW
EXECUTE FUNCTION employee_audit();
The WHEN Clause
You can add a WHEN condition to fire the trigger only when certain criteria are met. This is evaluated before the trigger function is called, so it is more efficient than checking inside the function:
-- Only fire when salary actually changes
CREATE TRIGGER trg_salary_change
AFTER UPDATE ON employees
FOR EACH ROW
WHEN (OLD.salary IS DISTINCT FROM NEW.salary)
EXECUTE FUNCTION notify_salary_change();
-- Only fire for high-value orders
CREATE TRIGGER trg_large_order
AFTER INSERT ON orders
FOR EACH ROW
WHEN (NEW.total_price > 1000)
EXECUTE FUNCTION alert_large_order();
The WHEN clause can reference OLD and NEW but cannot contain subqueries.
UPDATE OF Specific Columns
You can restrict an UPDATE trigger to fire only when specific columns change:
CREATE TRIGGER trg_email_changed
BEFORE UPDATE OF email ON employees
FOR EACH ROW
EXECUTE FUNCTION validate_email_change();
This trigger only fires when the email column is included in the UPDATE's SET clause.
Managing Triggers
List Triggers on a Table
SELECT trigger_name, event_manipulation, action_timing
FROM information_schema.triggers
WHERE event_object_table = 'employees';
Disable and Enable
-- Disable a specific trigger
ALTER TABLE employees DISABLE TRIGGER trg_employee_audit;
-- Disable all triggers on a table
ALTER TABLE employees DISABLE TRIGGER ALL;
-- Re-enable
ALTER TABLE employees ENABLE TRIGGER trg_employee_audit;
ALTER TABLE employees ENABLE TRIGGER ALL;
Drop a Trigger
DROP TRIGGER IF EXISTS trg_employee_audit ON employees;
Note: dropping the trigger does not drop the trigger function. You must drop the function separately if desired.
Execution Order
When multiple triggers exist on the same table for the same event and timing:
- BEFORE STATEMENT triggers fire first.
- BEFORE ROW triggers fire for each row (alphabetically by trigger name).
- The actual data operation happens.
- AFTER ROW triggers fire for each row (alphabetically by trigger name).
- AFTER STATEMENT triggers fire last.
Try It Yourself
Think through these scenarios (or try them on your own database):
- Write a trigger that automatically lowercases the
emailcolumn on INSERT and UPDATE. - Write a BEFORE DELETE trigger that prevents deleting an employee if they are still active (
is_active = TRUE). - Write a statement-level trigger that logs the total number of rows affected by any DELETE on the
orderstable.
Summary
| Concept | Description |
|---|---|
| Trigger function | CREATE FUNCTION name() RETURNS TRIGGER LANGUAGE plpgsql |
| Create trigger | CREATE TRIGGER name {BEFORE|AFTER} {event} ON table ... |
| BEFORE trigger | Can modify NEW, return NULL to cancel |
| AFTER trigger | Runs after the write; cannot modify the row |
| FOR EACH ROW | Fires per affected row; has NEW/OLD |
| FOR EACH STATEMENT | Fires once per statement; no NEW/OLD |
| WHEN clause | Conditional firing without entering the function |
| UPDATE OF | Fire only when specific columns are updated |
| Disable/Enable | ALTER TABLE t DISABLE/ENABLE TRIGGER name |