×
triggers
CREATE OR REPLACE FUNCTION trigger_fn_insert_new_entry_into_logs()
RETURNS TRIGGER AS
$$
BEGIN
INSERT INTO
logs(account_id, old_sum, new_sum)
VALUES
(OLD.id, OLD.balance, NEW.balance);
RETURN NEW;
END;
$$
LANGUAGE plpgsql;
CREATE TRIGGER
tr_account_balance_change
AFTER UPDATE OF balance ON accounts
FOR EACH ROW
WHEN
(NEW.balance <> OLD.balance)
EXECUTE FUNCTION
trigger_fn_insert_new_entry_into_logs();
>
A trigger in PostgreSQL is a piece of code that runs automatically when a specific event happens on a table — for example, when a row is:
INSERTED
UPDATED
DELETED
Triggers are often used for:
Automatic logging
Validation before changes
Keeping audit trails
Enforcing business rules
In an INSERT trigger: only NEW exists.
In a DELETE trigger: only OLD exists.
In an UPDATE trigger: both exist — OLD shows the original row, NEW shows the modified version.
Why You Need RETURN NEW (or RETURN OLD)
In a trigger function, PostgreSQL expects you to return a record that represents what should happen to the row being affected.
BEFORE INSERT / BEFORE UPDATE RETURN NEW; Tells PostgreSQL to continue with the new (possibly modified) row.
BEFORE DELETE RETURN OLD; The deleted row is being removed — you return the old one to confirm deletion.
AFTER triggers RETURN NEW; (or nothing meaningful) Row is already changed — return value is ignored but required syntactically.
If you return the wrong thing (like returning NULL or OLD when PostgreSQL expects NEW), your trigger can become useless — or even break your data logic entirely.
CREATE TABLE deleted_employees (
employee_id SERIAL PRIMARY KEY,
first_name VARCHAR(20),
last_name VARCHAR(20),
middle_name VARCHAR(20),
job_title VARCHAR(50),
department_id INTEGER,
salary NUMERIC(19,4)
);
CREATE OR REPLACE FUNCTION deleted_employees_backup()
RETURNS TRIGGER
AS
$$
BEGIN
INSERT INTO deleted_employees(first_name, last_name, middle_name, job_title, department_id, salary)
VALUES (old.first_name, old.last_name, old.middle_name, old.job_title, old.department_id, old.salary);
RETURN OLD;
END;
$$
LANGUAGE plpgsql;
CREATE OR REPLACE TRIGGER backup_deleted_employees
AFTER DELETE ON employees
FOR EACH ROW EXECUTE PROCEDURE deleted_employees_backup();
FOR EACH ROW
The trigger fires once for each row affected by the operation. You can access OLD and NEW for that specific row.
FOR EACH STATEMENT
The trigger fires once per SQL statement, regardless of how many rows are affected. You cannot access OLD or NEW because multiple rows may have changed.
1. BEFORE Trigger
Runs before the triggering operation (INSERT, UPDATE, DELETE).
Often used to validate or modify data before it’s written.
You can modify the row by changing NEW, or cancel the operation by returning NULL.
Example: Ensure salary is positive before insert
CREATE OR REPLACE FUNCTION check_salary()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.salary < 0 THEN
RAISE EXCEPTION 'Salary cannot be negative';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER before_insert_employee
BEFORE INSERT ON employees
FOR EACH ROW
EXECUTE FUNCTION check_salary()
2. AFTER Trigger
Runs after the operation is complete.
Often used for logging, auditing, or cascading actions.
Can’t prevent the operation (it’s already done), but you can react to it.
Example: Backup deleted employees
CREATE TRIGGER after_delete_employee
AFTER DELETE ON employees
FOR EACH ROW
EXECUTE FUNCTION deleted_employees_backup();
3. INSTEAD OF Trigger (only on views)
Runs instead of the operation.
Only available for views, because views can’t normally be directly modified.
Lets you simulate inserts, updates, or deletes on views by performing operations on underlying tables.
Example: Update a view instead of the underlying tables
CREATE OR REPLACE VIEW employee_view AS
SELECT id, first_name, last_name, department_id FROM employees;
CREATE OR REPLACE FUNCTION instead_of_update_employee()
RETURNS TRIGGER AS $$
BEGIN
UPDATE employees
SET first_name = NEW.first_name,
last_name = NEW.last_name,
department_id = NEW.department_id
WHERE id = OLD.id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER view_update_trigger
INSTEAD OF UPDATE ON employee_view
FOR EACH ROW
EXECUTE FUNCTION instead_of_update_employee();
Now updating the view actually updates the underlying table.