Skip to content

Expert: design, speed and engines · Procedures, triggers, SQL engines and NoSQL · topic 53 of 62 · 35 min

Triggers

Explanation

A trigger is code the database runs by itself when a row is inserted, updated or deleted. It can fire BEFORE or AFTER the change (or INSTEAD OF it, on views) and can read the row as it was (OLD) and as it will be (NEW). Triggers are good for audit logs and keeping counts in sync, but they hide logic, so use them sparingly.

Why triggers?

Some things must happen every time data changes, no matter which app or person changed it: write an audit row when a price changes, reduce stock when an item is sold, refuse a change that breaks a rule. If you rely on every app to remember, one day one app forgets. A trigger puts the rule inside the database, where nobody can skip it.

Think of it like this: A trigger is like the motion sensor light in a building corridor. Nobody presses a switch; the light reacts to the event (someone walks in). In the same way, the trigger reacts to an INSERT, UPDATE or DELETE.

The parts of a trigger

PartChoicesMeaning
TimingBEFORE, AFTER, INSTEAD OFRun before the change (can stop or adjust it), after it, or instead of it (on views)
EventINSERT, UPDATE (OF columns), DELETEWhich change wakes the trigger
LevelFOR EACH ROW (row-level)Run once per changed row; SQLite and MySQL only have row-level triggers
ConditionWHEN (...)Optional filter: run only when it is true
Row valuesNEW and OLDNEW is the row after the change (INSERT, UPDATE); OLD is the row before (UPDATE, DELETE)

An audit log in SQLite

An audit log is a table that records who changed what and when. Here every price change is copied into price_log automatically:

sql
CREATE TABLE price_log (product_id INTEGER, old_price INTEGER, new_price INTEGER);

CREATE TRIGGER log_price
AFTER UPDATE OF price ON products
FOR EACH ROW
WHEN NEW.price <> OLD.price
BEGIN
  INSERT INTO price_log VALUES (OLD.id, OLD.price, NEW.price);
END;

UPDATE products SET price = price + 50 WHERE category = 'Kitchen';
SELECT product_id, old_price, new_price FROM price_log;
-- 3 | 450  | 500
-- 4 | 1450 | 1500
One UPDATE changed two rows, so the row-level trigger ran twice. A real log would also store a timestamp and the user.

Keeping stock in sync

sql
CREATE TRIGGER reduce_stock
AFTER INSERT ON order_items
FOR EACH ROW
BEGIN
  UPDATE products SET stock = stock - NEW.qty WHERE id = NEW.product_id;
END;

INSERT INTO orders VALUES (109, 4, '2025-04-05', 'Placed');
INSERT INTO order_items VALUES (109, 3, 2, 450), (109, 7, 1, 1999);
SELECT id, name, stock FROM products WHERE id IN (3, 7);
-- 3 | Steel Tiffin Box  | 10   (was 12)
-- 7 | Bluetooth Earbuds | 24   (was 25)

Stopping bad changes with RAISE

Inside a SQLite trigger, RAISE(ABORT, 'message') cancels the statement with an error and undoes what that statement already changed. RAISE(IGNORE) silently skips the current row instead. Use them in BEFORE triggers to guard rules that a CHECK constraint cannot express.

sql
CREATE TRIGGER no_negative_stock
BEFORE UPDATE OF stock ON products
FOR EACH ROW
WHEN NEW.stock < 0
BEGIN
  SELECT RAISE(ABORT, 'stock cannot go below zero');
END;

UPDATE products SET stock = stock - 5 WHERE id = 8;
-- Error: stock cannot go below zero   (the Power Bank has 0 in stock)

Other engines write the same ideas differently. PostgreSQL triggers call a trigger function (RETURNS trigger) and support FOR EACH STATEMENT as well as FOR EACH ROW. MySQL rejects a change with SIGNAL SQLSTATE '45000'. SQL Server triggers fire once per statement and read the changed rows from two pseudo-tables named inserted and deleted.

sql
-- PostgreSQL only
CREATE FUNCTION log_price_change() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
  INSERT INTO price_log (product_id, old_price, new_price, changed_at)
  VALUES (OLD.id, OLD.price, NEW.price, now());
  RETURN NEW;
END;
$$;

CREATE TRIGGER trg_price_log
AFTER UPDATE OF price ON products
FOR EACH ROW
WHEN (OLD.price IS DISTINCT FROM NEW.price)
EXECUTE FUNCTION log_price_change();
EXECUTE FUNCTION is the PostgreSQL 11+ spelling; older versions wrote EXECUTE PROCEDURE.

The dangers

  • Hidden logic: someone runs one UPDATE and three other tables change. Debugging feels like magic, the bad kind. Document every trigger.
  • Cascades: a trigger updates a table that has its own trigger, which updates another. Chains are slow and hard to follow.
  • Speed: a row-level trigger runs for every row, so a bulk update of a million rows runs it a million times.
  • Surprises in tools: bulk loads, replication or TRUNCATE may skip some triggers, depending on the engine.

Common mistakes

  • Using OLD in an INSERT trigger or NEW in a DELETE trigger. There is no such row. Fix: INSERT has only NEW, DELETE only OLD.
  • Logging rows that did not really change. Fix: add WHEN NEW.price <> OLD.price (or IS DISTINCT FROM in PostgreSQL).
  • Putting heavy business workflows in triggers. Fix: keep triggers small (audit, counters, guards) and put workflows in app code.
  • Expecting a trigger to fire for rows you load another way. Fix: check your engine's documentation for bulk loads and TRUNCATE.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Triggers · 1/7

Triggers

A trigger is code the database runs by itself when rows are inserted, updated or deleted.

Prefer a video by a teacher? Find free videos on “sql triggers before after insert update new old tutorial” on YouTube ↗

In real life: Audit trails in a hospital records system

Hospitals must be able to show who changed a patient's record and what it said before. A common design is an AFTER UPDATE trigger on the records table that copies the OLD values, the NEW values, the database user and the time into an audit table that ordinary users cannot edit. Because the trigger sits in the database, the audit happens even when someone fixes data with a manual SQL statement.

sql
CREATE TABLE status_log (order_id INTEGER, old_status TEXT, new_status TEXT);
CREATE TRIGGER log_status AFTER UPDATE OF status ON orders
FOR EACH ROW WHEN NEW.status <> OLD.status
BEGIN
  INSERT INTO status_log VALUES (OLD.id, OLD.status, NEW.status);
END;
The same idea on the SHOP orders table.

Practice (0/3 done)

Task 1 · Code · SQL

The setup has SHOP plus an empty table price_log(product_id, old_price, new_price). Write an AFTER UPDATE OF price trigger on products that inserts OLD.id, OLD.price and NEW.price into price_log when the price really changes. Then raise every Kitchen product's price by 50, and finally return product_id, old_price and new_price from price_log.

Task 2 · Code · SQL

Write an AFTER INSERT trigger on order_items that reduces products.stock by NEW.qty for NEW.product_id. Then insert order 109 for customer 4 (date '2025-04-05', status 'Placed') with two lines: product 3, qty 2, price 450 and product 7, qty 1, price 1999. Finally return id, name and stock of products 3 and 7.

Task 3 · Code · SQL

Write a BEFORE INSERT trigger on order_items that silently skips any line whose qty is 0 or less (use WHEN NEW.qty <= 0 and SELECT RAISE(IGNORE)). Then run INSERT INTO order_items VALUES (108, 2, 0, 160), (108, 3, 2, 450), (108, 4, -1, 1450); and return order_id, product_id and qty of every line of order 108.

Quick quiz

Question 1

1.In a DELETE trigger, which row values can you read?

Question 2

2.An UPDATE changes 5 rows of a table with a FOR EACH ROW trigger. How many times does the trigger run?

Question 3

3.In SQLite, what does RAISE(ABORT, 'msg') inside a BEFORE trigger do?

Question 4

4.Which is the BEST use of a trigger?

Question 5

5.In SQL Server, how does a trigger see the changed rows?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

In a DELETE trigger, which row values can you read?

Answer: Only OLD

A deleted row has a "before" but no "after", so only OLD exists.

An UPDATE changes 5 rows of a table with a FOR EACH ROW trigger. How many times does the trigger run?

Answer: 5 times

Row-level triggers run once per changed row (a WHEN clause can make some runs do nothing).

In SQLite, what does RAISE(ABORT, 'msg') inside a BEFORE trigger do?

Answer: Stops the statement with an error and undoes its changes so far

ABORT fails the statement; IGNORE is the quiet "skip this row" option.

Which is the BEST use of a trigger?

Answer: Writing an audit row whenever a salary changes

Small, data-only rules such as audit logs suit triggers. Emails and external calls belong in application code.

In SQL Server, how does a trigger see the changed rows?

Answer: Through the inserted and deleted pseudo-tables

SQL Server triggers fire per statement and read the rows from inserted and deleted.

Which statement adds a new row?Related

Answer: INSERT INTO

INSERT INTO table (columns) VALUES (...) adds rows.

From INSERT, UPDATE and DELETE

What does INSERT OR IGNORE do with a row whose email already exists?Related

Answer: Skips that row and continues with the others

It quietly skips rows that would break a constraint.

From Upserts: insert or update

In SQLite, you write INSERT INTO tickets (customer_id, subject, status) VALUES (3, 'Hi', DEFAULT). What happens?Related

Answer: A syntax error: SQLite does not accept DEFAULT in VALUES

Leave the column out instead. PostgreSQL, MySQL and SQL Server do accept DEFAULT there.

From INSERT

SQLite has no materialized views. What is the usual substitute?Related

Answer: A summary table rebuilt with DELETE and INSERT ... SELECT on a schedule

A summary table holds the stored result; the refresh is up to you.

From Views and materialized views

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 4 more topics in “Procedures, triggers, SQL engines and NoSQL” before its exam: MySQL vs PostgreSQL vs SQLite vs SQL Server, NoSQL: documents, key-value, wide-column and graph, MongoDB and Redis in practice, Choosing a database, and keeping it safe.
  2. Module 11 exam: procedures, triggers, engines and NoSQL · 13 questions · 25 min, without help. 6 topics of this module still to finish first.
  3. Then the next module, Interview questions and practice projects. 62 topics left in the course before the final exam.

← Previous: Stored procedures and functions