Module 1, Session 6. PRIMARY KEY and identity columns, FOREIGN KEY and orphan rows, 1:1, 1:N and M:N relationships, ON DELETE and ON UPDATE rules, self-referencing keys, and safe ALTER TABLE changes on live tables.
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.
Give every row one permanent identity, link tables so no row can point at something that does not exist, and decide what happens when a parent row is deleted.
Four pillars: give every row an identity, then link tables safely
PRIMARY KEY & Identity Columns
FOREIGN KEY & Relationship Types: 1:1, 1:N, M:N
ON DELETE: RESTRICT, CASCADE, SET NULL & the default NO ACTION
ON UPDATE
ALTER TABLE — Add & Drop Rules
NOT VALID
DISTINCT & Filtering
From rules on one column to links between tables
-- in the practice database (\c practice) CREATE SCHEMA IF NOT EXISTS keys; -- start clean, safe to re-run DROP TABLE IF EXISTS keys.exam_candidates, keys.halls CASCADE; CREATE TABLE keys.halls ( hall_id INT PRIMARY KEY, hall_name TEXT NOT NULL ); CREATE TABLE keys.exam_candidates ( roll_no INT PRIMARY KEY, name TEXT NOT NULL, hall_id INT REFERENCES keys.halls(hall_id) ON DELETE RESTRICT ); -- One roll number = one candidate. No ghost halls.
Each results-day mistake maps to one key or delete rule that PostgreSQL enforces on every write:
| Results-Day Mistake | Key or Delete Rule | Production Defense Guarantee |
|---|---|---|
| 👥 Roll 1047 Given to Two Candidates | PRIMARY KEY (roll_no) | The second INSERT with roll 1047 fails (Error 23505): one key value = one row, forever. |
| 📝 Candidate With No Roll Number | PRIMARY KEY ⇒ NOT NULL | A primary key column can never be NULL (Error 23502), so every row can be found by its key. |
| 🏢 Result Points at Hall 99 | FOREIGN KEY (hall_id) | hall_id must exist in halls; Hall 99 is rejected (Error 23503) — no orphan rows. |
| 🚫 Hall 4 Deleted With 300 Candidates | ON DELETE RESTRICT | The DELETE is blocked while candidates still point at Hall 4. With no rule written, the default NO ACTION blocks it too. |
| 🗑️ Exam Cancelled: Remove Its Seat List | ON DELETE CASCADE | Deleting the exam deletes its seat allocations with it — for child rows that mean nothing alone. |
| 👤 Invigilator Leaves the University | ON DELETE SET NULL | The halls stay; their invigilator_id becomes NULL until someone new is assigned. |
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).-- in the practice database (\c practice) CREATE SCHEMA IF NOT EXISTS keys; -- start clean, safe to re-run DROP TABLE IF EXISTS keys.taxpayers CASCADE; CREATE TABLE keys.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 keys.taxpayers (pan_number, email, full_name) VALUES ('ABCDE1234F', '[email protected]', 'Priya Sharma'); SELECT taxpayer_id, pan_number, email, full_name FROM keys.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.-- in the practice database (\c practice) CREATE SCHEMA IF NOT EXISTS keys; -- 1. Modern single-column surrogate primary key: -- start clean, safe to re-run DROP TABLE IF EXISTS keys.shop_order_items, keys.shop_orders CASCADE; CREATE TABLE keys.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 keys.shop_order_items ( order_id BIGINT REFERENCES keys.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 keys.shop_orders (total) VALUES (2499.00); -- order_id 1 is generated INSERT INTO keys.shop_order_items VALUES (1, 1, 2), (1, 2, 1); -- same order, different items: OK SELECT order_id, item_id, quantity FROM keys.shop_order_items LIMIT 10; -- Now try (FAILS with Error 23505): the pair (1, 1) already exists -- INSERT INTO keys.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 -- in the practice database (\c practice) CREATE SCHEMA IF NOT EXISTS keys; -- start clean, safe to re-run DROP TABLE IF EXISTS keys.orders, keys.customers CASCADE; CREATE TABLE keys.customers ( cust_id INT PRIMARY KEY, full_name VARCHAR(100) NOT NULL ); -- Step 2: Child table enforces referential relationship CREATE TABLE keys.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 keys.customers(cust_id) ); -- Valid write: customer 101 exists in parent table INSERT INTO keys.customers (cust_id, full_name) VALUES (101, 'Aarav Sharma'); INSERT INTO keys.orders (cust_id, amount) VALUES (101, 2499.00); -- OK! -- Blocked write: customer 999 does not exist (Orphan Shield fires) INSERT INTO keys.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".
Where you put the foreign key decides how two tables relate. Three shapes cover every database you will meet, and RetailMart has all three.
orders.cust_id → customers. RetailMart: 50,000 customers and 1,50,000 orders. The most common link there is.loyalty.members(customer_id), 30,000 members, one per customer.products.inventory, 1,14,153 rows linking 6,036 products and 200 stores.-- Many-to-many: students and courses, linked by a bridge table -- in the practice database (\c practice) CREATE SCHEMA IF NOT EXISTS keys; -- start clean, safe to re-run DROP TABLE IF EXISTS keys.enrollments, keys.courses, keys.students CASCADE; CREATE TABLE keys.students ( student_id INT PRIMARY KEY, full_name VARCHAR(100) NOT NULL ); CREATE TABLE keys.courses ( course_id INT PRIMARY KEY, title VARCHAR(100) NOT NULL ); -- The bridge: one row per (student, course) pair, two foreign keys CREATE TABLE keys.enrollments ( student_id INT REFERENCES keys.students(student_id), course_id INT REFERENCES keys.courses(course_id), enrolled_on DATE NOT NULL DEFAULT CURRENT_DATE, PRIMARY KEY (student_id, course_id) ); INSERT INTO keys.students VALUES (1, 'Aarav'), (2, 'Diya'); INSERT INTO keys.courses VALUES (10, 'SQL'), (20, 'Python'); -- Aarav takes 2 courses, SQL has 2 students: many-to-many INSERT INTO keys.enrollments (student_id, course_id) VALUES (1, 10), (1, 20), (2, 10); -- The same pair twice is blocked by the composite PRIMARY KEY INSERT INTO keys.enrollments (student_id, course_id) VALUES (1, 10); -- 💥 ERROR: duplicate key value violates unique constraint "enrollments_pkey" -- DETAIL: Key (student_id, course_id)=(1, 10) already exists.
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 -- in the practice database (\c practice) CREATE SCHEMA IF NOT EXISTS keys; -- start clean, safe to re-run DROP TABLE IF EXISTS keys.staff, keys.comments, keys.departments, keys.managers, keys.blog_posts CASCADE; CREATE TABLE keys.blog_posts (post_id INT PRIMARY KEY, title VARCHAR(100) NOT NULL); CREATE TABLE keys.managers (manager_id INT PRIMARY KEY, name VARCHAR(50) NOT NULL); CREATE TABLE keys.departments (dept_id INT PRIMARY KEY, name VARCHAR(50) NOT NULL); -- Children: one ON DELETE rule each CREATE TABLE keys.comments (comment_id SERIAL PRIMARY KEY, post_id INT REFERENCES keys.blog_posts(post_id) ON DELETE CASCADE); -- disposable: delete them too CREATE TABLE keys.staff (staff_id SERIAL PRIMARY KEY, manager_id INT REFERENCES keys.managers(manager_id) ON DELETE SET NULL, -- reassignable: unlink (must allow NULL) dept_id INT NOT NULL REFERENCES keys.departments(dept_id) ON DELETE RESTRICT); -- critical: block it INSERT INTO keys.blog_posts VALUES (1, 'Intro to SQL'); INSERT INTO keys.comments (post_id) VALUES (1), (1); INSERT INTO keys.managers VALUES (7, 'Priya'); INSERT INTO keys.departments VALUES (10, 'Tech'); INSERT INTO keys.staff (manager_id, dept_id) VALUES (7, 10); DELETE FROM keys.blog_posts WHERE post_id = 1; -- CASCADE: both comments are deleted too DELETE FROM keys.managers WHERE manager_id = 7; -- SET NULL: staff row stays, manager_id becomes NULL SELECT staff_id, manager_id, dept_id FROM keys.staff LIMIT 10; -- Now try RESTRICT: remove the two dashes below and run again. It is blocked with Error 23503. -- DELETE FROM keys.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.A foreign key can point at its own table: every employee’s manager is another employee. ON UPDATE decides what happens to the children when a parent’s key value changes.
manager_id REFERENCES employees(emp_id). The classic interview schema: employees and managers, or categories and sub-categories.manager_id is NULL. A NULL foreign key points at nothing and is not checked. Insert the boss first, then the team.manager_id follows by itself. With no ON UPDATE rule (NO ACTION), that UPDATE is blocked.-- A foreign key that points at its own table -- in the practice database (\c practice) CREATE SCHEMA IF NOT EXISTS keys; -- start clean, safe to re-run DROP TABLE IF EXISTS keys.employees CASCADE; CREATE TABLE keys.employees ( emp_id INT PRIMARY KEY, full_name VARCHAR(100) NOT NULL, manager_id INT REFERENCES keys.employees(emp_id) ON DELETE SET NULL ON UPDATE CASCADE ); -- Boss first (no manager), then the team INSERT INTO keys.employees VALUES (1, 'Meera', NULL), (2, 'Rohan', 1), (3, 'Kavita', 1), (4, 'Arjun', 2); -- ON UPDATE CASCADE: Rohan's id changes from 2 to 20, Arjun follows UPDATE keys.employees SET emp_id = 20 WHERE emp_id = 2; SELECT emp_id, full_name, manager_id FROM keys.employees ORDER BY emp_id; -- Arjun: manager_id 2 -> 20 -- ON DELETE SET NULL: Rohan leaves, Arjun stays with no manager DELETE FROM keys.employees WHERE emp_id = 20; SELECT emp_id, full_name, manager_id FROM keys.employees ORDER BY emp_id; -- Arjun: manager_id 20 -> NULL -- A manager who does not exist is still an orphan INSERT INTO keys.employees VALUES (5, 'Sana', 99); -- 💥 ERROR: ... violates foreign key constraint "employees_manager_id_fkey" -- DETAIL: Key (manager_id)=(99) is not present in table "employees".
Modifying column rules and locking down multi-crore row tables without table locking or live outages
-- Setup: 2 practice tables, one phone number missing -- in the practice database (\c practice) CREATE SCHEMA IF NOT EXISTS keys; -- start clean, safe to re-run DROP TABLE IF EXISTS keys.orders, keys.customers CASCADE; CREATE TABLE keys.customers ( customer_id SERIAL PRIMARY KEY, phone_number VARCHAR(15)); CREATE TABLE keys.orders ( order_id SERIAL PRIMARY KEY, order_status VARCHAR(20)); INSERT INTO keys.customers (phone_number) VALUES ('9876543210'), (NULL); -- ⚠️ GOTCHA: SET NOT NULL fails on NULLs; fix them first: UPDATE keys.customers SET phone_number = '0000000000' WHERE phone_number IS NULL; -- Make the column mandatory, then remove the rule: ALTER TABLE keys.customers ALTER COLUMN phone_number SET NOT NULL; ALTER TABLE keys.customers ALTER COLUMN phone_number DROP NOT NULL; -- Auto-fill default for future inserts, then remove it: ALTER TABLE keys.orders ALTER COLUMN order_status SET DEFAULT 'pending'; ALTER TABLE keys.orders ALTER COLUMN order_status DROP DEFAULT; -- 💡 Affects only FUTURE inserts; old rows stay untouched. SELECT customer_id, phone_number FROM keys.customers LIMIT 10;
-- 💥 OUTAGE HAZARD on 2 crore rows: bare ADD CONSTRAINT -- takes an EXCLUSIVE lock and scans every row (timeouts). -- 1,50,000 practice orders (as many as RetailMart has), -- made with generate_series, so it runs anywhere: -- in the practice database (\c practice) CREATE SCHEMA IF NOT EXISTS keys; -- start clean, safe to re-run DROP TABLE IF EXISTS keys.big_orders CASCADE; CREATE TABLE keys.big_orders AS SELECT g AS order_id, ROUND((g % 5000) * 1.5 + 99, 2) AS net_total FROM generate_series(1, 150000) AS g; -- ✅ STEP 1: NOT VALID (a very short lock) -- enforces the rule for all FUTURE writes: ALTER TABLE keys.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 keys.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 keys.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 Keys Capstone Arena! Build schemas that combine primary keys, foreign keys, ON DELETE rules and the constraints from last class.
Top real-world questions asked by staff engineers and senior database architects.
UNIQUE + NOT NULL. The PRIMARY KEY is the row’s official identity — the column other tables point at with a FOREIGN KEY — while UNIQUE protects business values like an email address.
Close your notes. Answer from memory. Click any card to reveal the verified answer.
The non-negotiable architectural principles for every key you design.
A PRIMARY KEY is UNIQUE + NOT NULL, one per table. Prefer a surrogate (GENERATED ALWAYS AS IDENTITY) and keep natural ids like email as UNIQUE.
The child column may only hold values that exist in the parent’s key. An order for customer 999 that does not exist is rejected with Error 23503.
Insert the parent row before the child that points at it; delete children before the parent. Give the FK column the same type as the parent key (INT → INT).
Never run a bare ADD CONSTRAINT on large tables. Add constraints with NOT VALID first (a very short lock), then run VALIDATE CONSTRAINT.
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.
Primary keys, foreign keys, ON DELETE rules and safe ALTER syntax, ready to copy into your terminal.
Complete these hands-on key drills in your local practice database before moving on to DISTINCT & Filtering.
🎯 400 practice questions for this session · Easy to Crazy hard, run and checked on the pagePRACTICE →In practice, create customers and orders, each with a GENERATED ALWAYS AS IDENTITY PRIMARY KEY. Add a FOREIGN KEY from orders.customer_id to customers.
Write 4 INSERTs that each fail for a different reason: NOT NULL (23502), UNIQUE (23505), CHECK (23514) and FOREIGN KEY (23503). Save each error message.
Add order_items with ON DELETE CASCADE to orders. Delete one order and confirm with a SELECT that its items are gone too.
Try to delete a customer who still has orders and read the 23503 error. Then add is_active BOOLEAN NOT NULL DEFAULT true and “delete” the customer with an UPDATE instead.