Free demoJob-oriented courses at Debugshala, Old Palasia, Indore. Freshers, non-IT and year-gap welcome.Book a free demo +91-8982385539

SQL Triggers Explained: BEFORE, AFTER and INSTEAD OF Examples

Lesson 26 of 29 · 11 min · Updated 13 Nov 2026

Learn SQL triggers with runnable SQLite examples: AFTER INSERT stock updates, audit logs with OLD and NEW, BEFORE checks and INSTEAD OF triggers on views.

Every time 500 rupees leaves your bank account, an SMS arrives within seconds: "Rs 500 debited, balance Rs 1,500". You never asked for that message. Nobody at the bank pressed a button. The debit itself caused it, every single time, whether you paid by UPI, card or cheque.

The bank set one rule: when a debit happens on an account, then send a message using the old and new balance. Every channel that moves money triggers it, so no channel can forget.

A database has the same feature. The debit is a triggering event (an INSERT, UPDATE or DELETE). The rule is a trigger. The balance before and after are the OLD and NEW rows that the trigger can read.

A SQL trigger is a named block of SQL attached to a table that the database executes automatically before, after or instead of an INSERT, UPDATE or DELETE on that table.

This article follows stored procedures and functions. A procedure runs when someone calls it; a trigger runs when data changes. Unlike procedures, triggers exist in SQLite, so every example here runs in your browser.

How does a SQL trigger work?

A trigger is defined by three choices: the table it watches, the event (INSERT, UPDATE or DELETE), and the timing (BEFORE, AFTER or INSTEAD OF). A row-level trigger then runs once for every row the statement touches.

SQL trigger firing order for an INSERT into order_items: the BEFORE trigger checks stock and can abort, the row is written, then the AFTER trigger reduces stock, all inside one transaction

The trigger runs inside the same transaction as the statement. If any trigger raises an error, the statement and everything its triggers did are undone.

The basic shape in SQLite:

SQL
CREATE TRIGGER trigger_name
{BEFORE | AFTER | INSTEAD OF} {INSERT | UPDATE [OF column] | DELETE} ON table_name
[WHEN condition]
BEGIN
    -- one or more statements, each ending with ;
END;

An AFTER INSERT trigger that updates stock

Malwa Mart keeps a stock count on each product. Every bill line in order_items should reduce it. Instead of trusting every till, app and import script to remember, a trigger does it.

Python
import sqlite3

con = sqlite3.connect(":memory:")
con.executescript("""
CREATE TABLE products (product_id INTEGER PRIMARY KEY, product_name TEXT, category TEXT, price INT, stock INT);
CREATE TABLE order_items (order_id INT, product_id INT, quantity INT, PRIMARY KEY (order_id, product_id));
INSERT INTO products VALUES (2,'Toor Dal 1kg','Grocery',165,40), (3,'Poha 1kg','Grocery',60,25), (8,'Sev 500g','Snacks',120,30);

CREATE TRIGGER trg_items_reduce_stock
AFTER INSERT ON order_items
FOR EACH ROW
BEGIN
    UPDATE products SET stock = stock - NEW.quantity WHERE product_id = NEW.product_id;
END;
""")
print("Before:", con.execute("SELECT product_name, stock FROM products").fetchall())
# order 110 from the sample data: 1 dal, 1 sev, 3 poha
con.executemany("INSERT INTO order_items VALUES (?, ?, ?)", [(110, 2, 1), (110, 8, 1), (110, 3, 3)])
print("After: ", con.execute("SELECT product_name, stock FROM products").fetchall())

One INSERT statement wrote three rows, so the trigger fired three times. Dal drops from 40 to 39, sev from 30 to 29 and poha from 25 to 22. NEW.quantity and NEW.product_id are the values of the row being inserted.

What are OLD and NEW in a trigger?

Row triggers can read the row involved in the change through two names. Which ones exist depends on the event.

SQL trigger OLD and NEW rows by event: INSERT has only NEW, UPDATE has OLD with price 165 and NEW with price 175, DELETE has only OLD

An INSERT has no "before" row, and a DELETE has no "after" row. An UPDATE has both, which makes it ideal for change logs.

Event OLD NEW Typical trigger
INSERT Not available The row being added Update stock, fill defaults
UPDATE The row before the change The row after the change Audit log of price changes
DELETE The row being removed Not available Archive deleted rows

An audit log with AFTER UPDATE

When the price of dal changes, the owner wants to know the old price, the new price and when it changed. UPDATE OF price limits the trigger to that column, and WHEN skips updates that do not really change it.

Python
import sqlite3

con = sqlite3.connect(":memory:")
con.executescript("""
CREATE TABLE products (product_id INTEGER PRIMARY KEY, product_name TEXT, category TEXT, price INT);
CREATE TABLE price_log (product_id INT, old_price INT, new_price INT, changed_at TEXT);
INSERT INTO products VALUES (1,'Basmati Rice 5kg','Grocery',520), (2,'Toor Dal 1kg','Grocery',165), (5,'Paneer 200g','Dairy',90);

CREATE TRIGGER trg_products_price_log
AFTER UPDATE OF price ON products
FOR EACH ROW
WHEN OLD.price <> NEW.price
BEGIN
    INSERT INTO price_log VALUES (NEW.product_id, OLD.price, NEW.price, datetime('now'));
END;
""")
con.execute("UPDATE products SET price = 175 WHERE product_id = 2")            # real change
con.execute("UPDATE products SET price = price WHERE category = 'Grocery'")     # no real change
con.execute("UPDATE products SET product_name = 'Paneer 200g Fresh' WHERE product_id = 5")  # other column
for row in con.execute("SELECT product_id, old_price, new_price FROM price_log"):
    print(row)

Only one log row appears: dal moved from 165 to 175. The second UPDATE touched two rows but changed nothing, so WHEN filtered it out. The third changed a different column, so UPDATE OF price never fired. datetime('now') stores UTC time.

Quick check

In an AFTER DELETE trigger, which row values can you read?

Using BEFORE triggers to block bad data

A BEFORE trigger runs before the row is written, so it can stop the change. In SQLite, the RAISE() function aborts the statement with your own message.

Python
import sqlite3

con = sqlite3.connect(":memory:")
con.executescript("""
CREATE TABLE products (product_id INTEGER PRIMARY KEY, product_name TEXT, price INT, stock INT);
CREATE TABLE order_items (order_id INT, product_id INT, quantity INT, PRIMARY KEY (order_id, product_id));
INSERT INTO products VALUES (1,'Basmati Rice 5kg',520,4), (7,'Detergent 2kg',340,10);

CREATE TRIGGER trg_items_check_stock
BEFORE INSERT ON order_items
FOR EACH ROW
WHEN NEW.quantity > (SELECT stock FROM products WHERE product_id = NEW.product_id)
BEGIN
    SELECT RAISE(ABORT, 'Not enough stock for this product');
END;
CREATE TRIGGER trg_items_reduce_stock AFTER INSERT ON order_items
BEGIN
    UPDATE products SET stock = stock - NEW.quantity WHERE product_id = NEW.product_id;
END;
""")
for line in [(107, 1, 3), (112, 7, 2), (113, 1, 2)]:
    try:
        con.execute("INSERT INTO order_items VALUES (?, ?, ?)", line)
        print("saved  ", line)
    except sqlite3.IntegrityError as err:
        print("blocked", line, "->", err)
print(con.execute("SELECT product_name, stock FROM products").fetchall())

Order 107's three bags of rice leave one in stock, so the next request for two is blocked. RAISE(ABORT, ...) undoes the current statement and raises an error, which Python reports as IntegrityError. Other options are ROLLBACK (end the whole transaction), FAIL and IGNORE (silently skip the row).

Quick check

A BEFORE INSERT trigger runs SELECT RAISE(ABORT, 'Not enough stock'). What happens to the INSERT?

INSTEAD OF triggers: making a view writable

The SQL views article showed that SQLite refuses an INSERT into a view. An INSTEAD OF trigger tells the database what to do instead: here, insert into the real table and supply a default city.

Python
import sqlite3

con = sqlite3.connect(":memory:")
con.executescript("""
CREATE TABLE customers (customer_id INTEGER PRIMARY KEY, name TEXT, city TEXT, phone TEXT);
INSERT INTO customers VALUES (1,'Asha Verma','Indore','9826011234'), (3,'Meera Joshi','Dewas','9826033456');
CREATE VIEW customer_directory AS SELECT customer_id, name, city FROM customers;

CREATE TRIGGER trg_directory_insert
INSTEAD OF INSERT ON customer_directory
BEGIN
    INSERT INTO customers (customer_id, name, city) VALUES (NEW.customer_id, NEW.name, COALESCE(NEW.city, 'Indore'));
END;
""")
con.execute("INSERT INTO customer_directory VALUES (8, 'Rohit Gupta', NULL)")
print(con.execute("SELECT * FROM customers").fetchall())

Rohit is now a real customer with city Indore and no phone number. The view stays a security layer, and the trigger decides exactly which columns a write through it may touch.

Trigger syntax in MySQL and PostgreSQL

The ideas are identical, but the syntax differs a lot.

SQL
-- MySQL 8: the body is written directly in the trigger
DELIMITER //
CREATE TRIGGER trg_items_check_stock
BEFORE INSERT ON order_items
FOR EACH ROW
BEGIN
    IF NEW.quantity > (SELECT stock FROM products WHERE product_id = NEW.product_id) THEN
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Not enough stock for this product';
    END IF;
END //
DELIMITER ;

-- PostgreSQL 16: a trigger calls a function that RETURNS trigger
CREATE OR REPLACE FUNCTION reduce_stock() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
    UPDATE products SET stock = stock - NEW.quantity WHERE product_id = NEW.product_id;
    RETURN NEW;
END;
$$;

CREATE TRIGGER trg_items_reduce_stock
AFTER INSERT ON order_items
FOR EACH ROW EXECUTE FUNCTION reduce_stock();
Feature SQLite MySQL 8 PostgreSQL 16
Body Inline BEGIN ... END Inline BEGIN ... END A separate trigger function
Row-level triggers Yes (the only kind) Yes (the only kind) Yes
Statement-level (FOR EACH STATEMENT) No No Yes
INSTEAD OF on views Yes No Yes
Change NEW in a BEFORE trigger No (update the row afterwards) SET NEW.col = ... NEW.col := ...; RETURN NEW;
Raise an error RAISE(ABORT, 'msg') SIGNAL SQLSTATE '45000' RAISE EXCEPTION 'msg'
Drop DROP TRIGGER name; DROP TRIGGER name; DROP TRIGGER name ON table;

MySQL also refuses a trigger that modifies the same table it is attached to, so a trigger on order_items cannot update order_items.

Common mistakes with SQL triggers

  • Hidden business logic. A new team member runs an INSERT and stock changes "by magic". Name triggers clearly (trg_table_purpose) and document them.
  • Slow bulk loads. A row trigger on a table loaded with 1 million rows runs 1 million times. Disable or drop it during large imports where your database allows, and rebuild derived values after.
  • Chains of triggers. Trigger A updates table B, whose trigger updates table C. Debugging that chain is painful. Keep triggers small and one level deep where possible.
  • Using a trigger where a constraint works. NOT NULL, CHECK, UNIQUE and FOREIGN KEY are clearer and faster.
  • Forgetting UPDATE OF and WHEN. Without them, an audit trigger fires on every update, even ones that change nothing.

Triggers are a favourite interview topic because they test whether you understand transactions and side effects together. Learners in Debugshala's Indore analytics batch build stock and audit triggers on retail tables, and database MCQ rounds on Debugshala Test check the theory. Next, SQL query optimisation shows how to find and fix slow queries.

Key takeaways

  • A trigger runs automatically on INSERT, UPDATE or DELETE, with BEFORE, AFTER or INSTEAD OF timing.
  • NEW is the incoming row (INSERT, UPDATE); OLD is the previous row (UPDATE, DELETE).
  • Use AFTER triggers for side effects such as stock and audit logs, BEFORE triggers for cross-table checks, and INSTEAD OF triggers for writable views.
  • UPDATE OF column and WHEN stop a trigger firing when it does not need to.
  • Triggers run in the statement's transaction, so an error undoes the whole change; prefer constraints for simple rules.
Take the free SQL quiz10 quick questions with explanations. No sign-up needed, and you can come back to this lesson after.

FAQ

What is a trigger in SQL with an example?

A trigger is SQL code that runs automatically when a table changes. For example, an AFTER INSERT trigger on order_items can reduce the matching product's stock by NEW.quantity each time a bill line is saved, so every till and app updates stock without extra code.

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

A stored procedure runs only when an application or user calls it with CALL, and it can take parameters. A trigger cannot be called; it fires automatically when its table receives an INSERT, UPDATE or DELETE, and it reads the changed row through OLD and NEW instead of parameters.

What are the types of triggers in SQL?

Triggers are classified by timing and event. Timing is BEFORE, AFTER or INSTEAD OF, and the event is INSERT, UPDATE or DELETE, giving combinations such as AFTER UPDATE. They are also row-level, running once per row, or statement-level, running once per statement; only PostgreSQL among these three databases supports statement-level triggers.

Are triggers bad for performance?

Triggers add work to every write they fire on, so a heavy trigger on a busy table slows inserts and updates. Small triggers that update one row or insert one log row are usually fine. Problems appear with bulk loads, chains of triggers and triggers that run slow queries.

Is there a SQL course in Indore that covers triggers and procedures?

Yes. Debugshala in Old Palasia, Indore, teaches SQL from SELECT queries through views, transactions, procedures and triggers in its Data Analytics course. Teaching is offline in English and Hindi, the course accepts non-IT graduates and people with a year gap, placement assistance is offered, and a free demo class is available before enrolment.

From the team behind this site

Go further with the Data Analytics Course at Debugshala

These tutorials are written by Debugshala, a training and placement institute at Old Palasia, Indore. The classroom course adds live mentoring, real projects and interview preparation.

  • Offline classes at Old Palasia, Indore
  • Free demo class before you pay
  • Non-IT, fresher and year-gap friendly
  • Placement assistance

700+ students placed · 200+ hiring partners · 4.9★ Google rating

SQL Stored Procedures and Functions in MySQL and PostgreSQLSQL Query Optimization: Practical Ways to Speed Up Slow Queries