Tid Scan
Appears in EXPLAIN asTid Scan
Fetches rows directly by their physical location (ctid).
What it is
A Tid Scan looks rows up by their tuple identifier (ctid) — the physical (block, offset) address of a row — so it can jump straight to the page without an index.
When the planner picks it
When the query filters on ctid directly, e.g. WHERE ctid = '(0,1)'.
Is it good or bad?
Extremely fast for the rows it targets, but ctids are not stable: UPDATE, VACUUM FULL, and CLUSTER move rows. Don't store ctids as if they were durable keys.
In depth
Reading rows by their address
A Tid Scan jumps directly to rows by CTID — a row's physical (block,
offset) location in the heap. There's no index lookup; PostgreSQL goes
straight to the page and returns the tuple.
SELECT * FROM big WHERE ctid = '(0, 1)';
SELECT * FROM big WHERE ctid IN ('(0, 1)', '(5, 12)', '(99, 3)');
It's the fastest possible single-row fetch — one page read, no traversal — which is why some chunked batch jobs use CTID lists to drive parallel work.
CTIDs are not stable
The catch is everything: a row's CTID can change. Any UPDATE may move the
row (a non-HOT update writes a new tuple at a new location); VACUUM FULL,
CLUSTER, and pg_repack rewrite the entire table; a logical replica may
have entirely different CTIDs for the same rows.
So never store CTIDs as if they were durable keys. Use them for transient work — a one-pass batch job that produces a CTID list, processes it immediately, and never persists it.
When the planner picks it
The planner picks a Tid Scan when the WHERE clause filters on ctid
directly. It's not a fallback for queries on a normal column — for "find
this row," a normal index works and is much more useful than CTID.
For range work over physical layout, see Tid Range Scan.