Query processing
Scan methods
A scan node is how PostgreSQL reads one table. Which one the planner picks decides how many pages are read, and in which order.
In plain words
A table is stored as a row of 8 kB pages, and rows sit on pages in no particular order. An index is a separate, sorted structure that says "value 12 is on page 7, slot 3".
There are a few ways to find rows: read every page from start to end (Seq Scan); look each value up in the index and jump to its page (Index Scan); answer from the index alone when it already holds every column you asked for (Index Only Scan); or first collect all the matching page numbers from the index, sort them, and then visit each page once in order (Bitmap Scan). If you already know the physical address of a row (its ctid), a TID Scan goes straight there.
Pick a method below and watch which pages get touched.
For DBAs and operators
Seq Scan
Reads blocks 0…N in order and evaluates the Filter on every visible tuple. Cheap per page and served by a read stream (PostgreSQL 17+) that combines neighbouring blocks into one read of up to io_combine_limit; since PostgreSQL 18 those reads are issued ahead with asynchronous I/O. Large scans use a small ring buffer so they do not evict the whole cache, and with synchronize_seqscans a second scan of the same big table joins the first one mid-way.
Index Scan
Descends the B-tree once per search (Index Searches in PostgreSQL 18 EXPLAIN), walks matching leaf entries in key order and fetches each heap tuple by TID to check visibility. Output is sorted by the index key, which can save a Sort. Cost grows with random heap reads unless the table is physically correlated with the index.
Index Only Scan
Possible when every referenced column can be returned by the index (including payload columns supplied with INCLUDE, where the access method supports index-only scans). For each entry it checks the page's bit in the visibility map: an all-visible heap page needs no per-tuple heap visibility check; otherwise the heap is visited. Watch Heap Fetches: a high number means many referenced pages are not all-visible, which can be normal on frequently modified data or can justify reviewing vacuum cadence.
Index Only Scan using orders_pkey on orders (actual rows=999.00 loops=1)
Index Cond: (id < 1000)
Heap Fetches: 0
Index Searches: 1
Buffers: shared hit=4 read=2Bitmap Index Scan + Bitmap Heap Scan
The index scan builds an in-memory TID bitmap (one entry per heap page, with per-tuple bits). Several bitmaps can be combined with BitmapAnd / BitmapOr, e.g. for a = 1 OR b = 2. The heap scan then visits pages in physical order, each at most once, and can prefetch them (effective_io_concurrency). If the bitmap would exceed work_mem, pages become lossy: only "something matches on this page" is remembered, so every tuple there must be rechecked.
SET work_mem = '64kB';
Bitmap Heap Scan on orders (actual rows=203374.00 loops=1)
Recheck Cond: ((created >= '2024-03-01'::date) AND (created <= '2024-04-30'::date))
Rows Removed by Index Recheck: 393133
Heap Blocks: exact=967 lossy=3641
-> Bitmap Index Scan on orders_created_idx (actual rows=203374.00 loops=1)TID Scan and TID Range Scan
WHERE ctid = '(5,2)' fetches block 5, line pointer 2 directly; ctid >= '(1000,0)' AND ctid < '(2000,0)' (TID Range Scan, PostgreSQL 14+) reads a block range, which is handy for batching big deletes or updates. A ctid changes when a row is updated or the table is rewritten, so never store it.
Diagnosing
Rows Removed by Filterlarge on a Seq Scan of a big table → missing or unusable index (function on the column, type mismatch, leading wildcardLIKE '%x').enable_seqscan = offetc. do not forbid a method: since PostgreSQL 18 the planner counts disabled nodes and prefers plans with fewer of them, but still uses a disabled method when there is no alternative (EXPLAIN then showsDisabled: true). Use them in a session to compare plans, never globally.pg_stat_user_tables.seq_scan/idx_scanandpg_stat_user_indexes.idx_scanshow which methods are used in practice.
For developers: inside the source
Where it lives
One executor file per node in src/backend/executor/: nodeSeqscan.c, nodeIndexscan.c, nodeIndexonlyscan.c, nodeBitmapIndexscan.c, nodeBitmapHeapscan.c, nodeTidscan.c, nodeTidrangescan.c. Each fetches tuples through an access-method function (SeqNext(), IndexNext(), …) wrapped by ExecScan() / ExecScanExtended() (execScan.c), which evaluates the qual and projection. Since PostgreSQL 18 ExecInitSeqScan() installs a specialised variant (ExecSeqScan, ExecSeqScanWithQual, ExecSeqScanWithProject, ExecSeqScanWithQualProject).
Below the executor sit the table AM (tableam.h, heap implementation in heapam_handler.c and heapam.c) and the index AM API (IndexAmRoutine in amapi.h: amgettuple, amgetbitmap, amcanreturn, amcostestimate) with generic wrappers in indexam.c.
Call flow per method
- Seq Scan:
SeqNext()→table_scan_getnextslot()→heap_getnextslot()→heapgettup_pagemode()→heap_fetch_next_buffer()→read_stream_next_buffer(); the stream callbackheap_scan_stream_read_next_serial()(or_parallel) supplies block numbers.initscan()picks aBAS_BULKREADring when the table is larger thanNBuffers / 4and gets the start block fromss_get_location()(syncscan.c). - Index Scan:
IndexNext()→index_getnext_slot()→index_getnext_tid()(callsamgettuple, e.g.btgettuple()) →index_fetch_heap()→heapam_index_fetch_tuple(), which follows HOT chains withheap_hot_search_buffer(). - Index Only Scan:
IndexOnlyNext()callsindex_getnext_tid(), testsVM_ALL_VISIBLE()itself and callsindex_fetch_heap()only when the bit is clear; column values come fromxs_ituporxs_hitup. - Bitmap:
MultiExecBitmapIndexScan()creates the bitmap withtbm_create(work_mem * 1024, …)and fills it viaindex_getbitmap()→btgetbitmap()→tbm_add_tuples()(tidbitmap.c);tbm_lossify()turns exact page entries into lossy chunks when the budget is exceeded.BitmapHeapNext()→table_scan_bitmap_next_tuple()→heapam_scan_bitmap_next_tuple(); since PostgreSQL 18 the heap pages come from a read stream (bitmapheap_stream_read_next()). - TID:
TidNext()andTidRangeNext().
Observe it
pageinspect(heap_page_items(),bt_page_items()) shows line pointers, ctids and index entries;pg_visibilityshows the VM bits that decide Heap Fetches;pg_buffercacheshows which pages a scan pulled into shared buffers.- READMEs:
nbtree/README,heap/README.HOT, andstorage/aio/README.mdfor the read stream. - Regression tests:
src/test/regress/sql/tidscan.sql,tidrangescan.sql,bitmapops.sql,btree_index.sql,create_index.sql.
Parameters that shape this
enable_seqscan enable_indexscan enable_indexonlyscan enable_bitmapscan enable_tidscan work_mem random_page_cost effective_io_concurrency synchronize_seqscans
Related topics
Planner cost model B-tree indexes Free space map and visibility map Page layout Asynchronous I/O VACUUM Parallel query