enable_tidscan is the enable_* knob you will almost certainly never set, and for once that is not a warning, just a fact about how TID scans happen. You do not stumble into one the way you stumble into a sequential scan or a sort. A TID scan appears only when you have explicitly written a qualification on ctid, so the plan type this parameter governs is one you get exactly when you asked for it and never otherwise.

(For the record, I have never seen a tidscan node in my entire life.)

Default on, context user. A ctid is a row’s physical address: a (block, offset) pair like (42,7), meaning item 7 on page 42. A TID scan uses that address to go straight to the row without consulting an index or scanning anything, which makes it the fastest possible way to reach a single tuple. The planner produces one when it sees WHERE ctid = '(42,7)', or WHERE ctid = ANY(...), or an UPDATE ... WHERE CURRENT OF against an open cursor.

So when would you turn it off? You would not. The only queries it can affect are ones where you deliberately named ctid, and for those a TID scan is essentially always the correct, cheapest plan; disabling it just forces a sequential scan to find the same row by filtering on ctid, which is strictly worse. The one case where you might worry about a side effect, cursor-driven WHERE CURRENT OF updates, is safe regardless: the optimizer has special machinery that forces a TID scan for the CURRENT OF case, so the setting cannot break it. This knob is, for practical purposes, a formality.

What is worth your attention is ctid itself, which has two genuinely useful jobs. The first is the cursor pattern above: fetch rows through a SELECT ... FOR UPDATE cursor and modify the current one with WHERE CURRENT OF, which is the tidy, TID-backed way to walk and update a result set. The second arrived in PostgreSQL 14, which added a Tid Range Scan node for range qualifications like WHERE ctid >= '(0,0)' AND ctid < '(10000,0)'. Before 14 such a predicate fell back to a sequential scan with a filter; now it reads exactly that contiguous span of pages, in physical order. That turns out to be a serious tool for batch work: a backfill driven by an index has to chase its matching rows across scattered heap pages one random read at a time, while the same backfill chunked into ctid ranges reads the table straight through, sequentially, and can run an order of magnitude faster.

The catch, and it is the whole catch, is that a ctid is not stable. It changes when the row is updated, and whenever the table is rewritten by VACUUM FULL, CLUSTER, or a tool like pg_repack; a freed ctid can also be handed to a different row later. A ctid is a location, not an identity, and the batch-by-range trick only holds while the table is not changing underneath you. Storing a ctid and expecting to find the same row through it later, or using one as a key, is the classic way to get this wrong.

Leave enable_tidscan on; there is nothing to win by turning it off. The thing to carry away is the shape of ctid: a fast, physical, impermanent handle on a row, ideal for a CURRENT OF update or a single pass over a table that is holding still, and wrong for anything that has to survive the next write.