Module 1, Session 5. NOT NULL, DEFAULT, CHECK, UNIQUE, composite rules and exclusion constraints. Primary and foreign keys are the next session.
Open in Present mode · 400 practice questions · Live class schedule · Try the playground
Jo class mein bola, yahan nukte mein likha hai.
What I said in class, written here as key points.
Rules the database itself enforces: NOT NULL, DEFAULT, CHECK, UNIQUE and EXCLUDE, so bad data is rejected before it ever reaches the table.
Four pillars: rules the database enforces on every single write
NOT NULL — Missing Data Defense
DEFAULT — Auto-Fill Fallbacks
CHECK — Numeric & State Guardrails
UNKNOWN)
UNIQUE
UNIQUE NULLS NOT DISTINCT
EXCLUDE USING gist Interval Overlaps
DEFERRABLE — Check at COMMIT
PRIMARY KEY & FOREIGN KEY
From physical byte containers to active engine guardrails
'abc' into NUMERIC are rejected at the write gate.
INT column happily accepts age = -47. A NUMERIC column happily accepts price = -5000. Types define how many bytes to allocate — constraints define what data is legally permitted to exist.
-- in the practice database (\c practice) CREATE SCHEMA IF NOT EXISTS constraints_and_keys; -- start clean, safe to re-run DROP TABLE IF EXISTS constraints_and_keys.exam_candidates, constraints_and_keys.halls CASCADE; CREATE TABLE constraints_and_keys.halls ( id INT PRIMARY KEY ); CREATE TABLE constraints_and_keys.exam_candidates ( roll_no INT PRIMARY KEY CHECK (roll_no > 0), name TEXT NOT NULL, seat_no INT UNIQUE NOT NULL, hall_id INT REFERENCES constraints_and_keys.halls(id) ); -- Next exam: Zero collisions. Zero blank names. -- Zero ghost halls. 100% physically guarded.
Every vulnerability in the Exam Hall disaster directly maps to a physical PostgreSQL constraint that rejects corrupted data at the hardware storage gate:
| Exam Hall Vulnerability | Protective SQL Constraint / Key | Production Defense Guarantee |
|---|---|---|
| 📝 Blank Name on Admit Card | NOT NULL | Write-time rejection (PostgreSQL Error 23502) — zero rows touch disk with missing mandatory data. |
| ⚡ Missing Status / Creation Date | DEFAULT | Automated fallback — engine supplies 'registered' or now() when client inserts omit optional columns. |
| 📏 Negative Roll No (-47) or Bad Score | CHECK (roll_no > 0) | Mathematical domain boundary — evaluates arithmetic conditions before writing to heap (Error 23514). |
| 💥 Seat 12 Double-Booking Collision | UNIQUE (seat_no) | Atomic B-Tree index scanner — deflects duplicate values and guarantees 1 desk = 1 student (Error 23505). |
| 🏢 Ghost Hall 99 (Does Not Exist) | FOREIGN KEY (hall_id) | Next class. Referential laser bridge — child row cannot reference non-existent parent hall; eliminates orphan rows (Error 23503). |
| 🔑 Student Identity & Primary Anchor | PRIMARY KEY (roll_no) | Next class. Surrogate / natural anchor — combines UNIQUE + NOT NULL with an automatic unique B-Tree index for permanent row identity. |
In database storage, an unconstrained column permits NULL (the physical absence of a value). NOT NULL acts as an engine-level write gate that rejects missing data before it ever touches disk.
NOT NULL eliminates missing-data bugs in APIs and documents the rule inside the schema itself.The simplest and most vital constraint in relational databases. Enforces that every single row must contain a real value — any INSERT or UPDATE attempting to write NULL is rejected immediately.
-- in the practice database (\c practice) CREATE SCHEMA IF NOT EXISTS constraints_and_keys; -- start clean, safe to re-run DROP TABLE IF EXISTS constraints_and_keys.users CASCADE; CREATE TABLE constraints_and_keys.users ( user_id SERIAL PRIMARY KEY, username VARCHAR(50) NOT NULL, email VARCHAR(100) NOT NULL ); -- Valid write: all required columns provided: INSERT INTO constraints_and_keys.users (username, email) VALUES ('aarav', 'aarav@shop.com'); -- OK! SELECT * FROM constraints_and_keys.users LIMIT 10; -- FAILS: username is omitted and has no default: INSERT INTO constraints_and_keys.users (email) VALUES ('missing@shop.com'); -- 💥 ERROR: null value in column "username" violates not-null constraint -- DETAIL: Failing row contains (2, null, missing@shop.com). -- In-Place Schema Evolution on Existing Tables: -- Step 1: Pre-clean dirty rows (otherwise SET NOT NULL aborts!) UPDATE constraints_and_keys.users SET username = 'anonymous' WHERE username IS NULL; -- Step 2: Lock the column against future NULL writes: ALTER TABLE constraints_and_keys.users ALTER COLUMN username SET NOT NULL;
When application code or bulk imports omit a column during INSERT, DEFAULT instructs PostgreSQL to automatically fill a predefined value or expression before persisting the tuple.
CURRENT_TIMESTAMP and gen_random_uuid() evaluate dynamically at the exact moment of INSERT execution, never when the table is created. Never hardcode static timestamps in defaults.Pre-configures standard values directly in the database table blueprint. Supports static literals, dynamic clock evaluations, and sequence increments without writing extra application logic.
-- in the practice database (\c practice) CREATE SCHEMA IF NOT EXISTS constraints_and_keys; -- start clean, safe to re-run DROP TABLE IF EXISTS constraints_and_keys.web_orders CASCADE; CREATE TABLE constraints_and_keys.web_orders ( order_id SERIAL PRIMARY KEY, customer_name VARCHAR(100) NOT NULL, order_status VARCHAR(20) NOT NULL DEFAULT 'pending', created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Provide ONLY the required column; defaults fill the rest automatically: INSERT INTO constraints_and_keys.web_orders (customer_name) VALUES ('Priya'); -- order_status='pending', created_at=now() auto-populated! -- Explicit override is still permitted: INSERT INTO constraints_and_keys.web_orders (customer_name, order_status) VALUES ('Aarav', 'shipped'); -- order_status becomes 'shipped' -- Managing defaults on existing tables: ALTER TABLE constraints_and_keys.web_orders ALTER COLUMN order_status SET DEFAULT 'placed'; ALTER TABLE constraints_and_keys.web_orders ALTER COLUMN order_status DROP DEFAULT; -- See what the defaults filled in: SELECT order_id, customer_name, order_status, created_at FROM constraints_and_keys.web_orders LIMIT 10;
CHECK constraints enforce mathematical and domain boundaries on every row. PostgreSQL evaluates the boolean expression before allocating storage. If the expression evaluates to FALSE, the write is aborted.
A custom boolean expression evaluated on every write. If the condition evaluates to FALSE, the write is aborted. Used for ranges, categorical dropdowns, and business boundaries.
-- in the practice database (\c practice) CREATE SCHEMA IF NOT EXISTS constraints_and_keys; -- start clean, safe to re-run DROP TABLE IF EXISTS constraints_and_keys.order_items CASCADE; CREATE TABLE constraints_and_keys.order_items ( item_id SERIAL PRIMARY KEY, item_name VARCHAR(100) NOT NULL, unit_price NUMERIC(10,2) NOT NULL CHECK (unit_price > 0), quantity INT NOT NULL CHECK (quantity >= 1), status VARCHAR(20) NOT NULL CHECK (status IN ('pending', 'packed', 'dispatched', 'delivered')) ); INSERT INTO constraints_and_keys.order_items (item_name, unit_price, quantity, status) VALUES ('Wireless Mouse', 799.00, 2, 'packed'); -- OK -- Also try (FAILS): status 'in-transit' is not in the approved whitelist -- INSERT INTO constraints_and_keys.order_items (item_name, unit_price, quantity, status) -- VALUES ('Keyboard', 1299.00, 1, 'in-transit'); -- ERROR: new row violates check constraint "order_items_status_check" -- FAILS: price cannot be zero or negative INSERT INTO constraints_and_keys.order_items (item_name, unit_price, quantity, status) VALUES ('Bad Item', -50.00, 1, 'packed'); -- ERROR: new row violates check constraint "order_items_unit_price_check"
A UNIQUE constraint guarantees that no two rows share the same non-NULL value. Under the hood, PostgreSQL automatically constructs a unique B-Tree index to verify uniqueness in O(log N) time.
Guarantees no two rows share identical non-null data. Single-column identity, multi-column composite combinations, and PostgreSQL 15+ NULL handling.
email, phone). Backed automatically by an index scan B-Tree.UNIQUE (tenant_id, slug). Individual columns may repeat freely.NULL in the unique column.-- in the practice database (\c practice) CREATE SCHEMA IF NOT EXISTS constraints_and_keys; -- 1. Single-column UNIQUE (email must be unique across all users): -- start clean, safe to re-run DROP TABLE IF EXISTS constraints_and_keys.tax_filings, constraints_and_keys.tenant_workspaces, constraints_and_keys.accounts CASCADE; CREATE TABLE constraints_and_keys.accounts ( account_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, email VARCHAR(150) UNIQUE NOT NULL ); -- 2. Multi-column composite UNIQUE (slug unique within the same tenant): CREATE TABLE constraints_and_keys.tenant_workspaces ( workspace_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, tenant_id INT NOT NULL, slug VARCHAR(50) NOT NULL, CONSTRAINT uq_tenant_slug UNIQUE (tenant_id, slug) ); -- 3. PostgreSQL 15+ (allows at most ONE null row instead of unlimited NULL duplicates): CREATE TABLE constraints_and_keys.tax_filings ( filing_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, tax_id VARCHAR(20) UNIQUE NULLS NOT DISTINCT ); -- Same slug in two different tenants is fine: INSERT INTO constraints_and_keys.tenant_workspaces (tenant_id, slug) VALUES (1, 'sales'), (2, 'sales'); SELECT tenant_id, slug FROM constraints_and_keys.tenant_workspaces LIMIT 10; -- Now try (FAILS with Error 23505): the same slug twice in tenant 1 -- INSERT INTO constraints_and_keys.tenant_workspaces (tenant_id, slug) VALUES (1, 'sales');
Standard UNIQUE constraints only compare equality (=). When modeling hotel rooms, doctor appointments, or meeting spaces, standard UNIQUE cannot detect overlapping time intervals. EXCLUDE USING gist prevents schedule conflicts at the engine level.
&& for overlap). This eliminates complex, race-condition-prone application locking loops.Neither UNIQUE nor CHECK can stop overlapping time intervals (e.g. 10:00-11:30 vs 11:00-12:00) because start and end times differ. Exclusion Constraints guarantee no two rows share conflicting ranges.
-- in the practice database (\c practice) CREATE SCHEMA IF NOT EXISTS constraints_and_keys; CREATE EXTENSION IF NOT EXISTS btree_gist; -- start clean, safe to re-run DROP TABLE IF EXISTS constraints_and_keys.doctor_schedules CASCADE; CREATE TABLE constraints_and_keys.doctor_schedules ( booking_id SERIAL PRIMARY KEY, doctor_id INT NOT NULL, slot_window TSRANGE NOT NULL, -- For any two rows, if doctor_id is EQUAL (=), slot_window CANNOT OVERLAP (&&): CONSTRAINT no_double_booking EXCLUDE USING gist (doctor_id WITH =, slot_window WITH &&) ); -- Doctor 101 booked 10:00 to 11:00 INSERT INTO constraints_and_keys.doctor_schedules (doctor_id, slot_window) VALUES (101, tsrange('2026-10-10 10:00', '2026-10-10 11:00')); -- FAILS: 10:30 to 11:30 collides with 10:00 to 11:00! INSERT INTO constraints_and_keys.doctor_schedules (doctor_id, slot_window) VALUES (101, tsrange('2026-10-10 10:30', '2026-10-10 11:30')); -- ERROR: conflicting key value violates exclusion constraint "no_double_booking" -- DETAIL: Key (doctor_id, slot_window)=(101, ["2026-10-10 10:30:00","2026-10-10 11:30:00")) -- conflicts with existing key (101, ["2026-10-10 10:00:00","2026-10-10 11:00:00")).
Time to lock down your tables! Enforce NOT NULL, DEFAULT, UNIQUE, composite rules, exclusion constraints, and zero-downtime ALTER TABLE migrations directly in PostgreSQL.
Top real-world questions asked by staff engineers and senior database architects.
NULL BETWEEN 1 AND 5 yields UNKNOWN. PostgreSQL CHECK constraints reject writes only when the condition evaluates to strictly FALSE — an UNKNOWN result is treated as passing! To guarantee that nulls cannot be inserted, you must explicitly pair the rule with NOT NULL.
Close your notes. Answer from memory. Click any card to reveal the verified answer.
The non-negotiable architectural principles for every constraint you write.
Application validations are regularly bypassed by direct psql maintenance, ETL jobs, and race conditions. Declarative constraints are the engine's unbypasable source of truth.
In SQL, NULL > 0 evaluates to UNKNOWN, and CHECK only rejects FALSE. A column with bare CHECK will quietly accept NULL — always write NOT NULL CHECK (...).
DEFAULT runs only when the INSERT skips the column. An explicit NULL is stored as NULL — so pair DEFAULT with NOT NULL when the column must always have a value.
When modeling appointments, hotel rooms, or reservations, standard UNIQUE fails. Use PostgreSQL Exclusion Constraints with GIST to physically guarantee zero overlapping time ranges.
Plain UNIQUE blocks duplicate values but treats every NULL as different, so many NULL rows are allowed. Use NOT NULL, or PostgreSQL 15+ UNIQUE NULLS NOT DISTINCT, when that is not what you want.
Every constraint from today, plus the ALTER syntax to add or drop one, ready to copy into your terminal.
Complete these hands-on constraint drills in your local practice database before the Keys class.
🎯 400 practice questions for this session · Easy to Crazy hard, run and checked on the pagePRACTICE →Create constraints_and_keys.departments with composite UNIQUE (hospital_id, department_code). Prove that code 'CAR' can exist in Hospital 1 and Hospital 2, but fails inside Hospital 1.
Add conditional check constraint ensuring that if is_emergency = true, the emergency_contact_phone column cannot be NULL.
Enable btree_gist and build an exclusion constraint on constraints_and_keys.operating_rooms preventing overlapping surgery times in the same room.
Make a table with a few NULL phone numbers. Try ALTER COLUMN phone SET NOT NULL and read the error. Fix the rows with an UPDATE ... WHERE phone IS NULL, then run the ALTER again.