Step 1 Understand it
Why you need it. Application code has bugs, and data arrives from many places. Tom could sign up twice because nothing said emails must be unique. A rule in the is checked on every insert and update, from every program, every time.
New words in this topic (tap one to see what it means)
Watch it happen
Read the animation as text
- subscribers: email is the PRIMARY KEY (unique, never NULL), and a CHECK allows only plan = 'free' or 'pro'. Every INSERT is checked against every rule.
- ('priya@mail.com', 'pro'): a new email, an allowed plan. It passes, and is stored.
- ('tom@mail.com', 'free'): passes too.
- ('tom@mail.com', 'pro') breaks the primary key: that email already exists. ON CONFLICT (email) DO NOTHING says: skip it quietly instead of failing. Tom stays on 'free'.
- Elsewhere: an order for customer 99, who does not exist, breaks the FOREIGN KEY on orders.customer_id. Refused with an error — nothing is changed.
- And a department with dept_id 1, which is taken, breaks the PRIMARY KEY on departments. Refused. The rules keep bad data out at the door, not after the fact.
Picture it
flowchart LR
W["INSERT or UPDATE"] --> N{"NOT NULL?"}
N -->|"ok"| U{"UNIQUE / PRIMARY KEY?"}
U -->|"ok"| F{"FOREIGN KEY exists?"}
F -->|"ok"| K{"CHECK true?"}
K -->|"ok"| S["row saved"]
N -->|"broken"| X["refused with an error:<br/>nothing changes"]
U -->|"broken"| X
F -->|"broken"| X
K -->|"broken"| XThe idea
: a value is required. : no two may share the value. : unique and not — the row's identity. (REFERENCES): the value must exist in another table's key, and you choose what happens when that row is deleted (refuse, , or ON DELETE SET NULL). : any condition, such as price > 0 or plan IN ('free', 'pro'). If a write breaks a rule, the whole statement fails with an error and nothing is changed.
The shape of it
email TEXT NOT NULL UNIQUE,
price INTEGER CHECK (price > 0),
dept_id INTEGER REFERENCES departments(dept_id) ON DELETE SET NULLWords in lower case are the parts you fill in.
How it works, step by step
- Every and is checked against every on the table.
- subscribers.email is the primary key, so a second row with the same email is a conflict.
ON CONFLICT (email) DO NOTHINGtells the database to skip that row quietly instead of failing.- Inserting an order for customer 99, who doesn't exist, breaks the foreign key: refused.
- Inserting a department with an existing dept_id breaks the primary key: refused.
Analogy
The rules at a passport desk. No photo, no passport (NOT NULL). One passport number per person (UNIQUE). The country you list must be a real country (FOREIGN KEY). Your age can't be negative (CHECK). The officer refuses the form; they don't "fix it later".
Where the analogy breaks
An officer might let a small mistake through. A constraint never does — which is the point, but it also means a bulk load can fail on row 9,999,999. Clean or check the data before loading, or load into a staging table first.
Worked examples
UNIQUE and CHECK, and skipping duplicates on purpose
CREATE TABLE subscribers (
email TEXT PRIMARY KEY,
plan TEXT NOT NULL CHECK (plan IN ('free', 'pro'))
);
INSERT INTO subscribers VALUES ('priya@mail.com', 'pro');
INSERT INTO subscribers VALUES ('tom@mail.com', 'free');
INSERT INTO subscribers VALUES ('tom@mail.com', 'pro')
ON CONFLICT (email) DO NOTHING;
SELECT * FROM subscribers ORDER BY email;2 rows
| plan | |
|---|---|
| priya@mail.com | pro |
| tom@mail.com | free |
Tom's second sign-up is skipped. Try it yourself with plan 'gold' and watch the CHECK refuse it.
A foreign key refuses an order for a customer who doesn't exist
INSERT INTO orders (order_id, customer_id, order_date, amount, status)
VALUES (111, 99, '2024-03-20', 10, 'pending');Error: FOREIGN KEY constraint failed
There is no customer 99, so the row is never stored.
A primary key refuses a duplicate id
INSERT INTO departments VALUES (1, 'Legal', 'York');Error: UNIQUE constraint failed: departments.dept_id
Department 1 already exists. A primary key is a UNIQUE rule plus NOT NULL.
Must-know points
- NOT NULL, UNIQUE, PRIMARY KEY, FOREIGN KEY, CHECK and are the six to know.
- PRIMARY KEY = UNIQUE + NOT NULL, one per table.
- Foreign key delete options: refuse (default), ON DELETE CASCADE, ON DELETE SET NULL.
- A failed constraint fails the whole statement; nothing is half-written.
- Put rules in the database, not only in the app — the database sees every write.
Common mistakes
- Relying on the app alone to prevent duplicates (that is how Tom signed up twice).
- Using ON DELETE CASCADE without thinking: deleting one customer silently deletes all their orders.
- Expecting UNIQUE to stop two NULLs. In most databases it doesn't — NULL isn't equal to NULL.
In other databases
- checks foreign keys only after
PRAGMA foreign_keys = ON;. ignored CHECK constraints before version 8.0.16. - syntax: and SQLite
ON CONFLICT … DO NOTHING / DO UPDATE; MySQLON DUPLICATE KEY UPDATE;MERGE.
Step 2 Go further (free, optional)
Optional. The notes above are written to be enough on their own. These free resources give a second explanation and more practice.
-
Read
short page with a try-it editor, free
-
Read
official documentation, free
-
Read
SQLite docs — Foreign Key Support
official documentation, free
Step 3 Practise
Try each one yourself first: out loud, or written and run in the practice box below, which saves every query you run. Then open the answer and compare.
-
ExplainSay what it does and when to use it, in plain words, with no notes.
Explain each of the six constraints with one example from the practice tables.
Show answer
• NOT NULL, a value is required: employees.name. Every employee must have a name.
• UNIQUE, no two rows may share the value: customers.email should have had it. It doesn't, which is exactly how Tom signed up twice.
• PRIMARY KEY, the row's identity (unique and not NULL): employees.emp_id, and in order_items the pair (order_id, product_id).
• FOREIGN KEY, the value must exist in another table: orders.customer_id must be a real customer_id in customers.
• CHECK, any rule about the row: none here yet, butprice > 0on products orstatus IN ('delivered', 'shipped', 'cancelled', 'pending')on orders would be natural.
• DEFAULT, the value used when an INSERT leaves the out: for examplestatus TEXT NOT NULL DEFAULT 'pending'on orders. -
Writefrom a blank page, then run it
Create a table with an email that must be unique and a price that must be positive, then insert one valid row and one row of each kind that fails.
Show answer
The table and the valid row are below. Each of these is refused, and the table stays unchanged:
• A second row with the same email:
INSERT INTO listings VALUES (2, 'shop@mail.com', 35);gives "UNIQUE constraint failed: listings.email".
• A price that isn't positive:INSERT INTO listings VALUES (3, 'other@mail.com', 0);gives "CHECK constraint failed: price > 0".Add either line after the INSERT and run it to see the error.
sqlCREATE TABLE listings ( listing_id INTEGER PRIMARY KEY, email TEXT NOT NULL UNIQUE, price INTEGER NOT NULL CHECK (price > 0) ); INSERT INTO listings VALUES (1, 'shop@mail.com', 20); SELECT * FROM listings;1 row
listing_id email price 1 shop@mail.com 20 -
Predictsay it before you run it
Will
UPDATE employees SET dept_id = 9 WHERE emp_id = 7;succeed? Why or why not?Show answer
No. employees.dept_id is a foreign key to departments, and there is no department 9, so the update is refused: "FOREIGN KEY constraint failed". Gus keeps his NULL dept_id.
(NULL itself is allowed in a foreign key column, meaning "no department"; a value that points at nothing is not.)
sqlUPDATE employees SET dept_id = 9 WHERE emp_id = 7;Error: FOREIGN KEY constraint failed
-
Edge casesSay what happens with NULLs, ties, duplicates or no rows at all, and fix it.
Can you insert two subscribers whose email is NULL into a table where email is UNIQUE but not the primary key? Try it.
Show answer
Yes, both go in. UNIQUE means "no two equal values", and NULL is not equal to anything, not even another NULL, so two NULLs don't clash. PostgreSQL and MySQL behave the same way.
SQL Server is the exception: it allows only one NULL in a UNIQUE column. And PostgreSQL 15+ lets you ask for that with
UNIQUE NULLS NOT DISTINCT. If an email is really required, the fix isNOT NULLas well as UNIQUE.sqlCREATE TABLE subscribers ( id INTEGER PRIMARY KEY, email TEXT UNIQUE ); INSERT INTO subscribers (id, email) VALUES (1, NULL), (2, NULL); SELECT id, email FROM subscribers ORDER BY id;2 rows
id email 1 NULL 2 NULL
Practice box: write and run your answers here
Your queries in this box
Step 4 Interview-ready, then prove it
Your interview answer
About 20 to 40 seconds, said out loud.
Constraints let the database enforce integrity on every write: NOT NULL for required values, UNIQUE and PRIMARY KEY for identity, FOREIGN KEY for valid references with a chosen delete behaviour, and CHECK for any rule on a row. I put these rules in the rather than only in application code, because the database sees every writer, and a violation fails the statement atomically instead of leaving bad data behind.
Follow-up questions
Can a UNIQUE column hold NULLs?
Yes, in most databases, and usually more than one NULL, because NULLs aren't equal to each other. (SQL Server allows only one by default.)
What does ON DELETE CASCADE do?
Deleting the parent row also deletes the child rows that reference it — handy for order_items when an order is deleted, dangerous elsewhere.
Why not just validate in the application?
Several apps, scripts and people write to the same database, and app checks can race. Constraints are checked by the database itself, atomically.
Tick each check when you can do it without notes
A topic is finished when you pass all four checks, not when you have read it. Be strict: if you had to look at the notes, it is not ticked yet.
- Explain Say what it does and when to use it, in plain words, with no notes.
- Write Write the query from a blank page, then run it to check.
- Predict Read a query and say which rows it returns before you run it.
- Edge cases Say what happens with NULLs, ties, duplicates or no rows at all, and fix it.
0 of 4 checks ticked.
Saved in this browser. Back it up or move it to another device.