读 PostgreSQL EXPLAIN:先找行数误差,再看 loops 和缓冲区
执行计划里的 cost 不是毫秒,actual rows 还要结合 loops 看。本文用查询示例解释估计偏差、扫描方式和缓冲区,并说明 ANALYZE 会真正执行语句。
文章目录
一条查询变慢,执行计划里看到 Seq Scan 就加索引,很容易改错地方。表很小、需要取出大部分记录时,顺序扫描可能正合适。更有用的起点是:数据库原本以为会返回多少行,实际又返回了多少行。
先看不执行查询的计划
假设有订单表 orders,包含 customer_id、created_at 和 amount。换成自己的表名和条件:
EXPLAIN
SELECT created_at, amount
FROM orders
WHERE customer_id = 42
ORDER BY created_at DESC
LIMIT 20;
EXPLAIN 默认显示规划结果。cost 是优化器使用的成本单位,不是耗时;rows 是该节点预计输出的行数,也不是整张表的行数。顶部的成本包含下方子节点,不能把每一行 cost 相加当总成本。
这一步适合先确认查询结构。若执行计划与预想不同,检查条件有没有函数转换、字段类型是否一致、索引列顺序是否能服务筛选与排序。计划里出现索引名称,只说明计划选择了它,尚未证明速度。
ANALYZE 会真正执行
需要实际行数时,可以在低风险环境运行:
BEGIN READ ONLY;
SET LOCAL statement_timeout = '5s';
EXPLAIN (ANALYZE, BUFFERS)
SELECT created_at, amount
FROM orders
WHERE customer_id = 42
ORDER BY created_at DESC
LIMIT 20;
ROLLBACK;
这里针对普通 SELECT,五秒只是示例超时。读事务不会让昂贵查询变便宜,也不能保证用户自定义函数没有外部副作用。先限制对象和时间,避免把高负载问题放大。EXPLAIN ANALYZE UPDATE、DELETE 等会执行写入;不要把它当作只读诊断按钮。
三处数字连起来看
先比较同一节点的预计 rows 与 actual rows。差距很大时,检查统计信息、数据分布和条件间的关联。一个客户拥有大量订单,可能与普通客户的计划需求不同;拿一个编号测完,不能代替所有参数。
再看 loops。实际时间和行数在多次循环时通常按每次执行的平均值呈现。嵌套循环内层每次只返回少量行,但如果执行很多次,累计工作仍可能很大。用 actual rows × loops 理解数量级,别把它当作精确的整体耗时公式;父子节点的时间也不能简单相加。
最后看 BUFFERS。shared hit 表示共享缓冲区命中,shared read 表示需要把块读进缓冲区;read 不等于必然从物理磁盘读取,操作系统缓存仍可能介入。还要看临时块读写和排序方式,判断是否有落盘。只比较总毫秒数,会漏掉这些工作量变化。
改动以后怎么比较
保存原查询、参数、计划和数据库版本。改一个因素后,用相同参数比较;冷缓存与热缓存、并发量、数据变化都要记录。若新增索引,除了查询耗时,也看写入开销和索引大小。不要通过关闭顺序扫描把一个“好看”的计划当成优化结果。
EXPLAIN 命令文档给出执行选项与注意事项;计划阅读指南可用来核对节点含义。结果少得不合理时,还应先排除 NOT IN 与 NULL 的筛选问题。