SQL Constraints: NOT NULL, UNIQUE, CHECK, DEFAULT and Keys
On this page 12 sections ▾
- All six, in one table
- NOT NULL: this column must have a value
- PRIMARY KEY: the identity of a row
- UNIQUE: no two rows the same
- FOREIGN KEY: the value must exist over there
- CHECK: any condition you can write
- DEFAULT: fill it in when nobody says
- Adding and naming constraints
- Why in the database and not in the application?
- Practice this topic
- Frequently asked questions
- Related reading
A constraint is a rule the database refuses to break. Not a validation you hope everybody remembers to run — a rule enforced on every insert and update, from every application, every script and every person with a query window.
That distinction is the whole argument for using them. Application code can be bypassed by the next application; a constraint cannot. This page covers all six, shows the error each one produces on the engine running in your browser, and ends with which to reach for.
All six, in one table
CREATE TABLE Invoices (
invoice_id INT PRIMARY KEY,
customer_id INT NOT NULL,
amount NUMBER CHECK (amount > 0),
status STRING DEFAULT 'draft',
FOREIGN KEY (customer_id) REFERENCES Customers (customer_id)
);
INSERT INTO Invoices (invoice_id, customer_id, amount)
VALUES (1, 101, 250);
SELECT * FROM Invoices;
| invoice_id | customer_id | amount | status |
|---|---|---|---|
| 1 | 101 | 250 | draft |
● 1 row · produced by running this query on the sample database
One insert, five rules checked, and note what happened to status: it was not supplied, so the DEFAULT filled it in. Every rule in that table is now enforced for the lifetime of the table.
| Constraint | Guarantees | Allows NULL? | How many per table |
|---|---|---|---|
| NOT NULL | the column always has a value | no | any number of columns |
| UNIQUE | no two rows share the value | usually yes, once | any number |
| PRIMARY KEY | unique and not null - the row identity | no | exactly one |
| FOREIGN KEY | the value exists in another table | yes, unless also NOT NULL | any number |
| CHECK | the value satisfies a condition | yes - unknown is not a violation | any number |
| DEFAULT | a value is supplied when none is given | n/a | any number |
NOT NULL: this column must have a value
The simplest and most under-used constraint. Leave it off and every column silently accepts “unknown”, which is how a NULL ends up in an amount column and an arithmetic expression returns nothing.
CREATE TABLE Contacts (
contact_id INT PRIMARY KEY,
full_name STRING NOT NULL,
phone STRING
);
INSERT INTO Contacts VALUES (1, 'Asha Patel', NULL);
SELECT * FROM Contacts;
| contact_id | full_name | phone |
|---|---|---|
| 1 | Asha Patel | NULL |
● 1 row · produced by running this query on the sample database
The phone number is allowed to be missing; the name is not. Try inserting a row with no full_name and the engine answers Wrong NULL value in NOT NULL column full_name — the insert is rejected, not silently patched.
Default to NOT NULL and justify the exceptions
The habit worth building is making every column NOT NULL unless you can say what a missing value would mean. NULL is genuinely useful — “no manager”, “not shipped yet” — but a nullable column that never legitimately holds NULL only creates work for every query that reads it.
PRIMARY KEY: the identity of a row
A primary key is UNIQUE and NOT NULL together, and it is how every other table refers to this one:
CREATE TABLE Members (
email STRING PRIMARY KEY,
joined_on STRING NOT NULL
);
INSERT INTO Members VALUES ('asha@example.com', '2026-01-04');
SELECT * FROM Members;
| joined_on | |
|---|---|
| asha@example.com | 2026-01-04 |
● 1 row · produced by running this query on the sample database
Insert that same email twice and the engine refuses with Cannot insert record, because it already exists in primary key index. That refusal is the point: it is what stops duplicate rows from ever existing, rather than leaving you to find and delete them later.
One design note that comes up in every interview: a key made of data that can change — an email address, a phone number — means every table referencing it has to change when the data does. A meaningless id avoids that, which is why most schemas use one. The primary key vs foreign key page goes into the trade-off.
UNIQUE: no two rows the same
UNIQUE is a primary key without the identity role, and you can have as many as you like. A users table might have an id as its primary key and a unique constraint on the email as well — both are enforced, but only one identifies the row.
The difference from a primary key that catches people out: a unique column can usually hold NULL, because NULL is not equal to anything including another NULL. Most databases therefore allow one NULL (SQL Server) or several (PostgreSQL, MySQL) in a unique column, which is a genuine portability difference worth checking if it matters.
FOREIGN KEY: the value must exist over there
A foreign key says this column’s values must appear in another table — the rule that keeps a database internally consistent:
CREATE TABLE Reviews (
review_id INT PRIMARY KEY,
product_id INT NOT NULL,
stars INT CHECK (stars BETWEEN 1 AND 5),
FOREIGN KEY (product_id) REFERENCES Products (product_id)
);
INSERT INTO Reviews VALUES (1, 201, 5);
SELECT * FROM Reviews;
| review_id | product_id | stars |
|---|---|---|
| 1 | 201 | 5 |
● 1 row · produced by running this query on the sample database
Product 201 exists, so the review is accepted. Change it to 999 and the engine answers Foreign key "999" not found in table "Products". Without that line the column would accept 999 happily and you would have a review of nothing — a row that no join will ever find, which is how orphaned data accumulates.
Foreign keys also control what happens when the referenced row is deleted. ON DELETE CASCADE deletes the children with the parent; ON DELETE SET NULL orphans them deliberately; the default, RESTRICT, refuses the delete while children exist. Choosing CASCADE without thinking is how people lose more data than they meant to.
CHECK: any condition you can write
CHECK takes an arbitrary boolean expression and refuses rows that fail it:
CREATE TABLE Shipments (
shipment_id INT PRIMARY KEY,
quantity INT CHECK (quantity > 0),
ship_status STRING CHECK (ship_status IN ('packed', 'sent', 'delivered'))
);
INSERT INTO Shipments VALUES (1, 12, 'packed');
SELECT * FROM Shipments;
| shipment_id | quantity | ship_status |
|---|---|---|
| 1 | 12 | packed |
● 1 row · produced by running this query on the sample database
A negative quantity is now impossible, and so is a status nobody planned for. Try (2, -5, 'packed') and the engine answers Violation of CHECK constraint.
CHECK and NULL: unknown is not a violation
A CHECK constraint rejects a row only when the condition evaluates to false. If the column is NULL the condition is unknown, which is not false, so the row is accepted. CHECK (quantity > 0) does not stop a NULL quantity — pair it with NOT NULL when that matters.
One caveat for MySQL specifically: CHECK was parsed but silently ignored before version 8.0.16. On an older MySQL the constraint is documentation, not enforcement.
DEFAULT: fill it in when nobody says
Not a rule so much as a convenience, and it removes a whole category of NULL:
CREATE TABLE Tickets (
ticket_id INT PRIMARY KEY,
subject STRING NOT NULL,
priority STRING DEFAULT 'normal',
is_open INT DEFAULT 1
);
INSERT INTO Tickets (ticket_id, subject) VALUES (1, 'Cannot log in');
SELECT * FROM Tickets;
| ticket_id | subject | priority | is_open |
|---|---|---|---|
| 1 | Cannot log in | normal | 1 |
● 1 row · produced by running this query on the sample database
Two columns were never mentioned in the insert and both came back filled. Note that DEFAULT applies when a column is omitted, not when NULL is passed explicitly — INSERT ... VALUES (2, 'x', NULL, NULL) stores NULLs, which is a distinction that surprises people debugging an ORM.
Adding and naming constraints
Constraints can be added to an existing table with ALTER TABLE, and giving them names is worth the extra words:
ALTER TABLE Products ADD CONSTRAINT chk_price_positive CHECK (unit_price > 0);
An unnamed constraint gets a generated name like products_check1, which is what appears in the error message your users see and what you have to reference to drop it later. A name like chk_price_positive turns a violation from a puzzle into a sentence.
Why in the database and not in the application?
- There is never only one writer. The web app validates; the nightly import does not. The admin script does not. A constraint covers all of them at once.
- Race conditions. Two requests that both check “is this email taken?” and then both insert will both succeed. Only a unique constraint actually prevents it.
- They document the rules. The schema states what is true about the data, in a form that cannot drift from reality.
- The planner uses them. Knowing a column is unique or not null lets the optimiser choose better plans.
- Bad data is expensive. Rejecting one row at insert time costs nothing. Finding and repairing a million rows later costs a week.
Application-level validation is still worth having — it gives better error messages and catches mistakes before a round trip. It is a nicety on top of the guarantee, not a replacement for it.
Practice this topic
Reading explains the idea; producing it yourself is what makes it stick. These exercises use exactly what this page covers, and your answer is checked by running it against the same sample database:
- Who reports to nobody?Medium
- Every department, even the quiet onesHard
- Customers still waitingMedium
Frequently asked questions
What are the constraints in SQL?
NOT NULL, UNIQUE, PRIMARY KEY, FOREIGN KEY, CHECK and DEFAULT. NOT NULL requires a value, UNIQUE forbids repeats, PRIMARY KEY is both together and identifies the row, FOREIGN KEY requires the value to exist in another table, CHECK enforces any condition you can write, and DEFAULT supplies a value when none is given.
What is the difference between PRIMARY KEY and UNIQUE?
A primary key is unique and not null, and there is exactly one per table because it identifies the row. A unique constraint only forbids duplicates, can usually hold NULL, and a table may have as many as it needs.
Does a CHECK constraint reject NULL values?
No. A CHECK rejects a row only when its condition evaluates to false, and a condition involving NULL evaluates to unknown. CHECK (quantity > 0) therefore accepts a NULL quantity, so pair it with NOT NULL when a value is genuinely required.
Can a foreign key be NULL?
Yes, unless the column is also declared NOT NULL. A NULL foreign key means "no related row yet", which is often exactly right - an order not yet assigned to an employee, for example. A non-NULL value must exist in the referenced table.
How do I add a constraint to an existing table?
Use ALTER TABLE table ADD CONSTRAINT name CHECK (...) or the equivalent for the constraint you want. Name it explicitly: the name appears in every violation error and is what you reference to drop it later, and generated names are unhelpful.
Should constraints be in the database or in application code?
In the database. Application checks are bypassed by the next application, the nightly import and the admin console, and they cannot prevent two concurrent requests from both inserting the same value. Keep application validation for friendly error messages, but let the database hold the guarantee.
Related reading
About the author
SQL Practice
SQL Practice publishes free SQL tutorials, an in-browser playground and 42 practice exercises, built so you can run what you read. Every runnable example in our tutorials is executed against the site’s sample database before it is published. Read how we keep tutorials accurate, or report a mistake.