Module 1, Session 5. NOT NULL, DEFAULT, UNIQUE, composite rules, conditional CHECK, exclusion constraints, and zero-downtime ALTER TABLE migrations.
Open in Present mode · 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.
Enforcing physical database integrity, declarative domain rules, unique identity, referential laser bridges, and zero-downtime production migrations.
Four architectural pillars to guarantee database correctness and eliminate dirty data
NOT NULL — Missing Data Defense
DEFAULT — Auto-Fill Fallbacks
CHECK — Numeric & State Guardrails
UNKNOWN)
EXCLUDE USING gist Interval Overlaps
UNIQUE
UNIQUE NULLS NOT DISTINCT
PRIMARY KEY & FOREIGN KEY Laser Shield
ON DELETE: RESTRICT, CASCADE & SET NULL
NOT VALID
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.
CREATE TABLE 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 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) | Referential laser bridge — child row cannot reference non-existent parent hall; eliminates orphan rows (Error 23503). |
| 🔑 Student Identity & Primary Anchor | PRIMARY KEY (roll_no) | 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.
CREATE SCHEMA IF NOT EXISTS practice; CREATE TABLE practice.users ( user_id SERIAL PRIMARY KEY, username VARCHAR(50) NOT NULL, email VARCHAR(100) NOT NULL ); -- Valid write: all required columns provided: INSERT INTO practice.users (username, email) VALUES ('aarav', '[email protected]'); -- OK! SELECT * FROM practice.users LIMIT 10; -- FAILS: username is omitted and has no default: INSERT INTO practice.users (email) VALUES ('[email protected]'); -- 💥 ERROR: null value in column "username" violates not-null constraint -- DETAIL: Failing row contains (2, null, [email protected]). -- In-Place Schema Evolution on Existing Tables: -- Step 1: Pre-clean dirty rows (otherwise SET NOT NULL aborts!) UPDATE practice.users SET username = 'anonymous' WHERE username IS NULL; -- Step 2: Lock the column against future NULL writes: ALTER TABLE practice.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.
CREATE SCHEMA IF NOT EXISTS practice; CREATE TABLE practice.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 practice.web_orders (customer_name) VALUES ('Priya'); -- order_status='pending', created_at=now() auto-populated! -- Explicit override is still permitted: INSERT INTO practice.web_orders (customer_name, order_status) VALUES ('Aarav', 'shipped'); -- order_status becomes 'shipped' -- Managing defaults on existing tables: ALTER TABLE practice.web_orders ALTER COLUMN order_status SET DEFAULT 'placed'; ALTER TABLE practice.web_orders ALTER COLUMN order_status DROP DEFAULT; -- See what the defaults filled in: SELECT order_id, customer_name, order_status, created_at FROM practice.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.
CREATE SCHEMA IF NOT EXISTS practice; CREATE TABLE practice.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 practice.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 practice.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 practice.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.CREATE SCHEMA IF NOT EXISTS practice; -- 1. Single-column UNIQUE (email must be unique across all users): CREATE TABLE practice.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 practice.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 practice.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 practice.tenant_workspaces (tenant_id, slug) VALUES (1, 'sales'), (2, 'sales'); SELECT tenant_id, slug FROM practice.tenant_workspaces LIMIT 10; -- Now try (FAILS with Error 23505): the same slug twice in tenant 1 -- INSERT INTO practice.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.
CREATE SCHEMA IF NOT EXISTS practice; CREATE EXTENSION IF NOT EXISTS btree_gist; CREATE TABLE practice.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 practice.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 practice.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.
From minimal Candidate Keys to physical Surrogate anchors: how relational database engines establish immutable entity identity without ambiguity.
id, email, pan_number).UNIQUE NOT NULL).CREATE SCHEMA IF NOT EXISTS practice; CREATE TABLE practice.taxpayers ( -- 1. SURROGATE PRIMARY KEY: the chosen candidate key (synthetic 8-byte anchor) taxpayer_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- 2. NATURAL ALTERNATE KEY: real-world business identifier (immutable) pan_number VARCHAR(10) UNIQUE NOT NULL, -- 3. SECONDARY ALTERNATE KEY: candidate key enforced via UNIQUE NOT NULL email VARCHAR(150) UNIQUE NOT NULL, -- 4. NON-KEY ATTRIBUTE: descriptive domain payload full_name TEXT NOT NULL ); INSERT INTO practice.taxpayers (pan_number, email, full_name) VALUES ('ABCDE1234F', '[email protected]', 'Priya Sharma'); SELECT taxpayer_id, pan_number, email, full_name FROM practice.taxpayers LIMIT 10; -- 💡 The Golden Rule: use surrogate keys for joins and primary keys; -- enforce natural keys using alternate keys (UNIQUE NOT NULL)!
In relational database theory, every physical table requires an unbypasable identity anchor. PostgreSQL enforces PRIMARY KEY by combining NOT NULL + UNIQUE and building a unique B-Tree index.
BIGINT GENERATED ALWAYS AS IDENTITY) as the Primary Key for fast joins and storage stability. Protect real-world natural keys (PAN, GSTIN, Email) using UNIQUE NOT NULL.The ultimate entity anchor in relational tables. Combines NOT NULL + UNIQUE with an automatic unique B-Tree index, for fast lookups and joins.
SERIAL by preventing accidental manual sequence desynchronization.PRIMARY KEY (order_id, item_id)) ensuring no duplicate combinations can exist in junction records.CONSTRAINT pk_orders PRIMARY KEY (order_id) so production logs output actionable names.CREATE SCHEMA IF NOT EXISTS practice; -- 1. Modern single-column surrogate primary key: CREATE TABLE practice.shop_orders ( order_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, order_date DATE NOT NULL DEFAULT CURRENT_DATE, total NUMERIC(10,2) NOT NULL CHECK (total >= 0) ); -- 2. Composite primary key on a junction / line-items table: CREATE TABLE practice.shop_order_items ( order_id BIGINT REFERENCES practice.shop_orders(order_id), item_id INT, quantity INT NOT NULL CHECK (quantity > 0), CONSTRAINT pk_shop_order_items PRIMARY KEY (order_id, item_id) ); INSERT INTO practice.shop_orders (total) VALUES (2499.00); -- order_id 1 is generated INSERT INTO practice.shop_order_items VALUES (1, 1, 2), (1, 2, 1); -- same order, different items: OK SELECT order_id, item_id, quantity FROM practice.shop_order_items LIMIT 10; -- Now try (FAILS with Error 23505): the pair (1, 1) already exists -- INSERT INTO practice.shop_order_items VALUES (1, 1, 5);
A Foreign Key connects a column in a child table to the primary key of a parent table. It physically prevents orphan records from entering your database by validating relational existence.
A child table cannot reference a parent record that does not exist. The database engine validates referential existence before writing, permanently eliminating corrupt orphan rows.
-- Step 1: Parent table must exist with a unique identifier CREATE SCHEMA IF NOT EXISTS practice; CREATE TABLE practice.customers ( cust_id INT PRIMARY KEY, full_name VARCHAR(100) NOT NULL ); -- Step 2: Child table enforces referential relationship CREATE TABLE practice.orders ( order_id SERIAL PRIMARY KEY, cust_id INT NOT NULL, amount NUMERIC(10,2) NOT NULL CHECK (amount >= 0), CONSTRAINT fk_orders_customer FOREIGN KEY (cust_id) REFERENCES practice.customers(cust_id) ); -- Valid write: customer 101 exists in parent table INSERT INTO practice.customers (cust_id, full_name) VALUES (101, 'Aarav Sharma'); INSERT INTO practice.orders (cust_id, amount) VALUES (101, 2499.00); -- OK! -- Blocked write: customer 999 does not exist (Orphan Shield fires) INSERT INTO practice.orders (cust_id, amount) VALUES (999, 499.00); -- 💥 ERROR: insert or update on table "orders" violates foreign key constraint "fk_orders_customer" -- DETAIL: Key (cust_id)=(999) is not present in table "customers".
When a parent row is deleted, foreign keys enforce referential actions: RESTRICT blocks the delete with a shield, CASCADE sweeps child rows away, and SET NULL severs the link safely.
When you delete a parent row that still has children, the foreign key’s ON DELETE clause decides the outcome. Pick it per relationship.
-- Parents CREATE SCHEMA IF NOT EXISTS practice; CREATE TABLE practice.blog_posts (post_id INT PRIMARY KEY, title VARCHAR(100) NOT NULL); CREATE TABLE practice.managers (manager_id INT PRIMARY KEY, name VARCHAR(50) NOT NULL); CREATE TABLE practice.departments (dept_id INT PRIMARY KEY, name VARCHAR(50) NOT NULL); -- Children: one ON DELETE rule each CREATE TABLE practice.comments (comment_id SERIAL PRIMARY KEY, post_id INT REFERENCES practice.blog_posts(post_id) ON DELETE CASCADE); -- disposable: delete them too CREATE TABLE practice.staff (staff_id SERIAL PRIMARY KEY, manager_id INT REFERENCES practice.managers(manager_id) ON DELETE SET NULL, -- reassignable: unlink (must allow NULL) dept_id INT NOT NULL REFERENCES practice.departments(dept_id) ON DELETE RESTRICT); -- critical: block it INSERT INTO practice.blog_posts VALUES (1, 'Intro to SQL'); INSERT INTO practice.comments (post_id) VALUES (1), (1); INSERT INTO practice.managers VALUES (7, 'Priya'); INSERT INTO practice.departments VALUES (10, 'Tech'); INSERT INTO practice.staff (manager_id, dept_id) VALUES (7, 10); DELETE FROM practice.blog_posts WHERE post_id = 1; -- CASCADE: both comments are deleted too DELETE FROM practice.managers WHERE manager_id = 7; -- SET NULL: staff row stays, manager_id becomes NULL SELECT staff_id, manager_id, dept_id FROM practice.staff LIMIT 10; -- Now try RESTRICT: remove the two dashes below and run again. It is blocked with Error 23503. -- DELETE FROM practice.departments WHERE dept_id = 10;
ON DELETE at all, PostgreSQL uses NO ACTION, which blocks the delete just like RESTRICT (it only differs for deferred constraints). For money tables, keep the block and use a soft delete (an is_active flag) instead of CASCADE.Modifying column rules and locking down multi-crore row tables without table locking or live outages
-- Setup: 2 practice tables, one phone number missing CREATE SCHEMA IF NOT EXISTS practice; CREATE TABLE practice.customers ( customer_id SERIAL PRIMARY KEY, phone_number VARCHAR(15)); CREATE TABLE practice.orders ( order_id SERIAL PRIMARY KEY, order_status VARCHAR(20)); INSERT INTO practice.customers (phone_number) VALUES ('9876543210'), (NULL); -- ⚠️ GOTCHA: SET NOT NULL fails on NULLs; fix them first: UPDATE practice.customers SET phone_number = '0000000000' WHERE phone_number IS NULL; -- Make the column mandatory, then remove the rule: ALTER TABLE practice.customers ALTER COLUMN phone_number SET NOT NULL; ALTER TABLE practice.customers ALTER COLUMN phone_number DROP NOT NULL; -- Auto-fill default for future inserts, then remove it: ALTER TABLE practice.orders ALTER COLUMN order_status SET DEFAULT 'pending'; ALTER TABLE practice.orders ALTER COLUMN order_status DROP DEFAULT; -- 💡 Affects only FUTURE inserts; old rows stay untouched. SELECT customer_id, phone_number FROM practice.customers LIMIT 10;
-- 💥 OUTAGE HAZARD on 2 crore rows: bare ADD CONSTRAINT -- takes an EXCLUSIVE lock and scans every row (timeouts). -- Practice copy of RetailMart's 1.5 lakh orders -- (RetailMart itself stays read-only): CREATE SCHEMA IF NOT EXISTS practice; CREATE TABLE practice.big_orders AS SELECT order_id, net_total FROM sales.orders; -- ✅ STEP 1: NOT VALID (instant <1ms lock!) -- enforces the rule for all FUTURE writes: ALTER TABLE practice.big_orders ADD CONSTRAINT chk_orders_positive_total CHECK (net_total >= 0) NOT VALID; -- ✅ STEP 2: validate old rows (SHARE UPDATE EXCLUSIVE -- lock): the app keeps reading and WRITING meanwhile! ALTER TABLE practice.big_orders VALIDATE CONSTRAINT chk_orders_positive_total; -- Proof: convalidated = true SELECT conname, convalidated FROM pg_constraint WHERE conname = 'chk_orders_positive_total' LIMIT 10; -- Drop any constraint by name (try it next): -- ALTER TABLE practice.big_orders -- DROP CONSTRAINT chk_orders_positive_total;
UPDATE before running SET NOT NULL. 2) In high-throughput databases (Flipkart, Amazon, Swiggy), never run bare ADD CONSTRAINT; always pair NOT VALID with background VALIDATE CONSTRAINT.How RetailMart protects ~24 lakh rows across 55 tables. These are its real CHECK rules: run the query below to see them yourself.
-- Query all constraints on any table via information_schema: SELECT constraint_name, constraint_type FROM information_schema.table_constraints WHERE table_schema = 'sales' AND table_name = 'orders' LIMIT 10; -- Inspect the exact CHECK expressions defined on your tables: SELECT constraint_name, check_clause FROM information_schema.check_constraints WHERE constraint_schema = 'sales' LIMIT 10;
The Ultimate Capstone Arena! Architect production schemas combining primary keys, foreign key laser bridges, domain checks, unique natural keys, and zero-downtime evolution.
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 governing relational database constraints and schema integrity.
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 (...).
Never run a bare ADD CONSTRAINT on large tables. Add constraints with NOT VALID first for instant sub-second locking, then run background VALIDATE CONSTRAINT.
When modeling appointments, hotel rooms, or reservations, standard UNIQUE fails. Use PostgreSQL Exclusion Constraints with GIST to physically guarantee zero overlapping time ranges.
Never place ON DELETE CASCADE across top-level financial entities. Rely on the default (NO ACTION, which blocks like RESTRICT) to shield master customer history and transaction ledgers from accidental cascading deletion.
Core PostgreSQL constraint definitions, advanced patterns, and ALTER syntax ready to copy directly into your terminal.
Complete these hands-on constraint drills in your local practice database before moving on to DISTINCT & Filtering.
Create hospital.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 hospital.operating_rooms preventing overlapping surgery times in the same room.
Take an existing table, simulate dirty rows, sanitize them, and apply a named CHECK rule using NOT VALID followed by VALIDATE CONSTRAINT.