SQL Table Constraints
A constraint is a rule the database enforces on every write. Bad data stops at the door instead of turning up in a report three weeks later.
Most SQL you write reads data and hopes it is sound. Aconstraint goes the other way: it states what the data must look like, and the database rejects anything that disagrees. The rule then holds no matter who writes to the table or which tool they use.
create or replace table order_imports (
order_id integer primary key,
customer_email varchar not null,
status varchar default 'pending',
quantity integer check (quantity > 0),
coupon_code varchar unique
)What each one rejects
| Constraint | Rejects | DuckDB's error |
|---|---|---|
NOT NULL | A missing value | NOT NULL constraint failed: order_imports.customer_email |
CHECK | A value breaking your rule | CHECK constraint failed on table order_imports |
UNIQUE | A repeat of a value | Duplicate key "coupon_code: SPRING" violates unique constraint |
PRIMARY KEY | A repeated or missing key | Duplicate key "order_id: 1" violates primary key constraint |
FOREIGN KEY | A reference to a missing row | Violates foreign key constraint because key "id: 99" does not exist |
Every one of those is a Constraint Error, raised on theINSERT or UPDATE that broke the rule. The whole statement fails; nothing partial lands.
NOT NULL and DEFAULT
NOT NULL says the column must always have a value.DEFAULT supplies one when the insert does not, and the two work well together: the column can never be empty, and callers do not have to care.
create or replace table order_imports (
order_id integer,
status varchar not null default 'pending',
loaded_at timestamp not null default current_timestamp
)A default only applies when the column is left out of the insert. Passing an explicit null is not leaving it out — that hits theNOT NULL and fails, which is usually what you want.
CHECK
CHECK takes any expression that evaluates to true or false. This is where the rules specific to your business live.
create or replace table order_imports (
quantity integer check (quantity > 0),
discount_pct decimal(5, 2) check (discount_pct between 0 and 100),
status varchar check (status in ('pending', 'shipped', 'cancelled'))
)A CHECK is documentation that cannot go stale. Six months later the table itself still says a discount is a percentage between 0 and 100.
UNIQUE and PRIMARY KEY
Both refuse duplicates. The difference is that a primary key also forbids nulls and marks the column as the identifier for a row — a table gets exactly one, and any number of unique constraints.
create or replace table order_lines (
order_id integer,
line_no integer,
product_id integer,
primary key (order_id, line_no)
)A composite key is unique across the combination, so(1, 1) and (1, 2) both fit while a second(1, 1) does not.
UNIQUE lets you insert as many nulls as you like — two rows with no coupon code are not duplicates of each other, because null is not equal to anything, including another null. If a column must be both present and distinct, it needs NOT NULL as well.
FOREIGN KEY
A foreign key says a value here must exist over there, which is what keeps orders from pointing at customers who were never created.
create or replace table customers_ref (
customer_id integer primary key,
name varchar
);
create or replace table orders_ref (
order_id integer primary key,
customer_id integer references customers_ref(customer_id)
);The reference is enforced in both directions. Inserting an order for a customer who does not exist fails, and so does deleting a customer who still has orders — DuckDB reports that the keyis still referenced by a foreign key in a different table.
DuckDB has no ON DELETE CASCADE. In PostgreSQL or MySQL you can declare that deleting a customer deletes their orders; here you delete the children yourself, in order, before the parent. Scripts ported from those engines will fail at exactly that step.
Reading the rules off a table
select constraint_type, constraint_text
from duckdb_constraints()
where table_name = 'order_imports'
order by constraint_type, constraint_textWorth running before you write to an unfamiliar table. It is faster than finding out from an error, and much faster than finding out from a report.
Constraints on a table that already exists
You can add most rules later withALTER TABLE, but only if the rows already comply — adding NOT NULL to a column containing nulls fails, and that is the constraint doing its job. Clean the data first, then add the rule.
CTAS does not copy constraints.create table copy as select * from original gives you the rows and the column types and none of the rules, so the copy will accept duplicates the original would have rejected. If the copy needs the rules, declare it and insert into it.
How much is worth enforcing
Constraints cost you on write and pay you on read. For a table people load into repeatedly, that trade is good — the rule catches the bad row on the day it arrives, next to the code that produced it. For a throwaway analysis table, skip them; you are the only writer and you will drop it this afternoon.
- Give every table people write to a primary key.
NOT NULLthe columns your queries would silently mis-count without — everyjoinkey, every amount you sum.- Turn the rules you already check in a
WHEREclause into aCHECK, so nobody has to remember them. - Add foreign keys where a broken reference would be a real incident, and accept that you will delete children first.
Open the DuckDB playground with the matching dataset and query already filled in.
Related
Declare columns, types, defaults, and constraints.
Add, rename, retype, and drop columns on a live table.
Remove tables, views, schemas, and more without breakage.
Generate ID values with nextval and column defaults.