EXPLAIN 展示 PostgreSQL 的执行计划,EXPLAIN ANALYZE 还会真正执行语句并测量工作量。最有用的比较,是同一个节点的估计行数与实际行数。两者都不能单独当成结论:估计来自模型,实测则只描述特定数据、缓存、并发和插桩条件下的一次执行。不能把前者称为谎言,也不能把后者当成脱离环境的永恒事实。
先比较同一种量
计划中的 cost 是相对成本单位,不是毫秒。两个成本值分别描述启动成本与完成成本,带 LIMIT 的查询可能更重视尽早得到前几行,因此选择不同计划。actual time 才使用毫秒。即使成本和耗时恰好都打印成一百,也不能把它们直接相减或求比例;它们本来就不表示同一件事。
一个节点被重复执行时,actual rows 和节点耗时是每次循环的平均值,需要结合 loops 理解总工作量。但不能把所有节点耗时简单相加,因为父节点已经包含子节点的工作,重复相加会重复计算。并行执行时更不能用这种方式猜总延迟。先沿树看输入输出、循环次数、过滤条件和行数流向,再决定哪个位置值得调查。
BUFFERS 区分 shared、local 和临时块活动。shared hit 表示 PostgreSQL 在共享缓冲区里找到了块;shared read 只表示需要读入,操作系统仍可能从页缓存满足它,不能直接断言每次都访问了物理磁盘。临时表使用本地缓冲区,排序或哈希溢出则可能读写临时文件。父节点计数包含子节点,逐行把所有计数加起来同样会夸大 I/O。
找到有意义的估计偏差
某个选择性条件估计只返回十行,实际却返回一万行,通常值得先调查,再考虑重写其上方的连接。统计信息陈旧、分布倾斜、列间相关、参数值变化或选择性模型能力不足,都可能造成偏差。ANALYZE 可以刷新统计信息,却不能保证每个估计精确,也不会自动修好所有慢计划。先看真实数据和缺失的信息,再决定是否调整统计目标或增加扩展统计。
行数估计准确也不能证明查询高效。缺少访问路径、昂贵表达式、排序溢出、锁等待和并发争用仍可能存在。反过来,小表或需要返回大量行的查询采用顺序扫描可能正合适,不应把“没用索引”一律判为错误。分析时应保留应用实际使用的参数和查询形状,避免用一个稀有值的漂亮计划替代整个生产分布。
坏掉的样子
最危险的误解,是把 ANALYZE 当成只读预演。UPDATE 会改变行,触发器会执行,锁会被获取,SELECT 也可能调用带副作用的函数。事务末尾执行 ROLLBACK 可以恢复事务性表变化,却不能一般性撤销序列前进或外部系统副作用。对语句影响范围不确定时,应使用可丢弃的数据集,不能靠“最后会回滚”就放心在生产运行。
另一个误判来自测量本身。第一次执行会加热缓存,频繁读取时钟的成本也可能占据廉价节点的大部分时间。TIMING OFF 仍保留实际行数和整条语句的总耗时,同时避免逐节点计时。EXPLAIN 不会把普通结果集完整发送给应用,所以它不能测出传送整个响应的网络耗时。重复执行、代表性负载和应用延迟各自回答不同问题,应结合解释而不是只挑最小的毫秒值。
快速验证
用 psql 连接一个可丢弃的 PostgreSQL 18 数据库,把代码保存为 explain.sql,再运行 psql -X -v ON_ERROR_STOP=1 -f explain.sql;连接参数应明确指向实验库。这里创建的表和序列都是临时对象,最后回滚会清除它们,不需要借用已有 orders 表,也不会假定生产中存在某个字段。
\set ON_ERROR_STOP on
BEGIN;
SET LOCAL statement_timeout = '5s';
CREATE TEMP TABLE orders (id integer, status text);
INSERT INTO orders SELECT g, CASE WHEN g <= 100 THEN 'paid' ELSE 'new' END
FROM generate_series(1, 10000) AS g;
ANALYZE orders;
EXPLAIN (COSTS ON) SELECT * FROM orders WHERE status = 'paid';
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF)
SELECT * FROM orders WHERE status = 'paid';
CREATE TEMP SEQUENCE measured_calls;
SAVEPOINT before_measurement;
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF)
UPDATE orders SET id = id + 10000 WHERE id <= 3;
EXPLAIN (ANALYZE, TIMING OFF) SELECT nextval('measured_calls');
ROLLBACK TO SAVEPOINT before_measurement;
SELECT count(*) AS updated_rows_after_rollback FROM orders WHERE id > 10000;
SELECT nextval('measured_calls') AS sequence_after_rollback;
DO $$ BEGIN
IF (SELECT count(*) FROM orders WHERE status = 'paid') <> 100 THEN
RAISE EXCEPTION 'unexpected selected row count';
END IF;
IF EXISTS (SELECT FROM orders WHERE id > 10000) THEN
RAISE EXCEPTION 'UPDATE was not rolled back';
END IF;
IF (SELECT last_value FROM measured_calls) <> 2 THEN
RAISE EXCEPTION 'sequence advance unexpectedly rolled back';
END IF;
END $$;
ROLLBACK;
夹具恰好包含一百条 paid 订单。实际执行的 SELECT 应显示一百行通过过滤、九千九百行被过滤。UPDATE 下方的扫描节点处理三行,但没有 RETURNING 时顶层节点返回零行;这个零不代表没有更新。回滚到保存点后,更新后的行数回到零,但下一次序列值是二,因为被测 SELECT 已经消耗了第一个值。代码为这些结果提供断言,而不是仅打印一个看似合理的计划。
在 PostgreSQL 18.6 上这些断言通过。具体计划、缓冲区计数、耗时和打印格式可以变化,稳定预期是行数、回滚效果与序列前进。五秒 statement timeout 限制本实验中单条语句的执行时间,但不会把任意生产写操作变成安全检查。临时对象和受控数据是这个实验可复现的前提,不能移除后仍声称风险相同。
来源
- PostgreSQL EXPLAIN,真实执行的副作用和测量选项,已核验 2026-09-10。
- PostgreSQL 使用 EXPLAIN,成本、循环次数与缓冲区解释,已核验 2026-09-10。
- PostgreSQL 规划器统计信息,估计与统计模型的边界,已核验 2026-09-10。
- PostgreSQL 序列函数,不随回滚撤销的序列前进,已核验 2026-09-10。