SQL Triggers: BEFORE, AFTER and INSTEAD OF
A trigger is SQL code stored in the database that runs automatically when rows are inserted, updated or deleted. Nobody calls it: the change itself sets it off. Triggers are used for audit logs, for checks the application might forget, and for keeping derived values in sync.
What Is a Trigger?
Suppose every price change has to be recorded. You could ask every developer to remember to write to a log table, or you can let the database do it. This SQLite trigger copies the old and new price into price_changes whenever a product's price is updated:
CREATE TABLE price_changes (
product_id INTEGER,
old_price REAL,
new_price REAL,
changed_at TEXT
);
CREATE TRIGGER trg_products_price_audit
AFTER UPDATE OF price ON products
FOR EACH ROW
WHEN OLD.price <> NEW.price
BEGIN
INSERT INTO price_changes (product_id, old_price, new_price, changed_at)
VALUES (OLD.id, OLD.price, NEW.price, datetime('now'));
END;
From then on, UPDATE products SET price = 49.99 WHERE id = 7 also inserts a row into price_changes, whichever application, script or person ran the UPDATE. The trigger is part of the table, not of any one program.
Parts of a Trigger
CREATE TRIGGER trigger_name
{BEFORE | AFTER | INSTEAD OF} {INSERT | UPDATE [OF column] | DELETE}
ON table_name
[FOR EACH ROW]
[WHEN condition]
BEGIN
-- statements
END;
| Part | Meaning |
|---|---|
| Timing | BEFORE the change, AFTER it, or INSTEAD OF it |
| Event | INSERT, UPDATE (optionally of specific columns) or DELETE |
| FOR EACH ROW | Run once per affected row (row-level). Without it, some databases run once per statement |
| WHEN | Only run when a condition is true, such as "the price actually changed" |
| Body | The statements to run; they can read OLD and NEW |
OLD and NEW
| Event | OLD | NEW |
|---|---|---|
| INSERT | Not available | The row being inserted |
| UPDATE | The row before the change | The row after the change |
| DELETE | The row being deleted | Not available |
SQLite and MySQL write OLD.col and NEW.col, PostgreSQL the same inside its trigger function, Oracle :OLD.col and :NEW.col. SQL Server has no per-row variables: it gives the trigger two tables, inserted and deleted, holding all affected rows.
BEFORE, AFTER and INSTEAD OF
BEFORE: check or adjust the incoming row
A BEFORE trigger runs before the row is written, so it can reject it. This one refuses a product price below cost:
CREATE TRIGGER trg_products_price_check
BEFORE UPDATE OF price ON products
FOR EACH ROW
WHEN NEW.price < NEW.cost
BEGIN
SELECT RAISE(ABORT, 'price cannot be below cost');
END;
The rule it protects holds for every product in the Playground today:
SELECT COUNT(*) AS products,
SUM(price < cost) AS priced_below_cost
FROM products;
▶ Run this query in the SQL Playground
| products | priced_below_cost |
|---|---|
| 120 | 0 |
A simple rule like this can also be a CHECK constraint (CHECK (price >= cost)), which is simpler and should be preferred. Triggers are for rules a constraint cannot express, such as ones that look at other tables.
AFTER: react to a change that happened
AFTER triggers suit audit logs and follow-up updates, because the change is already in the table. The price audit at the top of the page is an AFTER UPDATE trigger. Another example keeps the review count on products in sync:
CREATE TRIGGER trg_reviews_count
AFTER INSERT ON reviews
FOR EACH ROW
BEGIN
UPDATE products
SET review_count = review_count + 1
WHERE id = NEW.product_id;
END;
INSTEAD OF: make a view accept changes
INSTEAD OF triggers replace the statement with your own logic. Their usual job is letting a view accept INSERT, UPDATE or DELETE. A soft delete is a common example: "deleting" through the view only marks the row:
CREATE VIEW active_users AS
SELECT id, first_name, last_name, email
FROM users
WHERE status = 'active';
CREATE TRIGGER trg_active_users_delete
INSTEAD OF DELETE ON active_users
FOR EACH ROW
BEGIN
UPDATE users SET status = 'inactive' WHERE id = OLD.id;
END;
After DELETE FROM active_users WHERE id = 3, user 3 is still in users, now with status inactive, and no longer appears in the view. The Playground already has both kinds of user:
SELECT status, COUNT(*) AS users
FROM users
GROUP BY status;
▶ Run this query in the SQL Playground
| status | users |
|---|---|
| active | 73 |
| inactive | 77 |
Triggers in PostgreSQL, MySQL and SQL Server
The same price audit in other databases:
-- PostgreSQL: the logic lives in a function
CREATE FUNCTION log_price_change() RETURNS trigger AS $$
BEGIN
IF NEW.price <> OLD.price THEN
INSERT INTO price_changes VALUES (OLD.id, OLD.price, NEW.price, now());
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_products_price_audit
AFTER UPDATE OF price ON products
FOR EACH ROW EXECUTE FUNCTION log_price_change();
-- MySQL
CREATE TRIGGER trg_products_price_audit
AFTER UPDATE ON products
FOR EACH ROW
BEGIN
IF NEW.price <> OLD.price THEN
INSERT INTO price_changes VALUES (OLD.id, OLD.price, NEW.price, NOW());
END IF;
END;
-- SQL Server: once per statement, all rows in inserted / deleted
CREATE TRIGGER trg_products_price_audit
ON products
AFTER UPDATE
AS
BEGIN
INSERT INTO price_changes (product_id, old_price, new_price, changed_at)
SELECT d.id, d.price, i.price, SYSDATETIME()
FROM deleted d
JOIN inserted i ON i.id = d.id
WHERE i.price <> d.price;
END;
| Feature | SQLite | PostgreSQL | MySQL | SQL Server |
|---|---|---|---|---|
| BEFORE / AFTER | Yes | Yes | Yes | AFTER only (plus INSTEAD OF) |
| INSTEAD OF | On views | On views | No | On tables and views |
| Row-level | Yes (always) | FOR EACH ROW | Yes (always) | No, statement-level with inserted/deleted |
| Statement-level | No | FOR EACH STATEMENT | No | Yes |
| WHEN condition | Yes | Yes | No (use IF in the body) | No (use WHERE in the body) |
List existing triggers with SELECT name FROM sqlite_master WHERE type = 'trigger' in SQLite, SHOW TRIGGERS in MySQL, information_schema.triggers in PostgreSQL, and sys.triggers in SQL Server. Remove one with DROP TRIGGER name; dropping a table drops its triggers too.
Triggers vs Stored Procedures vs Application Code
| Trigger | Stored procedure | Application code | |
|---|---|---|---|
| Runs when | A row changes, automatically | Someone calls it | The application runs it |
| Applies to every writer | Yes | Only callers | Only that application |
| Visible to developers | Easy to overlook | Called by name | In the codebase |
| Good for | Audit logs, integrity rules across tables, derived columns | Reusable multi-step operations | Business workflows that change often |
Triggers are strongest for rules that must hold whichever program writes to the table. They are weakest for complex workflows: logic that runs invisibly on every write surprises people, is hard to test and slows down bulk changes. See stored procedures.
Triggers and Performance
| Risk | What to do |
|---|---|
| A row-level trigger on a table that gets bulk loads runs once per row | Keep the body small; consider statement-level triggers (PostgreSQL, SQL Server) or disabling the trigger during a controlled load |
| A trigger that writes to a table with its own triggers (nested triggers) | Keep chains short and documented |
| A trigger that fires itself again (recursive) | Add a WHEN condition or guard so its own change cannot set it off endlessly; SQLite has recursive triggers off by default |
| Slow work, such as calling another system | Do not do it in a trigger: it runs inside the transaction and holds its locks |
Common Trigger Mistakes
Five errors and how to avoid them.
1. Validating in an AFTER trigger
CREATE TRIGGER t AFTER INSERT ON products ...
-- checks NEW.price and raises an error
CREATE TRIGGER t BEFORE INSERT ON products ...
-- or, simpler, a CHECK constraint
BEFORE triggers can stop or fix the row before it is stored. An AFTER trigger has to undo the work.
2. Assuming one row per statement in SQL Server
DECLARE @id INT = (SELECT id FROM inserted);
-- fails or picks one row when 50 rows change
INSERT INTO audit (...)
SELECT ... FROM inserted i JOIN deleted d ON d.id = i.id;
SQL Server triggers fire once per statement; inserted and deleted can hold many rows.
3. Logging when nothing changed
AFTER UPDATE ON products
BEGIN
INSERT INTO price_changes ...;
END;
AFTER UPDATE OF price ON products
WHEN OLD.price <> NEW.price
BEGIN
INSERT INTO price_changes ...;
END;
An UPDATE that sets other columns, or the same price, would otherwise fill the log with noise.
4. A trigger that triggers itself
AFTER UPDATE ON products
BEGIN
UPDATE products SET updated_at = now() WHERE id = NEW.id;
END;
-- set the column in a BEFORE trigger on NEW instead,
-- or add WHEN OLD.updated_at IS NEW.updated_at
The trigger's own UPDATE fires it again. Databases stop this with depth limits or by default settings, but the design is still wrong.
5. Hiding business workflows in triggers
-- inserting an order also emails, charges a card
-- and updates five tables, all from triggers
-- triggers: audit and integrity only
-- application or procedure: the order workflow
People reading the application code cannot see what the database does on their behalf.
Triggers Cheat Sheet
| Task | SQLite |
|---|---|
| Audit an update | AFTER UPDATE OF col ON t FOR EACH ROW WHEN OLD.col <> NEW.col BEGIN INSERT ...; END; |
| Reject a row | BEFORE INSERT ON t ... BEGIN SELECT RAISE(ABORT, 'message'); END; |
| Make a view writable | INSTEAD OF INSERT ON v ... BEGIN INSERT INTO base ...; END; |
| List triggers | SELECT name FROM sqlite_master WHERE type = 'trigger'; |
| Remove | DROP TRIGGER IF EXISTS name; |
Practice Triggers Queries
4 exercises on the real Playground tables.
Write each query yourself first. Every solution has been run against the Playground database.
-
1Easy Check whether the Playground database has any triggers. Table:
sqlite_masterShow solution
SELECT COUNT(*) AS triggers FROM sqlite_master WHERE type = 'trigger';▶ Run this query in the SQL Playground
The Playground has none. In your own SQLite database, triggers you create are listed here.
-
2Medium A trigger is meant to keep products.review_count equal to the number of rows in reviews. Check how many products currently match. Table:
products, reviewsShow solution
SELECT COUNT(*) AS products, SUM(p.review_count = (SELECT COUNT(*) FROM reviews r WHERE r.product_id = p.id)) AS counts_in_sync FROM products p;▶ Run this query in the SQL Playground
In the Playground the two are independent sample data, so they do not match. This is the kind of drift a trigger such as trg_reviews_count is meant to prevent.
-
3Medium Before adding a BEFORE INSERT trigger that rejects salaries under 40,000, check whether any existing employee would break the rule. Table:
employeesShow solution
SELECT COUNT(*) AS below_40000 FROM employees WHERE salary < 40000;▶ Run this query in the SQL Playground
Check existing data before enforcing a new rule; a trigger only checks rows changed after it exists.
-
4Medium An audit trigger would log every status change. Show how many orders are in each status today, the baseline such a log starts from. Table:
ordersShow solution
SELECT status, COUNT(*) AS orders FROM orders GROUP BY status ORDER BY orders DESC;▶ Run this query in the SQL Playground
Triggers record changes from the moment they are created; the starting state has to be captured separately.
📝 Quiz — Triggers
Test your understanding · 0/41 answered
What is a trigger in SQL?
The data change sets it off; nobody calls it.
Which events can a trigger fire on?
Some databases also have DDL or logon triggers, but the usual ones are DML.
Which statement creates a trigger?
CREATE TRIGGER name ... ON table ...
When does a BEFORE trigger fire?
So it can reject or adjust the row.
When does an AFTER trigger fire?
Good for audit logs.
What does an INSTEAD OF trigger do?
Mostly used to make views writable.
What does NEW refer to inside a trigger?
Available for INSERT and UPDATE.
What does OLD refer to inside a trigger?
Available for UPDATE and DELETE.
In an INSERT trigger, which is available?
There was no previous row.
In a DELETE trigger, which is available?
There is no new row.
What are inserted and deleted in SQL Server?
They can hold many rows at once.
Which trigger type lets a non-updatable view accept INSERT?
It writes to the base tables itself.
What is a common use of an AFTER INSERT trigger?
Or updating a derived count.
What is a common use of a BEFORE UPDATE trigger?
Simple checks are better as constraints.
What is a soft delete?
The row can be restored later.
Which trigger type usually implements a transparent soft delete?
The example on this page uses INSTEAD OF DELETE on a view.
What is the main difference between a trigger and a stored procedure?
That is also why triggers are easy to overlook.
Can a trigger call a stored procedure?
SQLite has no stored procedures.
What is a recursive trigger?
SQLite has recursive triggers off by default.
What is a nested trigger?
Chains can be hard to follow.
Why keep triggers on busy tables lightweight?
Bulk changes multiply the cost.
What happens with an endless trigger loop?
Guard the trigger's condition.
What does an audit trigger usually record?
Exactly what depends on the requirement.
How can a trigger help with fraud detection?
Usually by writing to a review table.
What is the key difference between a trigger and a rule in application code?
Scripts and admin tools are covered too.
What is a fair concern about putting lots of logic in triggers?
Document them and keep them small.
How does SQL Server give a trigger the changed rows?
Write set-based trigger code in SQL Server.
How does Oracle refer to old and new values in a trigger?
With a colon.
Why add error handling to trigger logic?
An error in a trigger fails the statement that fired it.
What is a common mistake in a trigger that enforces a rule?
Validate before the write.
What is the benefit of an audit trigger over "remember to write the log" in every application?
Nobody can forget it.
What does FOR EACH ROW mean?
SQLite and MySQL triggers are always row-level.
Why can bulk inserts be slow with a row-level AFTER INSERT trigger?
Statement-level triggers can help where supported.
What is a WHEN clause in a trigger for?
Supported by SQLite, PostgreSQL and Oracle.
What is the difference between a trigger and a function?
In PostgreSQL a trigger calls a special trigger function.
Why do regulated companies use audit triggers?
The log is written whichever system makes the change.
How do you decide between a trigger and application code?
Integrity in the database, workflow in the application.
What happens to a trigger when its table is dropped?
A trigger belongs to its table.
What is the safest way to prevent an endless trigger loop?
Or set the value in a BEFORE trigger on NEW.
Why is DROP TRIGGER useful when debugging?
Re-create it afterwards from its saved definition.
What sums up a trigger?
That is the core idea.
Triggers Interview Questions
Q1. What is the difference between BEFORE, AFTER and INSTEAD OF triggers?
BEFORE runs before the row is written and can change or reject it. AFTER runs once the change is in the table, which suits logging and follow-up updates. INSTEAD OF replaces the statement, usually to make a view writable.
Q2. What are OLD and NEW?
The row before and after the change. INSERT has only NEW, DELETE only OLD, UPDATE both. SQL Server uses the inserted and deleted tables instead.
Q3. Why must SQL Server triggers handle several rows?
They fire once per statement, not per row. An UPDATE of 50 rows fires the trigger once with 50 rows in inserted and deleted, so the code must be set-based.
Q4. When should you not use a trigger?
For complex or frequently changing business workflows, for slow work such as calling external services, and for simple rules a constraint can express.
Q5. How do you audit changes with a trigger?
An AFTER UPDATE (and INSERT, DELETE) trigger writes the key, old and new values, time and user to an audit table, with a WHEN or WHERE condition so only real changes are logged.
Frequently Asked Questions
What is a trigger in SQL?
A trigger is SQL code stored in the database that runs automatically when an INSERT, UPDATE or DELETE happens on a table, without anyone calling it.
Why use SQL triggers?
So that logging, checks and derived values are handled the same way for every change, whichever application, script or person makes it.
What is a BEFORE trigger?
A trigger that runs before the change is written to the table. It can check or adjust the new values, or reject the change.
What is an AFTER trigger?
A trigger that runs after the change has been applied. It is the usual choice for audit logs and for updating other tables in response.
What is an INSTEAD OF trigger?
A trigger that replaces the statement with its own logic. It is mostly used to let a view accept INSERT, UPDATE or DELETE by writing to the underlying tables.
What is the difference between a trigger and a stored procedure?
A stored procedure runs only when it is called, for example with CALL or EXEC. A trigger runs automatically when the data change it is attached to happens.
Can triggers slow down a database?
Yes. Every trigger adds work to the statement that fires it, and row-level triggers run once per affected row, which adds up on bulk changes. Keep them small.
Are triggers important in SQL interviews?
Yes. CREATE TRIGGER syntax, BEFORE versus AFTER versus INSTEAD OF, OLD and NEW values, SQL Server's inserted and deleted tables, and triggers versus procedures are common questions.