Module 3

PostgreSQL

The world's most advanced open-source relational database. Learn SQL from your first table to joins, indexes, transactions, window functions and query tuning.

Tables & CRUD Joins Indexes Transactions JSONB
Level 1 — BasicsDatabases, setup, data types, tables and CRUD

What is PostgreSQL? Basic

A database stores data permanently and lets you query it quickly. PostgreSQL (often called "Postgres") is a relational database. Data lives in tables made of rows and columns, and tables are linked through keys.

Think of it like thisA database is an Excel workbook. Each table is a sheet, each column is a header with a fixed data type, and each row is one record. Unlike Excel, the rules (types, uniqueness, links between sheets) are enforced.
Anatomy of a table
users
idnameemailcity
1Ashaasha@mail.comPune
2Rahulrahul@mail.comDelhi
3Meerameera@mail.comPune

Primary key = unique ID for each row · highlighted = one row (record) · each header = a column (field)

Why Postgres?

Reliable (ACID)

Your data is safe even if the server crashes mid-write.

Feature-rich

JSONB, arrays, full-text search, window functions, extensions such as PostGIS and pgvector.

Free & open source

Used by Instagram, Reddit and Apple, and hosted by Neon, Supabase, RDS and others.

Kinds of SQL commands

GroupPurposeCommands
DDL DefinitionDefine structureCREATE ALTER DROP TRUNCATE
DML ManipulationChange dataINSERT UPDATE DELETE
DQL QueryRead dataSELECT
DCL ControlPermissionsGRANT REVOKE
TCL TransactionGroup changesBEGIN COMMIT ROLLBACK

Install & the psql shell Basic

Windows / macOS

Download the installer from postgresql.org. It includes pgAdmin, a GUI tool.

Docker (recommended)

One command, and it's easy to reset.

Cloud

Neon or Supabase give you a free hosted Postgres with a connection URL.

psql meta-commands
  • \llist databases
  • \c dbnameconnect
  • \dtlist tables
  • \d usersdescribe table
  • \dulist roles
  • \xexpanded output
  • \i file.sqlrun a file
  • \qquit
GUI tools
  • pgAdminofficial GUI
  • DBeaverfree, multi-DB
  • TablePlusfast & clean
  • VS CodePostgreSQL extension
Syntax rulesEnd every SQL statement with ;. Keywords are case-insensitive, but people usually write them in UPPERCASE. Use 'single quotes' for text and "double quotes" for identifiers. Comments start with --.

Data types Basic

CategoryTypeUse forExample
NumbersINT / BIGINTWhole numbers42
SERIAL / GENERATED … AS IDENTITYAuto-increment IDs1, 2, 3…
NUMERIC(10,2)Money and exact decimals1999.99
REAL / DOUBLE PRECISIONScientific values (approximate)3.14159
TextVARCHAR(n)Text with a max length'Asha'
TEXTAny lengthblog body
BooleanBOOLEANtrue/falseTRUE
Date/timeDATECalendar date'2026-10-06'
TIMESTAMPTZDate + time with time zone (preferred)NOW()
INTERVALA duration'7 days'
IDsUUIDGlobally unique IDsgen_random_uuid()
StructuredJSONBFlexible JSON documents'{"color":"red"}'
TEXT[]Arrays'{a,b,c}'
MoneyNever store money in REAL/FLOAT, because 0.1 + 0.2 ≠ 0.3. Use NUMERIC(12,2), or an INT holding paise/cents.

Creating tables & constraints Basic

Constraints are rules the database enforces, so bad data can't get in, whichever app writes it.

PRIMARY KEY

Unique + not null. Identifies each row.

FOREIGN KEY

Must match a row in another table (REFERENCES).

NOT NULL

A value is required.

UNIQUE

No duplicates allowed in this column.

CHECK

A custom condition, e.g. price >= 0.

DEFAULT

The value used when none is given.

ON DELETE options

OptionWhen the parent user is deleted…
CASCADETheir posts are deleted too
SET NULLposts.user_id becomes NULL
RESTRICT / defaultThe delete is blocked while posts exist

Changing a table

CRUD: Insert, Select, Update, Delete Basic

CRUD ↔ SQL ↔ HTTP
Create

INSERT
POST

Read

SELECT
GET

Update

UPDATE
PATCH/PUT

Delete

DELETE
DELETE

Always use WHERE with UPDATE / DELETEDELETE FROM users; deletes every row. To be safe, run the matching SELECT … WHERE first, or wrap the change in BEGIN; … ROLLBACK; to test it.

Upsert (insert or update)

Filtering, sorting & limits Basic

Logical order a SELECT is executed in
FROM / JOIN
WHERE
GROUP BY
HAVING
SELECT
ORDER BY
LIMIT

That's why you can't use a SELECT alias inside WHERE: WHERE runs before SELECT.

OperatorMeaning
= <> != < > <= >=Comparison
AND OR NOTCombine conditions (use brackets!)
LIKE 'a%' / ILIKEPattern (case-sensitive / insensitive)
IN (…), BETWEEN a AND bList, range
IS NULL, COALESCE(x, 'default')Handle missing values

Which query finds users with no phone number?

Level 2 — IntermediateAggregates, relationships, joins, subqueries, CTEs and normalization

Aggregates & GROUP BY Intermediate

Aggregate functions turn many rows into one value: COUNT, SUM, AVG, MIN and MAX. GROUP BY makes one group per distinct value, and the aggregate runs once per group.

GROUP BY city → COUNT(*)
namecity
AshaPune
RahulDelhi
MeeraPune
VikramPune
NehaDelhi
GROUP BY city
citycount
Pune3
Delhi2
WHERE
  • Filters individual rows
  • Runs before GROUP BY
  • Can't use aggregates
VS
HAVING
  • Filters groups
  • Runs after GROUP BY
  • Uses aggregates: HAVING SUM(x) > 100

Relationships & keys Intermediate

ER diagram — a small shop
users 🔑 idemailname profiles 🔑 id🔗 user_id UNIQUE orders 🔑 id🔗 user_idtotal order_items 🔗 order_id🔗 product_idqty products 🔑 idnameprice 1 : N 1 : 1 1 : N N : 1 orders ⇄ products = N : M (via join table order_items)
One-to-One

user ↔ profile. FK + UNIQUE on the child.

One-to-Many

user → orders. The FK goes on the "many" side (orders.user_id).

Many-to-Many

orders ↔ products. Needs a junction table with two FKs.

JOINs Intermediate

A JOIN combines rows from two tables using a matching column, usually FK = PK. A = users (left), B = orders (right).

Which rows come back?
AB INNER JOINonly matches in both
AB LEFT JOINall of A + matches (else NULL)
AB RIGHT JOINall of B + matches
AB FULL JOINeverything from both
Example: users LEFT JOIN orders
users
idname
1Asha
2Rahul
3Meera
+
orders
iduser_idtotal
101500
111300
122900
LEFT JOIN
result
nameordertotal
Asha10500
Asha11300
Rahul12900
MeeraNULLNULL

Meera has no orders. A LEFT JOIN keeps her with NULLs; an INNER JOIN would drop her.

You need ALL products, including ones never ordered. Which join from products to order_items?

Subqueries & CTEs Intermediate

A subquery is a query inside another query. A CTE (WITH) gives a subquery a name, so complex SQL reads top-to-bottom like steps.

Normalization (table design) Intermediate

Normalization means storing each fact in one place, so updates can't leave copies that disagree.

Splitting a bad table
❌ orders (repeated data)
idcustomercust_emailproducts
1Ashaasha@m.comMouse, Pad
2Ashaasha@m.comKeyboard
normalize
✓ users
idnameemail
1Ashaasha@m.com
✓ orders
iduser_id
11
21
FormRuleFix
1NFOne value per cell, no repeating groups"Mouse, Pad" → separate rows in order_items
2NFEvery column depends on the whole keyProduct name moves to products, not order_items
3NFNo column depends on another non-key columncustomer_email moves to users
Denormalize on purposeSometimes you copy data for speed or history. For example, order_items.unit_price stores the price at purchase time. That's a deliberate choice.
Level 3 — AdvancedIndexes, transactions, window functions, views, triggers, JSONB and tuning

Indexes Advanced

Book indexTo find "JOIN" in a 900-page book, you check the index at the back and go straight to page 412. You don't read every page. A database index works the same way for a column.
B-tree index on users.email
[ g | p ] a … f g … o p … z asha@… → row 1dev@… → row 9 meera@… → row 3neha@… → row 5 rahul@… → row 2vikram@… → row 4

A lookup takes a few hops (O(log n)) instead of scanning millions of rows.

Index when…
  • Column is used in WHERE / JOIN / ORDER BY
  • Foreign keys
  • High selectivity (many distinct values)
Avoid when…
  • Tiny tables
  • Write-heavy tables (each index slows INSERT/UPDATE)
  • Low selectivity (e.g. a boolean)

EXPLAIN ANALYZE & tuning Advanced

Before index
After CREATE INDEX
Plan nodeMeaning
Seq ScanReads the whole table. Fine for small tables, a red flag for big ones.
Index Scan / Index Only ScanUses an index (best)
Bitmap Heap ScanAn index finds many rows, which are then fetched in batches
Nested Loop / Hash Join / Merge JoinJoin strategies
Index killersWHERE LOWER(email) = … doesn't use a plain index on email; create an expression index instead. A leading wildcard like LIKE '%abc' can't use a B-tree. Run ANALYZE after big imports so the planner has fresh statistics.

Transactions & ACID Advanced

Atomic

All or nothing

Consistent

Constraints always hold

Isolated

Concurrent transactions don't interfere

Durable

Committed data survives a crash

Isolation levelPreventsNote
READ COMMITTEDDirty readsPostgres default
REPEATABLE READ+ non-repeatable readsA consistent snapshot for the whole transaction
SERIALIZABLE+ serialization anomaliesSafest. Be ready to retry on error 40001.
MVCCPostgres uses Multi-Version Concurrency Control: readers never block writers, and writers never block readers. Each transaction sees a snapshot. VACUUM (usually run by autovacuum) cleans up old row versions.

Window functions Advanced

Like GROUP BY, but rows are not collapsed. Each row keeps its detail and also gets a value calculated over a "window" of related rows.

RANK() OVER (PARTITION BY dept ORDER BY salary DESC)
namedeptsalaryrank
AshaEng150k1
RahulEng120k2
DevEng120k2
MeeraSales90k1
NehaSales70k2

Ranking restarts in each partition (dept). Every row is still there.

Views, functions & triggers Advanced

View

A saved query you can use like a table. It always shows live data.

Materialized view

Stores the result. Very fast to read, but you must REFRESH it.

Trigger

Runs a function automatically on INSERT, UPDATE or DELETE.

JSONB, arrays & full-text search Advanced

JSONB stores JSON in a binary format that can be indexed. Use it for flexible attributes (product specs, settings). Keep core fields in normal columns.

Roles, security & backups Advanced

Practice & ReviewInterview questions, exercises and a cheat sheet

Interview questions

DELETE removes the rows that match a WHERE clause, fires triggers and can be rolled back. TRUNCATE quickly removes all rows and keeps the table. DROP removes the table itself.

WHERE filters rows before grouping. HAVING filters groups after aggregation, so it can use COUNT, SUM and the other aggregates.

An index is a separate data structure (a B-tree by default) that speeds up lookups. It costs disk space, and it slows down writes because every INSERT and UPDATE must also update the index.

For tied values: ROW_NUMBER gives 1,2,3. RANK gives 1,2,2,4 (it leaves a gap). DENSE_RANK gives 1,2,2,3 (no gap).

SELECT DISTINCT salary FROM employees ORDER BY salary DESC LIMIT 1 OFFSET 1; You can also use DENSE_RANK() = 2 in a subquery.

Practice exercises

  1. Design a schemaLibrary system: books, authors (many-to-many), members and loans.
  2. Write queriesTop 5 customers by spend, products never ordered, monthly revenue growth using LAG.
  3. OptimizeInsert 1M rows with generate_series, compare EXPLAIN ANALYZE before and after adding indexes.

Cheat sheet

Structure
  • CREATE TABLEnew table
  • ALTER TABLE … ADD COLUMNchange
  • REFERENCES t(id)foreign key
  • CREATE INDEXspeed up
Data
  • INSERT … RETURNINGcreate
  • ON CONFLICT DO UPDATEupsert
  • UPDATE … WHEREupdate
  • DELETE … WHEREdelete
Query
  • JOIN … ONcombine
  • GROUP BY / HAVINGaggregate
  • WITH x AS (…)CTE
  • OVER (PARTITION BY)window
Ops
  • EXPLAIN ANALYZEprofile
  • BEGIN / COMMITtransaction
  • pg_dumpbackup
  • VACUUM ANALYZEmaintenance
PostgreSQL notes