Himanshu SharmaSoftware engineer
Let’s talk

MySQL · Lesson 32 of 50

NOT NULL, UNIQUE, DEFAULT and CHECK

Protect data quality with column constraints. Use NOT NULL, UNIQUE, DEFAULT and CHECK, name them, read the error messages, and know what each one guarantees.

14 minBuilding databasesUpdated 22 October 2026

Awaiting technical review. This lesson is readable but not yet listed in search engines.

By the end of this lessonAdd NOT NULL, UNIQUE, DEFAULT and CHECK constraints to tables and explain the errors they raise.

Let the database enforce the rules

Your application can validate input, but an application is not the only thing that writes to a database. Scripts, other services and people with a SQL client all do. Constraints are rules attached to the table itself, and MySQL checks them on every insert and update, so the data stays correct whatever sent it.

NOT NULL

A column marked NOT NULL must have a value. If it has no default, leaving it out of an insert is an error. Use it for everything that is genuinely required:

sql
CREATE DATABASE IF NOT EXISTS practice;
USE practice;
DROP TABLE IF EXISTS accounts;
CREATE TABLE accounts (
  id       INT PRIMARY KEY AUTO_INCREMENT,
  username VARCHAR(30) NOT NULL,
  email    VARCHAR(100) NOT NULL,
  nickname VARCHAR(30)
);
sql
INSERT INTO accounts (username) VALUES ('ria');
result
ERROR 1364 (HY000): Field 'email' doesn't have a default value

Fields that really may be unknown (here nickname) are left nullable. Many bugs begin with columns that allow NULL without a good reason, so make NOT NULL your default choice.

UNIQUE

UNIQUE forbids two rows from having the same value in the column (or combination of columns). Add it to natural identifiers such as email addresses:

sql
ALTER TABLE accounts ADD CONSTRAINT uq_accounts_email UNIQUE (email);
INSERT INTO accounts (username, email) VALUES ('ria', 'ria@example.com');
sql
INSERT INTO accounts (username, email) VALUES ('ria2', 'ria@example.com');
result
ERROR 1062 (23000): Duplicate entry 'ria@example.com' for key 'accounts.uq_accounts_email'

The error names the constraint, which is why a good name pays off. A unique constraint also creates an index, so searching by that column is fast.

Unique over several columns means the combination must be unique. For example UNIQUE (username, email) allows the same username with different emails. A UNIQUE column that also allows NULL may contain many NULLs, because NULL is never equal to NULL.

DEFAULT

DEFAULT supplies a value when an insert leaves the column out. It can be a constant or, from MySQL 8.0.13, an expression in parentheses. Timestamps are the classic use:

sql
DROP TABLE IF EXISTS tickets;
CREATE TABLE tickets (
  id         INT PRIMARY KEY AUTO_INCREMENT,
  title      VARCHAR(100) NOT NULL,
  status     VARCHAR(20) NOT NULL DEFAULT 'open',
  priority   TINYINT NOT NULL DEFAULT 3,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO tickets (title) VALUES ('Printer is broken');
INSERT INTO tickets (title, status) VALUES ('Reset password', 'closed');
SELECT id, title, status, priority FROM tickets;
result
+----+-------------------+--------+----------+
| id | title             | status | priority |
+----+-------------------+--------+----------+
|  1 | Printer is broken | open   |        3 |
|  2 | Reset password    | closed |        3 |
+----+-------------------+--------+----------+

The created_at value is filled by the server's clock, which is why it is left out of the result above. You can write DEFAULT explicitly in an insert to request the default for a column: VALUES ('x', DEFAULT).

CHECK

CHECK enforces a condition on the row. It is accepted syntax in all versions but actually enforced from MySQL 8.0.16; older versions parsed and ignored it.

sql
DROP TABLE IF EXISTS items;
CREATE TABLE items (
  id    INT PRIMARY KEY AUTO_INCREMENT,
  name  VARCHAR(50) NOT NULL,
  price DECIMAL(8,2) NOT NULL,
  stock INT NOT NULL DEFAULT 0,
  CONSTRAINT chk_items_price CHECK (price >= 0),
  CONSTRAINT chk_items_stock CHECK (stock >= 0)
);
INSERT INTO items (name, price, stock) VALUES ('pen', 1.50, 100);
sql
INSERT INTO items (name, price) VALUES ('free sample', -5.00);
result
ERROR 3819 (HY000): Check constraint 'chk_items_price' is violated.
sql
UPDATE items SET stock = stock - 500 WHERE name = 'pen';
result
ERROR 3819 (HY000): Check constraint 'chk_items_stock' is violated.

The check also protects updates, so stock cannot be driven below zero. A check can compare columns in the same row, for example CHECK (end_date >= start_date). It cannot look at other rows or other tables.

Seeing the constraints

sql
SHOW CREATE TABLE items\G
result
*************************** 1. row ***************************
       Table: items
Create Table: CREATE TABLE `items` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  `price` decimal(8,2) NOT NULL,
  `stock` int NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  CONSTRAINT `chk_items_price` CHECK ((`price` >= 0)),
  CONSTRAINT `chk_items_stock` CHECK ((`stock` >= 0))
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

Constraints are part of the table definition. To change one later, use ALTER TABLE items DROP CHECK chk_items_price; (and DROP INDEX for a unique constraint), which the alter lesson covers.

Constraints and existing data

Adding a constraint to a table that already holds data will fail if some rows violate it. Fix or remove the bad rows first. Find them with a SELECT that uses the opposite condition, such as SELECT * FROM items WHERE price < 0.

Common mistakes

  • Leaving columns nullable out of habit.
  • Relying on application code alone for uniqueness. Two requests at once can both pass an application check. Only a UNIQUE constraint is race-proof.
  • Assuming CHECK works on an old MySQL (before 8.0.16).
  • Using DEFAULT to hide missing data, such as defaulting a price to 0.
  • Forgetting that UNIQUE allows multiple NULLs.

Exercise

Create a table coupons with an auto-increment id, a required unique code (up to 20 characters), a percent_off integer that must be between 1 and 90, and an active flag that defaults to true. Show that an invalid percentage and a duplicate code are rejected.

Show one possible answer
sql
DROP TABLE IF EXISTS coupons;
CREATE TABLE coupons (
  id          INT PRIMARY KEY AUTO_INCREMENT,
  code        VARCHAR(20) NOT NULL UNIQUE,
  percent_off TINYINT NOT NULL,
  active      BOOLEAN NOT NULL DEFAULT TRUE,
  CONSTRAINT chk_coupons_percent CHECK (percent_off BETWEEN 1 AND 90)
);
INSERT INTO coupons (code, percent_off) VALUES ('WELCOME10', 10);
sql
INSERT INTO coupons (code, percent_off) VALUES ('BIG', 95);
result
ERROR 3819 (HY000): Check constraint 'chk_coupons_percent' is violated.
sql
INSERT INTO coupons (code, percent_off) VALUES ('WELCOME10', 20);
result
ERROR 1062 (23000): Duplicate entry 'WELCOME10' for key 'coupons.code'

Key points

  • Constraints are rules the database enforces on every write, so bad data is rejected no matter which application sent it.
  • NOT NULL requires a value, UNIQUE forbids duplicates (but allows several NULLs), DEFAULT supplies a value when none is given.
  • CHECK (condition) rejects rows that make the condition false. It is enforced from MySQL 8.0.16.
  • Give important constraints a name with CONSTRAINT name ..., so error messages and later changes are easy to read.

Check yourself

1. A column is UNIQUE and allows NULL. How many rows can hold NULL in it?
2. What does DEFAULT 'pending' do?
3. Which constraint ensures price is never negative?
4. Why name a constraint?