SQL Triggers
A trigger is code the database runs automatically when a row is inserted, updated or deleted in a table. Triggers can validate or adjust data before it is saved and react after it is saved: writing an audit log, keeping a stock level in sync, blocking an invalid change. They run no matter which application or user made the change.
- Create BEFORE and AFTER triggers for INSERT, UPDATE and DELETE
- Read the row values with
NEWandOLD - Build an audit log and validate data with
SIGNAL - Know when not to use triggers
Trigger syntax
CREATE TRIGGER trigger_name{BEFORE | AFTER} {INSERT | UPDATE | DELETE}ON table_nameFOR EACH ROWtrigger_body;| Event | OLD (row before) | NEW (row after) |
|---|---|---|
| INSERT | not available | the row being inserted |
| UPDATE | values before the change | values after the change |
| DELETE | the row being deleted | not available |
In a BEFORE trigger you can change NEW values; in an AFTER trigger they are read-only.
BEFORE INSERT: clean data
Store every new customer email in lower case:
CREATE TRIGGER trg_customers_biBEFORE INSERT ON customersFOR EACH ROWSET NEW.email = LOWER(TRIM(NEW.email)); INSERT INTO customers (customer_id, customer_name, email, city)VALUES (8, 'Kiran Rao', ' Kiran.Rao@Example.COM ', 'Pune');SELECT customer_id, email FROM customers WHERE customer_id = 8;| customer_id | |
|---|---|
| 8 | kiran.rao@example.com |
- Before the row is written, the trigger replaces
NEW.emailwith the trimmed, lower-case version. - The application sent messy input, but the table stores
kiran.rao@example.com. - A single-statement body needs no
BEGIN ... ENDorDELIMITER.
AFTER UPDATE: audit log
Record every salary change, who it was for, and the old and new values:
CREATE TABLE salary_audit ( audit_id INT AUTO_INCREMENT PRIMARY KEY, emp_id INT, old_salary INT, new_salary INT, changed_by VARCHAR(80), changed_at DATETIME DEFAULT CURRENT_TIMESTAMP); DELIMITER //CREATE TRIGGER trg_salary_auditAFTER UPDATE ON employeesFOR EACH ROWBEGIN IF NEW.salary <> OLD.salary THEN INSERT INTO salary_audit (emp_id, old_salary, new_salary, changed_by) VALUES (NEW.emp_id, OLD.salary, NEW.salary, CURRENT_USER()); END IF;END //DELIMITER ; UPDATE employees SET salary = salary + 5000 WHERE dept_id = 3;UPDATE employees SET city = 'Pune' WHERE emp_id = 101;SELECT audit_id, emp_id, old_salary, new_salary, changed_by FROM salary_audit;| audit_id | emp_id | old_salary | new_salary | changed_by |
|---|---|---|---|---|
| 1 | 108 | 88000 | 93000 | root@localhost |
| 2 | 109 | 45000 | 50000 | root@localhost |
- The trigger fires once for each row the
UPDATEchanges. - Raising Marketing salaries changes two rows, so two audit rows are written.
- Changing Asha’s city fires the trigger too, but the
IFsees the salary did not change and writes nothing. CURRENT_USER()records which account made the change.
BEFORE UPDATE: validate with SIGNAL
Reject salary cuts of more than 20%:
DELIMITER //CREATE TRIGGER trg_salary_checkBEFORE UPDATE ON employeesFOR EACH ROWBEGIN IF NEW.salary < OLD.salary * 0.8 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Salary cut above 20% needs HR approval'; END IF;END //DELIMITER ; UPDATE employees SET salary = 90000 WHERE emp_id = 102;UPDATE employees SET salary = 50000 WHERE emp_id = 102;ERROR 1644 (45000): Salary cut above 20% needs HR approval
- Cutting Rahul’s salary from 98,000 to 90,000 (8%) passes.
- Cutting it to 50,000 is more than 20%, so
SIGNALraises error 1644 with the custom message and the update is cancelled.
AFTER INSERT: keep stock in sync
Reduce a product’s stock whenever an order line is added:
CREATE TRIGGER trg_order_items_aiAFTER INSERT ON order_itemsFOR EACH ROWUPDATE products SET stock = stock - NEW.quantity WHERE product_id = NEW.product_id; INSERT INTO order_items (order_id, product_id, quantity) VALUES (1009, 6, 10);SELECT product_id, product_name, stock FROM products WHERE product_id = 6;| product_id | product_name | stock |
|---|---|---|
| 6 | Notebook Pack | 290 |
- The new line orders 10 notebook packs.
- The trigger subtracts 10 from product 6’s stock: 300 becomes 290.
- A matching
AFTER DELETEtrigger (addingOLD.quantityback) would complete the logic.
AFTER DELETE: keep a history
CREATE TABLE deleted_customers AS SELECT * FROM customers WHERE 1 = 0;CREATE TRIGGER trg_customers_adAFTER DELETE ON customersFOR EACH ROWINSERT INTO deleted_customers VALUES (OLD.customer_id, OLD.customer_name, OLD.email, OLD.city, OLD.joined_on); DELETE FROM customers WHERE customer_id = 7;SELECT * FROM deleted_customers;| customer_id | customer_name | city | joined_on | |
|---|---|---|---|---|
| 7 | Sara Thomas | sara.t@example.com | Bengaluru | 2025-03-02 |
WHERE 1 = 0 copies the table’s columns without any rows.
Manage triggers
| Task | Statement |
|---|---|
| List | SHOW TRIGGERS FROM company; |
| See the code | SHOW CREATE TRIGGER trg_salary_audit; |
| Remove | DROP TRIGGER IF EXISTS trg_salary_audit; |
Pitfalls
- Hidden logic: someone reading
INSERT INTO order_itemscannot see that stock changes. Document triggers well. - Performance: triggers run for every row; a bulk update of a million rows runs the trigger a million times.
- Not fired by
TRUNCATE,DROP TABLEor changes made by foreign-keyCASCADEactions. - A trigger cannot modify the same table it is defined on (other than
NEWvalues inBEFOREtriggers).
Quick quiz
1. Which row values are available in a DELETE trigger?
The deleted row has no new version.
2. Where can you change NEW values?
After the row is saved it is too late to change it.
3. An UPDATE changes 50 rows. How often does a FOR EACH ROW trigger run?
MySQL triggers are row-level.
4. How does a trigger reject a change?
SIGNAL raises an error that cancels the statement.
Triggers FAQ
What is a trigger in SQL?
A stored program that the database executes automatically before or after an INSERT, UPDATE or DELETE on a table, once for each affected row.
What is the difference between BEFORE and AFTER triggers?
A BEFORE trigger runs before the row is written and can change or reject it. An AFTER trigger runs after the change and is typically used to update other tables or write logs.
What are NEW and OLD in MySQL triggers?
NEW holds the row’s values after an INSERT or UPDATE; OLD holds the values before an UPDATE or DELETE.
Can a trigger call a stored procedure?
Yes, with CALL, as long as the procedure does not return result sets or use transaction statements.