Lesson 12 of 12

SQL Constraints and Keys

A table without rules will accept anything: two customers with the same ID, an order for a customer who does not exist, a product with a price of minus ₹50. SQL constraints are rules you attach to columns in CREATE TABLE. The database checks them on every insert and update and refuses changes that break them. That protection works no matter which app, script or person touches the data.

The six common constraints

ConstraintRule it enforcesExample
PRIMARY KEYUniquely identifies each row; cannot repeatCustomerID INTEGER PRIMARY KEY
FOREIGN KEYValue must exist in another tableCustomerID INTEGER REFERENCES Customers(CustomerID)
NOT NULLA value is requiredCustomerName TEXT NOT NULL
UNIQUENo two rows may share the valueEmail TEXT UNIQUE
CHECKValue must pass a conditionCHECK (Price > 0)
DEFAULTValue used when none is givenStatus TEXT DEFAULT 'Pending'

Strictly speaking, DEFAULT does not reject anything, but it is defined in the same place and usually taught alongside the others.

NOT NULL, UNIQUE, CHECK and DEFAULT

The example below creates a table for a loyalty programme. Every member must have a name, each email can be registered only once, the age must be 18 or more, and new members start with 0 points.

CREATE TABLE Members (
  MemberID INTEGER PRIMARY KEY,
  FullName TEXT NOT NULL,
  Email    TEXT NOT NULL UNIQUE,
  Age      INTEGER CHECK (Age >= 18),
  Points   INTEGER NOT NULL DEFAULT 0
);

INSERT INTO Members (FullName, Email, Age)
VALUES ('Lakshmi R', '[email protected]', 34),
       ('Arun K', '[email protected]', 27);

SELECT * FROM Members;

Returns 2 rows, each with Points 0 from the DEFAULT.

Now break the rules on purpose. Change Arun’s email to [email protected] and run again: SQLite stops with UNIQUE constraint failed: Members.Email. Change his age to 16 and you get CHECK constraint failed. Replace his name with NULL and you get NOT NULL constraint failed. Reading these messages is a skill in itself – they tell you exactly which rule was broken.

Tip: MySQL has enforced CHECK constraints only since version 8.0.16; older versions accepted the syntax but silently ignored it. If you work on an older server, validate such rules in your PHP code as well.

Primary keys

A primary key is the column (or set of columns) that identifies a row. Every table should have one. It is automatically unique and, in standard SQL, cannot be NULL. (SQLite keeps an old quirk that allows NULL in a non-integer primary key, so add NOT NULL there to be safe – as in the next example.) Most tables use a meaningless auto-numbered ID, called a surrogate key, because real-world values such as phone numbers or email addresses can change.

Sometimes a pair of columns identifies a row. A warehouse stock table has one row per product per city, so the key is the combination. This is a composite primary key, written as a separate line at the end of the column list:

CREATE TABLE Stock (
  WarehouseCity TEXT NOT NULL,
  ProductID     INTEGER NOT NULL,
  Qty           INTEGER NOT NULL DEFAULT 0 CHECK (Qty >= 0),
  PRIMARY KEY (WarehouseCity, ProductID)
);

INSERT INTO Stock VALUES
  ('Chennai', 1, 120),
  ('Chennai', 2, 80),
  ('Puducherry', 1, 45);

SELECT * FROM Stock ORDER BY WarehouseCity, ProductID;

Returns 3 rows. Product 1 appears twice, but each city-product pair appears only once.

Try adding ('Chennai', 1, 10) to the insert: the pair already exists, so it is rejected. ('Madurai', 1, 10) is fine.

Foreign keys

A foreign key links a column to the primary key of another table and guarantees the link is valid. In the sample database, Orders.CustomerID references Customers(CustomerID), so an order cannot point to a customer who does not exist. SQLite can list the foreign keys of a table:

PRAGMA foreign_key_list('OrderDetails');

Returns 2 rows: ProductID references Products(ProductID) and OrderID references Orders(OrderID).

Watch out: SQLite does not enforce foreign keys unless you switch them on with PRAGMA foreign_keys = ON; – and you must do it for every new connection. Without it, the REFERENCES clauses are stored but ignored. MySQL’s default InnoDB engine enforces foreign keys automatically, but the older MyISAM engine ignores them.

What happens when a parent row is deleted?

When a customer is deleted, what should happen to their orders? You decide with an ON DELETE action:

  • RESTRICT / NO ACTION (the default) – refuse to delete the parent while children exist.
  • CASCADE – delete the child rows too.
  • SET NULL – keep the child rows but empty their foreign key column.
PRAGMA foreign_keys = ON;

CREATE TABLE Branches (
  BranchID INTEGER PRIMARY KEY,
  City     TEXT NOT NULL
);

CREATE TABLE Staff (
  StaffID  INTEGER PRIMARY KEY,
  Name     TEXT NOT NULL,
  BranchID INTEGER NOT NULL
           REFERENCES Branches(BranchID) ON DELETE CASCADE
);

INSERT INTO Branches VALUES (1, 'Puducherry'), (2, 'Madurai');
INSERT INTO Staff (Name, BranchID) VALUES
  ('Meena', 1), ('Senthil', 2), ('Kavya', 2);

DELETE FROM Branches WHERE BranchID = 2;

SELECT * FROM Staff;

Returns 1 row: Meena. Closing the Madurai branch also removed Senthil and Kavya.

Change CASCADE to RESTRICT and run again: the delete now fails with FOREIGN KEY constraint failed. Or add INSERT INTO Staff (Name, BranchID) VALUES ('Ravi', 9); – there is no branch 9, so it is refused. CASCADE is convenient, but use it only where losing the child rows is truly what you want; for orders and invoices, RESTRICT is usually safer.

SQL constraints in a real application

Constraints and app-side validation work together. Your form should check input and show friendly messages, while SQL constraints are the final safety net. In PHP with PDO, a broken constraint throws a PDOException with SQLSTATE 23000 (integrity constraint violation), which you can catch to show “This email is already registered”. See how in PHP MySQL with PDO.

This is the last lesson of the SQL track. You can now read, filter, sort, summarise, join and change data, and design tables that protect it. Go back to the SQL track page to revise any topic, or start again with SQL Introduction.

Further reading

Key takeaways

  • Constraints are rules the database enforces on every insert and update.
  • PRIMARY KEY identifies each row; a composite key uses several columns together.
  • FOREIGN KEY makes sure a value exists in another table; ON DELETE decides what happens to child rows.
  • NOT NULL, UNIQUE and CHECK reject missing, duplicate or invalid values; DEFAULT fills missing ones.
  • SQLite enforces foreign keys only after PRAGMA foreign_keys = ON; MySQL enforces CHECK from 8.0.16.

Quick quiz

Check what you learned – pick an answer.

1. Which constraint stops two members from registering the same email?

2. With ON DELETE CASCADE on Staff.BranchID, what happens when a branch is deleted?

3. Your SQLite app accepts orders for CustomerID 999, which does not exist, even though Orders has REFERENCES Customers. Why?

Practice exercise

Create a Coupons table where Code is the primary key, Discount is a required whole number between 1 and 50, and ValidTill is a required date. Insert two coupons and show them.

Show a hint

Use CHECK (Discount BETWEEN 1 AND 50) and NOT NULL.

Show the solution
CREATE TABLE Coupons (
  Code      TEXT PRIMARY KEY,
  Discount  INTEGER NOT NULL CHECK (Discount BETWEEN 1 AND 50),
  ValidTill TEXT NOT NULL
);

INSERT INTO Coupons VALUES
  ('PONGAL10', 10, '2026-01-20'),
  ('DIWALI25', 25, '2026-11-15');

SELECT * FROM Coupons;

Progress is saved in this browser only.