EXPLAIN · 读懂 PostgreSQL 的执行计划
EXPLAIN 让优化器摊开它打算怎么执行一条 SQL:走哪个索引、怎么连接、估算多少行;EXPLAIN ANALYZE 真跑一遍给出实测行数与耗时。这是排查慢查询的第一手段——看懂扫描类型、连接方式、估算与实测的偏差,你才知道慢在哪、该加什么索引。
EXPLAIN · 读懂 PostgreSQL 的执行计划
前面几章讲了索引、分区、物化视图这些「让查询变快」的手段。但用哪个、有没有生效,不能靠猜——得让数据库自己告诉你。EXPLAIN 就是那扇窗:它让优化器把「我打算怎么执行这条 SQL」摊开给你看。看懂执行计划,是排查慢查询的基本功。
EXPLAIN 家族:从「看计划」到「真跑一遍」
EXPLAIN SELECT ...; -- 只看计划,不执行
EXPLAIN ANALYZE SELECT ...; -- 真跑一遍,给出实测行数与耗时
EXPLAIN (ANALYZE, BUFFERS) SELECT ...; -- 再加上缓存命中情况
EXPLAIN (FORMAT JSON) SELECT ...; -- 机器可读,给工具用EXPLAIN ANALYZE 会真正执行语句。对 SELECT 无妨,但对 UPDATE / DELETE / INSERT,它是真的改数据!要看写语句的计划又不想改数据,把它包在事务里最后回滚:BEGIN; EXPLAIN ANALYZE UPDATE ...; ROLLBACK;。
一个索引的立竿见影
最能说明问题的,是「加索引前后」同一条查询的计划对比。实测灌了 20000 行订单,查 WHERE user_id = 1。
没有索引时——全表扫:
Seq Scan on ea_orders (cost=0.00..398.00 rows=19 width=26)
(actual time=0.023..0.997 rows=25 loops=1)
Filter: (user_id = 1)
Rows Removed by Filter: 19975 ← 为了找 25 行,读了 20000 行,扔掉 19975 行
Execution Time: 1.013 ms建了 user_id 的 B-tree 索引后:
Bitmap Heap Scan on ea_orders (cost=4.43..57.84 rows=19 width=26)
(actual time=0.038..0.067 rows=25 loops=1)
-> Bitmap Index Scan on ea_orders_user_idx (actual rows=25)
Index Cond: (user_id = 1)
Execution Time: 0.086 ms ← 1.013ms → 0.086ms,快了十倍关键信号是那句 Rows Removed by Filter: 19975:数据库为了返回 25 行,白读了近两万行。看到它,基本就是「这里缺个索引」的铁证。加索引后,Bitmap Index Scan 先从索引拿到匹配行的位置,Bitmap Heap Scan 再只去读那 22 个相关数据块——不再全表扫。
读计划:三个必看的数字
执行计划是一棵树,从最内层缩进往外读(内层先执行)。每个节点盯三样:
① 扫描 / 连接类型(节点名) —— 数据是怎么取的、表是怎么连的:
| 扫描类型 | 含义 |
|---|---|
Seq Scan | 全表顺序扫(小表 OK,大表 + 高过滤 = 危险信号) |
Index Scan | 走索引取行,再回表 |
Index Only Scan | 只读索引就够(覆盖索引,不回表,最快) |
Bitmap Heap Scan | 索引找到一批行,批量按块读(中等选择度) |
| 连接类型 | 适合 |
|---|---|
Nested Loop | 一侧行很少 + 另一侧有索引 |
Hash Join | 两侧都较大、等值连接(建哈希表) |
Merge Join | 两侧都已按连接键有序 |
② rows 估算 vs actual rows 实测 —— 优化器的「预判」准不准:
(cost=... rows=19 ...) (actual ... rows=25 ...) -- 估 19,实 25,很准估算靠 ANALYZE 收集的统计信息。如果 rows 和 actual rows 差了几个数量级(估 10 行实际 100 万),说明统计信息陈旧——优化器基于错误预判选了烂计划。解法:ANALYZE 表名 重新收集统计。这是慢查询排查里最常见的一类病根。
③ cost(成本) —— 优化器的选择依据:
cost=0.00..398.00 -- 启动成本..总成本cost 是优化器内部的相对单位,不是毫秒——它以「顺序读一个页 = 1.0」为基准折算 I/O 与 CPU。两个数是「启动成本..总成本」:启动成本是「吐出第一行前的花费」(比如排序要先读完全部才能出第一行,启动成本就高)。优化器的工作,就是在所有候选计划里挑总成本最低的那个。你要真实耗时,得看 EXPLAIN ANALYZE 里的 actual time 和 Execution Time。
加上 BUFFERS 还能看到 shared hit=148(命中缓存的页)vs read=2(从磁盘读的页)——read 高说明数据没在内存里,可能需要更大的 shared_buffers 或更好的索引。
估算从哪来:统计信息
优化器不是瞎猜。ANALYZE 会采样每张表,把「某列有多少不同值、最常见的值是哪些、数据怎么分布」记进系统目录(pg_statistic)。查 WHERE user_id = 1 时,它据此估「大概匹配多少行」,再决定走索引还是全表扫。
日常里 autovacuum 会自动触发 ANALYZE,多数时候不用管。但大批量导入 / 删除之后统计会瞬间过时——这时手动跑一次 ANALYZE,能避免优化器拿着旧地图选错路。排查「昨天还好好的、今天突然变慢」的查询,先看 EXPLAIN ANALYZE 里估算和实测差多少,往往就是统计过时。
横向对比
| 能力 | PostgreSQL | MySQL | Oracle | SQL Server |
|---|---|---|---|---|
| 看计划 | EXPLAIN | EXPLAIN | EXPLAIN PLAN | 执行计划(图形 / 文本) |
| 实测执行 | EXPLAIN ANALYZE | EXPLAIN ANALYZE(8.0.18+) | DBMS_XPLAN + 实测 | Actual Execution Plan |
| 缓存 / IO 明细 | BUFFERS | 部分 | 强 | 强 |
| 机器可读 | FORMAT JSON/YAML | FORMAT=JSON | XML | XML / 图形 |
各家都有执行计划工具,思路一致。PostgreSQL 的 EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) 信息密度很高,配合 explain.dalibo.com 这类可视化工具,读起来相当顺手。
应用场景
- 慢查询定位:线上某接口慢,把它的 SQL 拿去
EXPLAIN ANALYZE,看是全表扫、缺索引,还是估算跑偏。 - 验证索引是否生效:建了索引却没变快?
EXPLAIN看它到底走没走索引——很多时候是查询写法(函数包裹列、类型不匹配)让索引失效。 - JOIN 顺序 / 方式调优:大表连接慢,看是不是选了糟糕的
Nested Loop,或统计过时导致连接顺序错。 - 上线前体检:核心查询上线前先
EXPLAIN过一遍,别等生产环境全表扫才发现。
小结与下一章
EXPLAIN 是数据库调优的听诊器:Rows Removed by Filter 大 → 缺索引;rows 与 actual 差远 → 统计过时;Seq Scan 大表 → 警惕。学会读这三样,你就从「盲目加索引」进阶到「对症下药」。
规模与性能这一站到此结束。下一站转向另一个维度——把逻辑放进数据库里:用 PL/pgSQL 写函数与触发器,让数据库不只是存数据,还能主动执行逻辑。👉 PL/pgSQL 与触发器:让数据库自己干活。
参考文献
- [1] PostgreSQL 官方文档.「14.1. Using EXPLAIN.」https://www.postgresql.org/docs/current/using-explain.html —— 计划节点、成本、
ANALYZE、BUFFERS解读。 - [2] PostgreSQL 官方文档.「14.2. Statistics Used by the Planner.」https://www.postgresql.org/docs/current/planner-stats.html ——
ANALYZE与pg_statistic。 - [3] PostgreSQL 官方文档.「EXPLAIN(SQL Command).」https://www.postgresql.org/docs/current/sql-explain.html —— 全部选项与输出格式。