Triggers & Active Databases
Event–Condition–Action rules that let the database react on its own. SQL:1999 syntax, transition variables, the BEFORE/AFTER × row/statement grid, execution order, cascading and termination — then the two things exams actually ask for: a correct event inventory and incrementally-maintained derived data.
01 · Motivation
From passive constraints to active rules
A classical database is passive: it stores what you tell it and answers what you ask. An active database also reacts — on its own, inside the DBMS, to events it observes.
The story is one of steadily more expressive integrity control. In 1975 the idea was simply
“integrity constraints” as a quality-of-data concern. SQL-92 delivered the declarative half: key
constraints, referential integrity, domain constraints, CHECK. You say what must be true and the
system enforces it.
CREATE TABLE author (
idAut INT PRIMARY KEY,
name VARCHAR(30) NOT NULL,
salary DECIMAL(10,2),
CHECK (salary >= 10000)
);
CREATE TABLE book (
idBook SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
authorref INT NOT NULL REFERENCES author(idAut) ON DELETE CASCADE
);That schema already rejects a book whose author does not exist, a duplicate author id, and a salary below 10 000 — and cascades deletions from author to book. All without a line of procedural code.
But declarative constraints can only express conditions on a state. They cannot say “when this changes, also do that”, they cannot compare the value before a change with the value after, and they cannot maintain a derived quantity. SQL:1999 answers with triggers: a procedural specification of reactive behaviour, compiled and stored in the DBMS like a stored procedure but invoked by events rather than by a client.
The motivating example
Log every grade change so the history survives. The event is an update of Grade in
Exam; the condition is that the new grade differs from the old; the action is an
insert into Log. No CHECK can express it, because the rule is about the
transition, not the state. You could put the logic in every application that touches
grades — and hope none is ever written by somebody who forgets. Or you put it once, in the
database.
02 · Syntax
ECA and the SQL:1999 trigger
Every trigger, in every dialect, has the same three parts: whenever event E occurs, if condition C holds, execute action A.
- Event — normally a modification of the database state:
insert,delete,update. - Condition — a predicate identifying the situations in which the action is actually required.
- Action — a generic update statement or stored procedure; usually database updates, but it may also raise an error.
The SQL:1999 syntax maps one clause to each:
create trigger <TriggerName>
{ before | after } -- when, relative to the change
{ insert | delete | update [of <Column>] } on <Table> -- E: the event
referencing { [ old table [as] <OldTableAlias> ]
[ new table [as] <NewTableAlias> ]
| [ old [row] [as] <OldTupleName> ]
[ new [row] [as] <NewTupleName> ] }
[ for each { row | statement } ] -- granularity
[ when <Condition> ] -- C: the condition
<SQLProceduralStatement> -- A: the actionTwo clauses do most of the work and are where marks are lost: for each chooses the
granularity, and referencing names the transition variables the condition and action can
see. The next two sections take them in turn.
Triggers — asked in 5 of 14 sessions, 55 points
Trigger exercises appear in 5 of the 14 sessions in the question bank (2024-02-12, 2024-07-17, 2025-06-04, 2025-07-02, 2026-07-02), worth 10–12 points each. The ask is always the same shape: write a set of triggers that maintains X. Graders want the
event, the granularity and the WHEN clause stated explicitly
for each trigger — a body that happens to work but fires on the wrong event scores badly. Two papers (2024-02-12, 2025-06-04) additionally ask you to enumerate which events need a trigger at all and justify the ones that do not; that inventory carries its own marks.
03 · Mechanics
Transition variables and transition tables
A trigger’s power comes from seeing both states — before and after the modification. The syntax for that depends on the granularity.
- Row-level (
for each row): the tuple variables old and new hold the values of the row under consideration, before and after the change. - Statement-level (
for each statement): the table variables old table and new table hold all affected rows, before and after.
Which of them exist depends on the event, and this is a common slip:
| Event | old / old table | new / new table |
|---|---|---|
insert | undefined | the inserted rows |
delete | the deleted rows | undefined |
update | rows before | rows after |
An insert has no before-state and a delete has no after-state — referring to the missing one is a syntax error, not an empty set.
The canonical illustration is replicating a table. Keep T2 a copy of T1:
CREATE TRIGGER REPLIC_INS
AFTER INSERT ON T1
FOR EACH ROW
INSERT INTO T2 VALUES (new.ID, new.VALUE);
CREATE TRIGGER REPLIC_DEL
AFTER DELETE ON T1
FOR EACH ROW
DELETE FROM T2 WHERE T2.ID = old.ID;
CREATE TRIGGER REPLIC_UPD
AFTER UPDATE OF VALUE ON T1
FOR EACH ROW
WHEN new.ID = old.ID
UPDATE T2 SET T2.VALUE = new.VALUE WHERE T2.ID = new.ID;Handle every case of UPDATE
The update trigger above deliberately fires only when the id is unchanged — and therefore silently does nothing when a row’s key changes. The lecture slides admit this is a simplification made to keep the code readable, and warn that “in the real world, and in exams too, we would need to handle all cases”. When an exercise says “the primary keys are not updated”, it is removing exactly this obligation; when it does not say so, you owe the extra trigger.
Conditional replication makes the point sharper. Replicate only rows with VALUE >= 10, and a
single UPDATE event now splits into three rules, because a row can enter the replica, stay
in it, or leave it:
-- newly relevant: insert into the replica
WHEN (old.VALUE < 10 AND new.VALUE >= 10) → INSERT INTO T2 …
-- still relevant: propagate the change
WHEN (old.VALUE >= 10 AND new.VALUE >= 10 AND old.VALUE != new.VALUE) → UPDATE T2 …
-- no longer relevant: remove from the replica
WHEN (old.VALUE >= 10 AND new.VALUE < 10) → DELETE FROM T2 …That enter/stay/leave split is the shape of most trigger exercises. Recognising it early saves time.
04 · Mechanics
BEFORE vs AFTER, row vs statement
Two independent binary choices give four combinations. Picking the wrong cell is the single most common way to lose marks on a trigger question.
BEFORE
The action runs before the state change. Used to validate a modification and possibly
condition its effect. Safeness rule: a BEFORE trigger may not update the
database — it may only alter the transition variables, at row granularity, via
set new.col = ….
AFTER
The action runs after the modification is applied. The common mode, suitable for most applications, and the only one that can issue arbitrary updates.
FOR EACH ROW
Considered once per affected tuple. Simpler to write, potentially less efficient.
FOR EACH STATEMENT
Considered once per activating statement, independently of how many tuples were affected — even if none were. Closer to SQL’s set-oriented nature.
The classic drill is the FLIGHT exercise: given
Flight(passengerName, airline, flightNumber, date, flightMiles), reset the miles to 300 whenever
an insert or update would leave them below 300. Written four ways:
-- BEFORE, row level: modify the modification in flight
create trigger 300Miles before insert on Flight
for each row when new.flightMiles < 300
begin set new.flightMiles = 300; end;
-- AFTER, row level: re-update the row you just wrote
create trigger 300Miles after insert on Flight
for each row when new.flightMiles < 300
begin
update Flight set flightMiles = 300
where passengerName = new.passengerName
and flightNumber = new.flightNumber and date = new.date;
end; -- NOT POSSIBLE IN SOME DBMSs
-- AFTER, statement level: one set-oriented repair
create trigger 300Miles after insert on Flight
for each statement referencing new table as NEW_T
begin
update Flight set flightMiles = 300
where (passengerName, flightNumber, date) IN
(select passengerName, flightNumber, date from NEW_T where flightMiles < 300);
end;The BEFORE version is not merely stylistically nicer — it is one statement where the AFTER version is two, and several DBMSs (MySQL among them) forbid a trigger from updating the very table whose modification fired it. When the goal is to “modify a modification”, BEFORE is both correct and cheaper.
Row versus statement is graded explicitly
The July 2026 paper devotes a whole sub-question to it: requirements 1–2 are row-level reactions
to a single record, while requirement 3 says outright
“write an AFTER UPDATE FOR EACH STATEMENT”
because one UPDATE may change the capacity of several courses at once and the trigger
must see them together. If a requirement mentions
multiple rows changed by a single statement, that is the examiner telling you the
granularity.
05 · Execution
Execution order, cascading, and termination
Several triggers may respond to one event, and a trigger’s action may fire further triggers. Two questions follow: in what order, and does it stop?
SQL:1999 prescribes the ordering between categories:
- BEFORE statement-level triggers
- BEFORE row-level triggers
- the modification is applied and integrity constraints are checked
- AFTER row-level triggers
- AFTER statement-level triggers
Within one category the order is implementation-defined — typically by definition time (older first) or alphabetically by name. Do not rely on it.
Cascading is when the action of T1 fires T2. Recursive cascading is when a
statement on a table eventually re-generates the same event on the same table — looping.
The tool for reasoning about termination is the triggering graph: one node per trigger, and an
arc from i to j when the action of Ti may activate Tj. It is built by simple syntactic
analysis, and the theorem is one-directional:
Acyclicity is sufficient, not necessary
If the triggering graph is acyclic, the system is guaranteed to terminate. If it has cycles, triggers may or may not terminate — you must reason about the data. A terminating system with a cyclic graph is perfectly possible: the standard example halves salaries only while a budget threshold is exceeded, so each pass strictly decreases a quantity and the cycle drains.
How bad can it get? The lectures trace a two-trigger example on Postgres that never stops, and it repays walking through slowly, because the surprise is not the loop — it is what the delayed triggers see.
The setup
Table ttest has one attribute x. T1 halves x when x >= 10; T2 increases x by 40%
when x >= 6. Both are AFTER UPDATE OF x … FOR EACH ROW. We update x to 12.
Step 1 — both activate, T1 wins
The update to 12 activates both triggers. T1 takes precedence (alphabetical), so it runs first
and halves x to 6. T2 is delayed, not cancelled.
Step 2 — the change re-activates T2
T1’s own update activates another instance of T2, which raises x from 6 to 8.4.
Step 3 — and again
Another delayed T2 instance runs, taking x from 8.4 to 11.76.
Step 4 — T1 pre-empts once more
Now x >= 10 so both activate again; T1 wins and halves x to 5.88. Another T2 is queued.
Step 5 — the delayed instance sees a stale value
Here is the trap. The current value is 5.88, but the T2 instance now running was activated
after step 3, so the new value it sees is still 11.76. It writes 11.76 × 1.4 = 16.46.
The value jumps back up.
The end
The chain continues indefinitely; eventually the system reports an error. Older delayed instances
of T2 are never executed at all. The lesson: with cascading triggers, a delayed instance carries
the transition values from its activation moment, not from the moment it runs.
Real systems put a lid on it in different ways, and knowing which is which is worth a line in an answer: MySQL forbids a trigger from touching a table already in use by the invoking statement; PostgreSQL places no limit on cascade depth and makes it the programmer’s responsibility; SQL Server and Oracle both cap nesting at 32 levels.
06 · Worked example
Incrementally maintaining derived data
This is the exercise the exam actually sets. A quantity is defined by a query; you must keep a table holding that quantity correct, updating it by deltas rather than recomputing it.
A view is a virtual table defined by a stored query. When queries against a view are far more
frequent than updates to its base tables, you can materialize it — store the result — and some
systems offer CREATE MATERIALIZED VIEW to do so automatically. Where they do not, triggers do the
job.
Take the personnel cost per department:
CREATE VIEW deptcost AS
SELECT d.DeptNum AS dept, coalesce(sum(e.salary), 0) AS totCost
FROM dept d LEFT JOIN emp e ON e.dept = d.DeptNum
GROUP BY d.DeptNum;Recomputing that sum on every change would be absurd, because most changes touch one row of the result:
- updating an employee’s id — no effect at all;
- inserting, deleting, or changing the salary of an employee — one row of
deptcost; - moving an employee between departments — exactly two rows;
- inserting or deleting a department — one row.
So the maintenance is a set of small delta updates. The two representative triggers:
create trigger Incremental_InsEmp
after insert on emp for each row
update deptcost set totCost = totCost + new.salary where dept = new.dept;
create trigger Incremental_SalaryUpdate
after update of salary on emp for each row
when old.dept = new.dept
update deptcost set totCost = totCost - old.salary + new.salary where dept = new.dept;totCost = totCost - old.salary + new.salary is the delta-update idiom, and it recurs in every
exam of this family. Note also the when old.dept = new.dept guard: a salary change and a
department change in one statement is a different case, handled by its own trigger that decrements
the old department and increments the new one.
The event inventory carries its own marks
February 2024 asks you to fill a 3 × 3 grid — for each of PROJECT, EMP, ASSIGNMENT and each of
INSERT/UPDATE/DELETE, does maintaining BUDGET(PID, cost) need a trigger? Three of the
nine cells are “no”, and each needs a justification: updates never touch primary keys
(the text says so), and inserting or deleting an employee cannot change any project’s cost until
an assignment links them. Writing five correct triggers but leaving the grid blank loses 3 of 12
points.
A trigger maintains BUDGET(PID, cost) as the sum of the fundings of a department's projects. The exercise states that a project's funding can never be modified. Which event still needs a trigger?
07 · Worked example
Hierarchies and recursive cascading
The second worked family is a tree whose aggregate must propagate upward — the case where recursive cascading is not a bug but the mechanism.
Take Product(ID, Name, SuperProduct, OwnWeight, TotalWeight), where each product has an own weight
and a parent, and TotalWeight is the product’s own weight plus the total weights of its children.
Root products have SuperProduct = NULL. Users may insert products, delete products, and change a
product’s parent.
Four triggers, and the third is the interesting one:
- T1 — after insert of a product: set its total weight to its own weight.
- T2 — after delete, when the parent is not null: decrease the parent’s total weight.
- T3 — after update of
totalweight, when the parent is not null and the value actually changed: apply the same delta to the parent. This is the propagation rule, and it fires itself all the way to the root. - T4 — after update of
superproduct: decrease the old parent, increase the new one.
CREATE TRIGGER product_AFTER_UPDATE_TOTALWEIGHT
AFTER UPDATE of totalweight ON product
FOR EACH ROW
WHEN new.superproduct is not null AND new.totalweight != old.totalweight
-- remember the three-valued logic
BEGIN
UPDATE product SET totalweight = totalweight + (new.totalweight - old.totalweight)
WHERE ID = new.superproduct;
ENDT3’s own action is an update of totalweight, so it re-activates itself on the parent, then the
grandparent, and terminates at the root where superproduct IS NULL. The triggering graph is
cyclic; termination comes from the data — the tree is finite and each step moves strictly upward.
Three-valued logic in the WHEN clause
The comment in the slide is not decoration. new.totalweight != old.totalweight is
unknown, not true, when either side is NULL — and a WHEN clause only fires on
true. That is why the null check on superproduct is written separately and
explicitly. A freshly inserted product whose TotalWeight starts as NULL is exactly
the case that bites, which is why the lectures offer a T1b variant: a BEFORE-insert
trigger doing SET new.TotalWeight = 0, or equivalently a
DECIMAL(10,2) NOT NULL DEFAULT 0 column.
Deletion of a deleted product’s children needs no trigger at all — ON DELETE CASCADE on the
self-referencing foreign key handles it. Which brings us to the design rule.
08 · Design
Triggers versus declarative constraints
The first design principle in the lectures is also the one graders check: do not write a trigger for something the DBMS already does declaratively.
The full list of principles is short and worth knowing:
- Use triggers to guarantee that a specific operation is accompanied by its related actions.
- Do not duplicate features already built into the DBMS — do not write a trigger to reject bad data if a declarative integrity constraint expresses the same rule.
- Keep triggers small; past roughly 60 lines, move the body into a stored procedure.
- Use them only for centralized, global operations that must fire regardless of which user or application issued the statement.
- Avoid recursion unless it is genuinely the mechanism.
- Always document them — their behaviour is hidden from anyone reading the application code.
The July 2025 paper turns this into an explicit instruction: it asks for a trigger system enforcing
an ISA hierarchy’s coherence, and adds “you can manage some of the constraints in the table
definition, but if you do so please include the table creation statements”. The official solution
splits the work exactly along the principle — a CHECK constraint for the selector/attribute
consistency, ON DELETE SET NULL / NO ACTION / CASCADE for every deletion rule, and triggers
only for the two conditions SQL cannot state declaratively:
CREATE TRIGGER supervisorFK
BEFORE INSERT ON EMP REFERENCING NEW AS N
FOR EACH ROW
WHEN (EXISTS (SELECT * FROM EMP WHERE N.supervisor = EMP.ID AND EMP.isProf IS FALSE))
SIGNAL SQLSTATE '70001' SET MESSAGE_TEXT = 'The supervisor is not a Prof!';Note the pattern for rejecting a modification: a BEFORE trigger whose condition detects the
violation and whose action raises SIGNAL. An AFTER trigger would have to compensate with a delete.
Deep dive Why triggers are still worth the trouble
Given the warnings, why use them at all? Because they put business and management rules once, in the database, under the DBMS’s control, instead of replicating them across every application that touches the data. Vendors themselves use triggers internally for data replication, for integrity constraints beyond declarative SQL, and for materialized-view maintenance — the three application families this chapter walked through.
The cost is real: understanding the interaction between triggers is genuinely hard, most products
implement only a subset of the SQL-99 standard, several deviate on the subtler points of the
execution model, and some rely on proprietary languages — so portability across DBMSs is poor. Oracle
allows multiple events per trigger, has no table variables, permits when only with row-level
granularity, and places no limit on the expressive power of BEFORE trigger actions. PostgreSQL adds
INSTEAD OF and TRUNCATE events and deferrable constraint triggers. MySQL is the most restrictive
of the three.
The evolution of active databases points further: execution modes beyond immediate (deferred, detached), system-defined and temporal events, complex event calculus, and rule administration with priorities and dynamic activation.
Load-bearing ideas
- ECA. Every trigger is event → condition → action. SQL-92 gave declarative constraints; SQL:1999 added procedural reaction for the rules constraints cannot express — anything about a transition.
- Granularity picks your variables.
for each rowgivesold/new;for each statementgivesold table/new tableand fires once even when zero tuples were affected.oldis undefined for inserts,newfor deletes. - BEFORE modifies the modification (
set new.col = …, no database updates allowed); AFTER does everything else. BEFORE is one statement where AFTER is two, and some DBMSs forbid an AFTER trigger from updating its own table. - Order: BEFORE-statement → BEFORE-row → apply + check constraints → AFTER-row → AFTER-statement. Within a category, system-defined — never rely on it.
- Termination: acyclic triggering graph ⇒ terminates; cyclic ⇒ maybe. Delayed trigger instances see the transition values from when they were activated, not when they run.
- The exam pattern: enumerate the events that need triggers (and justify those that do not), then
write each with its event, granularity and
WHENstated, maintaining derived data by deltas. - Exam radar. Re-derive before the exam: the enter/stay/leave three-way split of a conditional
UPDATE; the delta-update idiom
total = total - old + new; the BEFORE-with-SIGNALrejection pattern; and the rule that anything aCHECKorON DELETEclause can say should not be a trigger.