Post

SQL Constraints Cheatsheet

A comprehensive cheatsheet covering all SQL constraint types, including PRIMARY KEY, FOREIGN KEY, UNIQUE, NOT NULL, CHECK, DEFAULT, AUTO_INCREMENT, and more. Includes inline and table-level syntax, add/drop/modify examples, cross-engine compatibility, naming best practices, and common patterns.

SQL Constraints Cheatsheet

πŸ”’ SQL Constraints Cheatsheet

All constraint types Β· Inline & table-level syntax Β· Add/Drop/Modify Β· Cross-engine compatibility


Legend

| Symbol | Meaning | |——–|β€”β€”β€”| | βœ… | Supported (same or very similar syntax) | | ⚠️ | Supported with different syntax or behavior | | ❌ | Not supported / no direct equivalent |


IN Article

Here’s your SQL Constraints Cheatsheet! It covers 14 sections:

#SectionWhat’s Inside
1PRIMARY KEYInline, table-level, composite PKs
2FOREIGN KEYReferential actions (CASCADE, SET NULL, RESTRICT…), composite FK, disable/enable
3UNIQUEInline, named, composite, NULL behavior per engine
4NOT NULLColumn defaults, adding/removing NOT NULL
5CHECKInline, named, multi-column, expression checks
6DEFAULTLiterals, timestamps, expressions, ON UPDATE
7AUTO_INCREMENT / IdentityFull syntax comparison across all 6 engines
8Adding ConstraintsALTER TABLE ADD for every constraint type
9Dropping ConstraintsDROP PK, FK, UNIQUE, CHECK, DEFAULT, NOT NULL
10Inspecting ConstraintsQuery system tables in MySQL, PostgreSQL, SQL Server, Oracle, SQLite
11Deferrable ConstraintsDEFERRED, NOCHECK, DISABLE/ENABLE
12Naming Best Practicespk_, fk_, uq_, chk_, df_ conventions + full example
13Cross-Engine Master TableAll constraints Γ— all engines at a glance
14Common PatternsCascade delete, CHECK validation, soft delete, tree/hierarchy, SQLite workaround

Two particularly useful gotchas are highlighted: SQLite silently ignores FKs unless you enable them with a PRAGMA, and MySQL < 8.0.16 silently ignores CHECK constraints.


Categories

  1. PRIMARY KEY
  2. FOREIGN KEY
  3. UNIQUE
  4. NOT NULL
  5. CHECK
  6. DEFAULT
  7. AUTO_INCREMENT / Identity
  8. Adding Constraints to Existing Tables
  9. Dropping Constraints
  10. Viewing & Inspecting Constraints
  11. Deferrable Constraints
  12. Constraint Naming Best Practices
  13. Cross-Engine Master Table
  14. Common Patterns & Examples

1. PRIMARY KEY

Uniquely identifies each row. Cannot be NULL. Only one per table.

Inline (single column)

1
2
3
4
CREATE TABLE employees (
  id    INT         NOT NULL AUTO_INCREMENT PRIMARY KEY,
  name  VARCHAR(100) NOT NULL
);
1
2
3
4
5
CREATE TABLE employees (
  id    INT          NOT NULL AUTO_INCREMENT,
  name  VARCHAR(100) NOT NULL,
  CONSTRAINT pk_employees PRIMARY KEY (id)
);

Composite Primary Key (multiple columns)

1
2
3
4
5
6
CREATE TABLE order_items (
  order_id    INT  NOT NULL,
  product_id  INT  NOT NULL,
  qty         INT  NOT NULL DEFAULT 1,
  CONSTRAINT pk_order_items PRIMARY KEY (order_id, product_id)
);

Cross-Engine β€” PRIMARY KEY

FeatureMySQLSQLitePostgreSQLSQL ServerOracleMS Access
Single-column PKβœ…βœ…βœ…βœ…βœ…βœ…
Composite PKβœ…βœ…βœ…βœ…βœ…βœ…
Named PK constraintβœ…βš οΈ ignored internallyβœ…βœ…βœ…βŒ
PK auto-creates indexβœ…βœ…βœ…βœ…βœ…βœ…

2. FOREIGN KEY

Enforces referential integrity between two tables.

Basic Foreign Key

1
2
3
4
5
6
7
CREATE TABLE orders (
  id          INT  NOT NULL AUTO_INCREMENT,
  customer_id INT  NOT NULL,
  CONSTRAINT pk_orders     PRIMARY KEY (id),
  CONSTRAINT fk_orders_cust FOREIGN KEY (customer_id)
    REFERENCES customers(id)
);

With Referential Actions

1
2
3
4
5
6
7
8
9
CREATE TABLE orders (
  id          INT  NOT NULL AUTO_INCREMENT,
  customer_id INT,
  CONSTRAINT pk_orders      PRIMARY KEY (id),
  CONSTRAINT fk_orders_cust FOREIGN KEY (customer_id)
    REFERENCES customers(id)
    ON DELETE SET NULL      -- set to NULL if parent deleted
    ON UPDATE CASCADE       -- follow parent PK if it changes
);

ON DELETE / ON UPDATE Actions

ActionBehavior
CASCADEDelete/update child rows automatically when parent changes
SET NULLSet FK column to NULL when parent is deleted/updated
SET DEFAULTSet FK column to its DEFAULT value
RESTRICTPrevent parent delete/update if children exist (checked immediately)
NO ACTIONSame as RESTRICT but checked at end of transaction (default)

Composite Foreign Key

1
2
3
4
5
6
7
8
9
CREATE TABLE shipment_items (
  shipment_id INT NOT NULL,
  product_id  INT NOT NULL,
  qty         INT NOT NULL,
  CONSTRAINT fk_shipment_item
    FOREIGN KEY (shipment_id, product_id)
    REFERENCES order_items (order_id, product_id)
    ON DELETE CASCADE
);

Disable / Enable FK Checks (MySQL)

1
2
3
SET FOREIGN_KEY_CHECKS = 0;   -- disable (e.g., during bulk load)
-- ... load data ...
SET FOREIGN_KEY_CHECKS = 1;   -- re-enable

Cross-Engine β€” FOREIGN KEY

FeatureMySQLSQLitePostgreSQLSQL ServerOracleMS Access
Basic FKβœ… InnoDB only⚠️ parsed but NOT enforced by defaultβœ…βœ…βœ…βœ…
ON DELETE CASCADEβœ…βš οΈ PRAGMA foreign_keys = ON requiredβœ…βœ…βœ…βœ…
ON DELETE SET NULLβœ…βœ… (with pragma)βœ…βœ…βœ…βœ…
ON DELETE SET DEFAULTβŒβœ… (with pragma)βœ…βœ…βŒβŒ
ON UPDATE CASCADEβœ…βœ… (with pragma)βœ…βœ…βŒβœ…
Composite FKβœ…βœ…βœ…βœ…βœ…βœ…
Disable FK checksβœ… SET FOREIGN_KEY_CHECKS=0⚠️ PRAGMA foreign_keys=OFF⚠️ SET session_replication_role='replica'⚠️ ALTER TABLE NOCHECK CONSTRAINT ALL⚠️ DISABLE CONSTRAINT❌

⚠️ SQLite: Foreign keys are parsed but silently ignored unless you run PRAGMA foreign_keys = ON at the start of each connection.


3. UNIQUE

Ensures all values in a column (or combination) are distinct. Allows NULL (multiple NULLs allowed in most engines).

Inline

1
2
3
4
CREATE TABLE users (
  id    INT          NOT NULL AUTO_INCREMENT PRIMARY KEY,
  email VARCHAR(150) NOT NULL UNIQUE
);

Table-level (named)

1
2
3
4
5
6
7
8
CREATE TABLE users (
  id       INT          NOT NULL AUTO_INCREMENT,
  email    VARCHAR(150) NOT NULL,
  username VARCHAR(50)  NOT NULL,
  CONSTRAINT pk_users       PRIMARY KEY (id),
  CONSTRAINT uq_users_email UNIQUE (email),
  CONSTRAINT uq_users_uname UNIQUE (username)
);

Composite UNIQUE (combination must be unique)

1
2
3
4
5
CREATE TABLE team_members (
  team_id INT NOT NULL,
  user_id INT NOT NULL,
  CONSTRAINT uq_team_member UNIQUE (team_id, user_id)
);

NULL behavior in UNIQUE columns

EngineMultiple NULLs allowed in UNIQUE column?
MySQLβœ… Yes (NULLs are not considered equal)
SQLiteβœ… Yes
PostgreSQLβœ… Yes
SQL Serverβœ… Yes (only one NULL in older versions; fixed in newer)
Oracleβœ… Yes
MS Access❌ No β€” only one NULL allowed

Cross-Engine β€” UNIQUE

FeatureMySQLSQLitePostgreSQLSQL ServerOracleMS Access
Inline UNIQUEβœ…βœ…βœ…βœ…βœ…βœ…
Named UNIQUE constraintβœ…βš οΈ name ignoredβœ…βœ…βœ…βŒ
Composite UNIQUEβœ…βœ…βœ…βœ…βœ…βœ…
UNIQUE auto-creates indexβœ…βœ…βœ…βœ…βœ…βœ…

4. NOT NULL

Prevents NULL values from being stored in a column.

Inline (most common)

1
2
3
4
5
6
CREATE TABLE products (
  id    INT           NOT NULL AUTO_INCREMENT PRIMARY KEY,
  name  VARCHAR(100)  NOT NULL,
  price DECIMAL(10,2) NOT NULL,
  notes TEXT          NULL           -- explicitly nullable
);

Columns are nullable by default

1
2
3
-- These two are equivalent:
description TEXT NULL
description TEXT          -- NULL is the default if omitted

NOT NULL with DEFAULT (best practice)

1
2
3
4
5
6
CREATE TABLE events (
  id         INT          NOT NULL AUTO_INCREMENT PRIMARY KEY,
  title      VARCHAR(200) NOT NULL,
  is_active  TINYINT(1)   NOT NULL DEFAULT 1,
  created_at DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP
);

Cross-Engine β€” NOT NULL

FeatureMySQLSQLitePostgreSQLSQL ServerOracleMS Access
NOT NULL inlineβœ…βœ…βœ…βœ…βœ…βœ…
Columns nullable by defaultβœ…βœ…βœ…βœ…βœ…βœ…
ALTER COLUMN to add/remove NOT NULLβœ…βŒ recreate tableβœ…βš οΈ ALTER COLUMN col TYPE NOT NULL⚠️ MODIFY col NOT NULL❌

5. CHECK

Validates that column values satisfy a boolean expression.

Inline

1
2
3
4
5
6
CREATE TABLE employees (
  id     INT            NOT NULL AUTO_INCREMENT PRIMARY KEY,
  name   VARCHAR(100)   NOT NULL,
  salary DECIMAL(10,2)  NOT NULL CHECK (salary >= 0),
  age    INT            CHECK (age BETWEEN 18 AND 70)
);
1
2
3
4
5
6
7
8
9
CREATE TABLE products (
  id       INT           NOT NULL AUTO_INCREMENT,
  name     VARCHAR(100)  NOT NULL,
  price    DECIMAL(10,2) NOT NULL,
  discount DECIMAL(5,2)  NOT NULL DEFAULT 0,
  CONSTRAINT pk_products        PRIMARY KEY (id),
  CONSTRAINT chk_price_positive CHECK (price > 0),
  CONSTRAINT chk_discount_range CHECK (discount BETWEEN 0 AND 100)
);

Multi-column CHECK

1
2
3
4
5
6
CREATE TABLE bookings (
  id         INT      NOT NULL AUTO_INCREMENT PRIMARY KEY,
  start_date DATE     NOT NULL,
  end_date   DATE     NOT NULL,
  CONSTRAINT chk_dates CHECK (end_date >= start_date)
);

CHECK with expression

1
2
3
4
5
6
7
8
CREATE TABLE accounts (
  id       INT          NOT NULL AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(50)  NOT NULL,
  email    VARCHAR(150) NOT NULL,
  role     VARCHAR(20)  NOT NULL,
  CONSTRAINT chk_valid_role CHECK (role IN ('admin', 'editor', 'viewer')),
  CONSTRAINT chk_email_fmt  CHECK (email LIKE '%@%.%')
);

Cross-Engine β€” CHECK

FeatureMySQLSQLitePostgreSQLSQL ServerOracleMS Access
CHECK inlineβœ… 8.0.16+ (enforced)βœ…βœ…βœ…βœ…βœ…
Named CHECKβœ…βš οΈ name ignoredβœ…βœ…βœ…βŒ
Multi-column CHECKβœ…βœ…βœ…βœ…βœ…βœ…
CHECK with subquery❌❌❌❌❌❌
Enforce existing data on addβœ…βœ…βœ…βš οΈ use WITH CHECKβœ…βŒ

⚠️ MySQL < 8.0.16: CHECK constraints are parsed but silently ignored β€” they have no effect. Upgrade to 8.0.16+ for enforcement.


6. DEFAULT

Provides an automatic value when no value is specified on INSERT.

Basic DEFAULT

1
2
3
4
5
6
7
8
CREATE TABLE orders (
  id          INT          NOT NULL AUTO_INCREMENT PRIMARY KEY,
  status      VARCHAR(20)  NOT NULL DEFAULT 'pending',
  is_paid     TINYINT(1)   NOT NULL DEFAULT 0,
  created_at  DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at  DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  notes       TEXT                  DEFAULT NULL
);

DEFAULT with expressions (MySQL 8.0.13+)

1
2
3
4
5
CREATE TABLE events (
  id         INT         NOT NULL AUTO_INCREMENT PRIMARY KEY,
  code       VARCHAR(20) NOT NULL DEFAULT (CONCAT('EVT-', LPAD(id, 6, '0'))),
  expires_at DATETIME    NOT NULL DEFAULT (NOW() + INTERVAL 30 DAY)
);

Using DEFAULT in INSERT

1
2
3
4
5
-- Omit the column β€” default applies
INSERT INTO orders (status) VALUES ('processing');

-- Explicitly use DEFAULT keyword
INSERT INTO orders (status, is_paid) VALUES (DEFAULT, 1);

Cross-Engine β€” DEFAULT

FeatureMySQLSQLitePostgreSQLSQL ServerOracleMS Access
Literal DEFAULTβœ…βœ…βœ…βœ…βœ…βœ…
DEFAULT CURRENT_TIMESTAMPβœ…βœ…βœ…βš οΈ DEFAULT GETDATE()⚠️ DEFAULT SYSDATE⚠️ DEFAULT Now()
DEFAULT expressionβœ… 8.0.13+βœ…βœ…βœ…βœ…βŒ
ON UPDATE CURRENT_TIMESTAMPβœ…βŒ use trigger❌ use trigger❌ use trigger❌ use trigger❌

7. AUTO_INCREMENT / Identity

Automatically generates a unique integer for each new row.

MySQL β€” AUTO_INCREMENT

1
2
3
4
5
6
7
8
9
10
CREATE TABLE users (
  id   INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(100) NOT NULL
);

-- Reset the counter
ALTER TABLE users AUTO_INCREMENT = 1000;

-- Get last inserted ID
SELECT LAST_INSERT_ID();

Cross-Engine β€” Auto-generated Keys

EngineSyntaxGet Last ID
MySQLINT AUTO_INCREMENT PRIMARY KEYSELECT LAST_INSERT_ID();
SQLiteINTEGER PRIMARY KEY (implicit) or INTEGER PRIMARY KEY AUTOINCREMENTSELECT last_insert_rowid();
PostgreSQLSERIAL / BIGSERIAL or GENERATED ALWAYS AS IDENTITYSELECT lastval(); or RETURNING id
SQL ServerINT IDENTITY(1,1)SELECT SCOPE_IDENTITY(); or OUTPUT INSERTED.id
OracleNUMBER GENERATED ALWAYS AS IDENTITY or Sequence + TriggerSELECT seq.CURRVAL FROM DUAL;
MS AccessAUTOINCREMENT or COUNTERSELECT @@IDENTITY;

PostgreSQL β€” IDENTITY (modern approach)

1
2
3
4
5
6
7
CREATE TABLE users (
  id   INT  GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name TEXT NOT NULL
);

-- Or allow manual override:
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY

SQL Server β€” IDENTITY

1
2
3
4
CREATE TABLE users (
  id   INT IDENTITY(1,1) PRIMARY KEY,  -- start=1, increment=1
  name NVARCHAR(100) NOT NULL
);

8. Adding Constraints to Existing Tables

Use ALTER TABLE to add constraints after table creation.

Add PRIMARY KEY

1
2
ALTER TABLE employees
ADD CONSTRAINT pk_employees PRIMARY KEY (id);

Add FOREIGN KEY

1
2
3
4
ALTER TABLE orders
ADD CONSTRAINT fk_orders_customer
  FOREIGN KEY (customer_id) REFERENCES customers(id)
  ON DELETE CASCADE;

Add UNIQUE

1
2
ALTER TABLE users
ADD CONSTRAINT uq_users_email UNIQUE (email);

Add CHECK

1
2
ALTER TABLE products
ADD CONSTRAINT chk_price CHECK (price > 0);

Add DEFAULT

1
2
3
4
5
-- MySQL
ALTER TABLE orders MODIFY COLUMN status VARCHAR(20) NOT NULL DEFAULT 'pending';

-- PostgreSQL / SQL Server
ALTER TABLE orders ALTER COLUMN status SET DEFAULT 'pending';

Add NOT NULL

1
2
3
4
5
6
7
8
-- MySQL
ALTER TABLE users MODIFY COLUMN email VARCHAR(150) NOT NULL;

-- PostgreSQL
ALTER TABLE users ALTER COLUMN email SET NOT NULL;

-- SQL Server
ALTER TABLE users ALTER COLUMN email VARCHAR(150) NOT NULL;

Cross-Engine β€” ALTER TABLE ADD CONSTRAINT

ConstraintMySQLSQLitePostgreSQLSQL ServerOracleMS Access
ADD PRIMARY KEYβœ…βŒ recreateβœ…βœ…βœ…βŒ
ADD FOREIGN KEYβœ…βŒ recreateβœ…βœ…βœ…βœ…
ADD UNIQUEβœ…βŒ recreateβœ…βœ…βœ…βŒ
ADD CHECKβœ…βŒ recreateβœ…βœ…βœ…βŒ
SET DEFAULT⚠️ use MODIFY❌ recreateβœ…βš οΈ ADD DEFAULT⚠️ MODIFY❌
SET NOT NULL⚠️ use MODIFY❌ recreateβœ…βš οΈ ALTER COLUMN⚠️ MODIFY❌

⚠️ SQLite: Does not support ALTER TABLE ADD CONSTRAINT for most cases. The workaround is: create a new table with the desired constraints β†’ copy data β†’ drop old table β†’ rename new table.


9. Dropping Constraints

Drop PRIMARY KEY

1
2
3
4
5
-- MySQL
ALTER TABLE employees DROP PRIMARY KEY;

-- PostgreSQL / SQL Server / Oracle
ALTER TABLE employees DROP CONSTRAINT pk_employees;

Drop FOREIGN KEY

1
2
3
4
5
-- MySQL
ALTER TABLE orders DROP FOREIGN KEY fk_orders_customer;

-- PostgreSQL / SQL Server / Oracle
ALTER TABLE orders DROP CONSTRAINT fk_orders_customer;

Drop UNIQUE

1
2
3
4
5
6
7
8
-- MySQL
ALTER TABLE users DROP INDEX uq_users_email;

-- PostgreSQL / Oracle
ALTER TABLE users DROP CONSTRAINT uq_users_email;

-- SQL Server
ALTER TABLE users DROP CONSTRAINT uq_users_email;

Drop CHECK

1
2
-- MySQL / PostgreSQL / SQL Server / Oracle
ALTER TABLE products DROP CONSTRAINT chk_price;

Drop DEFAULT

1
2
3
4
5
6
7
8
-- MySQL
ALTER TABLE orders MODIFY COLUMN status VARCHAR(20) NOT NULL;  -- remove DEFAULT by omitting it

-- PostgreSQL
ALTER TABLE orders ALTER COLUMN status DROP DEFAULT;

-- SQL Server
ALTER TABLE orders DROP CONSTRAINT df_orders_status;   -- must know the constraint name

Drop NOT NULL

1
2
3
4
5
6
7
8
-- MySQL
ALTER TABLE users MODIFY COLUMN email VARCHAR(150) NULL;

-- PostgreSQL
ALTER TABLE users ALTER COLUMN email DROP NOT NULL;

-- SQL Server
ALTER TABLE users ALTER COLUMN email VARCHAR(150) NULL;

Cross-Engine β€” DROP CONSTRAINT

ConstraintMySQLSQLitePostgreSQLSQL ServerOracleMS Access
DROP PRIMARY KEYβœ…βŒ recreateβœ…βœ…βœ…βŒ
DROP FOREIGN KEYβœ… (separate keyword)βŒβœ…βœ…βœ…βœ…
DROP UNIQUE⚠️ DROP INDEXβŒβœ…βœ…βœ…βŒ
DROP CHECKβœ…βŒβœ…βœ…βœ…βŒ
DROP DEFAULT⚠️ via MODIFYβŒβœ… DROP DEFAULT⚠️ drop named constraint⚠️ MODIFY❌

10. Viewing & Inspecting Constraints

MySQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- All constraints on a table
SELECT constraint_name, constraint_type, table_name
FROM information_schema.TABLE_CONSTRAINTS
WHERE table_schema = 'your_db' AND table_name = 'employees';

-- Foreign key details
SELECT constraint_name, column_name, referenced_table_name, referenced_column_name
FROM information_schema.KEY_COLUMN_USAGE
WHERE table_schema = 'your_db' AND table_name = 'employees'
  AND referenced_table_name IS NOT NULL;

-- Check constraints (MySQL 8.0.16+)
SELECT constraint_name, check_clause
FROM information_schema.CHECK_CONSTRAINTS
WHERE constraint_schema = 'your_db';

-- Quick view using SHOW
SHOW CREATE TABLE employees;

PostgreSQL

1
2
3
4
5
6
-- All constraints
SELECT conname AS name, contype AS type, pg_get_constraintdef(oid) AS definition
FROM pg_constraint
WHERE conrelid = 'employees'::regclass;

-- contype codes: p=primary key, f=foreign key, u=unique, c=check, n=not null

SQL Server

1
2
3
4
5
6
7
8
9
SELECT name, type_desc, definition
FROM sys.check_constraints
WHERE parent_object_id = OBJECT_ID('employees');

SELECT fk.name, col.name AS column, ref.name AS ref_table
FROM sys.foreign_keys fk
JOIN sys.foreign_key_columns fkc ON fk.object_id = fkc.constraint_object_id
JOIN sys.columns col ON fkc.parent_column_id = col.column_id AND fkc.parent_object_id = col.object_id
JOIN sys.tables ref ON fkc.referenced_object_id = ref.object_id;

Oracle

1
2
3
SELECT constraint_name, constraint_type, search_condition, status
FROM user_constraints
WHERE table_name = 'EMPLOYEES';

SQLite

1
2
3
PRAGMA table_info(employees);         -- columns + NOT NULL + DEFAULT
PRAGMA foreign_key_list(employees);   -- FK details
PRAGMA index_list(employees);         -- indexes (includes UNIQUE)

11. Deferrable Constraints

Delay constraint checking until end of transaction (useful for circular FKs or bulk operations).

PostgreSQL β€” DEFERRABLE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CREATE TABLE employees (
  id         INT  NOT NULL PRIMARY KEY,
  manager_id INT,
  CONSTRAINT fk_manager
    FOREIGN KEY (manager_id) REFERENCES employees(id)
    DEFERRABLE INITIALLY DEFERRED   -- checked at COMMIT, not on each statement
);

-- Or defer at runtime within a transaction:
BEGIN;
SET CONSTRAINTS fk_manager DEFERRED;
  INSERT INTO employees (id, manager_id) VALUES (1, 2);
  INSERT INTO employees (id, manager_id) VALUES (2, 1);
COMMIT;

SQL Server β€” WITH NOCHECK

1
2
3
4
5
6
7
8
9
-- Add FK but don't validate existing data
ALTER TABLE orders
ADD CONSTRAINT fk_orders_customer
  FOREIGN KEY (customer_id) REFERENCES customers(id)
  WITH NOCHECK;

-- Temporarily disable a constraint
ALTER TABLE orders NOCHECK CONSTRAINT fk_orders_customer;
ALTER TABLE orders  CHECK CONSTRAINT fk_orders_customer;

Oracle β€” DISABLE / ENABLE

1
2
3
ALTER TABLE orders DISABLE CONSTRAINT fk_orders_customer;
ALTER TABLE orders ENABLE  CONSTRAINT fk_orders_customer;
ALTER TABLE orders ENABLE  NOVALIDATE CONSTRAINT fk_orders_customer;  -- enable without validating existing rows

Cross-Engine β€” Deferrable / Disable

FeatureMySQLSQLitePostgreSQLSQL ServerOracleMS Access
DEFERRABLE INITIALLY DEFERREDβŒβŒβœ…βŒβœ…βŒ
DISABLE CONSTRAINT⚠️ SET FOREIGN_KEY_CHECKS=0❌⚠️ drop & recreateβœ… NOCHECK CONSTRAINTβœ…βŒ
WITH NOCHECK (skip existing)βŒβŒβŒβœ…βš οΈ ENABLE NOVALIDATE❌

12. Constraint Naming Best Practices

Always name your constraints β€” it makes ALTER, DROP, and error messages far easier.

ConstraintPrefixPatternExample
Primary Keypk_pk_<table>pk_employees
Foreign Keyfk_fk_<table>_<ref_table>fk_orders_customers
Uniqueuq_uq_<table>_<column>uq_users_email
Checkchk_chk_<table>_<rule>chk_products_price
Defaultdf_df_<table>_<column>df_orders_status
Indexidx_idx_<table>_<column>idx_employees_dept

Full Example β€” All Constraints Named

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CREATE TABLE employees (
  id          INT            NOT NULL,
  name        VARCHAR(100)   NOT NULL,
  email       VARCHAR(150)   NOT NULL,
  dept_id     INT,
  salary      DECIMAL(10,2)  NOT NULL DEFAULT 0.00,
  role        VARCHAR(20)    NOT NULL DEFAULT 'viewer',
  start_date  DATE           NOT NULL,
  end_date    DATE,
  created_at  DATETIME       NOT NULL DEFAULT CURRENT_TIMESTAMP,

  CONSTRAINT pk_employees          PRIMARY KEY (id),
  CONSTRAINT uq_employees_email    UNIQUE (email),
  CONSTRAINT fk_employees_dept     FOREIGN KEY (dept_id)
                                     REFERENCES departments(id)
                                     ON DELETE SET NULL
                                     ON UPDATE CASCADE,
  CONSTRAINT chk_salary_positive   CHECK (salary >= 0),
  CONSTRAINT chk_valid_role        CHECK (role IN ('admin', 'editor', 'viewer')),
  CONSTRAINT chk_dates_order       CHECK (end_date IS NULL OR end_date >= start_date)
);

13. Cross-Engine Master Table

ConstraintMySQLSQLitePostgreSQLSQL ServerOracleMS Access
PRIMARY KEYβœ…βœ…βœ…βœ…βœ…βœ…
FOREIGN KEY (enforced)βœ… InnoDB⚠️ pragma neededβœ…βœ…βœ…βœ…
UNIQUEβœ…βœ…βœ…βœ…βœ…βœ…
NOT NULLβœ…βœ…βœ…βœ…βœ…βœ…
CHECK (enforced)βœ… 8.0.16+βœ…βœ…βœ…βœ…βœ…
DEFAULTβœ…βœ…βœ…βœ…βœ…βœ…
Named constraintsβœ…βš οΈ ignoredβœ…βœ…βœ…βŒ
ALTER TABLE ADD CONSTRAINTβœ…βŒβœ…βœ…βœ…βš οΈ
ALTER TABLE DROP CONSTRAINTβœ…βŒβœ…βœ…βœ…βŒ
Deferrable constraintsβŒβŒβœ…βŒβœ…βŒ
Disable / re-enable constraint⚠️ FK onlyβŒβŒβœ…βœ…βŒ
Auto-incrementβœ… AUTO_INCREMENTβœ… AUTOINCREMENTβœ… SERIAL / IDENTITYβœ… IDENTITYβœ… IDENTITY / Sequenceβœ… AUTOINCREMENT

14. Common Patterns & Examples


Pattern 1 β€” Prevent orphan rows (FK + Cascade)

1
2
3
4
5
6
7
8
-- Deleting a customer automatically deletes their orders
CREATE TABLE orders (
  id          INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  customer_id INT NOT NULL,
  CONSTRAINT fk_orders_cust
    FOREIGN KEY (customer_id) REFERENCES customers(id)
    ON DELETE CASCADE
);

Pattern 2 β€” Enforce valid status values (CHECK)

1
2
CONSTRAINT chk_order_status
  CHECK (status IN ('draft', 'pending', 'confirmed', 'shipped', 'cancelled'))

Pattern 3 β€” Prevent negative numbers (CHECK)

1
2
CONSTRAINT chk_qty_positive   CHECK (qty > 0),
CONSTRAINT chk_price_positive CHECK (price >= 0.00)

Pattern 4 β€” Enforce date logic (multi-column CHECK)

1
CONSTRAINT chk_date_range CHECK (end_date IS NULL OR end_date > start_date)

Pattern 5 β€” Unique combination, not individual columns

1
2
-- A user can only have one membership per team
CONSTRAINT uq_team_user UNIQUE (team_id, user_id)

Pattern 6 β€” Soft delete with UNIQUE only on active rows (PostgreSQL partial index)

1
2
3
4
-- Only enforce uniqueness on active (non-deleted) rows
CREATE UNIQUE INDEX uq_active_email
  ON users(email)
  WHERE deleted_at IS NULL;

Pattern 7 β€” Self-referencing FK (tree/hierarchy)

1
2
3
4
5
6
7
8
CREATE TABLE categories (
  id        INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  name      VARCHAR(100) NOT NULL,
  parent_id INT,
  CONSTRAINT fk_category_parent
    FOREIGN KEY (parent_id) REFERENCES categories(id)
    ON DELETE SET NULL
);

Pattern 8 β€” Audit timestamps (DEFAULT + AUTO UPDATE)

1
2
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP

Pattern 9 β€” SQLite workaround for adding constraints

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- SQLite does not support ALTER TABLE ADD CONSTRAINT.
-- Standard workaround:

-- Step 1: Create new table with desired constraints
CREATE TABLE employees_new (
  id     INTEGER PRIMARY KEY AUTOINCREMENT,
  name   TEXT    NOT NULL,
  email  TEXT    NOT NULL UNIQUE,
  salary REAL    NOT NULL CHECK (salary >= 0)
);

-- Step 2: Copy data
INSERT INTO employees_new SELECT * FROM employees;

-- Step 3: Drop old table
DROP TABLE employees;

-- Step 4: Rename new table
ALTER TABLE employees_new RENAME TO employees;

Pattern 10 β€” Find tables with no PK (data quality check)

1
2
3
4
5
6
7
8
9
10
11
-- MySQL
SELECT table_name
FROM information_schema.tables t
WHERE table_schema = DATABASE()
  AND table_type = 'BASE TABLE'
  AND table_name NOT IN (
    SELECT table_name
    FROM information_schema.table_constraints
    WHERE constraint_type = 'PRIMARY KEY'
      AND table_schema = DATABASE()
  );

Quick Reference Card

ConstraintPurposeNULL allowed?Per table limit
PRIMARY KEYUnique row identity❌ Never1 only
FOREIGN KEYLink to another table’s PKβœ… (means no parent)Many
UNIQUENo duplicate valuesβœ… (multiple NULLs OK)Many
NOT NULLValue requiredβ€”Per column
CHECKCustom validation ruleβœ… (NULL skips check)Many
DEFAULTAuto-fill when omittedβ€”Per column

πŸ’‘ NULL skips CHECK: A NULL value in a CHECK column does not trigger a violation β€” the check is simply skipped. Combine NOT NULL + CHECK to fully control a column.


Syntax primarily shown for MySQL 8.x. Engine-specific alternatives noted throughout.

This post is licensed under CC BY 4.0 by the author.