Integrity Constraints and Referential Actions

The Relational Model · 25 min

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
A foreign key is a check, not a comment. It runs on every insert into the child and every delete from the parent, and ON DELETE is where you write down which side gives way.

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.

Write the rule once, beside the data, and it is checked on every write forever. Write the same rule in your application and it is checked on the writes that happen to go through your application.
Integrity constraintA condition on the data that the DBMS enforces on every insert, update and delete. Declared in CREATE TABLE or ALTER TABLE, and from then on it applies to every program, script and person that connects.
Candidate keyA minimal set of attributes that is unique across every instance the relation is ever allowed to hold. Any unique set at all is a superkey; a candidate key is a superkey with nothing to spare, so removing any attribute from it destroys the uniqueness.
Foreign keyA set of attributes in one relation whose non-NULL values must appear as the value of a candidate key in the referenced relation. It is allowed to be NULL, which means "no related row", unless you also declare it NOT NULL.

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.

1 · Domainevery value is in its column's domain: 'D9' is two characters, 30000 fits NUMERIC(8,2), and CHECK (salary >= 0) is true
2 · Keyemp_id 'E4' is not already in the table, and neither is badge 'B2', so no candidate key value is duplicated
3 · Entity integrityemp_id is not NULL, so the row has an identity the table can address it by
4 · Referential integrity'D9' must appear in department.dept_id. That column holds D1 and D2. It does not appear
5 · The statementrejected in full. No row is written, and the error names the foreign-key constraint that stopped it

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.

ConstraintHow you declare itChecked onOn violationMay 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.

ActionON DELETE of the parent rowON UPDATE of the parent keyIt 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.

ON DELETE is not a detail of the foreign key. It is your answer to "who gives way". CASCADE says the children go, SET NULL says the link goes, RESTRICT and NO ACTION say the parent stays and your delete fails. The default is NO ACTION, which is the safe answer, and safe is not the same as right for your table.

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.

ActionOn parent DELETEOn parent key UPDATEIt fails when
NO ACTION (default)refuse the deleterefuse the changechildren exist at check time; the check runs after the statement and can be deferred to COMMIT
RESTRICTrefuse the deleterefuse the changechildren exist; checked before the statement, never deferrable
CASCADEdelete the children toorewrite the children's keyhardly ever, which is the danger: one DELETE can empty tables the statement never named
SET NULLblank the children's foreign keyblank the children's foreign keythe child's foreign-key column is NOT NULL
SET DEFAULTwrite the column DEFAULT into the childrenwrite the column DEFAULT into the childrenthe default value is not itself a key in the parent table
A superkey is not a candidate keyAny set of attributes that is unique is a superkey. A candidate key is a superkey with nothing to spare: remove any attribute and uniqueness is lost. The primary key is one candidate key you chose; the rest become NOT NULL UNIQUE. In our table {emp_id} and {badge} are candidate keys, while {emp_id, badge} and {emp_id, emp_name} are superkeys and neither is a candidate key. And no set is a key because the rows you happen to be looking at are distinct: {emp_name} and {salary} have no repeats in our three rows and are not keys, because a key is a promise about every instance the table is ever allowed to hold.
NULL is where the marks goA primary key may never be NULL: that is entity integrity. A foreign key may be NULL, and then it satisfies referential integrity with nothing to check. A CHECK accepts NULL because the condition is UNKNOWN rather than FALSE. A UNIQUE column accepts many NULLs in PostgreSQL, MySQL, Oracle and SQLite, and exactly one in SQL Server. Four different NULL rules on four different constraints, and the examiner knows it.
The four integrity constraints, in orderDomain: a value belongs to its column's domain. Key: no two rows agree on a candidate key. Entity integrity: no part of a primary key is NULL. Referential integrity: a non-NULL foreign key names a row that exists. The first three read one table, the fourth reads two, and that is the whole reason the fourth is the one an application gets wrong.

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.

PostgreSQL
All five actions, and the only place the RESTRICT question is visible

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.

MySQL · InnoDB
Two words missing, and one that used to be ignored

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
ON DELETE only, and only two of the five

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
Every constraint declared, none of them checked

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.

Every one of these is a line somebody typed into a real schema. If you can say which action your last project used on its busiest parent table, and why the other four were wrong for it, you are ahead of most of the room, because most of the room has only ever read the list.

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?
A condition on the data that the DBMS itself enforces on every insert, update and delete. You declare it once in CREATE TABLE and there is nothing further to call: the check runs on writes from your application, from an import script, from a psql session, from anything that connects. The relational model groups them as domain, key, entity integrity and referential integrity, and SQL spells those as types, NOT NULL, CHECK, UNIQUE, PRIMARY KEY and FOREIGN KEY.
What is entity integrity, and why can a primary key not be NULL?
Entity integrity is the rule that no attribute of a primary key may be NULL. The reason is that the primary key is the only handle the relation gives you for naming a row. NULL means "unknown", and an unknown identifier identifies nothing, so a row with a NULL key could not be pointed at by a foreign key or reliably found by a query. It goes further than inconvenience: NULL = NULL evaluates to UNKNOWN rather than true, so even comparing two such keys would not tell you whether they are the same row.
What is referential integrity? Can a foreign key be NULL?
Referential integrity says that every non-NULL foreign key value must appear as the value of a candidate key in the referenced relation. Yes, a foreign key can be NULL, and doing so satisfies the constraint rather than breaking it: NULL means "no related row", so there is nothing to go looking for. Only NOT NULL makes the relationship compulsory. One depth point that gets asked about composite foreign keys: under the default MATCH SIMPLE, if any column of the key is NULL the constraint is satisfied whatever the other columns hold, and MATCH FULL is what forces all-or-nothing.
Superkey, candidate key, primary key. What is the difference?
A superkey is any set of attributes whose values are unique across the relation. A candidate key is a minimal superkey: drop any attribute from it and uniqueness is lost. The primary key is the one candidate key you nominated; the rest usually become NOT NULL UNIQUE. The minimality is the whole examinable part, so never define a candidate key as "a key that could be primary" and stop there. And uniqueness is a promise about every instance the table may ever hold, not an observation about the rows currently in it.
PRIMARY KEY or UNIQUE. When does it matter which?
PRIMARY KEY is UNIQUE plus NOT NULL, and a table may declare at most one of them. UNIQUE on its own permits NULL, and in PostgreSQL, MySQL, Oracle and SQLite it permits NULL in many rows at once, so a UNIQUE column is not by itself a candidate key. SQL Server is the odd one out and allows only a single NULL. Use PRIMARY KEY for the identifier other tables will reference, and UNIQUE for the other candidate keys and for business rules like "one active session per user".
I run a 10,000-row INSERT and row 9,999 breaks a CHECK. What is in the table?
Nothing from that statement. A constraint violation is a statement-level event: the statement has no effect at all, not the rows before the failure and not the rows after it. What happens to the surrounding transaction is where products differ, and it is worth naming: PostgreSQL puts the whole transaction into an aborted state so every later statement fails until you roll back, while Oracle, MySQL and SQL Server on their usual settings undo only the failed statement and leave the transaction open.
Explain ON DELETE CASCADE, SET NULL and SET DEFAULT.
They say what happens to the child rows when the parent row is deleted. CASCADE deletes the children too, and their children after them. SET NULL keeps the child rows and writes NULL into their foreign key, so the row survives with no parent. SET DEFAULT keeps the child rows and writes that column’s declared DEFAULT into the foreign key instead. The same three words also exist on ON UPDATE, where CASCADE rewrites the child key to the parent’s new value, which is the one genuinely useful case for a key that changes.
RESTRICT and NO ACTION look identical to me. What is the difference?
Both refuse the parent operation, and on a plain single-statement delete you cannot tell them apart. The difference is timing. The standard checks RESTRICT before the delete is carried out and NO ACTION after, once the row is gone and once any other referential action from the same statement has run. The version you can reach in practice: a DEFERRABLE INITIALLY DEFERRED constraint with NO ACTION postpones its check to COMMIT, so you can delete the parent and fix the children before committing, while RESTRICT can never be deferred. MySQL InnoDB has no deferred checking at all and treats the two as synonyms.
A column is declared CHECK (salary >= 0). I insert NULL. Accepted or rejected?
Accepted. A CHECK constraint is violated only when its condition evaluates to FALSE, and NULL >= 0 evaluates to UNKNOWN, which is not FALSE. This surprises people because they read the check as "salary must be a number that is at least zero" when it actually says "if there is a salary, it is at least zero". If you want both, add NOT NULL. And on MySQL before 8.0.16 the answer was accepted for a different reason again: CHECK constraints were parsed and then silently discarded.
Why not enforce all of this in the application instead?
Because a rule in the application binds the application. The same tables are written by your service, an admin console, a nightly import, a one-off data fix and whoever has a SQL client open at 2am, and the constraint is the only thing every one of those goes through. A declared constraint is also a fact that never drifts from the data, so it stays true when the team that wrote the rule has left and nobody remembers which service owned it. The application layer is the right place for rules about a workflow; the schema is the right place for rules about what the data is.
What do constraints cost you?
Every constraint is work on every write. A foreign key turns each child insert into a lookup in the parent, and if the child’s foreign-key column has no index, a parent delete scans the child table to find referencing rows, which is a common cause of a delete that is mysteriously slow. Cascading deletes lock and rewrite rows in tables the statement never named, so one small delete can hold locks across half the schema. And bulk loads are much faster with constraints dropped and re-added afterwards, which is exactly why that pattern exists.
Is there ever a good reason not to declare a foreign key?
Yes, and it is a trade you should be able to state. Very high write volumes where the parent lookup on every insert costs more than the occasional orphan is worth; a sharded or partitioned store where the parent row lives on a node the DBMS cannot check against; and bulk loading, where constraints are dropped, data is loaded, and the constraint is re-added with a validation pass. What you take on in exchange is real: orphan detection becomes a job somebody has to write and run, and nothing warns you the day it stops running.

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.

What row breaks this line?

Easy
A two-table library schema: CREATE TABLE title (isbn CHAR(4) PRIMARY KEY, name TEXT NOT NULL, copies INT DEFAULT 1 CHECK (copies >= 0)); and CREATE TABLE copy (copy_id CHAR(4) PRIMARY KEY, isbn CHAR(4) NOT NULL REFERENCES title(isbn) ON DELETE RESTRICT, shelf CHAR(2));. Take it one clause at a time. For each clause, write the single statement that would make it raise an error, and label the clause with one of the four integrity constraint types.
Follow-up
Two clauses cannot be made to raise an error by any INSERT at all, and they cannot for two different reasons. Only one of those two can be made to raise an error by a DELETE.
Show the hint
A clause that supplies a value is not the same kind of clause as one that tests a value, and one clause only has an opinion about statements aimed at the other table.

Four rows at the door

Easy
The table is CREATE TABLE seat (room CHAR(2), num INT, taken_by CHAR(2) UNIQUE, PRIMARY KEY (room, num));, starting empty. Four INSERTs arrive in this order: (R1, 1, S1), (R1, 2, S1), (R1, 2, S2), (R1, 2, S3). For each one say accepted or refused, and if refused name the exact constraint that stopped it.
Follow-up
One statement is refused by a constraint on a column whose own value looks perfectly fine, and one is refused only because an earlier statement succeeded. Now swap the second and third statements: the same two statements still fail, but one of them now breaks two constraints at once where before it broke one.
Show the hint
Apply each statement to the table as the previous statements left it, and test every declared constraint, not only the primary key.

Draw the aftermath

Medium
Three tables. student(sid PK, name) holds S1 Asha, S2 Ravi, S3 Meera. course(cid PK, title) holds C1 Maths, C2 Physics. enrolment(sid REFERENCES student ON DELETE CASCADE, cid REFERENCES course ON DELETE RESTRICT, PRIMARY KEY (sid, cid)) holds (S1,C2), (S2,C1), (S3,C2). Write out all three tables exactly as they stand after DELETE FROM student WHERE sid = 'S2'; followed by DELETE FROM course WHERE cid = 'C1';. Then do it again with the two statements in the opposite order.
Follow-up
The two orders do not give the same result, and one order ends with a statement refused. Both foreign keys sit on the same child table, which is what lets one delete clear the way for the other.
Show the hint
For each DELETE, find the foreign keys whose referenced table is the one you are deleting from. Those are the only constraints with anything to say about it.

Choose the action, defend the choice

Medium
Four parent-to-child pairs in a college system: student to hostel_allocation, department to employee, exam to answer_script, and student to fee_payment. For each pair choose one referential action for ON DELETE, and write the one sentence about the child rows that decides it.
Follow-up
For two of the four, the honest answer is that the parent row should never be deleted at all, and saying so needs something that is not one of the five actions.
Show the hint
For each pair, ask whether anyone would ever need to look at the child row again once the parent is gone.

Three ways out of one failure

Medium
enrolment.cid is declared CHAR(2) NOT NULL REFERENCES course(cid) ON DELETE SET NULL, and deleting a course fails. Give three different single-line schema changes that each make the delete succeed, and for each one name precisely what it costs you.
Follow-up
The obvious fix is not the cheapest one. No two of the three changes cost the same thing, and only one of them keeps both the meaning of the data and the ability to delete a course. Naming precisely what that one costs is the actual work here.
Show the hint
Something has to give: the column, the action, or the parent table. Pick one at a time and write down who notices.

The self-referencing delete

Hard
One table: employee(emp_id CHAR(2) PRIMARY KEY, name TEXT, manager_id CHAR(2) REFERENCES employee(emp_id)). Six rows: E1 has no manager; E2 and E3 report to E1; E4 and E5 report to E2; E6 reports to E4. Work out what DELETE FROM employee WHERE emp_id = 'E1'; does under each of the five referential actions, writing out every surviving row and the value in its manager_id.
Follow-up
Under one of the five, deleting a single named row removes an unbounded number of rows and raises no error. Under another, whether the delete succeeds at all depends on a line of the schema you have not been shown, and you should say exactly which line and what it would have to contain.
Show the hint
Delete the root, then ask of every surviving row whether the value in its manager_id still names a row that exists. Work one level of the tree at a time.