enable_seqscan 是一个名字很容易引起误解的 PostgreSQL 查询规划参数。把它设为 off,并不等于关闭顺序扫描;它表达的是“尽量不要选择顺序扫描”。当读取整张表是回答查询的唯一合理方式时,规划器仍然会生成 Seq Scan。
理解这一点很重要,因为这个参数适合诊断执行计划,却不适合被当作禁止某类算子的开关。
off 是成本倾向,不是执行禁令
PostgreSQL 的规划器会比较多条候选执行路径的估算成本,例如:
- 顺序读取整张表;
- 通过普通索引定位少量行;
- 使用位图索引扫描,再访问对应的数据页;
- 组合连接、排序和聚合等其他算子。
设置下面的参数后,规划器会强烈降低顺序扫描方案的吸引力:
SET enable_seqscan = off;
但顺序扫描不会从候选计划中彻底消失。假如查询需要访问表中绝大多数行,而又没有能够直接提供结果的其他路径,规划器仍可能选择它。
这与参数名称形成了明显反差,却符合规划器开关的设计边界:它们主要用于影响计划选择,而不是保证某种执行行为。
为什么有索引也不一定更快
索引不是表的缩小版。普通 B-tree 索引保存键值和指向表中元组的引用。通过索引读取大量记录时,数据库通常既要扫描索引页,又要回到表中读取数据页,访问模式还可能比较分散。
对于返回大量行的查询,一次连续的全表扫描往往更便宜。例如:
SELECT sum(amount)
FROM orders;
如果 amount 上只有普通索引,这个查询仍然需要检查所有相关记录。强迫规划器绕道索引,可能增加 I/O 和 CPU 消耗,并不会减少工作量。
选择率较高的条件则不同:
SELECT *
FROM orders
WHERE customer_id = 42;
当 customer_id = 42 只匹配少数行,并且统计信息准确时,customer_id 上的索引通常更有价值。不过,“存在索引”仍不等于“必须使用索引”;表大小、数据分布、缓存状态、相关性和成本参数都会参与决策。
可以这样实践:亲手观察两种计划
下面是一组可以直接在测试数据库中运行的 SQL。它会创建 20 万行测试数据,并比较全表聚合与高选择率查询的执行计划。请不要在生产库中直接创建这张测试表。
DROP TABLE IF EXISTS guc_scan_demo;
CREATE TABLE guc_scan_demo (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
category integer NOT NULL,
payload text NOT NULL
);
INSERT INTO guc_scan_demo (category, payload)
SELECT
n % 1000,
repeat(md5(n::text), 4)
FROM generate_series(1, 200000) AS n;
CREATE INDEX guc_scan_demo_category_idx
ON guc_scan_demo (category);
ANALYZE guc_scan_demo;
EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*)
FROM guc_scan_demo
WHERE category = 42;
EXPLAIN (ANALYZE, BUFFERS)
SELECT sum(length(payload))
FROM guc_scan_demo;
SET enable_seqscan = off;
EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*)
FROM guc_scan_demo
WHERE category = 42;
EXPLAIN (ANALYZE, BUFFERS)
SELECT sum(length(payload))
FROM guc_scan_demo;
RESET enable_seqscan;
观察时不要只盯着第一行的节点名称,还要比较:
cost:规划器预测的启动成本和总成本;actual time:实际启动时间和完成时间;rows:估算行数与实际行数是否接近;Buffers:命中缓存和读取数据页的数量;- 索引计划是否需要额外访问堆表。
实际计划会受到 PostgreSQL 版本、机器配置和缓存状态影响,因此不应预设每次实验都会出现完全相同的节点。这个实验真正要验证的是:设置 off 只会改变规划倾向,无法保证彻底消灭 Seq Scan。
如果只想让设置影响一次诊断,可以把范围限制在事务中:
BEGIN;
SET LOCAL enable_seqscan = off;
EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*)
FROM guc_scan_demo
WHERE category = 42;
ROLLBACK;
SET LOCAL 会在事务结束时自动失效,比修改实例级配置更适合临时排查。
把它当成诊断探针
当规划器选择了看似不合理的顺序扫描时,临时关闭其偏好可以帮助回答一个具体问题:如果鼓励 PostgreSQL 使用其他路径,候选计划长什么样,实际运行是否更快?
如果替代计划确实明显更好,下一步通常不是永久设置 enable_seqscan = off,而是检查根因:
- 对相关表执行
ANALYZE,确认统计信息不是陈旧的。 - 对比
EXPLAIN (ANALYZE, BUFFERS)中的估算行数与实际行数。 - 检查数据是否倾斜,单列统计信息能否描述谓词之间的相关性。
- 确认索引列、列顺序和查询条件真正匹配。
- 评估查询是否本来就要读取表中很大比例的数据。
- 结合真实存储与缓存环境审视成本参数,而不是只为某一条 SQL 调参。
enable_seqscan 最有价值的用法,是制造一个可比较的执行计划,帮助定位统计信息、索引设计或成本估算问题。把它作为全局性能修复手段,可能让少数查询看起来改善,却让大量适合顺序扫描的查询付出更高代价。