SQLab Hub
Lesson 35 · Triggers
Lesson 35 · Advanced

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.

⚡ BEFORE · AFTER · INSTEAD OF OLD and NEW 🧪 4 practice exercises 📝 41-question quiz ⏱ ~25 min

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:

SQLite · run in a local database
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.

The SQLabHub Playground only runs SELECT and WITH queries, so CREATE TRIGGER and the data changes that fire triggers cannot be tried there. Every SQLite example on this page was checked by running it in a local SQLite database; the PostgreSQL, MySQL and SQL Server versions are shown for comparison.

Parts of a Trigger

SQL · general shape (SQLite)
CREATE TRIGGER trigger_name
{BEFORE | AFTER | INSTEAD OF} {INSERT | UPDATE [OF column] | DELETE}
ON table_name
[FOR EACH ROW]
[WHEN condition]
BEGIN
  -- statements
END;
PartMeaning
TimingBEFORE the change, AFTER it, or INSTEAD OF it
EventINSERT, UPDATE (optionally of specific columns) or DELETE
FOR EACH ROWRun once per affected row (row-level). Without it, some databases run once per statement
WHENOnly run when a condition is true, such as "the price actually changed"
BodyThe statements to run; they can read OLD and NEW

OLD and NEW

EventOLDNEW
INSERTNot availableThe row being inserted
UPDATEThe row before the changeThe row after the change
DELETEThe row being deletedNot 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:

SQLite · run in a local database
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:

SQLite · runs in the Playground
SELECT COUNT(*) AS products,
       SUM(price < cost) AS priced_below_cost
FROM products;

▶ Run this query in the SQL Playground

Products priced below cost
productspriced_below_cost
1200
✓ 1 row · real output from the Playground database

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:

SQLite · run in a local database
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:

SQLite · run in a local database
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:

SQLite · runs in the Playground
SELECT status, COUNT(*) AS users
FROM users
GROUP BY status;

▶ Run this query in the SQL Playground

Active and inactive users
statususers
active73
inactive77
✓ 2 rows · real output from the Playground database

Triggers in PostgreSQL, MySQL and SQL Server

The same price audit in other databases:

SQL · PostgreSQL
-- 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();
SQL · MySQL
-- 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 · SQL Server
-- 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;
FeatureSQLitePostgreSQLMySQLSQL Server
BEFORE / AFTERYesYesYesAFTER only (plus INSTEAD OF)
INSTEAD OFOn viewsOn viewsNoOn tables and views
Row-levelYes (always)FOR EACH ROWYes (always)No, statement-level with inserted/deleted
Statement-levelNoFOR EACH STATEMENTNoYes
WHEN conditionYesYesNo (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

TriggerStored procedureApplication code
Runs whenA row changes, automaticallySomeone calls itThe application runs it
Applies to every writerYesOnly callersOnly that application
Visible to developersEasy to overlookCalled by nameIn the codebase
Good forAudit logs, integrity rules across tables, derived columnsReusable multi-step operationsBusiness 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

RiskWhat to do
A row-level trigger on a table that gets bulk loads runs once per rowKeep 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 systemDo 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

❌ The bad row is already written
SQL
CREATE TRIGGER t AFTER INSERT ON products ...
-- checks NEW.price and raises an error
✅ Validate BEFORE
SQL
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

❌ Breaks on multi-row updates
SQL
DECLARE @id INT = (SELECT id FROM inserted);
-- fails or picks one row when 50 rows change
✅ Work with the whole inserted table
SQL
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

❌ Audit rows for every UPDATE
SQL
AFTER UPDATE ON products
BEGIN
  INSERT INTO price_changes ...;
END;
✅ Only real changes
SQL
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

❌ Endless loop or recursion error
SQL
AFTER UPDATE ON products
BEGIN
  UPDATE products SET updated_at = now() WHERE id = NEW.id;
END;
✅ Guard the condition
SQL
-- 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

❌ Invisible side effects
SQL
-- inserting an order also emails, charges a card
-- and updates five tables, all from triggers
✅ Keep triggers small and documented
SQL
-- 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

TaskSQLite
Audit an updateAFTER UPDATE OF col ON t FOR EACH ROW WHEN OLD.col <> NEW.col BEGIN INSERT ...; END;
Reject a rowBEFORE INSERT ON t ... BEGIN SELECT RAISE(ABORT, 'message'); END;
Make a view writableINSTEAD OF INSERT ON v ... BEGIN INSERT INTO base ...; END;
List triggersSELECT name FROM sqlite_master WHERE type = 'trigger';
RemoveDROP 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.

  1. 1Easy Check whether the Playground database has any triggers. Table: sqlite_master

    Show solution
    SQLite · runs in the Playground
    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.

  2. 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, reviews

    Show solution
    SQLite · runs in the Playground
    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.

  3. 3Medium Before adding a BEFORE INSERT trigger that rejects salaries under 40,000, check whether any existing employee would break the rule. Table: employees

    Show solution
    SQLite · runs in the Playground
    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.

  4. 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: orders

    Show solution
    SQLite · runs in the Playground
    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

Question 1 of 41

What is a trigger in SQL?

Question 2 of 41

Which events can a trigger fire on?

Question 3 of 41

Which statement creates a trigger?

Question 4 of 41

When does a BEFORE trigger fire?

Question 5 of 41

When does an AFTER trigger fire?

Question 6 of 41

What does an INSTEAD OF trigger do?

Question 7 of 41

What does NEW refer to inside a trigger?

Question 8 of 41

What does OLD refer to inside a trigger?

Question 9 of 41

In an INSERT trigger, which is available?

Question 10 of 41

In a DELETE trigger, which is available?

Question 11 of 41

What are inserted and deleted in SQL Server?

Question 12 of 41

Which trigger type lets a non-updatable view accept INSERT?

Question 13 of 41

What is a common use of an AFTER INSERT trigger?

Question 14 of 41

What is a common use of a BEFORE UPDATE trigger?

Question 15 of 41

What is a soft delete?

Question 16 of 41

Which trigger type usually implements a transparent soft delete?

Question 17 of 41

What is the main difference between a trigger and a stored procedure?

Question 18 of 41

Can a trigger call a stored procedure?

Question 19 of 41

What is a recursive trigger?

Question 20 of 41

What is a nested trigger?

Question 21 of 41

Why keep triggers on busy tables lightweight?

Question 22 of 41

What happens with an endless trigger loop?

Question 23 of 41

What does an audit trigger usually record?

Question 24 of 41

How can a trigger help with fraud detection?

Question 25 of 41

What is the key difference between a trigger and a rule in application code?

Question 26 of 41

What is a fair concern about putting lots of logic in triggers?

Question 27 of 41

How does SQL Server give a trigger the changed rows?

Question 28 of 41

How does Oracle refer to old and new values in a trigger?

Question 29 of 41

Why add error handling to trigger logic?

Question 30 of 41

What is a common mistake in a trigger that enforces a rule?

Question 31 of 41

What is the benefit of an audit trigger over "remember to write the log" in every application?

Question 32 of 41

What does FOR EACH ROW mean?

Question 33 of 41

Why can bulk inserts be slow with a row-level AFTER INSERT trigger?

Question 34 of 41

What is a WHEN clause in a trigger for?

Question 35 of 41

What is the difference between a trigger and a function?

Question 36 of 41

Why do regulated companies use audit triggers?

Question 37 of 41

How do you decide between a trigger and application code?

Question 38 of 41

What happens to a trigger when its table is dropped?

Question 39 of 41

What is the safest way to prevent an endless trigger loop?

Question 40 of 41

Why is DROP TRIGGER useful when debugging?

Question 41 of 41

What sums up a trigger?

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.