Skip to content
EL
Contents

Constraints and data integrity

NOT NULL, UNIQUE and CHECK

ConceptFoundation3 min read10 min hands-on#fundamentals#sql#data-modeling

The idea in 30 seconds

Constraints are rules the database enforces on every write, from every program, forever. NOT NULL requires a value, UNIQUE forbids duplicates, and CHECK requires any condition you can write. Each has a NULL loophole worth knowing: UNIQUE allows many NULLs by default, and CHECK passes when its condition is unknown.

Helps to know first: SQL types and the type system

On this page

Why put rules in the database

Your application probably validates input already. Constraints still matter, because the application is not the only thing that writes data: there are other services, scripts, migrations, admin consoles and the colleague who fixes something by hand at 2 a.m. A constraint applies to every write, whoever makes it.

The practice schema already has several. Each one below was triggered on shop, inside a transaction we rolled back.

NOT NULL: a value is required

INSERT INTO customers (id, name, email, joined_on) VALUES (9, NULL, 'x@example.com', '2025-10-18');
ERROR:  null value in column "name" of relation "customers" violates not-null constraint
DETAIL:  Failing row contains (9, null, x@example.com, null, 2025-10-18).

Make every column NOT NULL unless “unknown” is a genuine, meaningful state for it, as customers.city is.

UNIQUE: no duplicates

customers.email is unique, and order_items has a two-column primary key, which is unique as a pair:

INSERT INTO order_items VALUES (101, 3, 1, 18999.00), (101, 3, 1, 18999.00);
ERROR:  duplicate key value violates unique constraint "order_items_pkey"
DETAIL:  Key (order_id, product_id)=(101, 3) already exists.

CHECK: any rule you can write

CHECK takes a condition that every row must satisfy. The shop uses it for positive prices, positive quantities and a fixed list of order statuses:

INSERT INTO orders (id, customer_id, status, ordered_at) VALUES (113, 1, 'shiped', now());
ERROR:  new row for relation "orders" violates check constraint "orders_status_check"

A typo in a status is exactly the kind of bad data that is easy to write and painful to find later.

The NULL loopholes

PredictNULL against UNIQUE and CHECK

A column code text UNIQUE. You insert two rows, both with code NULL. Does the second insert fail?

A column price numeric CHECK (price > 0). You insert a row with price NULL. Does it fail?

The fixes are explicit:

  • If NULL must not be allowed, add NOT NULL. That is the only constraint that rejects NULL.
  • If at most one row may have a NULL, declare UNIQUE NULLS NOT DISTINCT:
CREATE TEMP TABLE badges2 (code text UNIQUE NULLS NOT DISTINCT);
INSERT INTO badges2 VALUES (NULL), (NULL);
ERROR:  duplicate key value violates unique constraint "badges2_code_key"
DETAIL:  Key (code)=(null) already exists.

Check yourself

  1. Why enforce a rule with a constraint when the application already validates it?

    Show answer

    The database enforces a constraint on every write from every source: other services, scripts, migrations and manual fixes, not just the code path that validates.

  2. A column has CHECK (quantity > 0). Can it contain NULL? Why?

    Show answer

    Yes. NULL > 0 evaluates to unknown, and a check constraint only rejects a row when its condition is false. Add NOT NULL to forbid NULL.

  3. How many rows with a NULL value can a UNIQUE column hold by default?

    Show answer

    Any number. By default two NULLs are not considered equal, so they do not conflict. UNIQUE NULLS NOT DISTINCT allows only one.

Finished reading?

Saved in this browser only.

Sources

Where each claim on this page comes from.

  1. 1.
    PostgreSQL documentation: 5.5 Constraints
    Primary· §5.5.1 Check Constraints

    A check constraint requires a Boolean expression; it is satisfied if the expression is true or null.

  2. 2.
    PostgreSQL documentation: 5.5 Constraints
    Primary· §5.5.2 Not-Null Constraints

    A not-null constraint requires a column to have a value.

  3. 3.
    PostgreSQL documentation: 5.5 Constraints
    Primary· §5.5.3 Unique Constraints

    By default two NULLs are not equal for a unique constraint, so several rows may have NULL; NULLS NOT DISTINCT changes that.

Go further

What to read next, and why it is worth the time.

  1. 1.
    PostgreSQL documentation: 5.5 Constraints
    Primary· §5.5.1–5.5.3

    Short, with good examples, including constraints that span several columns.

Applies to PostgreSQL 18. Draft: not yet fact-checked end to end.