EXPLAIN 展示数据库为语句选择的执行计划,可以帮助定位昂贵工作与错误估算。不过,慢请求也可能涉及锁等待、网络传输或计划之外的应用行为。应利用计划检验一个明确假设,而不是认定每次性能问题都需要换扫描方式或增加索引。计划是诊断证据的一部分,不是整条请求链路的完整时间账本。
读计划树时避免重复计数
在 PostgreSQL 中,叶节点通常扫描关系,上层节点负责连接、排序、聚合或修改行。上层也会做真实工作,不能说全部工作都在叶子。上层成本包含子节点成本,把所有显示的 cost 相加会重复计算。rows 估算表示节点输出的行数,不一定是它检查过的全部行;过滤可以造成输入与输出差距,这本身并不能证明规划错误。
普通 EXPLAIN 只有估算;EXPLAIN ANALYZE 会真正执行语句并补充观察。loops 大于一时,actual rows 与时间通常是每次执行的平均值,要结合循环次数解释。比较估算时,应对照同一节点的实际输出,而不是无关的整表规模。buffers 也包含子节点活动,并不是去重后的页面计数,不能把所有层的数字直接相加当作总读取量。
统计信息影响选择
行数、分布、相关性、查询结构与成本设置都会影响规划。ANALYZE 更新统计信息,但行数估算严重错误,并不唯一指向统计过期。抽样误差、相关谓词、缺少扩展统计和参数选择率,也可能产生偏差。适当时刷新相关统计,再看差距是否真正改善,不要把一次较大的估算偏差当作已经完成根因诊断。
规划器也不是看见刚刚测到的缓存命中率变化,就自动决定改道。effective_cache_size 等设置表达缓存假设;真实缓存冷热可以改变执行时间,即使计划完全相同。比较结果时,应记录数据规模、参数、相关设置和测量条件。查询需要大部分表数据时,顺序扫描可能正是合理选择,不能把出现 Seq Scan 当成索引失效的通用判据。
坏掉的样子
预编译语句要按具体版本解释。PostgreSQL 自动策略可以比较定制计划与通用计划,并不是简单把首次参数的计划固定给所有后续值。应使用有代表性的参数执行 EXPLAIN EXECUTE,检查计划里出现的是常量还是参数占位符。不同数据库产品的缓存机制不同,不能把某一个产品的解释直接套到另一个实现。
函数、类型转换和排序规则,可能让某个索引无法匹配谓词,但不代表一定强制全表扫描。表达式索引可以支持 lower(column),一些跨类型运算也仍可使用索引。PostgreSQL 遇到不支持的 varchar 与整数比较时,还可能直接拒绝语句,而不是静默转换后扫描。决定改写之前,应检查真实运算符、类型和索引条件,避免凭语法外观推断执行路径。
EXPLAIN ANALYZE 不是无副作用的查看命令。写入、触发器、锁和被调用函数都会实际执行;事务回滚也无法撤销所有效果,例如已经消费的序列值或外部动作。应使用受控数据及合适的测试环境,即使 SELECT 也可能调用具有副作用的函数。诊断前评估负载,不能因为命令以 EXPLAIN 开头,就自动视为生产环境里的安全读取。
确认收益,而不只是计划形状变化
保留可比较的前后结果。计划变了可能更慢,计划形状没变也可能因实际工作量、数据或执行条件改善而更快。应一起查看耗时、输出正确性、行数估算、buffers、loops 和磁盘溢出。cost 单位不是毫秒,低成本估算不保证实际延迟。目标是能解释并复现的收益,不是让计划截图看起来更像预期答案。
快速验证
把下面程序保存为 plan_route_check.py,用 python3 plan_route_check.py 执行。它只使用标准库,在刻意错误的选择率下模拟两条候选路径,不调用 PostgreSQL,也不复现真实优化器。过期输入是这个用例预先设定的事实;告警要求继续调查,不能把所有真实计划里的估算偏差都诊断为统计过期。
#!/usr/bin/env python3
"""教学用规划器模型:按估算成本选路线,按实际行数验路线。
仅用标准库。它模拟统计信息过期:规划器以为只有 500 行命中,
实际有 600,000 行,并检查估算偏差告警是否触发。
"""
from __future__ import annotations
def seq_scan_cost(pages: float, rows: float) -> float:
return pages * 1.0 + rows * 0.01
def index_scan_cost(selectivity_est: float, pages: float, rows: float) -> float:
heap_pages = selectivity_est * pages
return 4.0 + heap_pages * 4.0 + selectivity_est * rows * 0.01
def main() -> None:
pages, rows = 8_000.0, 1_000_000.0
stale_selectivity = 0.0005 # 规划器的信念:500 行
true_selectivity = 0.60 # 现实:600,000 行
est_rows = stale_selectivity * rows
actual_rows = true_selectivity * rows
seq = seq_scan_cost(pages, rows)
idx_est = index_scan_cost(stale_selectivity, pages, rows)
idx_true = index_scan_cost(true_selectivity, pages, rows)
chosen = "索引扫描" if idx_est < seq else "顺序扫描"
print(f"估算行数:{est_rows:.0f};实际行数:{actual_rows:.0f}")
print(f"估算成本:顺序扫描={seq:.1f} 索引扫描={idx_est:.1f} -> 规划器选择{chosen}")
print(f"实际区分度下的真实成本:顺序扫描={seq:.1f} 索引扫描={idx_true:.1f}")
error = abs(actual_rows - est_rows) / actual_rows
print(f"估算偏差:{error:.1%}")
warned = False
if error > 0.50:
warned = True
print("告警:行数估算偏差超过 50%——请检查统计信息、谓词与参数选择率。")
if chosen == "索引扫描" and idx_true > seq:
warned = True
print("告警:在真实区分度下所选路线更贵——请对照 EXPLAIN ANALYZE。")
assert warned, "该用例应当触发统计信息过期告警"
assert est_rows < actual_rows / 10, "用例需保持估算比实际低一个数量级"
print("PASS:规划器选中了过期路线,估算偏差告警已触发")
if __name__ == "__main__":
main()
模型按估算选择索引扫描,成本二十五,小于顺序路径的一万八千。在给定实际选择率下,模型的索引成本变成二万五千二百零四;程序输出百分之九十九点九的估算偏差,在告警后输出 PASS。这些成本都是简化公式的结果,不是真实数据库执行耗时,也不是从生产计划中采集的数字。
实验确认算术与预先构造的反例,不验证某个 PostgreSQL 计划,不足以批准增加索引,也不能证明 ANALYZE 会修好线上查询。调整真实负载之前,仍需用实际计划和受控执行证据重复调查。保留玩具模型与数据库实验的区别,才能避免把教学示例的成功误报为优化器已经完成验证。
延伸阅读
来源
PostgreSQL:EXPLAIN,生成计划,已核验 2026-10-15。
PostgreSQL:使用 EXPLAIN,解读输出,已核验 2026-10-15。
PostgreSQL:规划器与优化器,路线如何被选定,已核验 2026-10-15。
MySQL:EXPLAIN 语句,计划格式与输出列,已核验 2026-10-15。
PostgreSQL PREPARE,通用计划与定制计划选择,已核验 2026-10-15。
PostgreSQL 表达式索引,包括 lower(column) 的表达式索引,已核验 2026-10-15。