Core CS · DBMS
Delete one department, and decide what happens to its people
A constraint is a rule the store checks on every write, from your web app, your admin console and the UPDATE somebody types by hand at 2am. You will declare six of them on two small tables, then watch one DELETE land under CASCADE, SET NULL and RESTRICT.
Delete a department three ways →01 The idea
Rules the data carries, not rules the code remembers
An integrity constraint is a condition on the data that the database checks itself, on every insert, update and delete. You write it once, in CREATE TABLE, and from that moment no statement can leave the table in a state that breaks it. There is nothing to call and nothing to remember. A statement that would break it does not partly succeed; it does not succeed at all.
The relational model names four families, and they arrive in a fixed order in every textbook and most interviews. Domain constraints say what values a single column may hold. Key constraints say which sets of columns identify a row uniquely. Entity integrity says no part of a primary key may be NULL. Referential integrity says a foreign key value must name a row that actually exists.
The first three can be decided by looking at one row of one table. Referential integrity is the only one that has to open a second table, and that difference is the whole of this lesson. It is also the one an application gets wrong, because the application checked a copy of the parent table a second ago rather than the table itself. And it raises a question the other three never do: when the row you point at is deleted, what should happen to you? SQL refuses to guess. You answer it in the schema, with ON DELETE and ON UPDATE.
02 Worked example
One INSERT, four checks, one refusal
Two tables for one college. Every employee belongs to a department, and the way that is written down is a dept_id column on employee that has to name a row in department. These are the rows on disk right now, and one row somebody is trying to add.
department employeedept_id dept_name emp_id emp_name badge salary dept_id------- --------- ------ -------- ----- ------ -------D1 Civil E1 Asha B7 40000 D1D2 Mining E2 Ravi B4 35000 D1 E3 Meera B9 52000 D2 -- the statement a clerk just ranINSERT INTO employee VALUES ('E4', 'Nikhil', 'B2', 30000, 'D9');
The server does not accept or reject that row in one go. It runs a series of checks, and the row survives all of them but one. Follow them left to right.
Node 4 is the only check that had to open a second table. Nodes 1 to 3 can all be decided from the row in front of you, which is exactly why an application can do them and gets away with it. Node 4 it cannot do honestly. An application that read department into memory at startup will wave 'D9' straight through, because what it is checking against is a copy, and the copy is as old as the last time it was refreshed.
Change one value and the same row goes in. Send 'D2' instead of 'D9' and all four checks pass, so node 5 writes the row instead of refusing it. Notice what is not checked either way: nothing here says an employee must have a department at all. If dept_id is left NULL, referential integrity is satisfied with nothing to compare, because a NULL foreign key points at no row and there is no row to go looking for. Only NOT NULL makes the link compulsory, and that turns out to matter enormously the day somebody deletes a department.
03 Mechanics
Every constraint you can declare, and what breaking it does
Seven things you can write in a column definition. Six of them are constraints, which means there is some row that would make them raise an error. One of them is not a constraint at all and is in the list because people put it there. The column that matters most is the third: when is this checked, because a rule that is checked at the wrong moment is a rule you cannot rely on.
| Constraint | How you declare it | Checked on | On violation | May the column hold NULL? |
|---|---|---|---|---|
| Domain (the type) | salary NUMERIC(8,2) |
every insert, and every update of the column | statement rejected, nothing written |
yes, unless NOT NULL is added |
| NOT NULL | emp_name TEXT NOT NULL |
insert, and update of the column | statement rejected |
no, that is the entire rule |
| CHECK | CHECK (salary >= 0) |
insert, and update of the column | rejected only when the condition is FALSE |
yes, and a NULL passes: the condition is UNKNOWN, and UNKNOWN is not FALSE |
| UNIQUE | badge CHAR(2) UNIQUE |
insert, and update of the column | statement rejected |
yes, and several rows may hold NULL at once, so UNIQUE on its own is not a candidate key |
| PRIMARY KEY | emp_id CHAR(2) PRIMARY KEY |
insert, and update of the column | statement rejected |
no, and that half of it is entity integrity |
| DEFAULT | dept_id CHAR(2) DEFAULT 'D1' |
insert only, and only when the column is left out of the statement | it cannot be violated; it supplies a value, and the real constraints are then checked against that value |
not applicable |
| FOREIGN KEY | dept_id REFERENCES department(dept_id) |
insert and update on the child, and delete or key-update on the parent | statement rejected, or the declared referential action runs instead |
yes, and NULL means "no parent row", which satisfies the constraint |
Here are the same two tables with all of it declared. Every rule in this lesson is on this page, and none of it is anywhere else.
CREATE TABLE department ( dept_id CHAR(2) PRIMARY KEY, dept_name TEXT NOT NULL UNIQUE);CREATE TABLE employee ( emp_id CHAR(2) PRIMARY KEY, -- the candidate key we picked emp_name TEXT NOT NULL, badge CHAR(2) NOT NULL UNIQUE, -- the candidate key we did not salary NUMERIC(8,2) CHECK (salary >= 0), -- a NULL salary passes this dept_id CHAR(2) DEFAULT 'D1' REFERENCES department(dept_id) ON DELETE SET NULL ON UPDATE CASCADE);
Line 2 is two constraints in one word. PRIMARY KEY is UNIQUE plus NOT NULL. The unique half is the key constraint; the not-null half is entity integrity, and it exists because the primary key is the only handle the relation gives you for naming a row. NULL means "unknown", an unknown identifier identifies nothing, and NULL = NULL does not even evaluate to true, so a NULL key could not be matched against anything.
Line 8 is the second candidate key, and the NOT NULL is doing real work. UNIQUE on its own would not give you a candidate key, because SQL lets a UNIQUE column hold NULL in many rows at once and a column that can be unknown cannot identify anything. NOT NULL UNIQUE is what makes badge a key you could have chosen as the primary key instead of emp_id. Both are candidate keys; both are minimal, since each is a single attribute; the primary key is whichever one you picked.
Line 9 is the trap that gets asked. CHECK (salary >= 0) rejects −1 and accepts NULL. A CHECK is violated only when it evaluates to FALSE, and NULL >= 0 evaluates to UNKNOWN. If a salary must be both present and non-negative, you need NOT NULL beside the CHECK, or you write the condition to catch it.
Lines 10 to 13 are one column and three separate decisions. DEFAULT says what to store when an INSERT leaves dept_id out. REFERENCES says the value has to exist in department. ON DELETE and ON UPDATE say what happens to this row when the department row it points at is deleted or changes its key. Note the quiet hazard in line 10: nothing maintains a DEFAULT that names a parent row, so delete D1 and every later INSERT that omits dept_id starts failing the foreign key for a value nobody typed.
What a violation does is worth being exact about, because half-right answers here are common. A constraint violation is a statement-level event: the statement has no effect at all, not on the rows before the offending one and not on the rows after it. A ten-thousand-row INSERT that trips a CHECK on row 9,999 writes nothing. What happens to the enclosing transaction is where products differ. PostgreSQL puts the whole transaction into an aborted state, so every statement after the error fails until you ROLLBACK or roll back to a savepoint. Oracle, MySQL and SQL Server on their usual settings undo only the failed statement and leave the transaction open. Give the statement-level answer unless you are asked about a specific product.
That leaves the question only foreign keys raise. Somebody deletes D1 while two employees still point at it. Refusing the delete is one answer, but it is not the only sensible one, so SQL makes you choose from five and defaults to the most cautious.
| Action | ON DELETE of the parent row | ON UPDATE of the parent key | It fails when |
|---|---|---|---|
| NO ACTION the default |
the children are untouched and the parent delete is refused | the parent key change is refused | children still reference the row when the constraint is checked, which is after the statement has been applied |
| RESTRICT | the children are untouched and the parent delete is refused | the parent key change is refused | children reference the row, checked before the statement is applied and never postponable |
| CASCADE | the children are deleted too, and their own children after them | the children's foreign key is rewritten to the new key value | on delete, almost never. That is the problem: it is the action that fails quietly, by deleting far more than the statement named |
| SET NULL | the children keep their rows and their foreign key is set to NULL | same, the foreign key is set to NULL | the child's foreign-key column is NOT NULL, and then the parent delete fails on the child's not-null constraint |
| SET DEFAULT | the children's foreign key is set to that column's DEFAULT | same, the foreign key is set to the DEFAULT | the default value is not itself a key in the parent, so writing it would leave the child an orphan and the whole delete is rejected |
RESTRICT against NO ACTION is the question in this topic that separates people who have read the manual from people who have read a summary. Both refuse the delete, and on a plain single-statement delete you cannot tell them apart, which is why most notes call them the same thing. The standard defines the difference by timing: RESTRICT is checked before the delete is carried out, and NO ACTION after, once the row is gone and once any other referential action the same statement triggered has run. So if a second cascade in the same statement clears the offending children out of the way first, NO ACTION succeeds where RESTRICT would already have refused.
The difference you can actually reach on a running system is deferral. Declare the constraint DEFERRABLE INITIALLY DEFERRED and a NO ACTION check waits until COMMIT, so you may delete the parent now and repoint or delete the children later in the same transaction. RESTRICT can never be deferred, so it refuses immediately whatever you do. Say it that way and you have answered the question in one sentence: NO ACTION can be postponed, RESTRICT cannot.
Three vendor differences worth one clause each, and no more than that. Oracle supports ON DELETE CASCADE and ON DELETE SET NULL and nothing else: no RESTRICT keyword, no SET DEFAULT, and no ON UPDATE clause at all. MySQL's InnoDB accepts SET DEFAULT in the parser and then refuses to create the table, and because it has no deferred constraint checking it treats NO ACTION as a plain synonym for RESTRICT. SQLite implements all five and enforces none of them until the connection runs PRAGMA foreign_keys = ON. Name the standard behaviour first, then the vendor that departs from it; that order is what makes it sound like knowledge rather than trivia.
05 Cheat sheet
The five referential actions on one card
Learn the fourth column. Anyone can recite what CASCADE does; the follow-up is always "and when does that fail", and it is the only part of the table that is hard to guess.
| Action | On parent DELETE | On parent key UPDATE | It fails when |
|---|---|---|---|
| NO ACTION (default) | refuse the delete | refuse the change | children exist at check time; the check runs after the statement and can be deferred to COMMIT |
| RESTRICT | refuse the delete | refuse the change | children exist; checked before the statement, never deferrable |
| CASCADE | delete the children too | rewrite the children's key | hardly ever, which is the danger: one DELETE can empty tables the statement never named |
| SET NULL | blank the children's foreign key | blank the children's foreign key | the child's foreign-key column is NOT NULL |
| SET DEFAULT | write the column DEFAULT into the children | write the column DEFAULT into the children | the default value is not itself a key in the parent table |
06 Where & why
Where these words actually work
Every keyword in this lesson is a line you can type into a system that is running somewhere right now. None of them behaves identically everywhere, and knowing one specific departure is worth more in an interview than knowing the list twice.
Supports CASCADE, SET NULL, SET DEFAULT, RESTRICT and NO ACTION on both ON DELETE and ON UPDATE, and since version 15 you can name which columns SET NULL and SET DEFAULT touch on a composite key. It is also where the RESTRICT-versus-NO-ACTION difference stops being theory: declare the constraint DEFERRABLE INITIALLY DEFERRED and NO ACTION waits for COMMIT, while RESTRICT refuses to be deferred at all.
InnoDB gives you CASCADE, SET NULL, RESTRICT and NO ACTION, and treats NO ACTION as a synonym for RESTRICT because it has no deferred constraint checking at all. SET DEFAULT is accepted by the parser and then rejected by the storage engine, so a schema that creates cleanly on PostgreSQL will not create here. Worth knowing too: CHECK constraints were parsed and silently discarded before MySQL 8.0.16, so plenty of live schemas contain checks that have never once run.
Oracle has ON DELETE CASCADE and ON DELETE SET NULL. There is no RESTRICT keyword, no SET DEFAULT, and no ON UPDATE clause of any kind, so changing a referenced primary key means updating the children yourself inside a transaction. That absence is one of the reasons Oracle shops are firm about primary keys being values that never change.
SQLite implements all five actions, and enforces no foreign key at all unless the connection has run PRAGMA foreign_keys = ON, which is off by default for backwards compatibility. Your schema reads back exactly as you wrote it, every REFERENCES is still there, and orphan rows go in without a murmur. It is the cleanest demonstration in this lesson that a declaration and an enforcement are two different things.
07 Interview questions
What they actually ask
Constraints come up in almost every DBMS interview, usually as "what are the integrity constraints" and then one follow-up designed to find out whether you have ever hit one. The follow-up is nearly always about NULL or about what happens on a delete.
What is an integrity constraint, and who checks it?
What is entity integrity, and why can a primary key not be NULL?
What is referential integrity? Can a foreign key be NULL?
Superkey, candidate key, primary key. What is the difference?
PRIMARY KEY or UNIQUE. When does it matter which?
I run a 10,000-row INSERT and row 9,999 breaks a CHECK. What is in the table?
Explain ON DELETE CASCADE, SET NULL and SET DEFAULT.
RESTRICT and NO ACTION look identical to me. What is the difference?
A column is declared CHECK (salary >= 0). I insert NULL. Accepted or rejected?
Why not enforce all of this in the application instead?
What do constraints cost you?
Is there ever a good reason not to declare a foreign key?
08 Practice problems
Six to work through
Write the rows down. Every one of these is decided by looking at an actual table state, and the people who get them wrong are the ones who answered from the schema without drawing what was in it.