Integrity constraints and triggers
Integrity constraints in SQL (NOT NULL, UNIQUE, PRIMARY KEY, CHECK, FOREIGN KEY with referential actions, DEFAULT, deferred checking inside a transaction, assertions) and what each one refuses; the all-or-nothing behaviour of a statement; triggers (event, timing, row-level and statement-level, WHEN condition, OLD and NEW, BEFORE and AFTER, INSTEAD OF on views) used for auditing, derived data and rules that constraints cannot express; every statement shown was executed on a real SQL engine.
Why it exists
A database should refuse data that makes no sense: a student without a name, an enrolment in a course that does not exist, an age of 400. Integrity constraints state such rules declaratively; the system checks them on every change. Triggers go further: they run an action when something happens, which lets the database maintain derived data, keep an audit trail, or enforce rules that constraints cannot express.
Prerequisites
You need SQL tables and data modification (SQL basics) and the notions of keys and foreign keys (relational model and keys).
Precise definitions
| constraint | meaning | violated by |
|---|---|---|
NOT NULL | the column may not be NULL | inserting or updating to NULL |
UNIQUE (c) | no two rows have the same non-NULL value of c | a duplicate non-NULL value |
PRIMARY KEY (c) | UNIQUE and NOT NULL; at most one per table | a duplicate or NULL key |
CHECK (condition) | the condition may not be FALSE for any row (UNKNOWN passes) | a row for which the condition is FALSE |
FOREIGN KEY (c) REFERENCES T(k) | c is NULL or equals the value of k in a row of T | inserting a dangling reference; deleting or updating a referenced row (depending on the action) |
DEFAULT v | value used when none is given | (not a restriction) |
Referential actions for a foreign key: ON DELETE / ON UPDATE followed by CASCADE, SET NULL, SET DEFAULT, RESTRICT, NO ACTION (relational model and keys).
An assertion (CREATE ASSERTION name CHECK (condition)) is a constraint over several tables; the standard defines it but few systems implement it (SQLite does not), which is why triggers or application code are used.
A trigger is an event–condition–action rule:
CREATE TRIGGER name {BEFORE | AFTER | INSTEAD OF} {INSERT | UPDATE [OF columns] | DELETE} ON table
[FOR EACH ROW] [WHEN (condition)]
BEGIN statements END;
Inside the body NEW is the row after the change and OLD the row before it (NEW for INSERT, OLD for DELETE). A row-level trigger (FOR EACH ROW) runs once for every changed row, a statement-level trigger once per statement (standard; SQLite has only row-level triggers).
How it works
What each constraint refuses
-- error
INSERT INTO Student (sid, sname) VALUES (9, NULL);Error: NOT NULL constraint failed: Student.sname-- error
INSERT INTO Student VALUES (1, 'Duplicate', 'CS', 30);Error: UNIQUE constraint failed: Student.sidThe primary key refuses a second student with sid 1. UNIQUE allows several NULLs (as most systems do, because NULL is not equal to NULL):
CREATE TABLE Person (email TEXT UNIQUE);
INSERT INTO Person VALUES (NULL);
INSERT INTO Person VALUES (NULL);
SELECT COUNT(*) AS rows_with_null_email FROM Person;rows_with_null_email
--------------------
2
(1 row)A CHECK constraint is satisfied when the condition is TRUE or UNKNOWN; only FALSE is refused. So a NULL passes a CHECK (m BETWEEN 0 AND 100):
CREATE TABLE Marks (sid INTEGER, m INTEGER CHECK (m BETWEEN 0 AND 100));
INSERT INTO Marks VALUES (1, NULL);
INSERT INTO Marks VALUES (2, 100);
SELECT COUNT(*) AS accepted FROM Marks;accepted
--------
2
(1 row)-- error
CREATE TABLE Marks (sid INTEGER, m INTEGER CHECK (m BETWEEN 0 AND 100));
INSERT INTO Marks VALUES (3, 101);Error: CHECK constraint failed: m BETWEEN 0 AND 100Foreign keys: inserting and deleting
-- error
INSERT INTO Enrolled VALUES (99, 'C1', 'A');Error: FOREIGN KEY constraint failedAn enrolment of a student that does not exist is refused. The opposite direction, deleting a referenced row, is refused under the default action (NO ACTION):
-- error
DELETE FROM Student WHERE sid = 1;Error: FOREIGN KEY constraint failedWith ON DELETE CASCADE the referencing rows would be deleted too (shown on the page on keys). Only the primary key or a UNIQUE column can be referenced.
A statement is all or nothing
If one row of a multi-row statement violates a constraint, the whole statement is undone:
-- allow-errors
INSERT INTO Course VALUES ('C7', 'Compilers', 4, 'CS'), ('C1', 'Duplicate id', 3, 'CS'), ('C8', 'Graphics', 3, 'CS');
SELECT COUNT(*) AS courses_after_failed_insert FROM Course;courses_after_failed_insert
---------------------------
6
(1 row)The first statement failed on its second row (the key C1 exists), and the table still has the original 6 courses: C7, which was valid by itself, was not kept.
Deferred constraints
A foreign key is normally checked at the end of each statement. A constraint declared DEFERRABLE INITIALLY DEFERRED is checked at the end of the transaction, so rows that refer to each other can be inserted in any order inside it:
CREATE TABLE P (id INTEGER PRIMARY KEY);
CREATE TABLE C (pid INTEGER REFERENCES P(id) DEFERRABLE INITIALLY DEFERRED);
BEGIN;
INSERT INTO C VALUES (1);
INSERT INTO P VALUES (1);
COMMIT;
SELECT * FROM C;pid
---
1
(1 row)The child row was inserted before its parent and the commit succeeded because the parent existed at commit time. If it does not, the commit is refused:
-- error
CREATE TABLE P (id INTEGER PRIMARY KEY);
CREATE TABLE C (pid INTEGER REFERENCES P(id) DEFERRABLE INITIALLY DEFERRED);
BEGIN;
INSERT INTO C VALUES (2);
COMMIT;Error: FOREIGN KEY constraint failedTriggers
Auditing. After a grade changes, record the old and the new value:
CREATE TABLE AuditLog (sid INTEGER, cid TEXT, old_grade TEXT, new_grade TEXT);
CREATE TRIGGER log_grade AFTER UPDATE OF grade ON Enrolled FOR EACH ROW BEGIN INSERT INTO AuditLog VALUES (OLD.sid, OLD.cid, OLD.grade, NEW.grade); END;
UPDATE Enrolled SET grade = 'A' WHERE cid = 'C1' AND grade <> 'A';
SELECT * FROM AuditLog ORDER BY sid;sid | cid | old_grade | new_grade
----+-----+-----------+----------
2 | C1 | B | A
8 | C1 | B | A
(2 rows)The update changed two rows, so the row-level trigger ran twice. Chen's grade in C1 is NULL, and NULL <> 'A' is UNKNOWN, so his row was not updated and not logged.
Triggers: start
Computed, not typed| sid | cid | grade |
|---|---|---|
| 1 | C1 | A |
| 1 | C2 | B |
| 2 | C1 | B |
| 2 | C5 | A |
| 3 | C1 | NULL |
| 3 | C2 | A |
| 3 | C3 | A |
| 5 | C1 | A |
| 5 | C2 | C |
| 6 | C5 | B |
| 7 | C6 | A |
| 8 | C1 | B |
| 8 | C3 | A |
| 8 | C4 | B |
| sid | cid | old_grade | new_grade |
|---|---|---|---|
| (empty) | |||
Step 0
A trigger is a rule 'when this event happens, run this action'. This one fires AFTER an update of the grade column of Enrolled, once FOR EACH ROW changed, and can read the old and new values of the row as OLD and NEW.
Maintaining derived data. Keep a column that counts the students of a course:
ALTER TABLE Course ADD COLUMN taken INTEGER DEFAULT 0;
UPDATE Course SET taken = (SELECT COUNT(*) FROM Enrolled e WHERE e.cid = Course.cid);
CREATE TRIGGER count_enrol AFTER INSERT ON Enrolled FOR EACH ROW BEGIN UPDATE Course SET taken = taken + 1 WHERE cid = NEW.cid; END;
INSERT INTO Enrolled VALUES (4, 'C1', 'B');
SELECT cid, taken FROM Course WHERE cid IN ('C1', 'C2') ORDER BY cid;cid | taken
----+------
C1 | 6
C2 | 3
(2 rows)C1 had 5 enrolments; the trigger made it 6, C2 is unchanged. (A derived column can go out of date if some statement changes Enrolled in a way the triggers do not cover, for example DELETE; a view or a query avoids that risk.)
Vetoing a change. A BEFORE trigger can refuse an operation with a message:
-- error
CREATE TRIGGER check_age BEFORE INSERT ON Student FOR EACH ROW WHEN NEW.age < 16 BEGIN SELECT RAISE(ABORT, 'student too young'); END;
INSERT INTO Student VALUES (9, 'Ishan', 'CS', 14);Error: student too youngMaking a join view updatable. An INSTEAD OF trigger replaces the operation on a view by the actions that the designer chooses:
CREATE VIEW CSGrades AS SELECT s.sid, s.sname, e.cid, e.grade FROM Student s JOIN Enrolled e ON s.sid = e.sid WHERE s.dept = 'CS';
CREATE TRIGGER upd_grade INSTEAD OF UPDATE OF grade ON CSGrades FOR EACH ROW BEGIN UPDATE Enrolled SET grade = NEW.grade WHERE sid = OLD.sid AND cid = OLD.cid; END;
UPDATE CSGrades SET grade = 'A' WHERE sname = 'Asha' AND cid = 'C2';
SELECT grade FROM Enrolled WHERE sid = 1 AND cid = 'C2';grade
-----
A
(1 row)Row-level and statement-level. An update of the four CS students runs a row-level trigger four times:
CREATE TABLE Log (n INTEGER);
CREATE TRIGGER per_row AFTER UPDATE ON Student FOR EACH ROW BEGIN INSERT INTO Log VALUES (NEW.sid); END;
UPDATE Student SET age = age + 1 WHERE dept = 'CS';
SELECT COUNT(*) AS trigger_runs FROM Log;trigger_runs
------------
4
(1 row)A statement-level trigger (standard SQL) would run once for the whole statement.
Constraints against triggers
Prefer a declarative constraint when it can express the rule: the system optimises it and it cannot be forgotten. Use a trigger for what constraints cannot say: rules across several tables, derived data, auditing, complex reactions. Triggers have costs: they can fire other triggers (cascades, possibly cyclic), they hide behaviour, and the order of several triggers is not always defined.
Worked examples
EX 1Which constraint
Easy"A student's age is between 16 and 60": CHECK (age BETWEEN 16 AND 60). "Every enrolment refers to an existing course": foreign key. "No two courses have the same title": UNIQUE (title). "A student must have a name": NOT NULL. "A course credit is positive and an enrolment is allowed only if the course has seats left": the first by CHECK, the second needs a trigger or an assertion (it involves another table's data).
EX 2NULL and constraints
EasyCHECK (m BETWEEN 0 AND 100) accepts NULL (UNKNOWN passes). UNIQUE accepts several NULLs. A PRIMARY KEY never accepts NULL. A foreign key column set to NULL is not checked.
EX 3Deferred constraints and circular references
MediumTwo tables that reference each other (a department's head is an employee, an employee works in a department) cannot be filled with immediate foreign keys: whichever row is inserted first has a dangling reference. Declaring one (or both) foreign keys DEFERRABLE INITIALLY DEFERRED and inserting both rows inside one transaction works, as in the example with P and C.
EX 4How many times does the trigger fire
MediumAn AFTER UPDATE ... FOR EACH ROW trigger on Student and an UPDATE Student SET age = age + 1 WHERE dept = 'CS': four CS students, so the body runs four times (once per changed row). A statement-level trigger runs once. An update that changes no row runs a row-level trigger zero times.
EX 5Typical questions
GATE level(a) "Which constraint can be violated by deleting a tuple?" Referential integrity (if the tuple is referenced). (b) "Which constraint can be violated by an insertion?" Key, domain, NOT NULL, referential (child) and CHECK constraints. (c) "An UPDATE of a primary key can violate:" uniqueness and referential integrity. (d) "A row-level AFTER trigger on a table updates 5 rows in one statement: runs:" 5 times. (e) "A CHECK on a NULL value:" passes. (f) "What does ON DELETE CASCADE do?" Deletes the referencing rows.
Cheat box
Cheat box
- Constraints: NOT NULL, UNIQUE (NULLs allowed), PRIMARY KEY (unique + not null), CHECK (only FALSE fails), FOREIGN KEY (NULL or existing), DEFAULT.
- Insert can violate: key, domain, referential (child); delete can violate: referential (parent); update: key, referential, CHECK.
- A statement is atomic: one bad row undoes the whole statement.
- Deferred constraints are checked at commit.
- Trigger = event + condition + action; BEFORE can veto; OLD/NEW; row-level runs once per row.
- Use constraints for simple rules, triggers for the rest.
Traps and GATE patterns
Connections
Builds on
Across subjects
No cross-subject links yet.
Constraint checks and triggers run inside transactions that are atomic and isolated (transactions and ACID); views and NULL behaviour are on the page on nested queries.
PYQs
3 previous year questions from your book. I re-solved each one myself (2 by running or simulating, 1 by reasoning only); the book's key is shown next to my answer. All PYQs
Consider the following tables T1 and T2. In table T1, P is the primary key and Q is the foreign key referencing R in table T2 with on-delete cascade. In table T2, R is the primary key and S is the foreign key referencing P in table T1 with on-delete set NULL and on- update cascade. In order to delete record (3, 8) from table T1, the number of additional records that need to be deleted from table T1 is _____.
The following table has two attributes A and C where A is the primary key and C is the foreign key referencing A with ON DELETE CASCADE. The set of all tuples that must be additionally deleted to preserve referential integrity when the tuple (2, 4) is deleted is:
- (a)(3, 4) and (6, 4)
- (b)(5, 2) and (7, 2)
- (c)(5, 2), (7, 2) and (9, 5)
- (d)(3, 4), (4, 3) and (6, 4)
Given the following statements: S1: A foreign key declaration can always be replaced by an equivalent check assertion in SQL. S2: Given the table R(a,b,c) where a and b together form the primary key, the following is a valid table definition. CREATE TABLES ( a INTEGER, d integer, e integer, PRIMARY KEY (d), FOREIGN key (a) references R) Which one of the following statements is CORRECT?
- (a)S1 is TRUE and S2 is FALSE.
- (b)Both S1 and S2 are TRUE.
- (c)S1 is FALSE and S2 is TRUE
- (d)Both S1 and S2 are FALSE.
Self-check
Question 1
Does CHECK (age >= 18) reject a row whose age is NULL?
Show answerHide
No: the condition is UNKNOWN, and only FALSE violates a CHECK.
Question 2
A statement inserts three rows, the second violates a constraint. How many rows are inserted?
Show answerHide
None: the statement is atomic and is undone.
Question 3
What is the difference between a BEFORE and an AFTER trigger?
Show answerHide
A BEFORE trigger runs before the change is applied and can modify the new row or veto the change; an AFTER trigger runs after it.
Question 4
When is a DEFERRABLE INITIALLY DEFERRED foreign key checked?
Show answerHide
At commit time (end of the transaction).
Revision card
One-screen revision
- NOT NULL, UNIQUE, PRIMARY KEY, CHECK (FALSE fails), FOREIGN KEY, DEFAULT.
- Statement atomicity; deferred constraints at commit.
- Triggers: event, timing, row/statement level, WHEN, OLD/NEW, INSTEAD OF for views.
- Constraints for simple rules, triggers for cross-table rules and derived data.
References
- ·Silberschatz, Korth, Sudarshan, Database System Concepts (7th edition), Section 4.4 (integrity constraints) and Section 5.3 (triggers)
- ·Elmasri, Navathe, Fundamentals of Database Systems (7th edition), Sections 6.2 (specifying constraints in SQL) and 7.2 (assertions and triggers)
Verification
scripts/db_sql.py executes every verified SQL block of this page on a fresh in-memory SQLite database with foreign keys enabled; scripts/enum-checks/db_sqlcheck.py re-runs them and compares the output, including the error messages. SQLite differences from the SQL standard are stated where they matter: it has no CREATE ASSERTION, only row-level triggers (the standard also has statement-level triggers), accepts several NULLs in a UNIQUE column (as most systems do), checks foreign keys immediately unless a constraint is declared DEFERRABLE INITIALLY DEFERRED, and allows INSTEAD OF triggers on views. The behaviour of a CHECK constraint with NULL (a check that evaluates to UNKNOWN is satisfied) is the standard rule and was demonstrated on SQLite. Textbook chapter references are from memory. SECOND PASS (2026-11-02, done by the assistant, not a human check): every Self-check question was solved before the printed answer was read and all agreed; the definitions, worked examples, cheat box and traps were read claim by claim and re-derived against the textbook definitions and the sample database. DEFECT FOUND AND FIXED: the trap on UNIQUE now says that UNIQUE with NOT NULL also forbids NULL. Status raised to self-checked; verified needs a textbook or human check.
last verified: never
My notes (saved in this browser)