PgPostgreSQL বাংলা
Open Courseware · Database Systems

PostgreSQL,
zero to advanced

A twenty three module class that assumes you know nothing. It goes from what a database is to recursive queries, PL/pgSQL, row level security, replication and pgvector, with a real PostgreSQL Practice Lab in your browser.

Start Module 01 →
1N1N studentsid PKfull_nameemail UNIQUEgpa coursesid PKcode UNIQUEtitlecredits enrollmentsstudent_id FKcourse_id FKgrade
Duration
≈ 80 hours
Modules
23
Prerequisite
None at all
Version
PostgreSQL 16+
  1. FoundationsModules 01 to 08

    What a database is, install on Windows, SQL basics, tables, CRUD, queries and expressions.

  2. IntermediateModules 09 to 14

    Constraints, ALTER, subqueries, indexes, full text search and transactions.

  3. AdvancedModules 15 to 22

    Window functions, recursion, PL/pgSQL, triggers, partitioning, replication, security, RLS and pgvector.

Module 01 · Beginner

Start Here: Class Zero

This course assumes you have never written code and never heard of a database. Read the modules in order, like classes. After each one, open the Practice Lab in the top bar and type the examples yourself.

What is data?

Data is just recorded facts: a student's name, a product's price, today's temperature. Every app you use (banking, social media, shopping) stores and reads data all day.

What is a database?

A database is an organised, safe place to keep data so it can be found fast. Think of a school office. A loose pile of papers is hard to search. A filing system with labelled cabinets is not. The program that runs the filing system, storing, finding and protecting the papers, is called a DBMS (database management system). PostgreSQL is one.

What is a table?

Data inside a relational database lives in tables, like one sheet in a spreadsheet. Columns are the labels, and each row is one record, here one student.

idfull_nameemailgpa
1Ayesha Rahmanayesha@uni.edu3.80
2Tanvir Hasantanvir@uni.edu3.40

What is SQL?

SQL (Structured Query Language) is how you talk to the database. You describe what you want, and the database works out how. SELECT full_name FROM students reads almost like English: "show me the full names from students".

What is PostgreSQL?

PostgreSQL (say "post-gres-Q-L", or just "Postgres") is a free, open source DBMS. It runs as a server. Like a restaurant, you (the client) give an order (SQL) to the waiter, and the kitchen (the server) prepares and returns the result.

How to study

  • Green badges are beginner, yellow intermediate, pink advanced. Do not skip ahead.
  • Type every example yourself. Press Try on a code block to send it to the lab.
  • Errors are normal and useful. Break things on purpose.
Module 02 · Beginner

Introduction and Setup

PostgreSQL is an open source object-relational database with more than 30 years of development. It is respected for correctness, strict SQL standards support and extensibility: JSONB, full text search, PostGIS, custom types and extensions.

How data is organised

  1. Clusterone running server
  2. Databaseisolated collection
  3. Schemanamespace, default public
  4. Tablerows and columns
Shell
# Debian or Ubuntu
sudo apt install postgresql
sudo -u postgres psql

# macOS (Homebrew)
brew install postgresql@16
brew services start postgresql@16
psql postgres

Connect with psql, create a database and look around:

SQL
CREATE DATABASE university;
-- Meta commands work only inside psql.
-- \c connect, \l list databases, \dt list tables, \d describe, \q quit
\c university
Module 03 · Beginner

Install PostgreSQL on Windows

The official installer is about 300 MB and includes the server, the psql command line and the pgAdmin app.

  1. Open postgresql.org/download/windows and choose Download the installer. Pick the newest version (16 or higher) for Windows x86 64.
  2. Run the downloaded .exe and allow administrator access.
  3. Keep the default folder and keep these components ticked: PostgreSQL Server, pgAdmin 4, Command Line Tools.
  4. Choose a password for the superuser postgres and write it down. It is hard to recover.
  5. Keep the default port 5432 and the default locale, then finish.
  6. Open the Start menu and run SQL Shell (psql). Press Enter four times to accept the defaults (server, database, port, user), then type your password. The password stays invisible while typing, which is normal.
  7. Test the installation:
SQL
SELECT version();
CREATE DATABASE university;
\c university

Other ways

You can install from PowerShell with winget, or run PostgreSQL in Docker without installing it.

Shell
# Optional: install with winget (PowerShell as Administrator)
winget install PostgreSQL.PostgreSQL.16

# Connect from PowerShell after adding C:\Program Files\PostgreSQL\16\bin to PATH
psql -U postgres -h localhost
Shell
docker run --name pg -e POSTGRES_PASSWORD=secret -p 5432:5432 -d postgres:16
docker exec -it pg psql -U postgres

If something goes wrong

ProblemFix
psql is not recognizedUse SQL Shell (psql) from the Start menu, or add the PostgreSQL bin folder to PATH
password authentication failedYou typed the wrong postgres password. Use the one chosen at install time
port 5432 already in useAnother PostgreSQL is running. Stop it or pick another port during install
Nothing appears when I type the passwordThat is normal, the shell hides it. Type it and press Enter
Module 04 · Beginner

SQL Basics: Your First Statements

Before real queries, learn the grammar. A SQL statement is like a sentence: a keyword says what to do, and the rest says to what.

  • Every statement ends with a semicolon ;.
  • Keywords such as SELECT are not case sensitive. Convention: UPPERCASE keywords, lowercase names.
  • Text goes in single quotes like 'this'. Double quotes are for unusual names.
  • Comments: -- one line or /* many lines */.
  • Spaces and line breaks do not matter, so format for readability.
  • Name things in lowercase_with_underscores, with no spaces.
SQL
-- Your first statements
SELECT 'Hello, PostgreSQL!';
SELECT 2 + 3 * 4 AS answer;
SELECT current_date, now();
SELECT 'Ayesha' || ' ' || 'Rahman' AS full_name;

Anatomy of a statement

SQL
SELECT full_name FROM students WHERE gpa > 3.5;
PartMeaning
SELECT full_namewhich columns to show
FROM studentswhich table to read
WHERE gpa > 3.5which rows to keep
;end of the statement

The five command groups

GroupCommandsPurpose
DDLCREATE, ALTER, DROPDefine structure
DMLINSERT, UPDATE, DELETEChange data
DQLSELECTRead data
DCLGRANT, REVOKEControl access
TCLBEGIN, COMMIT, ROLLBACKGroup changes safely

Reading error messages

  • syntax error at or near "x": a typo close to the word x.
  • relation "x" does not exist: wrong table name, or it is not created yet.
  • column "x" does not exist: misspelled column.
PracticeShow your own name and today's date in one statement.
SQL
SELECT 'Your Name' AS name, current_date AS today;
Module 05 · Beginner

Tables and Data Types

A table stores rows; every column has a type that PostgreSQL enforces. Picking the right type is the cheapest form of data validation.

TypeUse it for
SMALLINT, INTEGER, BIGINTWhole numbers of growing size
NUMERIC(p,s)Exact decimals such as money
TEXTStrings of any length (use VARCHAR(n) only when the limit is a business rule)
BOOLEANtrue or false
DATE, TIMESTAMPTZCalendar dates and exact moments in time
UUIDGlobally unique identifiers
JSONBSemi structured documents that can be indexed
type[], ENUMLists, and small fixed sets of labels
SQL
CREATE TABLE students (
    id          BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    full_name   TEXT         NOT NULL,
    email       TEXT         NOT NULL UNIQUE,
    gpa         NUMERIC(3,2) CHECK (gpa BETWEEN 0 AND 4),
    enrolled_at TIMESTAMPTZ  NOT NULL DEFAULT now()
);
Module 06 · Beginner

CRUD: Create, Read, Update, Delete

The four everyday operations map to INSERT, SELECT, UPDATE and DELETE. RETURNING gives back the affected rows without a second query, and ON CONFLICT turns an insert into an upsert.

SQL
INSERT INTO students (full_name, email, gpa)
VALUES ('Ayesha Rahman', 'ayesha@uni.edu', 3.80),
       ('Tanvir Hasan',  'tanvir@uni.edu', 3.40)
RETURNING id;

UPDATE students SET gpa = 3.90
WHERE email = 'ayesha@uni.edu'
RETURNING id, gpa;

DELETE FROM students WHERE gpa < 1.0;

-- Upsert: insert, or update when the key already exists
INSERT INTO courses (code, title, credits)
VALUES ('CS101', 'Intro to CS', 3)
ON CONFLICT (code) DO UPDATE SET title = EXCLUDED.title;
Module 07 · Beginner

Querying: Filters, Joins, Aggregates

SQL is written in one order but evaluated in another. Knowing the real order explains most beginner errors, such as why a column alias cannot be used in WHERE.

FROM, JOIN then WHERE then GROUP BY then HAVING then SELECT then ORDER BY then LIMIT.

SQL
SELECT c.code,
       count(*)              AS enrolled,
       round(avg(e.grade),2) AS avg_grade
FROM courses c
JOIN enrollments e ON e.course_id = c.id
GROUP BY c.code
HAVING count(*) >= 5
ORDER BY enrolled DESC
LIMIT 3;
Result
codeenrolledavg_grade
CS101423.12
MA201372.88
PH110293.41

Join types

  • INNER JOIN: only rows that match on both sides.
  • LEFT JOIN: every left row, with NULL where nothing matches.
  • FULL JOIN: everything from both sides.
SQL
-- Students who have not enrolled in anything
SELECT s.full_name
FROM students s
LEFT JOIN enrollments e ON e.student_id = s.id
WHERE e.student_id IS NULL;
PracticeList every course that has no enrolled student.
SQL
SELECT c.code
FROM courses c
LEFT JOIN enrollments e ON e.course_id = c.id
WHERE e.course_id IS NULL;
Module 08 · Beginner

Functions and Expressions

PostgreSQL ships with hundreds of built in functions. Four families cover most daily work: text, numbers, dates and NULL handling.

FamilyCommon tools
Textlower(), upper(), trim(), replace(), split_part(), format(), ||, ILIKE
Numbersround(), ceil(), floor(), abs(), mod(), random()
Date and timenow(), current_date, interval, date_trunc(), extract(), age(), to_char()
NULL and castingCOALESCE(), NULLIF(), IS DISTINCT FROM, ::type, CAST()
SQL
SELECT lower(email)                        AS email,
       full_name || ' <' || email || '>'   AS display,
       COALESCE(gpa, 0)                    AS gpa_or_zero,
       date_trunc('month', enrolled_at)    AS month,
       now() - enrolled_at                 AS time_enrolled,
       to_char(enrolled_at, 'DD Mon YYYY') AS pretty_date,
       gpa::int                            AS gpa_rounded
FROM students
WHERE full_name ILIKE 'a%'
  AND enrolled_at >= now() - interval '1 year';
Module 09 · Intermediate

Constraints, Relationships, Normalization

Constraints let the database defend its own data: PRIMARY KEY, FOREIGN KEY, UNIQUE, NOT NULL and CHECK. Application code has bugs; constraints do not forget.

  • One to many: a foreign key on the many side.
  • Many to many: a junction table with a composite key, as below.
  • One to one: a foreign key that is also UNIQUE.
SQL
CREATE TABLE courses (
    id      BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    code    TEXT     NOT NULL UNIQUE,
    title   TEXT     NOT NULL,
    credits SMALLINT NOT NULL CHECK (credits > 0)
);

-- Junction table for the many to many relationship
CREATE TABLE enrollments (
    student_id BIGINT NOT NULL REFERENCES students(id) ON DELETE CASCADE,
    course_id  BIGINT NOT NULL REFERENCES courses(id)  ON DELETE RESTRICT,
    grade      NUMERIC(3,2),
    PRIMARY KEY (student_id, course_id)
);

Normalization in three lines

  • 1NF: one value per cell, no repeating groups.
  • 2NF: every column depends on the whole key.
  • 3NF: no column depends on another non key column.
Module 10 · Intermediate

Changing Structure: ALTER, Schemas, Sequences

Real projects change. ALTER TABLE adds, renames or removes columns without recreating the table. A schema is a folder inside a database that groups tables. A sequence is a counter that hands out numbers (identity columns use one internally).

SQL
-- Add, rename, default and remove a column
ALTER TABLE students ADD COLUMN phone TEXT;
ALTER TABLE students RENAME COLUMN phone TO mobile;
ALTER TABLE students ALTER COLUMN mobile SET DEFAULT '';
ALTER TABLE students DROP COLUMN mobile;

-- A schema groups tables like a folder
CREATE SCHEMA library;
CREATE TABLE library.books (id INT PRIMARY KEY, title TEXT NOT NULL);
SET search_path TO library, public;

-- A sequence is a counter
CREATE SEQUENCE ticket_no START 1000;
SELECT nextval('ticket_no');
Module 11 · Intermediate

Subqueries, Set Operations, CASE

A subquery is a query inside a query: scalar (one value), IN (a list) or EXISTS (is there a match). EXISTS stops at the first hit, so on large data it often beats IN.

  • UNION: combine and remove duplicates. UNION ALL: combine and keep duplicates (faster).
  • INTERSECT: rows present in both. EXCEPT: rows in the first only.
SQL
-- Scalar subquery: students above the average GPA
SELECT full_name, gpa
FROM students
WHERE gpa > (SELECT avg(gpa) FROM students);

-- EXISTS stops at the first match
SELECT c.code
FROM courses c
WHERE EXISTS (SELECT 1 FROM enrollments e WHERE e.course_id = c.id);

-- Set operation (assumes an alumni table with the same email column)
SELECT email FROM students
EXCEPT
SELECT email FROM alumni;

-- CASE is a conditional expression
SELECT full_name,
       CASE WHEN gpa >= 3.7 THEN 'honours'
            WHEN gpa >= 2.0 THEN 'pass'
            ELSE 'probation' END AS standing
FROM students;
Module 12 · Intermediate

Indexes and Query Plans

Without an index PostgreSQL reads the whole table (Seq Scan). A B-tree index, the default, finds rows in logarithmic time and also serves sorting and range filters.

IndexBest for
B-treeEquality, ranges, ORDER BY (default)
GINJSONB, arrays, full text search
GiSTGeometry, ranges, nearest neighbour
BRINHuge append only tables ordered by time
HashEquality only (rarely better than B-tree)
SQL
-- Index foreign keys and columns used in WHERE, JOIN and ORDER BY
CREATE INDEX idx_enroll_course ON enrollments (course_id);

-- Partial index: smaller and faster for a common filter
CREATE INDEX idx_students_top ON students (gpa DESC) WHERE gpa >= 3.5;

EXPLAIN (ANALYZE, BUFFERS)
SELECT student_id, grade FROM enrollments WHERE course_id = 42;

Read the plan from the innermost line outward. Compare Seq Scan and Index Scan, the estimated and actual row counts, and the actual time. A big gap between estimate and actual usually means stale statistics: run ANALYZE.

Module 13 · Intermediate

Pattern Matching and Full Text Search

There are three levels: LIKE and ILIKE with % wildcards, regular expressions with ~, and full text search, which understands words, stemming (indexes matches index) and ranking.

ToolUse it forIndex
LIKE, ILIKESimple patternsB-tree helps only for 'abc%'; pg_trgm for '%abc%'
~ (regex)Complex patternspg_trgm
tsvector @@ tsqueryWord search with rankingGIN
SQL
-- Simple pattern and regular expression matching
SELECT full_name FROM students WHERE full_name ILIKE '%rahman%';
SELECT email FROM students WHERE email ~ '^[a-z]+@uni\.edu$';

-- Full text search with a generated column and a GIN index
CREATE TABLE articles (
    id    SERIAL PRIMARY KEY,
    title TEXT NOT NULL,
    body  TEXT NOT NULL,
    tsv   tsvector GENERATED ALWAYS AS (to_tsvector('english', title || ' ' || body)) STORED
);
CREATE INDEX idx_articles_tsv ON articles USING GIN (tsv);
INSERT INTO articles (title, body) VALUES
    ('Indexes', 'A B-tree index speeds up lookups'),
    ('Joins', 'Combine rows from two tables');

SELECT title, ts_rank(tsv, q) AS score
FROM articles, websearch_to_tsquery('english', 'index lookup') AS q
WHERE tsv @@ q
ORDER BY score DESC;
Module 14 · Intermediate

Transactions, MVCC and Locking

A transaction groups statements so they succeed or fail together (ACID: atomicity, consistency, isolation, durability). PostgreSQL uses MVCC: each transaction sees a consistent snapshot, so readers never block writers.

Isolation levelBehaviour
READ COMMITTED (default)Each statement sees data committed before it started
REPEATABLE READThe whole transaction sees one snapshot
SERIALIZABLEBehaves as if transactions ran one after another; retry on failure
SQL
BEGIN;
UPDATE accounts SET balance = balance - 500 WHERE id = 1;
UPDATE accounts SET balance = balance + 500 WHERE id = 2;
COMMIT;  -- or ROLLBACK to undo everything since BEGIN

-- Lock a row while you decide what to do with it
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
COMMIT;
Module 15 · Advanced

Advanced SQL: CTEs, Windows, JSONB, Views

CTEs (WITH) name an intermediate result and keep complex queries readable. Window functions compute across related rows without collapsing them like GROUP BY does: rank(), row_number(), lag(), running sums.

SQL
-- CTE plus window function: top 3 grades per course
WITH ranked AS (
    SELECT course_id, student_id, grade,
           rank() OVER (PARTITION BY course_id ORDER BY grade DESC) AS pos
    FROM enrollments
)
SELECT course_id, student_id, grade
FROM ranked
WHERE pos <= 3;

JSONB

Store flexible attributes next to relational columns, and index them with GIN.

SQL
ALTER TABLE students ADD COLUMN profile JSONB NOT NULL DEFAULT '{}';
CREATE INDEX idx_students_profile ON students USING GIN (profile);

SELECT full_name
FROM students
WHERE profile @> '{"club": "robotics"}';

Views

A view saves a query. A materialized view saves its result, trading freshness for speed.

SQL
CREATE MATERIALIZED VIEW course_stats AS
SELECT course_id, count(*) AS n, round(avg(grade),2) AS avg_grade
FROM enrollments
GROUP BY course_id;

CREATE UNIQUE INDEX ON course_stats (course_id);
REFRESH MATERIALIZED VIEW CONCURRENTLY course_stats;
Module 16 · Advanced

Recursive Queries and Hierarchies

A recursive CTE repeats a query on its own output. It suits trees and graphs: org charts, category menus, folders, bills of materials. It joins two parts with UNION ALL: the anchor (starting rows) and the recursive step (finds the next level). It stops when the step returns no new rows.

SQL
CREATE TABLE staff (
    id      INT PRIMARY KEY,
    name    TEXT NOT NULL,
    boss_id INT REFERENCES staff(id)
);
INSERT INTO staff VALUES (1,'Dean',NULL), (2,'Head of CS',1), (3,'Lecturer A',2), (4,'Lecturer B',2), (5,'Head of Math',1);

WITH RECURSIVE chain AS (
    SELECT id, name, boss_id, 0 AS depth
    FROM staff
    WHERE boss_id IS NULL
    UNION ALL
    SELECT s.id, s.name, s.boss_id, c.depth + 1
    FROM staff s
    JOIN chain c ON s.boss_id = c.id
    WHERE c.depth < 20
)
SELECT repeat('    ', depth) || name AS org_chart, depth
FROM chain
ORDER BY depth, id;
Module 17 · Advanced

PL/pgSQL Programming

PL/pgSQL adds variables, IF, loops and error handling to SQL. Use functions when a query needs a returned value, procedures (called with CALL) to run steps, and DO blocks for one off scripts.

  • DECLARE variables and SELECT ... INTO to fill them.
  • IF, ELSIF, FOR ... LOOP for control flow.
  • EXCEPTION WHEN ... THEN to handle errors, RAISE to report them.
SQL
CREATE OR REPLACE FUNCTION grade_letter(score NUMERIC) RETURNS TEXT
LANGUAGE plpgsql IMMUTABLE AS $$
BEGIN
    IF score IS NULL THEN
        RETURN NULL;
    ELSIF score >= 3.7 THEN
        RETURN 'A';
    ELSIF score >= 3.0 THEN
        RETURN 'B';
    ELSE
        RETURN 'C';
    END IF;
END;
$$;

SELECT full_name, grade_letter(gpa) AS letter FROM students;

CREATE PROCEDURE enroll_student(p_student BIGINT, p_course BIGINT)
LANGUAGE plpgsql AS $$
DECLARE
    seats INT;
BEGIN
    SELECT count(*) INTO seats FROM enrollments WHERE course_id = p_course;
    IF seats >= 30 THEN
        RAISE EXCEPTION 'Course % is full', p_course;
    END IF;
    INSERT INTO enrollments (student_id, course_id) VALUES (p_student, p_course);
EXCEPTION WHEN unique_violation THEN
    RAISE NOTICE 'Student % is already enrolled', p_student;
END;
$$;

CALL enroll_student(6, 1);
CALL enroll_student(6, 1);
Module 18 · Advanced

Functions, Triggers, Partitioning

PL/pgSQL lets you run logic inside the database. A trigger calls a function automatically on INSERT, UPDATE or DELETE, which suits audit columns and small invariants.

SQL
ALTER TABLE students ADD COLUMN updated_at TIMESTAMPTZ NOT NULL DEFAULT now();

CREATE FUNCTION touch_updated_at() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
    NEW.updated_at := now();
    RETURN NEW;
END;
$$;

CREATE TRIGGER trg_students_touch
BEFORE UPDATE ON students
FOR EACH ROW EXECUTE FUNCTION touch_updated_at();

Partitioning

Split an ever growing table by range, list or hash. Queries that filter on the partition key touch only the relevant partitions, and old data can be detached or dropped instantly.

SQL
CREATE TABLE audit_log (
    id        BIGINT GENERATED ALWAYS AS IDENTITY,
    logged_at TIMESTAMPTZ NOT NULL,
    payload   JSONB
) PARTITION BY RANGE (logged_at);

CREATE TABLE audit_log_2026_10 PARTITION OF audit_log
FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');
SQL
-- Keyset pagination: stays fast on huge tables, unlike large OFFSET values
SELECT id, full_name
FROM students
WHERE id > 1000
ORDER BY id
LIMIT 50;
Module 19 · Advanced

Replication, Extensions, Tuning

Physical (streaming) replication copies the WAL of the whole cluster for hot standbys and failover. Logical replication sends changes of chosen tables, which helps with major version upgrades and data distribution.

SQL
-- Primary (postgresql.conf): wal_level = logical
-- Publish selected tables
CREATE PUBLICATION pub_students FOR TABLE students, courses;

-- Subscriber: pull those changes continuously
CREATE SUBSCRIPTION sub_students
CONNECTION 'host=primary dbname=university user=repl'
PUBLICATION pub_students;

Extensions

Extensions add features without forking the database: pg_trgm (fuzzy search), pgcrypto, PostGIS (geography), pg_stat_statements (query statistics), TimescaleDB (time series).

SQL
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_students_name_trgm ON students USING GIN (full_name gin_trgm_ops);
SELECT full_name FROM students WHERE full_name ILIKE '%rahm%';

SHOW shared_buffers;
ALTER SYSTEM SET work_mem = '32MB';
SELECT pg_reload_conf();

First tuning knobs

  • shared_buffers: a common starting point is about 25% of RAM.
  • effective_cache_size: tells the planner how much the OS can cache, often 50 to 75% of RAM.
  • work_mem: per sort or hash step, so raise it carefully.
  • maintenance_work_mem: speeds up VACUUM and index builds.
  • max_connections: keep it low and use a pool.
Module 20 · Advanced

Security, Backup and Production

Use roles with the least privilege each application needs. Never let an app connect as a superuser.

SQL
CREATE ROLE app_user LOGIN PASSWORD 'change_me';
GRANT CONNECT ON DATABASE university TO app_user;
GRANT SELECT, INSERT, UPDATE ON students TO app_user;
REVOKE ALL ON enrollments FROM PUBLIC;

Backup and recovery

pg_dump gives logical backups. For point in time recovery combine a base backup with archived WAL. Streaming replication keeps a hot standby, which can also serve read only traffic.

Shell
# Compressed custom format dump
pg_dump -Fc -d university -f university.dump

# Restore into a separate copy first
createdb university_copy
pg_restore -d university_copy university.dump

Running it for real

  • Put PgBouncer or another pool in front of the server.
  • Leave autovacuum on, and watch table bloat.
  • Track slow queries with pg_stat_statements.
  • Migrate big tables without long locks.
SQL
-- Safe migration on a big table: nullable column, batched backfill, then constraint
ALTER TABLE students ADD COLUMN phone TEXT;
UPDATE students SET phone = '' WHERE id BETWEEN 1 AND 1000 AND phone IS NULL;
ALTER TABLE students ADD CONSTRAINT phone_required CHECK (phone IS NOT NULL) NOT VALID;
ALTER TABLE students VALIDATE CONSTRAINT phone_required;

-- Find the slowest statements (needs the pg_stat_statements extension)
SELECT query, calls, round(mean_exec_time::numeric, 2) AS avg_ms
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 5;
Module 21 · Advanced

Row Level Security

GRANT controls which tables a role can touch. Row level security (RLS) controls which rows, enforced by the database itself. It is the standard tool for multi tenant apps where every customer must see only their own rows. A policy is the rule: USING filters rows that can be read, updated or deleted, and WITH CHECK validates rows being written.

SQL
CREATE TABLE notes (
    id    SERIAL PRIMARY KEY,
    owner TEXT NOT NULL DEFAULT current_user,
    body  TEXT NOT NULL
);

ALTER TABLE notes ENABLE ROW LEVEL SECURITY;
ALTER TABLE notes FORCE ROW LEVEL SECURITY;

CREATE POLICY own_notes ON notes
    USING (owner = current_user)
    WITH CHECK (owner = current_user);
Module 22 · Advanced

Modern PostgreSQL: Docker Compose and pgvector

A docker-compose.yml gives every teammate the same database with one command, docker compose up -d. The pgvector extension stores embeddings (lists of numbers produced by AI models) in a vector column and finds the nearest ones. It is the base of semantic search and RAG apps.

Shell
# docker-compose.yml
services:
  db:
    image: pgvector/pgvector:pg16
    environment:
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: university
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
volumes:
  pgdata:
SQL
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE docs (
    id        BIGSERIAL PRIMARY KEY,
    content   TEXT NOT NULL,
    embedding vector(3)
);
INSERT INTO docs (content, embedding)
VALUES ('cat', '[1,0,0]'), ('kitten', '[0.9,0.1,0]'), ('car', '[0,0,1]');
CREATE INDEX ON docs USING hnsw (embedding vector_cosine_ops);

-- Nearest two documents by cosine distance
SELECT content FROM docs ORDER BY embedding <=> '[1,0,0]' LIMIT 2;
OperatorDistance
<->Euclidean (L2)
<=>Cosine
<#>Negative inner product
Module 23 · Reference

Cheat Sheet, Glossary, Next Steps

SQL quick reference

TaskStatement
Create a tableCREATE TABLE t (id INT PRIMARY KEY, name TEXT);
InsertINSERT INTO t (id, name) VALUES (1, 'a');
ReadSELECT name FROM t WHERE id = 1;
UpdateUPDATE t SET name = 'b' WHERE id = 1;
DeleteDELETE FROM t WHERE id = 1;
JoinSELECT a.x, b.y FROM a JOIN b ON b.a_id = a.id;
AggregateSELECT k, count(*) FROM t GROUP BY k;
IndexCREATE INDEX idx_t_name ON t (name);
TransactionBEGIN; ... COMMIT;
PlanEXPLAIN (ANALYZE) SELECT ...;

psql commands

CommandDoes
\lList databases
\c nameConnect to a database
\dtList tables
\d tableDescribe a table
\duList roles
\dnList schemas
\xToggle expanded output
\timingShow query time
\i file.sqlRun a file
\qQuit

Glossary

TermMeaning
Row, columnOne record, and one named field of every record
Primary keyColumn(s) that uniquely identify a row
Foreign keyColumn that points to a row in another table
IndexExtra structure that makes lookups fast
TransactionA group of statements that succeed or fail together
MVCCEach transaction sees a snapshot, readers never block writers
WALWrite ahead log used for durability, recovery and replication
VACUUMCleans dead row versions left by updates and deletes
SchemaA namespace (folder) of tables inside a database
ViewA saved query that looks like a table
RoleA user or group with privileges
CTEA named temporary result inside one query (WITH)
ReplicaA copy of the server kept in sync from the primary

Next steps

  • Rebuild the university schema from memory in the Practice Lab, then add your own tables.
  • Read the official PostgreSQL documentation for details of any command.
  • Practise EXPLAIN ANALYZE on your own slow queries.
  • Build a small app (any language) that talks to PostgreSQL through a driver.