SQL Constraints Explained Simply: NOT NULL, UNIQUE, CHECK and DEFAULT

Your application probably checks the data people type in. The email field must not be empty, the age must be a positive number, the username must not already be taken. That’s good. But what happens when someone imports a spreadsheet directly, a second app writes to the same table, or a developer runs a quick fix by hand? Application checks don’t help then.

Constraints are rules you put on the table itself. The database checks them on every insert and update, no matter where the data comes from. They’re your last line of defence for clean data, and they cost almost nothing to add.

Diagram showing SQL constraints accepting a valid row and rejecting invalid rows

The Main Types of Constraint

ConstraintWhat it guaranteesExample
NOT NULLThe column always has a valueEvery customer must have an email
UNIQUENo two rows share the same valueTwo customers can’t use the same email
CHECKA value meets a condition you defineAge must be 0 or more
DEFAULTA value is filled in when none is givenNew accounts start as ‘active’
PRIMARY KEYUnique and not null, identifies each rowcustomer_id
FOREIGN KEYA value must exist in another tableAn order’s customer_id must be a real customer

Primary and foreign keys have their own article (Primary Keys vs. Foreign Keys), so here I’ll focus on the other four.

A Table With Constraints

CREATE TABLE customers (
    customer_id INT PRIMARY KEY,
    name        VARCHAR(100) NOT NULL,
    email       VARCHAR(150) NOT NULL UNIQUE,
    age         INT CHECK (age >= 0 AND age <= 120),
    status      VARCHAR(10) NOT NULL DEFAULT 'active',
    created_at  DATE NOT NULL DEFAULT CURRENT_DATE
);

This works as written in PostgreSQL. In MySQL (8.0.13 and later) wrap the default in brackets: DEFAULT (CURRENT_DATE). In SQL Server use DEFAULT CAST(GETDATE() AS DATE).

Every rule about this table is now written right next to the column it protects. Let’s look at each one.

NOT NULL: This Value Is Required

By default, columns accept NULL (no value). NOT NULL turns that off:

INSERT INTO customers (customer_id, name, email)
VALUES (1, 'Lina', NULL);
-- Error: column "email" cannot be NULL

My advice: make columns NOT NULL unless you have a real reason to allow missing values. Fewer nulls means fewer surprises in your queries. (Nulls behave strangely, which I explain in NULL in SQL Explained.)

UNIQUE: No Duplicates Allowed

UNIQUE stops two rows from having the same value in a column:

INSERT INTO customers (customer_id, name, email) VALUES (1, 'Sara', 'sara@mail.com');
INSERT INTO customers (customer_id, name, email) VALUES (2, 'Omar', 'sara@mail.com');
-- Error: duplicate value violates unique constraint

UNIQUE vs. PRIMARY KEY

PRIMARY KEYUNIQUE
How many per tableOneAs many as you need
Allows NULL?NeverYes (how many nulls depends on the database)
PurposeIdentify the rowPrevent duplicate business values like emails or codes

On nulls: PostgreSQL and MySQL allow many NULL values in a unique column, while SQL Server allows only one. If you need “unique when filled in” in SQL Server, use a filtered unique index.

Unique across several columns

Sometimes the combination must be unique, not each column on its own. A student can enrol in many courses, but not the same course twice:

CREATE TABLE enrollments (
    student_id INT NOT NULL,
    course_id  INT NOT NULL,
    CONSTRAINT uq_student_course UNIQUE (student_id, course_id)
);

Behind the scenes, a unique constraint is enforced with an index, so it also speeds up searches on those columns.

CHECK: Your Own Business Rules

CHECK lets you write any condition that must be true for every row:

ALTER TABLE products
ADD CONSTRAINT chk_price_positive CHECK (price > 0);

ALTER TABLE products
ADD CONSTRAINT chk_discount_range CHECK (discount BETWEEN 0 AND 50);

ALTER TABLE bookings
ADD CONSTRAINT chk_dates CHECK (check_out > check_in);

The last one compares two columns in the same row, which is perfect for date ranges. One thing to remember: a CHECK passes when the result is unknown, so CHECK (age >= 0) still accepts NULL. Add NOT NULL if the value is required.

Also note that MySQL only started enforcing CHECK constraints in version 8.0.16. Older versions accepted the syntax and silently ignored it.

DEFAULT: Sensible Starting Values

DEFAULT fills in a value when an insert doesn’t provide one:

INSERT INTO customers (customer_id, name, email)
VALUES (3, 'Huda', 'huda@mail.com');

-- status is 'active' and created_at is today's date, automatically

Defaults are great for status flags, creation dates and counters that start at zero. Strictly speaking, DEFAULT isn’t a rule that rejects data, but it’s defined in the same place and it keeps inserts short and consistent.

Name Your Constraints

If you don’t name a constraint, the database invents a name like CK__customers__age__3B75D760. That’s painful when it shows up in an error message or when you need to drop it. Give each one a clear name:

CREATE TABLE customers (
    customer_id INT,
    email       VARCHAR(150) NOT NULL,
    age         INT,
    CONSTRAINT pk_customers       PRIMARY KEY (customer_id),
    CONSTRAINT uq_customers_email UNIQUE (email),
    CONSTRAINT chk_customers_age  CHECK (age >= 0)
);

-- Removing one later
ALTER TABLE customers DROP CONSTRAINT chk_customers_age;

Adding Constraints to an Existing Table

You can add constraints later with ALTER TABLE, but the existing data has to pass the rule first. If there are already duplicate emails, adding UNIQUE fails. Find the problem rows before you try:

-- Which emails are duplicated?
SELECT email, COUNT(*)
FROM customers
GROUP BY email
HAVING COUNT(*) > 1;

Fix or merge those rows, then add the constraint.

Constraints vs. Application Checks

People sometimes ask whether they need both. I always say yes:

  • Application checks give users friendly messages straight away (“Please enter a valid email”).
  • Database constraints guarantee the rule holds for every single row, from every source, forever.

Years of working with business systems have taught me that any rule only enforced in the application will be broken eventually by an import, a script or a second app.

Conclusion

Constraints are rules the database enforces for you: NOT NULL for required values, UNIQUE for no duplicates, CHECK for your own conditions and DEFAULT for sensible starting values. Add them when you create a table, give them clear names, and you’ll spend much less time cleaning up bad data later.

If you’re designing a new database, pair this with our guides to data types and normalization.

Scroll to Top