PostgreSQL 的 enable_seqscan=off 为什么仍会执行顺序扫描

2026-07-11 42 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:7 分钟

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,而是检查根因:

  1. 对相关表执行 ANALYZE,确认统计信息不是陈旧的。
  2. 对比 EXPLAIN (ANALYZE, BUFFERS) 中的估算行数与实际行数。
  3. 检查数据是否倾斜,单列统计信息能否描述谓词之间的相关性。
  4. 确认索引列、列顺序和查询条件真正匹配。
  5. 评估查询是否本来就要读取表中很大比例的数据。
  6. 结合真实存储与缓存环境审视成本参数,而不是只为某一条 SQL 调参。

enable_seqscan 最有价值的用法,是制造一个可比较的执行计划,帮助定位统计信息、索引设计或成本估算问题。把它作为全局性能修复手段,可能让少数查询看起来改善,却让大量适合顺序扫描的查询付出更高代价。


相关推荐