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.
- FoundationsModules 01 to 08
What a database is, install on Windows, SQL basics, tables, CRUD, queries and expressions.
- IntermediateModules 09 to 14
Constraints, ALTER, subqueries, indexes, full text search and transactions.
- AdvancedModules 15 to 22
Window functions, recursion, PL/pgSQL, triggers, partitioning, replication, security, RLS and pgvector.
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.
| id | full_name | gpa | |
|---|---|---|---|
| 1 | Ayesha Rahman | ayesha@uni.edu | 3.80 |
| 2 | Tanvir Hasan | tanvir@uni.edu | 3.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.
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
- Clusterone running server
- Databaseisolated collection
- Schemanamespace, default
public - Tablerows and columns
# Debian or Ubuntu
sudo apt install postgresql
sudo -u postgres psql
# macOS (Homebrew)
brew install postgresql@16
brew services start postgresql@16
psql postgresConnect with psql, create a database and look around:
CREATE DATABASE university;
-- Meta commands work only inside psql.
-- \c connect, \l list databases, \dt list tables, \d describe, \q quit
\c universityInstall PostgreSQL on Windows
The official installer is about 300 MB and includes the server, the psql command line and the pgAdmin app.
- Open postgresql.org/download/windows and choose Download the installer. Pick the newest version (16 or higher) for Windows x86 64.
- Run the downloaded
.exeand allow administrator access. - Keep the default folder and keep these components ticked: PostgreSQL Server, pgAdmin 4, Command Line Tools.
- Choose a password for the superuser
postgresand write it down. It is hard to recover. - Keep the default port
5432and the default locale, then finish. - 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.
- Test the installation:
SELECT version();
CREATE DATABASE university;
\c universityOther ways
You can install from PowerShell with winget, or run PostgreSQL in Docker without installing it.
# 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 localhostdocker run --name pg -e POSTGRES_PASSWORD=secret -p 5432:5432 -d postgres:16
docker exec -it pg psql -U postgresIf something goes wrong
| Problem | Fix |
|---|---|
| psql is not recognized | Use SQL Shell (psql) from the Start menu, or add the PostgreSQL bin folder to PATH |
| password authentication failed | You typed the wrong postgres password. Use the one chosen at install time |
| port 5432 already in use | Another PostgreSQL is running. Stop it or pick another port during install |
| Nothing appears when I type the password | That is normal, the shell hides it. Type it and press Enter |
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
SELECTare not case sensitive. Convention: UPPERCASE keywords, lowercase names. - Text goes in single quotes like
'this'. Double quotes are for unusual names. - Comments:
-- one lineor/* many lines */. - Spaces and line breaks do not matter, so format for readability.
- Name things in
lowercase_with_underscores, with no spaces.
-- 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
SELECT full_name FROM students WHERE gpa > 3.5;| Part | Meaning |
|---|---|
| SELECT full_name | which columns to show |
| FROM students | which table to read |
| WHERE gpa > 3.5 | which rows to keep |
| ; | end of the statement |
The five command groups
| Group | Commands | Purpose |
|---|---|---|
| DDL | CREATE, ALTER, DROP | Define structure |
| DML | INSERT, UPDATE, DELETE | Change data |
| DQL | SELECT | Read data |
| DCL | GRANT, REVOKE | Control access |
| TCL | BEGIN, COMMIT, ROLLBACK | Group 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.
SELECT 'Your Name' AS name, current_date AS today;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.
| Type | Use it for |
|---|---|
| SMALLINT, INTEGER, BIGINT | Whole numbers of growing size |
| NUMERIC(p,s) | Exact decimals such as money |
| TEXT | Strings of any length (use VARCHAR(n) only when the limit is a business rule) |
| BOOLEAN | true or false |
| DATE, TIMESTAMPTZ | Calendar dates and exact moments in time |
| UUID | Globally unique identifiers |
| JSONB | Semi structured documents that can be indexed |
| type[], ENUM | Lists, and small fixed sets of labels |
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()
);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.
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;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.
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;| code | enrolled | avg_grade |
|---|---|---|
| CS101 | 42 | 3.12 |
| MA201 | 37 | 2.88 |
| PH110 | 29 | 3.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.
-- 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.
SELECT c.code
FROM courses c
LEFT JOIN enrollments e ON e.course_id = c.id
WHERE e.course_id IS NULL;Functions and Expressions
PostgreSQL ships with hundreds of built in functions. Four families cover most daily work: text, numbers, dates and NULL handling.
| Family | Common tools |
|---|---|
| Text | lower(), upper(), trim(), replace(), split_part(), format(), ||, ILIKE |
| Numbers | round(), ceil(), floor(), abs(), mod(), random() |
| Date and time | now(), current_date, interval, date_trunc(), extract(), age(), to_char() |
| NULL and casting | COALESCE(), NULLIF(), IS DISTINCT FROM, ::type, CAST() |
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';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.
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.
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).
-- 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');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.
-- 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;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.
| Index | Best for |
|---|---|
| B-tree | Equality, ranges, ORDER BY (default) |
| GIN | JSONB, arrays, full text search |
| GiST | Geometry, ranges, nearest neighbour |
| BRIN | Huge append only tables ordered by time |
| Hash | Equality only (rarely better than B-tree) |
-- 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.
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.
| Tool | Use it for | Index |
|---|---|---|
| LIKE, ILIKE | Simple patterns | B-tree helps only for 'abc%'; pg_trgm for '%abc%' |
| ~ (regex) | Complex patterns | pg_trgm |
| tsvector @@ tsquery | Word search with ranking | GIN |
-- 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;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 level | Behaviour |
|---|---|
| READ COMMITTED (default) | Each statement sees data committed before it started |
| REPEATABLE READ | The whole transaction sees one snapshot |
| SERIALIZABLE | Behaves as if transactions ran one after another; retry on failure |
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;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.
-- 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.
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.
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;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.
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;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.
DECLAREvariables andSELECT ... INTOto fill them.IF,ELSIF,FOR ... LOOPfor control flow.EXCEPTION WHEN ... THENto handle errors,RAISEto report them.
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);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.
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.
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');-- 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;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.
-- 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).
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.
Security, Backup and Production
Use roles with the least privilege each application needs. Never let an app connect as a superuser.
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.
# 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.dumpRunning 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.
-- 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;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.
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);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.
# 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: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;| Operator | Distance |
|---|---|
| <-> | Euclidean (L2) |
| <=> | Cosine |
| <#> | Negative inner product |
Cheat Sheet, Glossary, Next Steps
SQL quick reference
| Task | Statement |
|---|---|
| Create a table | CREATE TABLE t (id INT PRIMARY KEY, name TEXT); |
| Insert | INSERT INTO t (id, name) VALUES (1, 'a'); |
| Read | SELECT name FROM t WHERE id = 1; |
| Update | UPDATE t SET name = 'b' WHERE id = 1; |
| Delete | DELETE FROM t WHERE id = 1; |
| Join | SELECT a.x, b.y FROM a JOIN b ON b.a_id = a.id; |
| Aggregate | SELECT k, count(*) FROM t GROUP BY k; |
| Index | CREATE INDEX idx_t_name ON t (name); |
| Transaction | BEGIN; ... COMMIT; |
| Plan | EXPLAIN (ANALYZE) SELECT ...; |
psql commands
| Command | Does |
|---|---|
| \l | List databases |
| \c name | Connect to a database |
| \dt | List tables |
| \d table | Describe a table |
| \du | List roles |
| \dn | List schemas |
| \x | Toggle expanded output |
| \timing | Show query time |
| \i file.sql | Run a file |
| \q | Quit |
Glossary
| Term | Meaning |
|---|---|
| Row, column | One record, and one named field of every record |
| Primary key | Column(s) that uniquely identify a row |
| Foreign key | Column that points to a row in another table |
| Index | Extra structure that makes lookups fast |
| Transaction | A group of statements that succeed or fail together |
| MVCC | Each transaction sees a snapshot, readers never block writers |
| WAL | Write ahead log used for durability, recovery and replication |
| VACUUM | Cleans dead row versions left by updates and deletes |
| Schema | A namespace (folder) of tables inside a database |
| View | A saved query that looks like a table |
| Role | A user or group with privileges |
| CTE | A named temporary result inside one query (WITH) |
| Replica | A 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 ANALYZEon your own slow queries. - Build a small app (any language) that talks to PostgreSQL through a driver.