enable_seqscan does not disable sequential scans. It cannot, and it was never meant to. The documentation says as much: sequential scans cannot be suppressed entirely, because sometimes reading the whole table is the only way to answer the query. What off actually does is tell the planner to avoid a sequential scan when it has any other option. That is a diagnostic instruction, not a configuration one, and the distinction is the entire point of this parameter.

Default on, context user. You set it off in a session to ask the planner a question. You do not set it off in postgresql.conf, ever, and we will get to why.

Start with what the planner actually does when you disable a node type. For most of PostgreSQL’s history, off was implemented by adding a fixed penalty of ten billion cost units (disable_cost) to every path of the disabled type, so the planner would avoid it unless it had nothing else. That approach aged badly. Analytical queries now routinely cost more than ten billion units on their own, at which point a ten-billion penalty stops being decisive and the planner can end up picking a “disabled” plan anyway; the same fixed penalty also quietly distorted join ordering. Recent versions replaced it with explicit disabled-node accounting, and in PostgreSQL 18 EXPLAIN prints Disabled: true on any node the planner was forced to use against your setting. Point it at a table with no indexes and that is exactly what you get: Seq Scan ... Disabled: true, because there was no other way to read the data.

That last part is where people trip. enable_seqscan = off does not force an index scan; it only penalizes the sequential one. The planner considers an index at all only when the index can actually contribute something: a restriction clause it can filter on, an ordering the query needs, a useful partial-index predicate, or an index-only scan. Run SELECT * FROM t WHERE unindexed_col > 10 with seqscan disabled and you get the same sequential scan, now labelled disabled, because no index on the table helps that query and the planner will not consider one that cannot. The primary key you were hoping it would reach for is never even in the running.

So when the planner really is choosing a sequential scan you believe is wrong, the sequential scan is almost never the bug. It is a symptom of the cost inputs being wrong, and the fix is upstream of the plan. The usual culprit is effective_cache_size set far below the memory the machine actually has available for caching, which makes repeated index access look more expensive than it is; raise it to reflect reality and index plans get correctly cheaper. The next is random_page_cost, whose default of 4.0 describes a spinning disk; on SSD or NVMe it belongs somewhere near 1.1, and until it is, every index scan on your flash storage is being quoted spinning-rust prices. And a sequential scan chosen because the planner thinks a predicate matches most of the table, when it matches a handful of rows, is a statistics problem: run ANALYZE, and raise default_statistics_target on the offending column if the estimate is still wrong.

enable_seqscan = off is how you confirm that diagnosis. Flip it in the session, re-run EXPLAIN, and read what the planner would do with an index and what it thinks that costs. If the index plan is genuinely cheaper once the cost settings are right, you have found your fix, and it lives in those settings, not in this parameter. Setting enable_seqscan = off in postgresql.conf to force the matter is the move that causes as many planner errors as it solves: the small-table scans that ought to be sequential start doing wasteful index access instead, and you have traded one bad plan for a whole category of them. Leave it on. It is a probe, and you point it at one query at a time.