Start here
Life of a SELECT
A SELECT is parsed and planned, takes a snapshot and a light table lock, finds the page in shared buffers or reads it from disk, checks which row versions it may see, and streams the rows back.
In plain words
When you run a SELECT, your session's backend process first works out what you asked for (parsing) and how to fetch it fastest (planning), for example by using an index.
It then takes a snapshot: a note of which transactions had finished at that moment. That snapshot decides which version of each row you are allowed to see, so you never see half-finished changes from other sessions.
The rows live in 8 kB pages. If the page is already in shared buffers (PostgreSQL's memory cache) it is read straight from memory. If not, it is read from disk into the cache first, which is much slower. Finally the matching rows are sent back to you.
A plain read does not change your data and does not wait for writers: readers and writers do not block each other in PostgreSQL.
For DBAs and operators
Step by step
- Protocol: with the simple query protocol the client sends one
Querymessage; drivers using prepared statements sendParse/Bind/Execute/Syncinstead (see wire protocol). - Parse, analyze, rewrite, plan: names are resolved through the system catalogs and
search_path, views are expanded by the rewriter, and the planner uses table statistics and the cost settings to choose a plan (see query lifecycle). - Snapshot: built from the proc array. In READ COMMITTED (the default) each statement gets a fresh snapshot; in REPEATABLE READ and SERIALIZABLE the whole transaction uses the first one.
- Lock: the table (and any index used) holds an
AccessShareLock, taken already while the statement is parsed and planned. It conflicts only withAccessExclusiveLock(for example manyALTER TABLEforms,DROP,TRUNCATE,VACUUM FULL), so a SELECT waits only for those. - Buffer lookup: the executor asks the buffer manager for each page it needs. A hit pins the buffer in shared memory. A miss takes a free buffer if there is one, otherwise chooses a victim with the clock-sweep algorithm (writing it out first if it is dirty), then reads the 8 kB page. The read may still be served by the operating system page cache rather than the disk. In PostgreSQL 18, sequential scans, bitmap heap scans and VACUUM can issue these reads through the new asynchronous I/O subsystem (
io_method). - Visibility: for each tuple the backend compares
xmin/xmaxwith the snapshot. If the tuple's hint bits do not yet say whether those transactions committed, it looks the answer up in pg_xact (CLOG) and sets the hint bits on the page. - Results: rows go back as
DataRowmessages, followed byCommandComplete(SELECT 1) andReadyForQuery. - End: in autocommit mode the transaction ends right away; locks and the snapshot are released.
Can a SELECT write?
Yes, a little. Setting hint bits dirties the page, and with data checksums enabled (the default for clusters created by initdb in PostgreSQL 18) or wal_log_hints = on, the first such change after a checkpoint writes a full-page image to WAL, shown by pg_waldump as FPI_FOR_HINT. A SELECT can also prune dead row versions from a page it visits (PRUNE_ON_ACCESS). Reads on a hot standby never generate WAL.
See it yourself
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM acc WHERE id = 3;
-- Buffers: shared hit=2 ← found in shared buffers
-- Buffers: shared read=2 ← had to be read in
SELECT locktype, relation::regclass, mode
FROM pg_locks WHERE pid = pg_backend_pid();For developers: inside the source
Call flow
exec_simple_query() tcop/postgres.c
pg_parse_query() → raw_parser() parser/gram.y
parse_analyze_fixedparams() parser/analyze.c
addRangeTableEntry() → parserOpenTable(…, AccessShareLock)
QueryRewrite() rewrite/rewriteHandler.c
pg_plan_query() → planner() → standard_planner()
get_relation_info() opens the indexes (same lock mode)
PortalStart(): PushActiveSnapshot(GetTransactionSnapshot())
→ ExecutorStart()
PortalRun() → ExecutorRun() → ExecIndexScan() → index_getnext_slot()
heapam_index_fetch_tuple() → ReleaseAndReadBuffer()
→ heap_hot_search_buffer() → HeapTupleSatisfiesVisibility()
printtup() sends DataRow; EndCommand(); ReadyForQuery()Key pieces
- Snapshot:
snapmgr.cGetTransactionSnapshot()callsGetSnapshotData(), which reads the proc array underProcArrayLock(shared) and fillsxmin,xmaxand thexip[]array of in-progress XIDs. Analysis and planning use their own snapshot; the one the executor uses is taken inPortalStart(). - Locks: relation locks go through
LockRelationOid()→LockAcquire()inlock.c. Weak modes such asAccessShareLocknormally use the per-backend fast path (FastPathGrantRelationLock()) and never touch the shared lock table. - Buffer lookup:
BufferAlloc()hashes theBufferTag, takes the buffer-mapping partition LWLock in shared mode and callsBufTableLookup(). A hit isPinBuffer(); a miss goes toGetVictimBuffer()→StrategyGetBuffer()(clock sweep viaClockSweepTick()). Reads are issued throughStartReadBuffer()/WaitReadBuffers(), and in 18 scans can batch them withread_stream.c. - Visibility:
HeapTupleSatisfiesMVCC()checksxmin/xmaxwithXidInMVCCSnapshot()andTransactionIdDidCommit()(pg_xact), thenSetHintBits()→MarkBufferDirtyHint(), which callsXLogSaveBufferForHint()(theFPI_FOR_HINTrecord) when checksums orwal_log_hintsrequire it. - Pruning on read:
heap_page_prune_opt()is called when a page is read and only acts ifpd_prune_xidis old enough and the page is nearly full.
Observe and hook
pageinspect(heap_page_items()showst_infomaskhint bits),pg_buffercache,EXPLAIN (ANALYZE, BUFFERS),trace_locks(only in builds withLOCK_DEBUG).- READMEs:
storage/buffer/README,storage/aio/README.md,parser/README. - Hooks along this path:
post_parse_analyze_hook,planner_hook,ExecutorStart_hook,ExecutorRun_hook,ExecutorFinish_hook,ExecutorEnd_hook(seeauto_explainandpg_stat_statements).
Parameters that shape this
shared_buffers default_transaction_isolation statement_timeout effective_cache_size random_page_cost io_method track_io_timing
Related topics
The big picture Life of a DELETE Life of a query Shared buffers Snapshots and visibility MVCC: row versions Commit log, hint bits and subtransactions Scan methods Wire protocol