Notes by VS  ·  Basic → Expert
// Complete Reference

PostgreSQL:
Basic to Expert

Architecture internals, query language mastery, performance tuning, replication, and production-grade patterns — all in one place.

15
Topics
4
Levels
60+
Code Examples
∞
Depth
L1
Basic
L2
Intermediate
L3
Advanced
L4
Expert
01 PostgreSQL Architecture BASIC → EXPERT

PostgreSQL is a process-based, object-relational DBMS. Understanding its architecture is key to tuning and troubleshooting everything else.

PostgreSQL Process & Memory Architecture
Client App (psql / app)
→
Postmaster (Main Process)
→
Backend Process (per connection)
↓ spawns background workers
WAL Writer
Checkpointer
Autovacuum
BG Writer
Stats Collector
Shared Memory (shared_buffers)
Buffer Pool
8KB pages
WAL Buffers
wal_buffers
Lock Table
max_locks_per_xact
Proc Array
max_connections
Disk Storage
Data Files (base/)
WAL Segments (pg_wal/)
pg_clog / pg_xact
pg_catalog
🔄 Process Model
Each client connection forks a dedicated backend process. The postmaster is the parent that listens on port 5432 and spawns children. This is different from MySQL's thread model — it's more memory-heavy but more isolated and crash-safe.
📦 Shared Buffers
The central shared memory area. PostgreSQL reads all data through the buffer pool. A cache hit means 0 disk I/O. Set shared_buffers = 25% of RAM as a starting point. Pages are 8KB by default.
📝 WAL (Write-Ahead Log)
All changes are written to WAL before touching data files. This ensures durability (D in ACID). WAL is also the basis for replication. Location: $PGDATA/pg_wal/
🗂️ MVCC
Multi-Version Concurrency Control — readers never block writers and vice versa. Every row has xmin/xmax transaction IDs. Old versions are cleaned by VACUUM. This is the heart of Postgres's concurrency model.

Key Configuration Files

FilePurposeLocation
postgresql.confMain server config (memory, WAL, connections)$PGDATA/
pg_hba.confClient authentication rules (host, user, method)$PGDATA/
pg_ident.confOS → DB user name mapping$PGDATA/
recovery.confStandby/recovery settings (old; now in postgresql.conf)$PGDATA/
02 Install & Connect BASIC
BASH · Ubuntu/Debian
# Install PostgreSQL 16
sudo apt install postgresql postgresql-contrib

# Start service
sudo systemctl start postgresql

# Connect as postgres superuser
sudo -u postgres psql

# Create a DB and user
CREATE USER vignesh WITH PASSWORD 'securepass';
CREATE DATABASE mydb OWNER vignesh;

# Connect from terminal
psql -h localhost -U vignesh -d mydb

Essential psql Commands

CommandAction
\lList all databases
\c dbnameConnect to database
\dtList tables in current schema
\d tablenameDescribe table structure
\dnList schemas
\duList roles/users
\timingToggle query timing
\xToggle expanded output
\i file.sqlRun SQL from a file
\qQuit psql
03 Data Types BASIC

PostgreSQL has a very rich type system — far beyond standard SQL. Choosing the right type matters for storage, performance, and correctness.

Numeric Types
INTEGER (4B), BIGINT (8B), SMALLINT (2B)
NUMERIC(p,s) — exact decimal (finances)
FLOAT8 / DOUBLE PRECISION — fast, approximate
SERIAL / BIGSERIAL — auto-increment (prefer IDENTITY)
Text Types
TEXT — unlimited, no padding (preferred)
VARCHAR(n) — length-limited text
CHAR(n) — fixed-length, padded
Postgres stores all three the same way internally — use TEXT.
Date & Time
DATE — calendar date only
TIME / TIMETZ — time with/without TZ
TIMESTAMP / TIMESTAMPTZ — prefer TIMESTAMPTZ for most use cases (stores UTC internally)
Special Types
UUID — 128-bit unique ID
BOOLEAN — true/false/null
JSONB — binary JSON (indexable)
ARRAY — any type as array, e.g. TEXT[]
ENUM, INET, CIDR, TSVECTOR
Best Practice
Use TIMESTAMPTZ over TIMESTAMP. Always store in UTC. Use TEXT over VARCHAR unless you need enforcement. Use NUMERIC for money — never FLOAT.
04 DDL — Tables, Schema, Sequences BASIC
SQL · DDL
-- Schema organization
CREATE SCHEMA banking;

-- Table with modern best practices
CREATE TABLE banking.accounts (
  id          BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  account_no  TEXT        NOT NULL UNIQUE,
  customer_id BIGINT      NOT NULL,
  balance     NUMERIC(15,2) NOT NULL DEFAULT 0,
  status      TEXT        NOT NULL DEFAULT 'ACTIVE',
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at  TIMESTAMPTZ
);

-- Alter table
ALTER TABLE banking.accounts ADD COLUMN branch_code TEXT;
ALTER TABLE banking.accounts ALTER COLUMN status SET NOT NULL;
ALTER TABLE banking.accounts RENAME TO bank_accounts;

-- Drop with safety
DROP TABLE IF EXISTS banking.accounts CASCADE;
IDENTITY vs SERIAL
Prefer GENERATED ALWAYS AS IDENTITY (SQL standard, pg10+) over SERIAL (legacy, creates hidden sequence with different ownership). Identity columns prevent accidental overrides with ALWAYS.
05 CRUD Operations BASIC
SQL · CRUD + Advanced Patterns
-- INSERT with RETURNING
INSERT INTO accounts (account_no, customer_id, balance)
VALUES ('AC001', 42, 10000.00)
RETURNING id, created_at;

-- Bulk INSERT
INSERT INTO accounts (account_no, customer_id)
SELECT gen_random_uuid()::TEXT, id FROM customers;

-- UPSERT (INSERT ON CONFLICT)
INSERT INTO accounts (account_no, balance)
VALUES ('AC001', 5000)
ON CONFLICT (account_no) DO UPDATE
  SET balance = EXCLUDED.balance;

-- UPDATE with JOIN using FROM clause
UPDATE accounts a
SET balance = a.balance + t.amount
FROM transactions t
WHERE t.account_id = a.id AND t.status = 'PENDING'
RETURNING a.id, a.balance;

-- DELETE with RETURNING
DELETE FROM accounts
WHERE status = 'CLOSED' AND balance = 0
RETURNING id;

SELECT — Full Power

SQL · SELECT
SELECT
  a.account_no,
  c.name AS customer_name,
  a.balance,
  CASE
    WHEN a.balance >= 100000 THEN 'PREMIUM'
    WHEN a.balance >= 10000  THEN 'REGULAR'
    ELSE 'BASIC'
  END AS tier,
  TO_CHAR(a.created_at, 'DD-Mon-YYYY') AS opened_on
FROM       accounts a
INNER JOIN customers c ON c.id = a.customer_id
WHERE  a.status = 'ACTIVE'
  AND  a.balance > 0
ORDER BY a.balance DESC
LIMIT 20 OFFSET 0;
06 JOINs — All Types BASIC → INTER
SQL · JOIN Types
-- INNER JOIN: rows matching in both
SELECT a.id, t.amount
FROM accounts a
INNER JOIN transactions t ON t.account_id = a.id;

-- LEFT JOIN: all accounts, with or without transactions
SELECT a.id, COUNT(t.id) AS txn_count
FROM accounts a
LEFT JOIN transactions t ON t.account_id = a.id
GROUP BY a.id;

-- FULL OUTER JOIN: all rows from both sides
SELECT a.id, b.id AS branch_id
FROM accounts a
FULL OUTER JOIN branches b ON b.code = a.branch_code;

-- CROSS JOIN: cartesian product
SELECT p.name, c.currency
FROM products p CROSS JOIN currencies c;

-- SELF JOIN: employee hierarchy
SELECT e.name, m.name AS manager
FROM employees e
LEFT JOIN employees m ON m.id = e.manager_id;

-- LATERAL JOIN: correlated subquery per row
SELECT c.name, recent.amount
FROM customers c,
LATERAL (
  SELECT amount FROM transactions
  WHERE customer_id = c.id
  ORDER BY created_at DESC LIMIT 1
) recent;
07 Aggregates, Grouping & Filtering BASIC
SQL · Aggregates
-- Standard aggregates
SELECT
  branch_code,
  COUNT(*)            AS account_count,
  SUM(balance)         AS total_balance,
  AVG(balance)::NUMERIC(12,2) AS avg_balance,
  MIN(balance)         AS min_bal,
  MAX(balance)         AS max_bal,
  STDDEV(balance)      AS std_dev
FROM accounts
WHERE status = 'ACTIVE'
GROUP BY branch_code
HAVING SUM(balance) > 1000000
ORDER BY total_balance DESC;

-- GROUPING SETS / ROLLUP / CUBE
SELECT branch_code, status, COUNT(*)
FROM accounts
GROUP BY ROLLUP(branch_code, status);

-- Filter within aggregate (pg9.4+)
SELECT
  COUNT(*) FILTER (WHERE status = 'ACTIVE')  AS active_count,
  COUNT(*) FILTER (WHERE status = 'CLOSED')  AS closed_count
FROM accounts;
08 Constraints BASIC
SQL · Constraints
CREATE TABLE transactions (
  id          BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  account_id  BIGINT      NOT NULL
              REFERENCES accounts(id) ON DELETE RESTRICT,
  amount      NUMERIC(15,2) NOT NULL
              CHECK (amount <> 0),
  type        TEXT        NOT NULL
              CHECK (type IN ('CREDIT','DEBIT')),
  ref_no      TEXT        UNIQUE,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
  CONSTRAINT chk_future_date CHECK (created_at <= now() + '1 day'::INTERVAL)
);

-- Deferrable constraint (evaluated at commit not statement)
ALTER TABLE transactions
  ADD CONSTRAINT fk_account
  FOREIGN KEY (account_id) REFERENCES accounts(id)
  DEFERRABLE INITIALLY DEFERRED;
── INTERMEDIATE LEVEL ──
09 Indexes — All Types & Strategies INTERMEDIATE

Indexes are the single biggest lever for query performance. PostgreSQL supports B-tree, Hash, GIN, GiST, SP-GiST, and BRIN — each suited to different access patterns.

Index TypeBest ForExample Use Case
B-treeDefault, equality + range, ORDER BYWHERE id = 5, WHERE age BETWEEN 20 AND 40
HashEquality only, smaller than B-treeWHERE session_token = 'abc'
GINArray containment, JSONB, Full-Text SearchWHERE tags @> ARRAY['pg']
GiSTGeometric, range types, nearest-neighborPostGIS spatial queries
BRINVery large sorted tables, append-onlyLogs table ordered by timestamp
PartialSubset of rowsWHERE status = 'ACTIVE' (only indexes active rows)
ExpressionFunction on columnLOWER(email) for case-insensitive search
SQL · Index Creation
-- Standard B-tree
CREATE INDEX idx_accounts_customer ON accounts(customer_id);

-- Composite index (column order matters!)
CREATE INDEX idx_txn_account_date ON transactions(account_id, created_at DESC);

-- Partial index — only active accounts
CREATE INDEX idx_active_accounts ON accounts(customer_id)
  WHERE status = 'ACTIVE';

-- Expression index — case insensitive email search
CREATE INDEX idx_email_lower ON customers(LOWER(email));

-- GIN index for JSONB
CREATE INDEX idx_meta_gin ON accounts USING GIN(metadata);

-- Concurrent — no table lock!
CREATE INDEX CONCURRENTLY idx_txn_status ON transactions(status);

-- Check index usage
SELECT indexname, idx_scan, idx_tup_read
FROM  pg_stat_user_indexes
WHERE schemaname = 'public'
ORDER BY idx_scan;
Index Design Rules
1. Index columns used in WHERE, JOIN ON, ORDER BY, GROUP BY.
2. Composite index — put equality columns first, range last.
3. Avoid over-indexing — every index slows writes.
4. Use CREATE INDEX CONCURRENTLY in production (no lock).
5. Drop unused indexes: check pg_stat_user_indexes.idx_scan = 0.
10 Transactions, ACID & MVCC INTERMEDIATE
SQL · Transaction Control
-- Basic transaction
BEGIN;
  UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
  UPDATE accounts SET balance = balance + 1000 WHERE id = 2;
COMMIT;  -- or ROLLBACK

-- SAVEPOINT for partial rollback
BEGIN;
  INSERT INTO audit_log VALUES ('started');
  SAVEPOINT sp1;
  UPDATE accounts SET balance = -999 WHERE id = 1;  -- risky
  ROLLBACK TO sp1;  -- undo just the UPDATE
  RELEASE SAVEPOINT sp1;
COMMIT;

-- Isolation levels
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;  -- default
Isolation LevelDirty ReadNon-RepeatablePhantom ReadUse When
READ COMMITTEDNoYesYesDefault — most OLTP
REPEATABLE READNoNoNo*Reports, analytics
SERIALIZABLENoNoNoFinancials, critical
MVCC Internals
Every row has xmin (created by transaction X) and xmax (deleted/updated by transaction Y). A query sees rows where xmin committed before the snapshot and xmax hasn't. Dead rows (old versions) accumulate until VACUUM reclaims them.
11 Window Functions INTERMEDIATE

Window functions compute values across rows related to the current row — without collapsing them into groups. Essential for ranking, running totals, moving averages.

SQL · Window Functions
SELECT
  account_no,
  branch_code,
  balance,

  -- Ranking
  ROW_NUMBER() OVER (ORDER BY balance DESC)           AS overall_rank,
  RANK()       OVER (PARTITION BY branch_code ORDER BY balance DESC) AS branch_rank,
  DENSE_RANK() OVER (PARTITION BY branch_code ORDER BY balance DESC) AS dense,
  NTILE(4)     OVER (ORDER BY balance)                AS quartile,
  PERCENT_RANK() OVER (ORDER BY balance)              AS pct_rank,

  -- Running total
  SUM(balance) OVER (PARTITION BY branch_code
                     ORDER BY created_at
                     ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
                  AS running_balance,

  -- Lag/Lead: compare to prev/next row
  LAG(balance, 1)  OVER (PARTITION BY branch_code ORDER BY created_at) AS prev_bal,
  LEAD(balance, 1) OVER (PARTITION BY branch_code ORDER BY created_at) AS next_bal,

  -- First/Last value in partition
  FIRST_VALUE(balance) OVER (PARTITION BY branch_code ORDER BY created_at) AS first_bal

FROM accounts;
12 CTEs, Recursive Queries & Subqueries INTERMEDIATE
SQL · CTE + Recursive
-- Standard CTE (named subquery)
WITH high_value AS (
  SELECT id, customer_id, balance
  FROM  accounts
  WHERE balance > 500000
),
customer_totals AS (
  SELECT customer_id, SUM(balance) AS total
  FROM  high_value
  GROUP BY customer_id
)
SELECT c.name, ct.total
FROM  customer_totals ct
JOIN  customers c ON c.id = ct.customer_id
ORDER BY ct.total DESC;

-- Recursive CTE: org chart / hierarchy
WITH RECURSIVE emp_tree AS (
  -- anchor: top-level manager
  SELECT id, name, manager_id, 1 AS depth
  FROM  employees
  WHERE manager_id IS NULL

  UNION ALL

  -- recursive: join to parent
  SELECT e.id, e.name, e.manager_id, t.depth + 1
  FROM  employees e
  JOIN  emp_tree t ON t.id = e.manager_id
)
SELECT REPEAT('  ', depth - 1) || name AS org_chart, depth
FROM  emp_tree
ORDER BY depth, name;
13 JSON & JSONB INTERMEDIATE
JSON vs JSONB
JSON stores as text (exact input preserved, slower to query). JSONB stores as binary (faster query, supports indexing, loses key order/duplicate keys). Always prefer JSONB unless you need exact text preservation.
SQL · JSONB Operations
-- Create with JSONB column
ALTER TABLE accounts ADD COLUMN metadata JSONB;

-- Insert JSON
UPDATE accounts SET metadata = '{"kyc": true, "tags": ["premium","nri"], "risk": 2}'
WHERE id = 1;

-- Extract operators
SELECT
  metadata->'kyc'           AS kyc_json,    -- returns JSON
  metadata->>'kyc'          AS kyc_text,    -- returns TEXT
  metadata#>>'{tags,0}'     AS first_tag,  -- nested path as text
  metadata->'tags'          AS tags_array
FROM accounts;

-- Containment
SELECT * FROM accounts
WHERE metadata @> '{"kyc": true}';    -- contains

-- Update nested key (jsonb_set)
UPDATE accounts
SET metadata = jsonb_set(metadata, '{risk}', '3')
WHERE id = 1;

-- GIN index on JSONB
CREATE INDEX idx_meta_gin ON accounts USING GIN(metadata);
── ADVANCED LEVEL ──
14 PL/pgSQL — Functions, Procedures & Triggers ADVANCED
PL/pgSQL · Function
-- Function: transfer funds between accounts
CREATE OR REPLACE FUNCTION transfer_funds(
  p_from_id   BIGINT,
  p_to_id     BIGINT,
  p_amount    NUMERIC
) RETURNS VOID LANGUAGE plpgsql AS $$
DECLARE
  v_balance NUMERIC;
BEGIN
  -- Lock source row
  SELECT balance INTO v_balance
  FROM accounts WHERE id = p_from_id FOR UPDATE;

  IF v_balance < p_amount THEN
    RAISE EXCEPTION 'Insufficient funds: balance=%, amount=%',
                    v_balance, p_amount;
  END IF;

  UPDATE accounts SET balance = balance - p_amount WHERE id = p_from_id;
  UPDATE accounts SET balance = balance + p_amount WHERE id = p_to_id;

  INSERT INTO audit_log(from_id, to_id, amount)
  VALUES(p_from_id, p_to_id, p_amount);
END;
$$;

-- Call it
SELECT transfer_funds(1, 2, 500.00);
PL/pgSQL · Trigger
-- Auto-update updated_at on any change
CREATE OR REPLACE FUNCTION set_updated_at()
RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN
  NEW.updated_at := now();
  RETURN NEW;
END;
$$;

CREATE TRIGGER trg_accounts_updated_at
  BEFORE UPDATE ON accounts
  FOR EACH ROW EXECUTE FUNCTION set_updated_at();
15 Table Partitioning ADVANCED

Declarative partitioning (pg10+) splits a logical table into physical child tables, enabling partition pruning, faster deletes, and parallel scans on huge datasets.

SQL · Range Partitioning
-- Parent table (no storage itself)
CREATE TABLE transactions_part (
  id         BIGINT NOT NULL,
  account_id BIGINT NOT NULL,
  amount     NUMERIC(15,2),
  txn_date   DATE NOT NULL
) PARTITION BY RANGE (txn_date);

-- Partitions by quarter
CREATE TABLE txn_2024_q1 PARTITION OF transactions_part
  FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');

CREATE TABLE txn_2024_q2 PARTITION OF transactions_part
  FOR VALUES FROM ('2024-04-01') TO ('2024-07-01');

-- Default partition catches everything else
CREATE TABLE txn_default PARTITION OF transactions_part DEFAULT;

-- Postgres auto-prunes: this only scans txn_2024_q1
SELECT * FROM transactions_part
WHERE txn_date BETWEEN '2024-01-01' AND '2024-03-31';

-- Detach old partition (instant, no data movement)
ALTER TABLE transactions_part DETACH PARTITION txn_2024_q1;
RANGE — dates, numbers LIST — fixed values, status HASH — even distribution Sub-partitioning supported
16 Query Plans & EXPLAIN ADVANCED
SQL · EXPLAIN
-- Basic plan (estimated costs only)
EXPLAIN
SELECT * FROM transactions WHERE account_id = 42;

-- Full analysis with actual timings
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM transactions WHERE account_id = 42;

-- Reading output example:
-- Index Scan using idx_account on transactions  (cost=0.56..8.58 rows=1 width=80)
--                                                      ↑            ↑
--                                               startup cost      total cost
-- Index Cond: (account_id = 42)
-- Buffers: shared hit=3                ← all from cache, 0 disk reads
-- Planning Time: 0.1 ms
-- Execution Time: 0.05 ms
Node TypeMeaningWhen to Worry
Seq ScanFull table scanOn large tables with selective WHERE
Index ScanUses index, random I/OUsually good
Index Only ScanReads from index, no heapIdeal — zero heap I/O
Bitmap Heap ScanBulk index + heap fetchNormal for many rows
Hash JoinBuild hash table + probeGood; slow if hash spills to disk
Nested LoopRow-by-row joinBad for large outer tables
Merge JoinSorted merge of two setsGood when inputs already sorted
Tuning Tip
Look for rows vs actual: huge discrepancy means stale statistics → run ANALYZE tablename. Look for Seq Scan on large table — missing index. Look for hash batches > 1 — increase work_mem.
17 Performance Tuning ADVANCED
ParameterDefaultRecommended (16GB RAM)Effect
shared_buffers128MB4GB (25% RAM)Buffer pool size
effective_cache_size4GB12GB (75% RAM)Planner estimate of OS cache
work_mem4MB64–256MBPer-sort/hash memory
maintenance_work_mem64MB1–2GBVACUUM, CREATE INDEX
max_connections100100–200 (use PgBouncer)Concurrent connections
wal_buffers-1 (auto)64MBWAL write buffer
checkpoint_completion_target0.90.9Spread checkpoint I/O
random_page_cost4.01.1 (SSD)Planner index preference
parallel_workers_per_gather24Parallel query workers
SQL · Finding Slow Queries
-- Enable pg_stat_statements extension first
CREATE EXTENSION pg_stat_statements;

-- Top 10 slowest queries
SELECT
  LEFT(query, 80)               AS query,
  calls,
  ROUND(total_exec_time::NUMERIC, 2) AS total_ms,
  ROUND(mean_exec_time::NUMERIC, 2)  AS avg_ms,
  rows
FROM  pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;

-- Table bloat check
SELECT
  schemaname, tablename,
  pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size,
  n_dead_tup, n_live_tup,
  ROUND(100*n_dead_tup/NULLIF(n_live_tup+n_dead_tup,0),1) AS dead_pct
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;
── EXPERT LEVEL ──
18 Replication & High Availability EXPERT
Streaming Replication Architecture
Primary
Read + Write
→ WAL stream
Standby 1
Read-only replica
→ WAL stream
Standby 2
DR replica
Failover managed by: Patroni + etcd or repmgr or pg_auto_failover
CONF + SQL · Replication Setup
-- On PRIMARY: postgresql.conf
wal_level = replica
max_wal_senders = 5
wal_keep_size = '1GB'

-- Create replication user
CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'replpass';

-- pg_hba.conf on primary
host replication replicator 10.0.0.0/24 scram-sha-256

-- On STANDBY: take base backup
pg_basebackup -h primary_host -U replicator -D /var/lib/postgresql/data -P -R

-- -R auto-creates standby.signal + postgresql.auto.conf with:
-- primary_conninfo = 'host=primary_host user=replicator ...'

-- Check replication lag on primary
SELECT client_addr, state, sent_lsn, replay_lsn,
  (sent_lsn - replay_lsn) AS lag_bytes
FROM pg_stat_replication;
TypeDescriptionUse Case
Streaming ReplicationWAL shipped in real-time; async (default) or syncHA, read replicas
Logical ReplicationRow-level changes, table-selective, cross-versionMigrations, CDC, Kafka
SynchronousCommit waits for standby ACK — zero data lossFinancial critical
CascadingStandby replicates to another standbyReduce primary load
19 VACUUM, Bloat & Transaction ID Wraparound EXPERT
Critical: XID Wraparound
Transaction IDs are 32-bit integers — they wrap at ~2 billion. If a table's oldest unfrozen xmin is within 1 million XIDs of wraparound, Postgres goes into read-only mode to prevent data loss. Monitor age(datfrozenxid) — should stay below 1.5 billion.
SQL · VACUUM & Monitoring
-- Manual vacuum
VACUUM accounts;              -- reclaim dead tuples
VACUUM ANALYZE accounts;     -- + update statistics
VACUUM FULL accounts;        -- rewrite table, exclusive lock!
VACUUM FREEZE accounts;      -- freeze old XIDs to prevent wraparound

-- Monitor autovacuum
SELECT
  schemaname, relname,
  last_autovacuum,
  last_autoanalyze,
  n_dead_tup,
  n_ins_since_vacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;

-- XID wraparound risk (danger if > 1.5B)
SELECT datname, age(datfrozenxid) AS xid_age
FROM  pg_database
ORDER BY xid_age DESC;

-- Use pg_repack (no lock, online table repack)
-- pg_repack -d mydb -t accounts
20 Must-Know Extensions EXPERT
ExtensionPurposeKey Feature
pg_stat_statementsQuery performance trackingPer-query execution stats
pgcryptoEncryption functionsAES encrypt, bcrypt hash
postgisGeospatial dataGeometry types, spatial indexes
pg_trgmFuzzy text search, LIKE optimizationGIN/GiST on LIKE '%..%'
uuid-osspUUID generationuuid_generate_v4()
hstoreKey-value pairs in columnLightweight alternative to JSONB
timescaledbTime-series data at scaleHypertables, continuous aggregates
pg_repackOnline table compactionReclaim bloat without LOCK
pg_partmanAutomated partition managementAuto-create/drop partitions
pgvectorVector similarity searchAI embeddings, ANN search
SQL · Extension Examples
-- pg_trgm: fast fuzzy search
CREATE EXTENSION pg_trgm;
CREATE INDEX idx_name_trgm ON customers USING GIN(name gin_trgm_ops);
SELECT * FROM customers WHERE name ILIKE '%vign%';  -- uses index!

-- pgvector: AI semantic search
CREATE EXTENSION vector;
ALTER TABLE documents ADD COLUMN embedding vector(1536);
CREATE INDEX ON documents USING hnsw(embedding vector_cosine_ops);
SELECT id, content
FROM  documents
ORDER BY embedding <-> '[0.1,0.2,...]'::vector
LIMIT 5;
21 Security, Roles & Row-Level Security EXPERT
SQL · RBAC + RLS
-- Create role hierarchy
CREATE ROLE app_readonly;
CREATE ROLE app_readwrite;
CREATE ROLE app_admin;

GRANT USAGE ON SCHEMA banking TO app_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA banking TO app_readonly;

GRANT app_readonly TO app_readwrite;
GRANT INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA banking TO app_readwrite;

-- Application user inherits from role
CREATE USER api_user WITH PASSWORD 'xxx';
GRANT app_readwrite TO api_user;

-- Row Level Security: users only see their own accounts
ALTER TABLE accounts ENABLE ROW LEVEL SECURITY;

CREATE POLICY own_accounts ON accounts
  FOR ALL
  USING (customer_id = current_setting('app.current_user_id')::BIGINT);

-- App sets context before queries
SET app.current_user_id = '42';
22 Storage Internals & Expert Diagnostics EXPERT
Page Layout (8KB)
Every heap page has a header (24B), item pointers (4B each), and actual row data. ctid = (page_no, item_no) — the physical row address. Used by indexes to point to rows.
TOAST
The Oversized-Attribute Storage Technique. Values > ~2KB are compressed and/or stored out-of-line in a separate TOAST table. Transparent to queries, but affects I/O patterns for wide rows.
System Catalog
All metadata lives in pg_catalog. Key tables: pg_class (tables/indexes), pg_attribute (columns), pg_index, pg_proc (functions), pg_namespace (schemas).
Checkpoint & Crash Recovery
At checkpoint, all dirty buffer pool pages are flushed to disk. On crash, Postgres replays WAL from last checkpoint forward. checkpoint_timeout and max_wal_size control frequency.
SQL · Expert Diagnostics
-- Active queries with lock info
SELECT pid, now() - query_start AS duration, state, wait_event, query
FROM  pg_stat_activity
WHERE state <> 'idle'
ORDER BY duration DESC;

-- Lock waits: who's blocking whom
SELECT
  blocking.pid AS blocked_by,
  blocking.query AS blocking_query,
  blocked.pid   AS blocked_pid,
  blocked.query AS blocked_query
FROM  pg_stat_activity blocked
JOIN  pg_stat_activity blocking
  ON  blocking.pid = ANY(pg_blocking_pids(blocked.pid));

-- Terminate a long-running query safely
SELECT pg_cancel_backend(12345);    -- sends SIGINT (graceful)
SELECT pg_terminate_backend(12345); -- sends SIGTERM (force)

-- Table sizes with toast & indexes
SELECT
  relname,
  pg_size_pretty(pg_relation_size(oid))            AS table_size,
  pg_size_pretty(pg_indexes_size(oid))             AS index_size,
  pg_size_pretty(pg_total_relation_size(oid))       AS total_size
FROM  pg_class
WHERE relkind = 'r' AND relnamespace = 'public'::regnamespace
ORDER BY pg_total_relation_size(oid) DESC;
Expert Checklist
✓ Monitor pg_stat_bgwriter for checkpoint pressure
✓ Set log_min_duration_statement = 500 to catch slow queries
✓ Use connection pooling (PgBouncer) — never use 500 raw connections
✓ Use pg_upgrade or logical replication for major version upgrades
✓ Monitor replication lag with pg_stat_replication
✓ Schedule regular VACUUM ANALYZE on high-churn tables
✓ Freeze tables proactively before XID age reaches 1.5B
NOTES BY VS
PostgreSQL Mastery Reference · Built for personal use