文章目录
-
- [一、为什么需要 EXPLAIN ?](#一、为什么需要 EXPLAIN ?)
-
- [1.1 为什么 EXPLAIN 是 DBA 的"X光机"?](#1.1 为什么 EXPLAIN 是 DBA 的“X光机”?)
- [1.2 优化 checklist](#1.2 优化 checklist)
- [1.3 EXPLAIN 使用建议](#1.3 EXPLAIN 使用建议)
- [二、EXPLAIN 基础语法与输出格式](#二、EXPLAIN 基础语法与输出格式)
-
- [2.1 基本命令](#2.1 基本命令)
- [2.2 输出格式选择](#2.2 输出格式选择)
- 三、执行计划核心组件解析
-
- [3.1 节点类型(Node Types)](#3.1 节点类型(Node Types))
- [3.2 关键指标解读](#3.2 关键指标解读)
- 四、实战:常见执行计划模式分析
-
- [场景 1:索引生效(理想情况)](#场景 1:索引生效(理想情况))
- [场景 2:全表扫描(危险信号)](#场景 2:全表扫描(危险信号))
- [场景 3:索引失效(隐式类型转换)](#场景 3:索引失效(隐式类型转换))
- [场景 4:连接查询优化](#场景 4:连接查询优化)
- [五、高级 EXPLAIN 技巧](#五、高级 EXPLAIN 技巧)
-
- [5.1 BUFFERS 详解(内存 vs 磁盘)](#5.1 BUFFERS 详解(内存 vs 磁盘))
- [5.2 WAL 与修改操作分析](#5.2 WAL 与修改操作分析)
- [5.3 SETTINGS 查看参数影响](#5.3 SETTINGS 查看参数影响)
- 六、性能优化实战流程
-
- [步骤 1:捕获慢查询](#步骤 1:捕获慢查询)
- [步骤 2:获取真实执行计划](#步骤 2:获取真实执行计划)
- [步骤 3:诊断瓶颈](#步骤 3:诊断瓶颈)
- [步骤 4:验证优化效果](#步骤 4:验证优化效果)
- 七、经典案例深度解析
-
- [案例 1:分页查询性能陷阱](#案例 1:分页查询性能陷阱)
- [案例 2:OR 条件导致索引失效](#案例 2:OR 条件导致索引失效)
-
- [方案 A:UNION 重写](#方案 A:UNION 重写)
- [方案 B:创建多列索引(如果 OR 条件固定)](#方案 B:创建多列索引(如果 OR 条件固定))
- [方案 C:PostgreSQL 11+ 支持 **BitmapOr**](#方案 C:PostgreSQL 11+ 支持 BitmapOr)
- [案例 3:函数索引拯救 LIKE 查询](#案例 3:函数索引拯救 LIKE 查询)
- [八、EXPLAIN 可视化工具推荐](#八、EXPLAIN 可视化工具推荐)
-
- [1. 官方工具](#1. 官方工具)
- [2. 第三方工具](#2. 第三方工具)
适用版本 :PostgreSQL 9.6+(推荐 12+)
目标读者 :DBA、后端开发、数据工程师
核心价值:看懂执行计划,精准定位慢查询,性能优化效率提升 10 倍+
一、为什么需要 EXPLAIN ?
1.1 为什么 EXPLAIN 是 DBA 的"X光机"?
当 SQL 查询变慢时:
- ❌ 盲目加索引 → 可能无效甚至恶化
- ❌ 重写 SQL → 可能忽略根本原因
- ✅ EXPLAIN 分析 → 精准定位瓶颈
黄金法则 :没有 EXPLAIN 的优化都是耍流氓
1.2 优化 checklist
✅ 执行计划分析:
- 使用
EXPLAIN (ANALYZE, BUFFERS) - 对比估算行数 vs 实际行数
- 检查是否有意外的 Seq Scan
- 确认连接顺序是否合理
✅ 索引策略:
- WHERE 条件列有索引
- ORDER BY 列考虑包含在索引中
- 高频 OR 查询考虑 BitmapOr 或 UNION
- 函数查询创建函数索引
✅ 配置调优:
-
work_mem足够大(避免磁盘排序) -
shared_buffers合理(通常为 RAM 的 25%) - 定期
ANALYZE更新统计信息
✅ SQL 重写:
- 分页用游标替代 OFFSET
- 避免 SELECT *
- 拆分复杂查询为 CTE
1.3 EXPLAIN 使用建议
"看执行计划,要像侦探破案:
每个节点都是线索,每个数字都是证据,
最终指向性能瓶颈的真凶。"
| 阶段 | 行动 |
|---|---|
| 观察 | 识别最耗时的节点(高 actual time) |
| 诊断 | 分析原因(缺索引?统计信息不准?配置不足?) |
| 实验 | 创建索引/调整参数/重写 SQL |
| 验证 | 对比优化前后执行计划 |
| 固化 | 将有效方案纳入代码规范 |
二、EXPLAIN 基础语法与输出格式
2.1 基本命令
sql
-- 基础执行计划(估算)
EXPLAIN SELECT * FROM users WHERE id = 1;
-- 包含实际执行时间(真实数据)
EXPLAIN (ANALYZE) SELECT * FROM users WHERE id = 1;
-- 详细输出(含缓冲区、I/O 等)
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM users WHERE id = 1;
2.2 输出格式选择
| 格式 | 命令 | 适用场景 |
|---|---|---|
| 文本(默认) | EXPLAIN |
快速查看 |
| JSON | EXPLAIN (FORMAT JSON) |
程序解析 |
| YAML | EXPLAIN (FORMAT YAML) |
人类可读结构化 |
| XML | EXPLAIN (FORMAT XML) |
企业系统集成 |
💡 推荐 :日常用文本格式 ,自动化分析用JSON格式
三、执行计划核心组件解析
3.1 节点类型(Node Types)
每个缩进行代表一个执行节点,常见类型:
| 节点类型 | 作用 | 性能提示 |
|---|---|---|
| Seq Scan | 全表扫描 | 大表无索引时危险! |
| Index Scan | 索引扫描(回表) | 高效,但需回表 |
| Index Only Scan | 索引覆盖扫描 | 最快!无需回表 |
| Bitmap Heap Scan | 位图扫描(多条件) | 中等效率 |
| Nested Loop | 嵌套循环连接 | 小结果集高效 |
| Hash Join | 哈希连接 | 中等结果集首选 |
| Merge Join | 归并连接 | 已排序数据高效 |
| Sort | 排序操作 | 内存不足会溢出到磁盘 |
| Hash | 构建哈希表 | 内存消耗大户 |
| Aggregate | 聚合操作 | 注意 GROUP BY 列 |
3.2 关键指标解读
以 EXPLAIN (ANALYZE, BUFFERS) 输出为例:
text
Index Scan using idx_users_email on users (cost=0.42..8.44 rows=1 width=200)
Index Cond: (email = 'user@example.com'::text)
Buffers: shared hit=4
I/O Timings: read=0.000
Execution Time: 0.050 ms
核心字段含义:
| 字段 | 含义 | 优化方向 |
|---|---|---|
| cost=X...Y | 启动成本...总成本(基于统计信息估算) | 关注总成本 Y |
| rows=Z | 估算返回行数 | 与实际偏差大?→ 更新统计信息 |
| width=N | 平均行宽(字节) | 影响内存和 I/O |
| actual time=A...B | 实际启动时间...总时间(ms) | B 是关键指标 |
| rows=Z(actual) | 实际返回行数 | 对比估算值 |
| Buffers: shared hit=X | 共享缓冲区命中次数 | hit 高 = 内存友好 |
| Buffers: shared read=Y | 从磁盘读取次数 | read 高 = I/O 瓶颈 |
| Execution Time | 总执行时间 | 最终性能指标 |
🔑 重点:
- 估算 vs 实际 :若
rows估算偏差 > 10 倍 →ANALYZE table- I/O 比例 :
read / (hit + read) > 0.1→ 考虑增加shared_buffers
四、实战:常见执行计划模式分析
场景 1:索引生效(理想情况)
sql
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM users WHERE email = 'test@example.com';
输出:
text
Index Scan using idx_users_email on users (cost=0.42..8.44 rows=1 width=200)
Index Cond: (email = 'test@example.com'::text)
Buffers: shared hit=4
✅ 健康指标:
- 使用
Index Scan(非 Seq Scan) Buffers hit=4(全内存操作)rows=1(精准定位)
场景 2:全表扫描(危险信号)
sql
EX PLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE amount > 1000;
输出:
text
Seq Scan on orders (cost=0.00..12345.00 rows=50000 width=100)
Filter: (amount > 1000)
Rows Removed by Filter: 950000
Buffers: shared hit=10000 read=5000
❌ 问题诊断:
Seq Scan→ 无合适索引Rows Removed by Filter: 950000→ 过滤掉 95% 数据(浪费 I/O)read=5000→ 大量磁盘 I/O
✅ 解决方案:
sql
CREATE INDEX CONCURRENTLY idx_orders_amount ON orders(amount);
场景 3:索引失效(隐式类型转换)
sql
-- email 是 VARCHAR,但传入整数
EXPLAIN SELECT * FROM users WHERE email = 12345;
输出:
text
Seq Scan on users (cost=0.00..12345.00 rows=1 width=200)
Filter: ((email)::numeric = 12345)
❌ 问题:
- 索引
idx_users_email未被使用 - 因
(email)::numeric导致函数扫描
✅ 修复:
sql
-- 传入字符串
SELECT * FROM users WHERE email = '12345';
场景 4:连接查询优化
1、低效写法(Nested Loop 失控)
sql
EXPLAIN (ANALYZE)
SELECT u.name, o.amount
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.country = 'US';
危险输出:
text
Nested Loop (cost=0.00..1234567.00 rows=100000 width=50)
-> Seq Scan on users u (rows=100000) -- 大表驱动
-> Index Scan on orders o (rows=1 per user)
⚠️ Nested Loop 成本 = 驱动表行数 × 内层查询成本
优化方案:
-
确保小表驱动 :先过滤
userssql-- 添加 country 索引 CREATE INDEX idx_users_country ON users(country); -
改用 Hash Join :
sqlSET enable_nestloop = off; -- 临时测试(生产慎用)
2、高效执行计划:
text
Hash Join (cost=1000.00..5000.00 rows=1000 width=50)
Hash Cond: (o.user_id = u.id)
-> Seq Scan on orders o
-> Hash (cost=500.00..500.00 rows=1000 width=20)
-> Index Scan using idx_users_country on users u
Index Cond: (country = 'US'::text)
五、高级 EXPLAIN 技巧
5.1 BUFFERS 详解(内存 vs 磁盘)
sql
EXPLAIN (ANALYZE, BUFFERS)
SELECT COUNT(*) FROM huge_table;
关键指标:
shared hit:从 shared_buffers 读取(内存)shared read:从 OS 缓存或磁盘读取temp read/write:排序/哈希溢出到临时文件
💡 调优方向:
read高 → 增加shared_bufferstemp write高 → 增加work_mem
5.2 WAL 与修改操作分析
sql
EXPLAIN (ANALYZE, WAL)
UPDATE users SET last_login = NOW() WHERE id = 1;
输出新增:
text
WAL records: 2
WAL bytes: 120
📌 用途:评估写操作对 WAL 日志的压力
5.3 SETTINGS 查看参数影响
sql
EXPLAIN (ANALYZE, SETTINGS)
SELECT * FROM test_table WHERE id = 1;
输出包含:
text
Settings: effective_cache_size = '12GB', work_mem = '64MB'
✅ 价值:确认当前会话参数是否符合预期
六、性能优化实战流程
步骤 1:捕获慢查询
sql
-- 开启慢查询日志
ALTER SYSTEM SET log_min_duration_statement = 1000; -- 记录 >1s 的查询
SELECT pg_reload_conf();
步骤 2:获取真实执行计划
sql
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
<你的慢查询>;
步骤 3:诊断瓶颈
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
| Seq Scan 大表 | 缺少索引 | 创建合适索引 |
| 估算 rows 偏差大 | 统计信息过期 | ANALYZE table |
| Nested Loop 驱动大表 | 连接顺序不佳 | 优化 WHERE 条件 |
| Sort 溢出到磁盘 | work_mem 不足 |
增加 work_mem |
| 大量 Bitmap Heap Scan | 位图过大 | 考虑部分索引 |
| 重复计算 CTE | PostgreSQL <12 | 升级或改用临时表 |
步骤 4:验证优化效果
sql
-- 优化前
EXPLAIN (ANALYZE) SELECT ...; -- Execution Time: 2500 ms
-- 创建索引后
CREATE INDEX ...;
EXPLAIN (ANALYZE) SELECT ...; -- Execution Time: 15 ms
七、经典案例深度解析
案例 1:分页查询性能陷阱
问题 SQL:
sql
SELECT * FROM products ORDER BY id LIMIT 10 OFFSET 999990;
执行计划:
text
Limit (cost=... rows=10)
-> Index Scan using pk_products on products (cost=... rows=1000000)
❌ 问题:
- 虽然用索引,但需扫描 999990+10 行
- OFFSET 越大越慢
✅ 优化方案 :游标分页(Cursor Pagination)
sql
-- 记住上一页最后的 id
SELECT * FROM products
WHERE id > 999990
ORDER BY id
LIMIT 10;
执行计划:
text
Limit (cost=0.43..8.45 rows=10)
-> Index Scan using pk_products on products
Index Cond: (id > 999990)
✅ 效果:时间从 2s → 2ms
案例 2:OR 条件导致索引失效
问题 SQL:
sql
SELECT * FROM users
WHERE name = 'John' OR email = 'john@example.com';
执行计划:
text
Seq Scan on users
Filter: ((name = 'John'::text) OR (email = 'john@example.com'::text))
❌ 原因:
- 单独
name或email有索引,但 OR 无法同时使用
✅ 解决方案:
方案 A:UNION 重写
sql
SELECT * FROM users WHERE name = 'John'
UNION
SELECT * FROM users WHERE email = 'john@example.com';
方案 B:创建多列索引(如果 OR 条件固定)
sql
CREATE INDEX idx_users_name_email ON users (name, email);
-- 但仅当查询是 (name, email) 组合时有效
方案 C:PostgreSQL 11+ 支持 BitmapOr
text
Bitmap Heap Scan on users
Recheck Cond: ((name = 'John'::text) OR (email = 'john@example.com'::text))
-> BitmapOr
-> Bitmap Index Scan on idx_users_name
-> Bitmap Index Scan on idx_users_email
💡 升级 PostgreSQL 可自动优化 OR 查询!
案例 3:函数索引拯救 LIKE 查询
问题 SQL:
sql
SELECT * FROM products WHERE UPPER(name) LIKE 'IPHONE%';
执行计划:
text
Seq Scan on products
Filter: (upper(name) ~~ 'IPHONE%'::text)
✅ 创建函数索引:
sql
CREATE INDEX idx_products_upper_name ON products (UPPER(name));
优化后计划:
text
Index Scan using idx_products_upper_name on products
Index Cond: (upper(name) ~~ 'IPHONE%'::text)
🔥 注意:查询必须与索引表达式完全一致!
八、EXPLAIN 可视化工具推荐
1. 官方工具
pgAdmin:内置可视化执行计划psql:\x命令切换扩展显示
2. 第三方工具
| 工具 | 特点 | 链接 |
|---|---|---|
| explain.depesz.com | 在线解析 + 耗时高亮 | https://explain.depesz.com |
| PEV2 | 本地可视化(开源) | https://github.com/AlexTatiyants/pev |
| Dalibo Labs | 专业级分析 | https://labs.dalibo.com |
✅ 推荐流程 :
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)→ 粘贴到 explain.depesz.com → 一键可视化