MySQL Constraints
Constraints are rules enforced at the database level that guarantee the accuracy and consistency of data. They catch invalid data before it ever gets stored, protecting your application from the class of bugs where "impossible" data somehow ends up in the database. MySQL supports six main constraint types: NOT NULL, UNIQUE, CHECK, DEFAULT, PRIMARY KEY, and FOREIGN KEY.
Constraint Types Overview
Constraint | Purpose | Notes |
|---|---|---|
NOT NULL | Disallow NULL values | Applied per column; default is nullable |
UNIQUE | Enforce distinct values | NULLs are allowed and not compared to each other |
CHECK | Custom boolean validation | MySQL 8.0.16+ fully enforced; earlier versions parsed but ignored |
DEFAULT | Provide fallback value | Literal constant or expression (expressions: 8.0.13+) |
PRIMARY KEY | Unique non-null row identifier | One per table; creates the clustered index in InnoDB |
FOREIGN KEY | Referential integrity | InnoDB only; automatically creates an index on FK column |
NOT NULL — NULL vs Empty String
The NOT NULL constraint prevents a column from storing a NULL value. NULL means "unknown" or
"not applicable" — it is not zero, not an empty string, and not false.
This distinction matters: an empty string '' is a valid value that passes NOT NULL. If you
want to reject both NULL and empty strings, add a CHECK constraint.
CREATE TABLE users (
id INT NOT NULL AUTO_INCREMENT,
email VARCHAR(255) NOT NULL, -- required
name VARCHAR(100) NOT NULL, -- required
bio TEXT, -- optional (nullable by default)
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
-- Fails: email is NOT NULL and no default
INSERT INTO users (name) VALUES ('Alice');
-- ERROR 1364: Field 'email' doesn't have a default value
-- Succeeds: both required columns provided
INSERT INTO users (email, name) VALUES ('alice@example.com', 'Alice');
-- Also succeeds (NOT NULL allows empty string)
INSERT INTO users (email, name) VALUES ('', 'Bob'); -- empty email passes NOT NULL!-- Reject both NULL and empty strings with a CHECK constraint ALTER TABLE users ADD CONSTRAINT chk_email_nonempty CHECK (email != '');
UNIQUE — Multiple NULLs Are Allowed
The UNIQUE constraint ensures all values in a column (or combination of columns) are distinct across all rows. Unlike PRIMARY KEY, a UNIQUE column can contain NULL values — and importantly, multiple NULLs are allowed because NULL is not equal to NULL in SQL semantics.
-- Single-column UNIQUE (inline syntax) CREATE TABLE users ( id INT NOT NULL AUTO_INCREMENT, email VARCHAR(255) NOT NULL UNIQUE, PRIMARY KEY (id) ); -- Named UNIQUE constraint (preferred — readable errors, easier ALTER) CREATE TABLE users ( id INT NOT NULL AUTO_INCREMENT, email VARCHAR(255) NOT NULL, PRIMARY KEY (id), CONSTRAINT uq_user_email UNIQUE (email) ); -- Composite UNIQUE: the combination must be unique CREATE TABLE memberships ( user_id INT NOT NULL, org_id INT NOT NULL, joined_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uq_user_org (user_id, org_id) ); -- Multiple NULLs in a UNIQUE nullable column are allowed CREATE TABLE employees ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, badge VARCHAR(20) UNIQUE -- nullable UNIQUE: multiple NULLs allowed ); INSERT INTO employees (badge) VALUES (NULL); INSERT INTO employees (badge) VALUES (NULL); -- succeeds! NULLs not compared
CHECK Constraint (MySQL 8.0.16+)
The CHECK constraint allows you to define a custom boolean expression that every row must satisfy. Before MySQL 8.0.16, CHECK clauses were parsed but silently ignored. From 8.0.16 onwards they are fully enforced.
CHECK constraints can reference multiple columns in the same row, allowing cross-column validation.
CREATE TABLE products (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
discount DECIMAL(5, 2) NOT NULL DEFAULT 0,
stock INT NOT NULL DEFAULT 0,
PRIMARY KEY (id),
CONSTRAINT chk_price_positive CHECK (price > 0),
CONSTRAINT chk_discount_range CHECK (discount >= 0 AND discount <= 100),
CONSTRAINT chk_stock_nonnegative CHECK (stock >= 0),
-- Cross-column: final price after discount must be positive
CONSTRAINT chk_net_price CHECK (price * (1 - discount/100) > 0)
);
-- Violates chk_price_positive
INSERT INTO products (name, price) VALUES ('Widget', -5.00);
-- ERROR 3819: Check constraint 'chk_price_positive' is violated.
-- Cross-column CHECK: end date must not precede start date
CREATE TABLE events (
id INT NOT NULL AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
PRIMARY KEY (id),
CONSTRAINT chk_dates CHECK (end_date >= start_date)
);NOW() or RAND().CHECK Constraint Examples
-- Enum-like CHECK (alternative to ENUM type)
CREATE TABLE orders (
id INT NOT NULL AUTO_INCREMENT,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
PRIMARY KEY (id),
CONSTRAINT chk_order_status
CHECK (status IN ('pending', 'paid', 'shipped', 'delivered', 'cancelled'))
);
-- Age range (nullable column)
CREATE TABLE employees (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
age INT,
PRIMARY KEY (id),
CONSTRAINT chk_age CHECK (age IS NULL OR (age >= 16 AND age <= 120))
);
-- Percentage rate stored as decimal (0.00 to 1.00)
CREATE TABLE commissions (
id INT NOT NULL AUTO_INCREMENT,
rate DECIMAL(5, 4) NOT NULL,
PRIMARY KEY (id),
CONSTRAINT chk_rate CHECK (rate >= 0 AND rate <= 1)
);DEFAULT Values
The DEFAULT constraint specifies a value to use when a column is omitted from an INSERT statement.
MySQL 8.0.13+ allows expression defaults (like UUID() or (col1 + col2)); earlier versions
only support literal constants and a few special values like CURRENT_TIMESTAMP.
CREATE TABLE articles (
id INT NOT NULL AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'draft', -- literal default
view_count INT NOT NULL DEFAULT 0, -- numeric default
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
-- MySQL 8.0.13+: expression defaults
CREATE TABLE logs (
id INT NOT NULL AUTO_INCREMENT,
uuid CHAR(36) NOT NULL DEFAULT (UUID()), -- function call
score INT NOT NULL DEFAULT (FLOOR(RAND() * 100)), -- expression
message TEXT,
PRIMARY KEY (id)
);
-- Using DEFAULT keyword explicitly in INSERT
INSERT INTO articles (title, status) VALUES ('Hello', DEFAULT);Naming Constraints
Always name your constraints with CONSTRAINT constraint_name. Named constraints:
- Produce clearer error messages (you see the constraint name, not just a generic error code).
- Are required when you need to drop a specific constraint with ALTER TABLE.
- Make the schema self-documenting and searchable in information_schema.
CREATE TABLE order_items (
id INT NOT NULL AUTO_INCREMENT,
order_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL,
unit_price DECIMAL(10, 2) NOT NULL,
PRIMARY KEY (id),
CONSTRAINT uq_order_product UNIQUE (order_id, product_id),
CONSTRAINT chk_quantity CHECK (quantity > 0),
CONSTRAINT chk_unit_price CHECK (unit_price > 0),
CONSTRAINT fk_item_order FOREIGN KEY (order_id)
REFERENCES orders (id) ON DELETE CASCADE,
CONSTRAINT fk_item_product FOREIGN KEY (product_id)
REFERENCES products (id) ON DELETE RESTRICT
);Adding and Dropping Constraints
-- Add a NOT NULL constraint (requires MODIFY to redefine column) ALTER TABLE users MODIFY COLUMN phone VARCHAR(20) NOT NULL; -- Add a named UNIQUE constraint ALTER TABLE users ADD CONSTRAINT uq_user_phone UNIQUE (phone); -- Drop a UNIQUE constraint (uses index name) ALTER TABLE users DROP INDEX uq_user_phone; -- Add a CHECK constraint (MySQL 8.0.16+) ALTER TABLE products ADD CONSTRAINT chk_price CHECK (price > 0); -- Drop a CHECK constraint (by name) ALTER TABLE products DROP CHECK chk_price; -- Add a FOREIGN KEY ALTER TABLE products ADD CONSTRAINT fk_product_category FOREIGN KEY (category_id) REFERENCES categories (id); -- Drop a FOREIGN KEY ALTER TABLE products DROP FOREIGN KEY fk_product_category; -- Audit constraints on a table SELECT CONSTRAINT_NAME, CONSTRAINT_TYPE FROM information_schema.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'products';
Constraint Violations — Error Codes
When a constraint is violated, MySQL raises an error and rolls back the offending statement. Knowing the error codes helps you handle violations appropriately in application code.
Constraint | Error Code | SQL State | Message |
|---|---|---|---|
NOT NULL | 1048 | 23000 | Column 'col' cannot be null |
UNIQUE / PRIMARY KEY | 1062 | 23000 | Duplicate entry for key |
FOREIGN KEY (insert/update) | 1452 | 23000 | Cannot add or update a child row |
FOREIGN KEY (delete/update parent) | 1451 | 23000 | Cannot delete or update a parent row |
CHECK | 3819 | HY000 | Check constraint 'name' is violated |
-- Application-side: skip duplicate key errors silently
INSERT IGNORE INTO users (email, name) VALUES ('alice@example.com', 'Alice');
-- Use ON DUPLICATE KEY UPDATE to upsert
INSERT INTO users (email, name)
VALUES ('alice@example.com', 'Alice Updated')
ON DUPLICATE KEY UPDATE name = VALUES(name);
-- Handle violations in stored procedures
DECLARE CONTINUE HANDLER FOR SQLSTATE '23000'
BEGIN
SET @error_msg = 'Constraint violation detected';
END;Disabling Constraint Checks (Risks)
MySQL allows temporarily disabling foreign key checks, which is sometimes needed for bulk data loads or circular FK references. Use this with extreme care — it bypasses referential integrity and can silently introduce orphaned rows.
-- Disable FK checks for bulk import (restore immediately after!) SET foreign_key_checks = 0; -- ... bulk INSERT or LOAD DATA ... SET foreign_key_checks = 1; -- After re-enabling, manually verify referential integrity SELECT COUNT(*) FROM order_items oi LEFT JOIN orders o ON o.id = oi.order_id WHERE o.id IS NULL; -- orphaned order_items
foreign_key_checks silently allows orphaned rows to be inserted. Always re-enable it immediately after the operation and verify data integrity before proceeding.Constraint Audit Query
-- Full constraint audit for a schema SELECT tc.TABLE_NAME, tc.CONSTRAINT_NAME, tc.CONSTRAINT_TYPE, GROUP_CONCAT(kcu.COLUMN_NAME ORDER BY kcu.ORDINAL_POSITION) AS columns FROM information_schema.TABLE_CONSTRAINTS tc JOIN information_schema.KEY_COLUMN_USAGE kcu ON kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME AND kcu.TABLE_SCHEMA = tc.TABLE_SCHEMA AND kcu.TABLE_NAME = tc.TABLE_NAME WHERE tc.TABLE_SCHEMA = 'myapp' GROUP BY tc.TABLE_NAME, tc.CONSTRAINT_NAME, tc.CONSTRAINT_TYPE ORDER BY tc.TABLE_NAME, tc.CONSTRAINT_TYPE;