PostgreSQL essentials: 50 cards on indexes, MVCC and JSONB
PostgreSQL know-how: data types, B-tree and GIN indexes, EXPLAIN, isolation levels, MVCC and VACUUM, JSONB, CTEs, window functions, constraints and roles.
Practice now
Study without an account
text vs varchar(n) in PostgreSQLScheduled with the default Askesis timings. Progress is saved in this browser.
All 50 cards
- 1
textvsvarchar(n)in PostgreSQLSame storage and speed;varchar(n)only adds a length check. Usetextunless you want that limitchar(n)pads values with spaces and is rarely what you want. - 2
timestampvstimestamptztimestamptzstores an absolute instant (in UTC) and displays it in the session’s time zone;timestampis a wall-clock value with no zone. Prefertimestamptz - 3
numericvsdouble precisionnumericdoes exact decimal arithmetic, right for money;double precisionis fast binary floating point, where0.1 + 0.2isn’t exactly0.3 - 4Declare an auto-incrementing primary key the modern waysql
CREATE TABLE users ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, email text NOT NULL UNIQUE );SQL-standard identity columns are preferred overserial. Sequence values are never rolled back, so gaps in IDs are normal. - 5Generate UUIDs in PostgreSQL
gen_random_uuid()makes random v4 UUIDs (built in since PostgreSQL 13);uuidv7()(PostgreSQL 18) makes time-ordered ones that are friendlier to B-tree indexes - 6What does
SELECT 5 / 2;return?2: dividing two integers truncates. Write5 / 2.0or5::numeric / 2to get 2.5 - 7What does this create?sql
CREATE TABLE Users (userId int);A tableuserswith a columnuserid: unquoted names are folded to lowercase. Quoted"userId"keeps its case but must then always be quoted, so use snake_case - 8What does
now()return inside a long transaction?The transaction’s start time, the same for every call in that transaction;clock_timestamp()returns the actual current time - 9What is the default index type good for?B-tree indexes handle equality and ranges (
=,<,BETWEEN),ORDER BY,IS NULLand prefix searches likeLIKE 'abc%'PrefixLIKEneeds the C collation or an index built withtext_pattern_ops. - 10When do you use a GIN index?For values that contain many elements:
jsonbdocuments, arrays, full-text searchtsvectors and trigram search withpg_trgm - 11What is a BRIN index for?Huge tables whose values follow the physical row order, like append-only timestamps; it stores only min and max per block range, so it is tiny
- 12Does PostgreSQL index foreign key columns automatically?No. Primary keys and unique constraints get indexes, but the referencing column doesn’t; index it to speed up joins and deletes on the parent table
- 13What is a partial index?An index over only the rows matching a
WHEREclause: smaller and faster, used when a query’s filter implies that conditionsqlCREATE INDEX ON orders (created_at) WHERE status = 'pending'; - 14Why doesn’t
WHERE lower(email) = $1use the index onemail?The index storesemail, notlower(email). Create an expression index:CREATE INDEX ON users (lower(email)); - 15What does
CREATE INDEX CONCURRENTLYdo?Builds the index without blocking writes to the table; it is slower, can’t run inside a transaction block, and a failed build leaves an invalid index to drop - 16What does
INCLUDEadd to an index?Extra columns stored in the index but not searched on, so queries that read only those columns can use an index-only scansqlCREATE INDEX ON orders (customer_id) INCLUDE (total); - 17Why can’t
WHERE name LIKE '%son'use a B-tree index?A B-tree is sorted from the first character, so a leading wildcard can’t narrow the search. Use a trigram GIN index (pg_trgm) or full-text search - 18Seq Scan, Index Scan, Index Only Scan, Bitmap Heap ScanReads the whole table; follows the index to each row; answers from the index alone; collects matches from an index, then reads those table pages in order
- 19What do these numbers mean in
EXPLAINoutput?sqlIndex Scan using users_pkey on users (cost=0.29..8.31 rows=1 width=72)Planner estimates: startup cost..total cost in arbitrary units, expected rows returned, and average row width in bytes - 20Why wrap
EXPLAIN ANALYZEof aDELETEin a transaction?EXPLAIN ANALYZEactually runs the statement. UseBEGIN; EXPLAIN ANALYZE …; ROLLBACK;to measure it without keeping the changes - 21The planner expected 10 rows but got 500,000. What should you check first?Stale statistics: run
ANALYZE table_name;so the planner has fresh row counts and value distributionsAutovacuum normally runsANALYZEfor you. - 22Is a Seq Scan in a plan always bad?No. For small tables, or queries that return a large share of the rows, reading the table sequentially is cheaper than many random index lookups
- 23What is PostgreSQL’s default isolation level?Read Committed: each statement sees the data committed before that statement beganRead Uncommitted behaves the same in PostgreSQL: dirty reads never happen.
- 24What does Repeatable Read guarantee in PostgreSQL?The whole transaction sees one snapshot taken at its first query, which also prevents phantom reads; updating a row another transaction changed meanwhile fails with a serialization error
- 25What must an app do when using Serializable isolation?Retry transactions that fail with SQLSTATE
40001: PostgreSQL aborts one side of any conflict so results match running them one at a time - 26Two sessions read a balance, subtract in app code and write it back. How do you prevent the lost update?Update atomically (
SET balance = balance - 10), lock the row first withSELECT … FOR UPDATE, or use Repeatable Read and retry - 27What does this do when several workers run it?sql
SELECT id FROM jobs WHERE status = 'queued' ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED;Each worker locks a different queued job, skipping rows other transactions have locked: a simple job queue - 28Can you roll back
CREATE TABLEorALTER TABLEin PostgreSQL?Yes. DDL is transactional, so a migration can run in one transaction and roll back cleanly; exceptions includeCREATE INDEX CONCURRENTLYandCREATE DATABASE - 29What is MVCC?Multi-version concurrency control: an
UPDATEwrites a new row version and leaves the old one as a dead tuple, so each transaction reads a consistent snapshotReaders don’t block writers and writers don’t block readers. - 30
VACUUMvsVACUUM FULLVACUUMmarks dead tuples’ space reusable and runs alongside normal traffic, but rarely shrinks the file;VACUUM FULLrewrites the table compactly under an exclusive lock that blocks all access - 31What does autovacuum do?Vacuums and analyzes tables in the background as they change, and freezes old rows to prevent transaction ID wraparound; don’t turn it off
- 32Why are long-running or idle-in-transaction sessions harmful?Their old snapshot stops VACUUM from removing any dead tuples newer than it, so tables and indexes bloat
- 33
jsonvsjsonbjsonstores the input text verbatim;jsonbstores a parsed binary form (no whitespace or key order, last duplicate key wins) that is faster to query and can be indexed. Usejsonb - 34
->vs->>on JSON->returnsjsonb(to keep navigating);->>returnstextsqlSELECT data->'address'->>'city' FROM users; - 35Find events whose JSONB payload has
"type": "signup", using an indexsqlCREATE INDEX ON events USING gin (payload); SELECT * FROM events WHERE payload @> '{"type": "signup"}';@>means “contains”. The default GIN operator class also supports key-exists checks likepayload ? 'email'. - 36Are CTEs an optimization fence in PostgreSQL?Not since PostgreSQL 12: a non-recursive, side-effect-free CTE referenced once is inlined into the main query. Write
AS MATERIALIZEDto force it to run separately - 37Move finished tasks to an archive table in one statementsql
WITH moved AS ( DELETE FROM tasks WHERE done RETURNING * ) INSERT INTO tasks_archive SELECT * FROM moved; - 38List an employee and everyone below them in the org chartsql
WITH RECURSIVE team AS ( SELECT id, name FROM employees WHERE id = 1 UNION ALL SELECT e.id, e.name FROM employees e JOIN team t ON e.manager_id = t.id ) SELECT * FROM team; - 39Get the 3 most expensive products per categorysql
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY category ORDER BY price DESC ) AS rn FROM products ) ranked WHERE rn <= 3;Window functions can’t appear inWHERE, hence the subquery. - 40What does
DISTINCT ONdo?PostgreSQL-specific: keeps the first row of each group, as decided byORDER BYsql-- latest login per user SELECT DISTINCT ON (user_id) * FROM logins ORDER BY user_id, created_at DESC; - 41Why do rows with the same
dayget the same running total?sqlSELECT day, amount, SUM(amount) OVER (ORDER BY day) AS running FROM sales;WithORDER BY, the default frame isRANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, which includes every peer with an equalday. UseROWS BETWEEN …to go row by row - 42Does
CHECK (price > 0)reject a NULL price?No. A check fails only when the expression is false, andNULL > 0is unknown. AddNOT NULLas well - 43What does this constraint prevent?sql
ALTER TABLE bookings ADD EXCLUDE USING gist (room_id WITH =, during WITH &&);Two bookings for the same room with overlappingduringtime rangesUsing=on a plain column in a GiST index needs thebtree_gistextension. - 44What does
DEFERRABLE INITIALLY DEFERREDdo on a constraint?Checks it atCOMMITinstead of after each statement, so rows can be temporarily inconsistent inside the transaction, e.g. for circular foreign keys - 45Users vs groups in PostgreSQLBoth are roles.
CREATE USERisCREATE ROLEwithLOGIN, and granting one role to another (GRANT readers TO alice) makes it a group - 46Create a read-only role that also covers future tablessql
CREATE ROLE readonly; GRANT USAGE ON SCHEMA public TO readonly; GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly;ON ALL TABLESonly covers existing tables; default privileges cover tables the granting role creates later. - 47What changed about the
publicschema in PostgreSQL 15?Ordinary users can no longer create objects in it by default; theCREATEprivilege must be granted explicitly - 48Restrict rows per user with row-level securitysql
ALTER TABLE documents ENABLE ROW LEVEL SECURITY; CREATE POLICY owner_only ON documents USING (owner_id = current_setting('app.user_id')::int);Superusers,BYPASSRLSroles and, unless you addFORCE ROW LEVEL SECURITY, the table owner bypass policies. - 49Why put PgBouncer in front of PostgreSQL?Each connection is a separate server process with its own memory, so thousands of connections hurt; a pooler shares a small number of them
- 50Essential
psqlmeta-commands\llists databases,\c dbconnects,\dtlists tables,\d namedescribes a table,\xtoggles expanded output,\qquits
Frequently asked questions
Which PostgreSQL version does this deck cover?
Current releases (PostgreSQL 15 to 18). Version notes call out newer features such as uuidv7 in 18 and the public schema permission change in 15.
Should I learn SQL first?
It helps. This deck assumes you can write SELECT, JOIN and GROUP BY queries; our SQL essentials deck covers those basics.
What is the difference between json and jsonb?
json keeps the original text exactly; jsonb stores a parsed binary form that drops whitespace and duplicate keys, is faster to query and supports GIN indexes. Use jsonb unless you must preserve the input as written.
More in Databases
SQL
Learn SQL that holds up in production: joins, grouping, NULL semantics, indexes, transactions, window functions and CTEs.
50 cardsMongoDB
MongoDB basics: documents and BSON, CRUD in mongosh, query operators, indexes, the aggregation pipeline, embedding vs referencing, replica sets and sharding.
50 cardsRedis
Redis basics: strings, lists, sets, sorted sets, hashes and streams, TTLs, RDB and AOF persistence, pub/sub, caching patterns, eviction and MULTI/EXEC.
50 cards