LearnThatStack Ace your next interview

PostgreSQL

Commands for PostgreSQL 14+ Checked Oct 2026 102 commands

The model

  • Under multiversion concurrency control (MVCC), an UPDATE writes a new row version and leaves the old one as a dead row.
  • Each version records xmin and xmax, the transactions that created and deleted it. A snapshot uses them to pick the visible version.
  • VACUUM frees dead rows for reuse and freezes old rows to prevent transaction ID wraparound.
  • Every change goes to the write-ahead log (WAL) first, and COMMIT waits only for that log. Crash recovery and replicas replay it.
  • The planner picks each plan from table statistics. ANALYZE refreshes them.
ReaderRead CommittedTablerow versionsWriteropen transactionAutovacuumbackgroundUPDATE adds v2reads v1 not blockedCOMMIT v2 is livereads v2 new snapshotVACUUM frees v1
An UPDATE adds v2 and sets xmax on v1. Readers see v1 until the writer commits, and nobody waits. VACUUM frees v1 once no snapshot needs it.

MVCC & VACUUM

row versions, and the cleanup they need

HOT updates and fillfactor

Leave room on each page for new row versionsALTER TABLE accounts SET (fillfactor = 80);It applies to pages filled from now on.

TipA heap-only tuple (HOT) update adds no index entry. The old version points to the new one on the same page.

TrapChanging any indexed column rules out HOT, except for BRIN indexes (16+). Avoid indexing columns that change often.

VACUUM, ANALYZE and REINDEX

Free the space of dead rows for reuseVACUUM orders;It rarely shrinks the file.
Give a bloated table's space back to the operating systemVACUUM FULL orders;It rewrites the table under an exclusive lock.
Refresh the planner's statisticsANALYZE orders;Run it after large data changes.
Rebuild a bloated or corrupt indexREINDEX INDEX CONCURRENTLY idx_cust;A plain REINDEX blocks writes.

TipWriters still block writers. A second UPDATE of a row waits for the first to finish.

TrapA long-running transaction stops VACUUM from removing any row that transaction might still see.

Transaction ID wraparound

Check how close each database is to wraparoundSELECT datname, age(datfrozenxid) FROM pg_database;Autovacuum forces a freeze on any table past 200 million, the autovacuum_freeze_max_age default.
Find what stops freezingA long open transaction, a forgotten prepared transaction or a stale replication slot.

TipTransaction IDs are 32-bit and compared in a circle. Unfrozen old rows would seem to come from the future.

TrapWith 3 million IDs left, PostgreSQL accepts only read-only transactions until a database-wide VACUUM runs.

Types, keys & JSONB

column types, constraints, and removing data

Column types and auto-numbered keys

Store money exactlyprice numeric(10,2)Not real, which is approximate.
Add an auto-numbered identity columnGENERATED ALWAYS AS IDENTITYALWAYS rejects your own value unless you add OVERRIDING SYSTEM VALUE.
Understand an older SERIAL columnid SERIAL PRIMARY KEYIt means a separate sequence and a nextval() default.
Generate a UUID keygen_random_uuid()uuidv7() (18+) gives time-ordered values.
Store a file's bytes in the rowcontent byteaTOAST compresses or moves large values out of the row, up to 1 GB each.
Store a file over 1 GB, or read part of onelo_import('/tmp/a.pdf')A large object, up to 4 TB. Deleting its row leaves it behind, so run vacuumlo.

Tipchar has no speed advantage in PostgreSQL. Use text or varchar.

TrapIdentity and SERIAL columns leave gaps. A value is used up even when its insert rolls back.

Primary, unique and foreign keys

Tell PRIMARY KEY from UNIQUEA table has one primary key, which forbids NULL. UNIQUE allows many NULLs, or one with NULLS NOT DISTINCT (15+).
Make a column point at a row in another tableREFERENCES departmentsDeleting a department still in use fails by default.
Delete the referencing rows with their parentON DELETE CASCADEON DELETE SET NULL keeps them and clears the key.

TrapPostgreSQL doesn't index a foreign key column. Without that index, each parent delete scans the child table.

DELETE, TRUNCATE or DROP

Remove the rows a condition matchesDELETE FROM users WHERE age < 18;It fires ON DELETE triggers and leaves dead rows for VACUUM.
Empty the whole table fastTRUNCATE TABLE users;It frees the space at once, but locks out all other access.
Remove the table itselfDROP TABLE users;

TipAll three roll back inside a transaction. A ROLLBACK after BEGIN even brings back a dropped table.

JSON and JSONB

Read a field, or a nested field, as textdata->>'name'data->'specs'->>'cpu'-> returns JSON, and ->> returns text.
Find documents that contain a value or a keyWHERE data @> '{"price": 999}'WHERE data ? 'price'
Index every key for @> and ? queriesUSING GIN (data)Only jsonb supports GIN. An expression index on one field works for json too.

Trapjsonb drops white space and key order, and keeps only the last value of a duplicate key.

Indexes & performance

find rows fast, and see why a query is slow

Create indexes and build them safely

Index a column you filter or join onCREATE INDEX ON users (email);A B-tree by default. Writes wait until the build ends.
Build an index without blocking writesCREATE INDEX CONCURRENTLYIt can't run inside a transaction block.
Drop the invalid index a failed build leavesDROP INDEX CONCURRENTLY idx_cust;Queries ignore it, but every write still updates it.
Index two columns togetherCREATE INDEX ON users (last_name, age);Best when queries filter on last_name, the leading column.
Index only the rows you queryCREATE INDEX ON users (email) WHERE active;
Index a computed valueCREATE INDEX ON users (lower(email));

TipPostgreSQL 18 can use a multicolumn B-tree without its leading column, by a skip scan, when that column has few distinct values.

TrapEvery index slows writes and takes disk space. Drop the ones pg_stat_user_indexes shows no query uses.

Index types and index-only scans

Serve = lookups onlyUSING HASH (email)B-tree, the default, serves = too, plus ranges and sorting.
Search inside arrays, jsonb or full textUSING GIN (tags)One entry per element inside a value.
Search geometry, ranges or nearest neighborsUSING GIST (location)Lossy for full-text search, so PostgreSQL rechecks each match.
Index a huge table that grows in time orderUSING BRIN (created_at)One small summary per range of table blocks.
Add the columns a query returns to its indexINCLUDE (total)A covering index. INCLUDE columns are stored, not searched.

TipIndex entries carry no visibility information. The scan skips the table only on pages VACUUM marked all-visible.

TrapGIN never supports index-only scans, because its entries hold only parts of each value.

Read a query plan

Show the plan without running the queryEXPLAINEach node shows an estimated cost and row count.
Run the query and see real rows and timesEXPLAIN (ANALYZE, BUFFERS)BUFFERS counts cache hits and disk reads. It is on by default in 18+.
Read the scan and join nodesA Seq Scan on a selective filter points to a missing index. Hash Join suits larger inputs than Nested Loop.

TipWhen estimated and actual rows differ a lot, run ANALYZE first. Stale statistics are the usual cause.

TrapEXPLAIN ANALYZE really runs the statement. Wrap an UPDATE or DELETE in BEGIN and ROLLBACK.

Find slow queries and tune memory

List the statements that take the most total timeSELECT * FROM pg_stat_statementsSort it by total_exec_time.
See what each session is runningSELECT * FROM pg_stat_activity;Look for long queries and idle in transaction sessions.
Cancel a slow query, or end its sessionpg_cancel_backend(12345)pg_terminate_backend(12345)The second ends the session and rolls back its transaction.
Set the two main memory settingsshared_buffers = 4GBwork_mem = 64MBStart shared_buffers at a quarter of RAM. work_mem applies per sort or hash step.

TrapA session left idle in transaction holds its locks and stops VACUUM from cleaning up.

Transactions & locks

keep data correct when sessions write at once

ACID in PostgreSQL

Make a transfer all or nothingBEGIN;COMMIT;Atomicity. If any statement fails, the whole transaction rolls back.
Reject data that breaks a ruleConsistency, through keys and CHECK constraints.
Hide unfinished changes from other transactionsIsolation. Each statement reads an MVCC snapshot.
Keep a committed change through a crashDurability. The WAL reaches disk before COMMIT returns.

TrapOutside a BEGIN block, each statement is its own transaction. A failed second update can't undo the first.

Isolation levels

READ UNCOMMITTEDAsk for Read Uncommitted, get Read Committed
READ COMMITTEDUse the default, a new snapshot per statement
REPEATABLE READKeep one snapshot for the whole transaction
SERIALIZABLEAct as if transactions ran one at a time
preventedcan happen

TipWrite skew means two transactions read the same rows, write different ones, and together break a rule.

TrapRepeatable Read and Serializable can fail with SQLSTATE 40001. The application must retry the whole transaction.

Row locks and deadlocks

Lock a row you will update based on what you readFOR UPDATEOther writers wait until you commit. Plain reads don't.
Let workers take jobs from a queue without waitingFOR UPDATE SKIP LOCKED
Prevent deadlocksLock rows in the same order everywhere, such as by id.
Fail fast instead of waiting on a lockSET lock_timeout = '5s';Or add NOWAIT to fail at once.
Find what is blocking a sessionpg_blocking_pids(12345)
Lock a key your application choosespg_advisory_lock(42)PostgreSQL doesn't enforce it. Release it with pg_advisory_unlock.

TipThe four modes, strongest first, are FOR UPDATE, FOR NO KEY UPDATE, FOR SHARE and FOR KEY SHARE. A plain UPDATE takes the second.

TrapPostgreSQL checks for a deadlock after deadlock_timeout, 1 second by default. It aborts one transaction with 40P01, so retry it.

Queries, views & functions

joins, CTEs, window functions and code in the database

CTEs and window functions

Walk a hierarchy, like an org chartWITH RECURSIVEIt starts from a base query and adds one level at a time with UNION ALL.
Choose whether a CTE runs on its ownAS MATERIALIZEDAS NOT MATERIALIZEDPostgreSQL folds a plain CTE used once into the main query. These force either choice.
Rank employees within each departmentRANK() OVER (PARTITION BY dept ORDER BY pay DESC)Ties share a rank. ROW_NUMBER never ties.
Average each row with the two rows before itROWS BETWEEN 2 PRECEDING AND CURRENT ROWWith no frame clause, the frame starts at the first row.

TipWindow functions run after WHERE and GROUP BY. Filter on their result in an outer query.

Views, functions and triggers

Store an expensive report's resultUse a materialized view. A plain view stores only the query and reruns it on every read.
Refresh a materialized viewREFRESH MATERIALIZED VIEW stats;A large refresh blocks reads until it finishes.
Refresh it while reads continueREFRESH MATERIALIZED VIEW CONCURRENTLY stats;It needs a unique index on the materialized view.
Compute a value inside a querySELECT calculate_bonus(42);A function. It can't COMMIT.
Run a batch job that commits in stepsCALL update_salaries();A procedure. It can COMMIT.
Run a function for every changed rowEXECUTE FUNCTION audit_trigger()Part of CREATE TRIGGER. A BEFORE trigger can change NEW or skip the row.
Write your own aggregate or operatorCREATE AGGREGATECREATE OPERATORAn aggregate needs a state type and a transition function.

TrapA procedure's COMMIT fails with "invalid transaction termination" when the CALL is inside a BEGIN block.

WAL, replication & backups

survive a crash, copy the data, and restore it

WAL, backups and recovery

Dump one database, then restore chosen objectspg_dump -Fc mydb > mydb.dumppg_restore -d mydb mydb.dumppg_dump doesn't block other users.
Copy the whole cluster's filespg_basebackup -D /backup/base -PIncremental backups need 17+, and pg_combinebackup merges them.
Archive every finished WAL filearchive_mode = onarchive_command = 'cp %p /backup/wal/%f'
Restore to just before a bad DELETEtouch $PGDATA/recovery.signalrecovery_target_time = '2026-10-01 12:00'First copy in a base backup and set restore_command.

TipAt COMMIT, PostgreSQL waits for one sequential WAL write. Changed data pages reach disk later, at checkpoints.

TrapA backup you have never restored may not work. Test restores regularly.

Replication and logical decoding

Keep an exact copy of every databaseA streaming standby, asynchronous by default. Synchronous mode loses no commits but slows them.
Copy chosen tables to another serverCREATE PUBLICATION pub FOR TABLE orders;Then CREATE SUBSCRIPTION on the other server. It works across major versions.
Stream row changes to a tool like Debeziumpg_create_logical_replication_slot('s', 'pgoutput')Logical decoding. It needs wal_level = logical.
Drop a slot nobody readspg_drop_replication_slot('s')

TipLogical replication copies no schema changes, and no sequence values before PostgreSQL 19. Automatic failover needs an external tool.

TrapAn abandoned slot keeps WAL on disk until someone reads it. Watch pg_replication_slots.

Upgrade to a new major version

Upgrade the cluster in placepg_upgrade --checkpg_upgrade --linkFar faster than a dump and restore. --check only reports problems.
Upgrade with only seconds of downtimeReplicate logically to the new version, then switch. Copy the schema first.

TrapOnce the new cluster starts after a --link upgrade, you can't go back to the old one.

Partitions, shards & pools

grow past one big table or one server

Partition a large table

Split a table by range, list or hashPARTITION BY RANGE (day)PARTITION BY LIST (region)PARTITION BY HASH (id)Then create each partition with PARTITION OF, such as one table per year.
Read only the partitions a query needsPartition pruning skips those the WHERE clause rules out.
Remove a year of old data fastDROP TABLE sales_2023;Far faster than a bulk DELETE, with no dead rows.

TipPartition when a table outgrows the server's memory, and queries or deletes follow the partition key.

TrapA primary key or unique constraint must include the partition key. Too many partitions slow planning.

Foreign tables and sharding

Query tables on another PostgreSQL serverCREATE EXTENSION postgres_fdw;IMPORT FOREIGN SCHEMA public FROM SERVER db2 INTO remote;Create a server and a user mapping first.
Pick a sharding approachRoute keys in the application, attach foreign tables as hash partitions, or use the Citus extension.

TrapJoins and transactions across shards are slow and hard to keep atomic. Pick a shard key that keeps most queries on one shard.

Connection pooling and PgBouncer

Say why PostgreSQL needs a poolEach connection is a separate server process. max_connections is 100 by default.
Share a few server connections among many clientspool_mode = transactionPgBouncer frees the connection after each transaction. Session mode waits for a disconnect.

TrapTransaction mode drops session state between transactions, so SET, LISTEN and session advisory locks break.

Security, audit & extensions

control access, record activity, and add features

Roles and privileges

Create a group role and a login roleCREATE ROLE analysts;CREATE ROLE app LOGIN PASSWORD 'pw';
Give a user a group's privilegesGRANT analysts TO app;
Let a role read every table in a schemaGRANT USAGE ON SCHEMA public TO analysts;GRANT SELECT ON ALL TABLES IN SCHEMA public TO analysts;
Cover tables created laterALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES TO analysts;Only tables made by the role that runs it, unless you add FOR ROLE.

TrapA role needs USAGE on the schema as well as rights on its tables.

Connections and row-level security

Require TLS and strong passwordshostssl all all 10.0.0.0/8 scram-sha-256A pg_hba.conf line. The first match wins. MD5 passwords are deprecated.
Show each user only their own rowsALTER TABLE docs ENABLE ROW LEVEL SECURITY;CREATE POLICY own ON docs USING (uid = current_setting('app.uid')::int);The app runs SET app.uid = '42' after it connects.

TipConnect the application as an ordinary role, never a superuser.

TrapSuperusers always bypass row-level security. Table owners do too, unless the table has FORCE ROW LEVEL SECURITY.

Extensions and audit logging

Add an extension to one databaseCREATE EXTENSION pgcrypto;Its files must already be on the server. Repeat it in each database.
Name extensions worth knowingPostGIS for geography, pgcrypto for encryption and hstore for key-value pairs.
Log slow statements and connectionslog_min_duration_statement = 1000log_connections = onlog_statement = 'all' logs requests, not what ran. That isn't a formal audit.
Write formal audit entriesshared_preload_libraries = 'pgaudit'Restart, run CREATE EXTENSION pgaudit, then set pgaudit.log.

TrapAn extension that needs shared memory, like pg_stat_statements, must be in shared_preload_libraries, then the server restarted.

Found this useful? Pass it on.
Pro · $10/mo

This sheet stays free. With Pro, you can:

  • Every answer on this sheet. Read the full answer to all 42 questions it links, not just the 5 free ones.
  • My sheet. The cards you star and the ones you miss in Quiz me, from every sheet, on one page to quiz and print.
  • PDFs. Download this sheet and My sheet, laid out for A4, with every question a link to its answer.
  • The rest of Pro. Every answer in the question library, and 1,000 AI credits a month.