AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

12 Schema design / Lesson 2 of 6

Constraints — the rules a table enforces

15 min · +10 XP · 2 exercises · 5 questions

Read the text15 min
Exercises0/2
Quiz5 q

Quick answer

What are constraints in SQL?

Constraints are rules declared on a table that the database enforces on every insert and update: PRIMARY KEY (unique and not null, one per table), UNIQUE (no duplicates), NOT NULL (a value is required), DEFAULT (the value when none is given), CHECK (a condition every row must meet) and FOREIGN KEY (the value must exist in another table). A statement that would break one is rejected.

A data type stops a date from landing in an integer column. Constraints go further: they state rules about values and rows — this column is required, no two rows share this email, a quantity is never negative, every order points at a real customer — and the database rejects any statement that would break them. Rules enforced in the schema hold for every application, script and late-night manual fix that touches the table, which rules in application code cannot promise. This lesson covers the six constraints, the primary key vs unique key question, composite keys, naming, and what happens when a rule is broken.

NOT NULL and DEFAULT

CREATE TABLE products (
  id       INT AUTO_INCREMENT PRIMARY KEY,
  name     VARCHAR(80)  NOT NULL,
  price    DECIMAL(8,2) NOT NULL,
  in_stock INT          NOT NULL DEFAULT 0,
  notes    TEXT
);

NOT NULL makes a value required. Make it the default habit: a column should allow NULL only when "unknown" or "not applicable" is a real state for it, because every nullable column brings three-valued logic into every query that touches it. DEFAULT supplies the value when an INSERT omits the column (remember from the INSERT lesson that an explicit NULL is not "omitted"). NOT NULL DEFAULT 0 together give a column that is always filled without every INSERT having to mention it.

PRIMARY KEY

The primary key identifies each row. It is unique and NOT NULL by definition, and a table has one. In InnoDB it is also the clustered index: rows are stored in primary-key order, and every secondary index stores the primary key to find the row. That is why a short, never-changing, ever-increasing key (an AUTO_INCREMENT integer) suits InnoDB, and a random UUID string as primary key spreads inserts across the whole table.

A key on one column is written inline; on several columns it is a table constraint:

CREATE TABLE enrolments (
  student_id  INT  NOT NULL,
  course_id   INT  NOT NULL,
  enrolled_on DATE NOT NULL,
  PRIMARY KEY (student_id, course_id)
);

This composite key says that the pair is unique: a student can take many courses and a course has many students, but the same student cannot be enrolled in the same course twice. The order of the columns matters for the index (it can serve lookups by student_id alone, not by course_id alone — the indexes module explains why).

UNIQUE

CREATE TABLE users (
  id       INT AUTO_INCREMENT PRIMARY KEY,
  email    VARCHAR(255) NOT NULL UNIQUE,
  username VARCHAR(30)  NOT NULL,
  phone    VARCHAR(20),
  CONSTRAINT uq_users_username UNIQUE (username),
  CONSTRAINT uq_users_phone UNIQUE (phone)
);

UNIQUE forbids two rows with the same value (or the same combination, for UNIQUE (a, b)). Unlike the primary key, a table can have many, and a unique column may be NULL — and since NULL is not equal to NULL, MySQL and PostgreSQL allow any number of NULLs in a unique column (SQL Server allows one). So phone above may be missing for many users but never shared.

Primary key vs unique key — the interview answer in one breath: both reject duplicates and both build an index; a table has exactly one primary key but any number of unique keys; the primary key can never be NULL, a unique key can hold NULLs; in InnoDB the primary key is the clustered index that orders the table, a unique key is a secondary index; foreign keys normally reference the primary key.

Natural and surrogate keys

An email address, an ISBN or a vehicle number is a natural key: the real world already guarantees it is unique. An AUTO_INCREMENT id is a surrogate key: meaningless, but stable. Natural keys change (people change email), can be long (every foreign key repeats them) and are sometimes less unique than promised. The common design is a surrogate primary key plus a UNIQUE constraint on the natural key — the database still refuses duplicates, and references stay small and permanent.

CHECK

CREATE TABLE order_lines (
  order_id INT NOT NULL,
  line_no  INT NOT NULL,
  qty      INT NOT NULL CHECK (qty > 0),
  discount DECIMAL(4,2) NOT NULL DEFAULT 0,
  PRIMARY KEY (order_id, line_no),
  CONSTRAINT chk_discount CHECK (discount BETWEEN 0 AND 0.5)
);

A CHECK constraint is a condition every row must satisfy. MySQL enforces them only since 8.0.16; earlier versions accepted the syntax and ignored it, a fact that still trips teams on old servers. A check that evaluates to NULL passes — CHECK (qty > 0) lets a NULL qty through — so pair it with NOT NULL for required columns. Checks can compare columns of the same row (CHECK (ends_on >= starts_on)) but cannot query other tables.

FOREIGN KEY

CREATE TABLE orders (
  id          INT AUTO_INCREMENT PRIMARY KEY,
  customer_id INT NOT NULL,
  placed_on   DATE NOT NULL,
  CONSTRAINT fk_orders_customer
    FOREIGN KEY (customer_id) REFERENCES customers (id)
    ON DELETE RESTRICT ON UPDATE CASCADE
);

A foreign key says every customer_id must exist as a customers.id: InnoDB refuses an order for a missing customer and refuses deleting a customer who still has orders (or cascades, per the ON DELETE action). It is the subject of the relationships lesson. InnoDB requires an index on the referencing column and creates one if needed. (The practice engine here accepts foreign-key declarations but does not enforce them, so exercises never depend on a refusal.)

Naming constraints and what a violation looks like

CONSTRAINT name before a constraint gives it a name you choose (uq_users_email, chk_discount, fk_orders_customer); unnamed ones get generated names. Named constraints make error messages readable and can be dropped by name in a migration (ALTER TABLE users DROP INDEX uq_users_phone, ALTER TABLE order_lines DROP CHECK chk_discount).

A statement that breaks a constraint fails with an error and changes nothing:

ERROR 1062 (23000): Duplicate entry 'ada@example.com' for key 'users.email'
ERROR 1048 (23000): Column 'name' cannot be null
ERROR 3819 (HY000): Check constraint 'chk_discount' is violated.
ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails

Applications catch these — a duplicate-key error on signup becomes "that email is already registered" — rather than checking first and racing. And INSERT IGNORE turns duplicate and check violations into skipped rows, which is a quick way to load data through the table's rules.

Key takeaways

  • NOT NULL by default; DEFAULT fills omitted columns.
  • One PRIMARY KEY per table: unique, never NULL, the clustered index in InnoDB; composite keys are declared at table level.
  • UNIQUE: many per table, NULLs allowed (many of them in MySQL).
  • Surrogate primary key plus UNIQUE natural key is the common design.
  • CHECK is enforced from MySQL 8.0.16; NULL passes a check.
  • FOREIGN KEY ... REFERENCES keeps references valid; name constraints so errors and migrations read well.

Common questions

What is the difference between a primary key and a unique key?

A table has exactly one primary key, it can never be NULL, and in InnoDB it is the clustered index that physically orders the rows. A table can have many unique keys, and a unique column may contain NULL, in MySQL any number of NULLs, because NULL is not equal to NULL. Both reject duplicate values and both create an index.

What is a composite primary key?

A primary key made of two or more columns, declared at table level as PRIMARY KEY (student_id, course_id). The combination must be unique, while each column on its own may repeat. It is the natural key of a junction table, where it also stops the same pair being stored twice.

Does MySQL enforce CHECK constraints?

Yes, since MySQL 8.0.16. Earlier versions parsed CHECK clauses and silently ignored them, so a schema copied from an old server may contain checks that were never enforced. A CHECK condition that evaluates to NULL passes, so pair it with NOT NULL when the column is required.

What is the difference between a natural key and a surrogate key?

A natural key is a real-world value that already identifies the row, such as an ISBN or an email address. A surrogate key is an artificial identifier with no meaning, usually an AUTO_INCREMENT integer. Surrogates never change and keep foreign keys small, so most schemas use one as the primary key and put a UNIQUE constraint on the natural key.

Mark as readSign in to keep your place.

SQL Constraints: Primary Key vs Unique Key, NOT NULL and CHECK

Constraints make the database refuse bad data: PRIMARY KEY, UNIQUE, NOT NULL, DEFAULT, CHECK and FOREIGN KEY in MySQL, primary vs unique key, composite keys.

  • Course: SQL study plan
  • Module: Schema design
  • Kind: Lesson
  • Reading time: 15 min
  • Runtime: MySQL 8 syntax on SQLite 3.49

What are constraints in SQL?

Constraints are rules declared on a table that the database enforces on every insert and update: PRIMARY KEY (unique and not null, one per table), UNIQUE (no duplicates), NOT NULL (a value is required), DEFAULT (the value when none is given), CHECK (a condition every row must meet) and FOREIGN KEY (the value must exist in another table). A statement that would break one is rejected.

Lesson

A data type stops a date from landing in an integer column. Constraints go further: they state rules about values and rows — this column is required, no two rows share this email, a quantity is never negative, every order points at a real customer — and the database rejects any statement that would break them. Rules enforced in the schema hold for every application, script and late-night manual fix that touches the table, which rules in application code cannot promise. This lesson covers the six constraints, the primary key vs unique key question, composite keys, naming, and what happens when a rule is broken.

NOT NULL and DEFAULT

CREATE TABLE products (
  id       INT AUTO_INCREMENT PRIMARY KEY,
  name     VARCHAR(80)  NOT NULL,
  price    DECIMAL(8,2) NOT NULL,
  in_stock INT          NOT NULL DEFAULT 0,
  notes    TEXT
);

NOT NULL makes a value required. Make it the default habit: a column should allow NULL only when "unknown" or "not applicable" is a real state for it, because every nullable column brings three-valued logic into every query that touches it. DEFAULT supplies the value when an INSERT omits the column (remember from the INSERT lesson that an explicit NULL is not "omitted"). NOT NULL DEFAULT 0 together give a column that is always filled without every INSERT having to mention it.

PRIMARY KEY

The primary key identifies each row. It is unique and NOT NULL by definition, and a table has one. In InnoDB it is also the clustered index: rows are stored in primary-key order, and every secondary index stores the primary key to find the row. That is why a short, never-changing, ever-increasing key (an AUTO_INCREMENT integer) suits InnoDB, and a random UUID string as primary key spreads inserts across the whole table.

A key on one column is written inline; on several columns it is a table constraint:

CREATE TABLE enrolments (
  student_id  INT  NOT NULL,
  course_id   INT  NOT NULL,
  enrolled_on DATE NOT NULL,
  PRIMARY KEY (student_id, course_id)
);

This composite key says that the pair is unique: a student can take many courses and a course has many students, but the same student cannot be enrolled in the same course twice. The order of the columns matters for the index (it can serve lookups by student_id alone, not by course_id alone — the indexes module explains why).

UNIQUE

CREATE TABLE users (
  id       INT AUTO_INCREMENT PRIMARY KEY,
  email    VARCHAR(255) NOT NULL UNIQUE,
  username VARCHAR(30)  NOT NULL,
  phone    VARCHAR(20),
  CONSTRAINT uq_users_username UNIQUE (username),
  CONSTRAINT uq_users_phone UNIQUE (phone)
);

UNIQUE forbids two rows with the same value (or the same combination, for UNIQUE (a, b)). Unlike the primary key, a table can have many, and a unique column may be NULL — and since NULL is not equal to NULL, MySQL and PostgreSQL allow any number of NULLs in a unique column (SQL Server allows one). So phone above may be missing for many users but never shared.

Primary key vs unique key — the interview answer in one breath: both reject duplicates and both build an index; a table has exactly one primary key but any number of unique keys; the primary key can never be NULL, a unique key can hold NULLs; in InnoDB the primary key is the clustered index that orders the table, a unique key is a secondary index; foreign keys normally reference the primary key.

Natural and surrogate keys

An email address, an ISBN or a vehicle number is a natural key: the real world already guarantees it is unique. An AUTO_INCREMENT id is a surrogate key: meaningless, but stable. Natural keys change (people change email), can be long (every foreign key repeats them) and are sometimes less unique than promised. The common design is a surrogate primary key plus a UNIQUE constraint on the natural key — the database still refuses duplicates, and references stay small and permanent.

CHECK

CREATE TABLE order_lines (
  order_id INT NOT NULL,
  line_no  INT NOT NULL,
  qty      INT NOT NULL CHECK (qty > 0),
  discount DECIMAL(4,2) NOT NULL DEFAULT 0,
  PRIMARY KEY (order_id, line_no),
  CONSTRAINT chk_discount CHECK (discount BETWEEN 0 AND 0.5)
);

A CHECK constraint is a condition every row must satisfy. MySQL enforces them only since 8.0.16; earlier versions accepted the syntax and ignored it, a fact that still trips teams on old servers. A check that evaluates to NULL passes — CHECK (qty > 0) lets a NULL qty through — so pair it with NOT NULL for required columns. Checks can compare columns of the same row (CHECK (ends_on >= starts_on)) but cannot query other tables.

FOREIGN KEY

CREATE TABLE orders (
  id          INT AUTO_INCREMENT PRIMARY KEY,
  customer_id INT NOT NULL,
  placed_on   DATE NOT NULL,
  CONSTRAINT fk_orders_customer
    FOREIGN KEY (customer_id) REFERENCES customers (id)
    ON DELETE RESTRICT ON UPDATE CASCADE
);

A foreign key says every customer_id must exist as a customers.id: InnoDB refuses an order for a missing customer and refuses deleting a customer who still has orders (or cascades, per the ON DELETE action). It is the subject of the relationships lesson. InnoDB requires an index on the referencing column and creates one if needed. (The practice engine here accepts foreign-key declarations but does not enforce them, so exercises never depend on a refusal.)

Naming constraints and what a violation looks like

CONSTRAINT name before a constraint gives it a name you choose (uq_users_email, chk_discount, fk_orders_customer); unnamed ones get generated names. Named constraints make error messages readable and can be dropped by name in a migration (ALTER TABLE users DROP INDEX uq_users_phone, ALTER TABLE order_lines DROP CHECK chk_discount).

A statement that breaks a constraint fails with an error and changes nothing:

ERROR 1062 (23000): Duplicate entry 'ada@example.com' for key 'users.email'
ERROR 1048 (23000): Column 'name' cannot be null
ERROR 3819 (HY000): Check constraint 'chk_discount' is violated.
ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails

Applications catch these — a duplicate-key error on signup becomes "that email is already registered" — rather than checking first and racing. And INSERT IGNORE turns duplicate and check violations into skipped rows, which is a quick way to load data through the table's rules.

Key takeaways

  • NOT NULL by default; DEFAULT fills omitted columns.
  • One PRIMARY KEY per table: unique, never NULL, the clustered index in InnoDB; composite keys are declared at table level.
  • UNIQUE: many per table, NULLs allowed (many of them in MySQL).
  • Surrogate primary key plus UNIQUE natural key is the common design.
  • CHECK is enforced from MySQL 8.0.16; NULL passes a check.
  • FOREIGN KEY ... REFERENCES keeps references valid; name constraints so errors and migrations read well.

Common questions

What is the difference between a primary key and a unique key?

A table has exactly one primary key, it can never be NULL, and in InnoDB it is the clustered index that physically orders the rows. A table can have many unique keys, and a unique column may contain NULL, in MySQL any number of NULLs, because NULL is not equal to NULL. Both reject duplicate values and both create an index.

What is a composite primary key?

A primary key made of two or more columns, declared at table level as PRIMARY KEY (student_id, course_id). The combination must be unique, while each column on its own may repeat. It is the natural key of a junction table, where it also stops the same pair being stored twice.

Does MySQL enforce CHECK constraints?

Yes, since MySQL 8.0.16. Earlier versions parsed CHECK clauses and silently ignored them, so a schema copied from an old server may contain checks that were never enforced. A CHECK condition that evaluates to NULL passes, so pair it with NOT NULL when the column is required.

What is the difference between a natural key and a surrogate key?

A natural key is a real-world value that already identifies the row, such as an ISBN or an email address. A surrogate key is an artificial identifier with no meaning, usually an AUTO_INCREMENT integer. Surrogates never change and keep foreign keys small, so most schemas use one as the primary key and put a UNIQUE constraint on the natural key.

Exercises

A composite key keeps enrolments unique

Create the enrolments table so that a student can be enrolled in many courses and a course can have many students, but the same student–course pair can be stored only once:

  • student_id integer, required;
  • course_id integer, required;
  • enrolled_on date, required;
  • a composite primary key on (student_id, course_id).

The starter then loads enrolment_requests with INSERT IGNORE in request order — a repeated pair is skipped by your key, so the earliest request wins — and prints student_id, course_id, enrolled_on ordered by student_id, course_id. Leave those statements as they are.

enrolment_requests(id, student_id, course_id, requested_on):

idstudent_idcourse_idrequested_on
11102024-01-05
21112024-01-06
32102024-01-06
41102024-01-09
53122024-01-10
62102024-01-11
student_id | course_id | enrolled_on
1 | 10 | 2024-01-05
1 | 11 | 2024-01-06
2 | 10 | 2024-01-06
3 | 12 | 2024-01-10

Let UNIQUE and CHECK filter an import

Create the accounts table so its own rules reject the bad rows of an import:

  • id integer primary key;
  • email up to 100 characters, required, unique;
  • age integer, required, with a CHECK that it is at least 13.

The starter then copies raw_accounts with INSERT IGNORE in id order — rows that break your UNIQUE or CHECK are skipped, so for a repeated email the lowest id wins — and prints id, email, age ordered by id. Leave those statements as they are.

raw_accounts(id, email, age): (1, ana@x.io, 20), (2, bo@x.io, 12), (3, ana@x.io, 30), (4, cy@x.io, 13), (5, di@x.io, 45).

id | email | age
1 | ana@x.io | 20
4 | cy@x.io | 13
5 | di@x.io | 45

In this module: Schema design

← CREATE TABLE and data types · ALTER TABLE and DROP — changing a schema →