Worked examples — Stored procedures, triggers, functions
The scenario matrix
Before any example, let us list every distinct situation. Think of it like a checklist: each row is a "shape of problem" this topic produces, and we will hit every single one.
| # | Case class | What is special about it | Which pseudo-row(s) exist | Example that covers it |
|---|---|---|---|---|
| A | INSERT trigger |
New row appears from nothing | NEW only |
Ex 1 |
| B | UPDATE trigger |
Row changes value | OLD and NEW |
Ex 2 |
| C | DELETE trigger |
Row vanishes | OLD only |
Ex 3 |
| D | BEFORE vs AFTER timing |
Can you edit the row, or only react? | depends | Ex 4 |
| E | Multi-row / degenerate UPDATE |
Statement hits 0, 1, or many rows | FOR EACH ROW |
Ex 5 |
| F | Function — deterministic vs not | The DETERMINISTIC promise |
— | Ex 6 |
| G | Procedure — atomic transaction, crash midway | Partial vs all-or-nothing | — | Ex 7 |
| H | Procedure with OUT / INOUT params |
Returning without RETURN |
— | Ex 8 |
| I | Word problem (real world) | Translate a business rule to code | mixed | Ex 9 |
| J | Exam-style twist | The "gotcha" that fails the naive answer | mixed | Ex 10 |
Below, each figure is drawn on a chalkboard so you can see which pseudo-rows are alive at which moment.
Case A — the INSERT trigger (only NEW exists)

Steps.
- Decide the timing →
BEFORE INSERT. Why this step? We want to change the row before it is written. Only aBEFOREtrigger lets you assign toNEW.col; anAFTERtrigger sees a row that is already committed and read-only. - Decide which pseudo-row →
NEWonly. Why this step? OnINSERTthe row is being born; there is no previous version, soOLDdoes not exist (using it would error). Look at the board: theOLDslot is crossed out. - Write the assignment.
DELIMITER //
CREATE TRIGGER stamp_created
BEFORE INSERT ON orders
FOR EACH ROW
BEGIN
IF NEW.created_at IS NULL THEN
SET NEW.created_at = NOW();
END IF;
END //
DELIMITER //- The
IF ... IS NULLguard. Why this step? If the app did supply a time, respect it; only fill the blank. This makes the trigger idempotent-ish and non-surprising.
Case B — the UPDATE trigger (both OLD and NEW)

Steps.
- To block, we must act before the write →
BEFORE UPDATE. Why? Once the row is written (AFTER) it is too late to refuse. To raise an error we useSIGNAL. - Compare
OLD.salarywithNEW.salary. Why this step? OnUPDATE, both pseudo-rows exist —OLDis the previous value,NEWis the incoming one. The board shows both slots lit. - Block if smaller.
DELIMITER //
CREATE TRIGGER guard_salary
BEFORE UPDATE ON employees
FOR EACH ROW
BEGIN
IF NEW.salary < OLD.salary THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Salary cut not allowed';
END IF;
END //
DELIMITER //- (Logging the raise would go in a separate
AFTER UPDATEtrigger, since AFTER is where you "react once final" — that is Case D below.)
Case C — the DELETE trigger (only OLD exists)

Steps.
- Timing →
BEFORE DELETE(orAFTER; both work here since we only read). Why? We just need to read the vanishing row and copy it — no need to alter it. - Read from
OLDonly. Why this step? OnDELETEthere is no incoming row — nothing new is being written — soNEWdoes not exist. The board crosses outNEW. This is the exact trap: "I'll useNEWto see what got deleted" is wrong.
DELIMITER //
CREATE TRIGGER archive_customer
BEFORE DELETE ON customers
FOR EACH ROW
BEGIN
INSERT INTO customers_archive(id, name, deleted_at)
VALUES (OLD.id, OLD.name, NOW());
END //
DELIMITER //Case D — BEFORE vs AFTER: the timing decision

Steps.
- Task (i) uppercases
NEW.code→ needs to modify the row. Why this fails in AFTER? In anAFTERtrigger the row is already written to disk;NEWis read-only. AssigningSET NEW.code = UPPER(NEW.code)in AFTER → error. - Task (ii) inserts into a different table → pure reaction, no modification of the current row. Why this is fine in AFTER? You are not changing the row that fired the trigger; you're writing elsewhere.
- Correct split:
-- (i) MUST be BEFORE
CREATE TRIGGER upcase_code
BEFORE INSERT ON products
FOR EACH ROW SET NEW.code = UPPER(NEW.code);
-- (ii) belongs AFTER
CREATE TRIGGER log_insert
AFTER INSERT ON products
FOR EACH ROW
INSERT INTO product_log(code, at) VALUES (NEW.code, NOW());Case E — degenerate multi-row: FOR EACH ROW on 0, 1, N rows

Steps.
FOR EACH ROWmeans the body runs once per affected row. Why this matters? A singleUPDATEstatement can touch many rows; the trigger is not "per statement." 50 affected rows → 50 executions.- Guard
IF NEW.salary <> OLD.salary. Why this step? Only log real changes. Here every salary changes (), so all 50 pass the guard. - Degenerate case:
dept='ghost'matches 0 rows → the trigger fires 0 times → 0 log rows. Why? No rows affected means no per-row iterations. This is the "zero input" cell — the trigger silently does nothing, which is correct.
Case F — function: the DETERMINISTIC promise (and its trap)
Steps.
- A function is deterministic iff same inputs always give same output. Why this definition? The optimizer may cache the result or use it in a functional index (see Indexes). That is only safe if the output can't drift.
net_price(100, 0.18). Why deterministic? Its output depends only on its two arguments. today, tomorrow, always. Honest to markDETERMINISTIC.age_days(d)readsNOW(). Why NOT deterministic? Samed, different day → different answer. If you lie and mark itDETERMINISTIC, the engine may cache yesterday's value and hand you a stale, silently-wrong number.
CREATE FUNCTION net_price(price DECIMAL(10,2), rate DECIMAL(4,3))
RETURNS DECIMAL(10,2) DETERMINISTIC -- honest
RETURN price * (1 + rate);
CREATE FUNCTION age_days(d DATE)
RETURNS INT NOT DETERMINISTIC -- honest
RETURN DATEDIFF(NOW(), d);Case G — procedure: atomic transaction, crash midway
Steps.
- Normal path: both
UPDATEs run, thenCOMMIT. Why COMMIT matters? It makes both changes permanent together. and . - Crash before
COMMIT: the transaction is not committed, so on restart the DB rolls back everything sinceSTART TRANSACTION(see Transactions and ACID). Why? Atomicity: all-or-nothing. Uncommitted work is discarded, not left half-done. - Therefore after crash: account 1 is still , account 2 still . No money vanished. Why this is the whole point of a procedure? The atomic unit lives server-side, not in fragile app code that could crash between two separate network calls.
Case H — procedure returning via OUT (no RETURN statement)
Steps.
- Declare an
OUTparameter. Why notRETURN? A procedure has noRETURNS type; it hands values back by writing intoOUT/INOUTparameters, which the caller then reads. - Compute into it.
DELIMITER //
CREATE PROCEDURE count_active(OUT n INT)
BEGIN
SELECT COUNT(*) INTO n FROM users WHERE active = 1;
END //
DELIMITER //
CALL count_active(@result);
SELECT @result; -- 2840 - 12 = 28active users; the procedure setsn = 28, the caller reads@result = 28. Why this shows the proc/function line? You cannot doSELECT count_active()— a procedure is not usable inside an expression; it isCALLed and communicates through@result.
Case I — word problem (real world): loyalty points
Steps.
- "Every time a row is inserted... automatically" → this screams trigger (no one should have to remember to call it). Timing
AFTER INSERT: we react once the purchase is final and write to a different table (users). Why AFTER not BEFORE? We are not changing the purchase row; we are updatingusers. Reacting = AFTER. - Read
NEW.amount(INSERT → onlyNEW). - Points .
DELIMITER //
CREATE TRIGGER award_points
AFTER INSERT ON purchases
FOR EACH ROW
UPDATE users SET points = points + FLOOR(NEW.amount / 100)
WHERE id = NEW.user_id;
DELIMITER //Case J — exam twist: the cascading trigger surprise

Steps.
- One
INSERTintoordersfires T1 once → 1 row intoorder_events. Why?FOR EACH ROW, one order row = one firing. - That insert into
order_eventsitself fires T2 → 1 row intoevent_audit. Why this is the twist? A trigger's action can trigger another trigger. This is a cascade — invisible chained control flow. The parent note's mistake "triggers are free magic" warns about exactly this: "spooky action at a distance." - Net: 1 row in
event_audit, but produced by a chain nobody explicitly called. Why it's dangerous? Deep cascades hurt performance, are hard to debug, and can even loop. Keep triggers for invariants, not general logic.
Full-matrix recall
Recall Did every cell get covered?
A INSERT trigger ::: Ex 1 — NEW only, BEFORE to stamp time
B UPDATE trigger ::: Ex 2 — OLD and NEW, block a cut
C DELETE trigger ::: Ex 3 — OLD only, archive before delete
D BEFORE vs AFTER ::: Ex 4 — modify needs BEFORE, react needs AFTER
E multi-row / zero-row ::: Ex 5 — 50 logs for 50 rows, 0 for 0 rows
F deterministic function ::: Ex 6 — net_price yes, age_days no
G atomic procedure + crash ::: Ex 7 — rollback keeps balances whole
H OUT parameter ::: Ex 8 — procedures return via OUT, not RETURN
I real-world word problem ::: Ex 9 — loyalty points via AFTER INSERT trigger
J exam twist ::: Ex 10 — trigger cascade writes a chained row
Connections
- 4.4.12 Stored procedures, triggers, functions (Hinglish) — the parent topic these examples drill.
- Transactions and ACID — Ex 7's rollback is atomicity in action.
- SQL Queries (SELECT, JOIN) — Ex 6's function plugs into a
SELECT. - Database Normalization — Ex 9's trigger maintains a derived column (
points). - Indexes — Ex 6's deterministic function can back a functional index.
- Concurrency Control — all these triggers/procs run under locks.