PostgreSQL EXPLAIN
EXPLAIN 用来告诉你:PostgreSQL 打算“如何执行”这条 SQL
EXPLAIN SELECT * FROM users WHERE id = 1;
👉 不执行 SQL,只生成 执行计划(Execution Plan)
| 命令 | 是否执行 SQL | 用途 |
|---|---|---|
| EXPLAIN | ❌ 不执行 | 看优化器“打算怎么跑” |
| EXPLAIN ANALYZE | ✅ 会执行 | 看真实执行情况(最准) |
EXPLAIN ANALYZE SELECT * FROM users WHERE id = 1;
⚠️ 生产环境慎用 ANALYZE(可能是 UPDATE / DELETE)
一个典型 EXPLAIN:
Seq Scan on users (cost=0.00..35.50 rows=5 width=64)
Filter: (age > 30)
核心结构
[执行节点]
└─ 子节点
👉 从内到外读,从上到下执行
(cost=0.00..35.50 rows=5 width=64)
| 字段 | 含义 |
|---|---|
| cost | 执行成本(估算值) |
| rows | 预估返回行数 |
| width | 每行平均字节数 |
| actual time | 真实耗时(ANALYZE) |
| loops | 节点执行次数 |
cost = 启动成本 .. 总成本
- 启动成本:返回第一行前的代价
- 总成本:返回所有行的代价
👉 cost 不是时间,是相对值
Seq Scan on users
含义
- 从头扫到尾
- 大表通常是性能杀手
出现原因
- 无索引
- 条件不走索引
- 返回行太多
Index Scan using users_pkey on users
特点
- 先走索引
- 再回表(heap)
适用
- 返回少量行
Index Only Scan using idx_users_age
特点(性能最好)
- 不回表
- 只读索引
条件
- 查询字段都在索引中
- visibility map 足够干净(VACUUM)
Bitmap Index Scan
Bitmap Heap Scan
适用
- 返回中等数量行
- 多条件索引
👉 常见于:
WHERE a = 1 AND b = 2
Nested Loop
特点
- 小表驱动大表
- 适合:
- 主键 JOIN
- 小结果集
危险
- 大表 × 大表 = 灾难
Hash Join
特点
- 构建 hash 表
- 适合等值 JOIN
ON a.id = b.id
Merge Join
特点
- 双方必须有序
- 常用于:
- 已排序索引
- ORDER BY + JOIN
Index Cond: (id = 1)
Filter: (status = 'active')
| 项 | 含义 |
|---|---|
| Index Cond | 在索引层过滤 |
| Filter | 回表后再过滤(慢) |
👉 Filter 越多,索引越没用
actual time=0.025..0.030 rows=1 loops=1
重点关注
- actual time 是否远大于 cost
- rows 预估 vs 实际是否差很多
- loops 是否异常
估算不准的原因
- 统计信息过旧
- 数据分布不均
解决
ANALYZE table_name;
Seq Scan on big_table
可能原因:
- 函数包裹字段
- 隐式类型转换
- LIKE ‘%xxx%’
rows=10 actual rows=100000
原因:
- 统计信息不准
- 需要 extended statistics
CREATE STATISTICS s1 ON a, b FROM t;
ANALYZE t;
Nested Loop (rows=100000 loops=100000)
解决:
- 加索引
- 改 JOIN 顺序
- 提高 work_mem(允许 Hash Join)
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT ...
| 参数 | 作用 |
|---|---|
| ANALYZE | 真实执行 |
| BUFFERS | 显示缓存命中 |
| VERBOSE | 显示详细字段 |
| TIMING | 显示精确时间 |
BUFFERS 示例
Buffers: shared hit=120 read=10
- hit:命中缓存
- read:磁盘读取(慢)
一句口诀:
先看 Scan -> 再看 Join -> 再看 rows -> 最后看 Buffers
标准流程
- 是否全表扫描?
- 索引是否命中?
- JOIN 方式是否合理?
- rows 预估是否离谱?
- 是否大量磁盘 IO?
A:不是,是相对代价单位。
A:返回行太多,优化器认为顺扫更快。
A:不回表,只读索引。
A:大表等值 JOIN。
A:ANALYZE、VACUUM、extended statistics。
EXPLAIN (ANALYZE, BUFFERS)
SELECT ...
这是一个非常典型、而且非常容易被误判的问题。
下面按 「结论 -> 原因分类 -> 如何验证 -> 解决建议」 可以直接对照排查的完整解释。
EXPLAIN ANALYZE 里的 Execution Time ≠ 客户端感知到的“查询耗时”
两者本来就不一定相等,而且差距大通常是正常现象。
EXPLAIN ANALYZE 统计的是:
| 包含 | 不包含 |
|---|---|
| SQL 执行器耗时 | 网络传输 |
| 表扫描 / 索引扫描 | 客户端渲染 |
| 排序 / Join / 聚合 | ORM 序列化 |
| WAL 生成 | 客户端解析 |
| CPU + IO | 客户端等待 |
📌 不包括:
- 结果集传输时间
- 客户端读取时间
- 客户端打印 / JSON 序列化
- ORM 映射对象
典型现象
EXPLAIN ANALYZE SELECT * FROM logs;
Execution Time: 20 ms
客户端却:
2.3 秒
原因
- PostgreSQL 很快算完
- 慢在“发数据”
📌 EXPLAIN 不计算 socket 发送时间
验证方法
EXPLAIN ANALYZE SELECT count(*) FROM logs;
如果 count(*) 很快,说明慢在结果传输。
常见场景
- SQLAlchemy
- Django ORM
- 返回对象数多
session.execute(stmt).scalars().all() # 慢
📌 ORM 创建对象、映射字段非常耗时
验证方法
- psql 里执行同样 SQL
- 对比 ORM 执行时间
| 环境 | 影响 |
|---|---|
| 本机 psql | 很快 |
| 远程 ORM | 很慢 |
| 跨地域 | 非常慢 |
📌 EXPLAIN 完全不涉及网络
- CLI 输出 1 万行
- GUI 工具(DBeaver / DataGrip)
📌 显示比查询还慢
EXPLAIN ANALYZE 连跑两次
- 第一次慢
- 第二次快
📌 Execution Time 是 缓存命中后的
- 禁用了并行?
- 不走缓存路径?
尤其是:
EXPLAIN ANALYZE INSERT / UPDATE
Workers Planned: 4
Execution Time: 100 ms
并不等于:
4 × 100 ms
👉 是 墙钟时间(wall clock)
客户端看到慢,其实慢在:
- commit
- fsync
- WAL flush
📌 EXPLAIN 统计不包含 commit
\timing on
SELECT ...;
你看到的才是 真实端到端耗时
| 时间 | 看哪里 |
|---|---|
| EXPLAIN Execution Time | DB 执行 |
| psql \timing | DB + 网络 |
| 应用日志 | 全链路 |
| 现象 | 结论 |
|---|---|
| EXPLAIN 快,psql 慢 | 网络 / 结果集 |
| psql 快,ORM 慢 | ORM |
| 首次慢,后续快 | 缓存 |
| count(*) 快,select 慢 | 传输 |
| 执行时间抖动 | IO / 云盘 |
SQL
SELECT * FROM articles ORDER BY datetime DESC LIMIT 10000;
| 场景 | 时间 |
|---|---|
| EXPLAIN ANALYZE | 15 ms |
| psql | 120 ms |
| Python ORM | 800 ms |
👉 数据库不是瓶颈
- LIMIT
- 只 SELECT 必要字段
- 游标 / 流式读取
- 服务器端分页
- 降低网络 RTT
- 数据库与应用同机房
EXPLAIN ANALYZE 的 Execution Time 只统计数据库内部执行时间, 不包含网络传输、客户端处理和结果渲染, 两者差距大通常是结果集或客户端问题,而非 SQL 本身慢。