Skip to content

PostgreSQL EXPLAIN与执行计划分析 ​

核心概念 ​

执行计划是PostgreSQL查询优化器为SQL语句生成的执行方案,详细描述了数据库如何执行查询。EXPLAIN命令用于获取和分析执行计划,是性能优化的核心工具。主要涉及以下核心概念:

  • 查询优化器:PostgreSQL内置的优化组件,负责生成最优执行计划
  • 执行计划节点:执行计划的基本组成单位,代表一种操作(如扫描、连接、排序等)
  • 操作符:执行计划中表示具体操作的符号(如Seq Scan、Index Scan、Hash Join等)
  • 成本估算:优化器对执行计划各节点的资源消耗估算
  • 实际执行统计:查询实际执行的资源消耗和时间

EXPLAIN命令用法 ​

1. 基本用法 ​

sql
-- 基本EXPLAIN命令,只显示执行计划,不执行查询
EXPLAIN SELECT * FROM users WHERE id = 1;

-- EXPLAIN ANALYZE,执行查询并显示实际执行统计
EXPLAIN ANALYZE SELECT * FROM users WHERE id = 1;

-- EXPLAIN (FORMAT JSON),输出JSON格式的执行计划
EXPLAIN (FORMAT JSON) SELECT * FROM users WHERE id = 1;

-- EXPLAIN (VERBOSE),显示详细的执行计划
EXPLAIN (VERBOSE) SELECT * FROM users WHERE id = 1;

2. 常用选项 ​

选项描述
ANALYZE执行查询并显示实际执行统计
VERBOSE显示详细信息,包括列名和表达式
COSTS显示成本估算(默认开启)
BUFFERS显示缓冲区使用情况
TIMING显示各节点的实际执行时间(默认开启,需配合ANALYZE)
SUMMARY显示查询执行的摘要信息(需配合ANALYZE)
FORMAT指定输出格式(TEXT、XML、JSON、YAML)

3. 组合使用选项 ​

sql
-- 显示执行计划、实际统计和缓冲区使用情况
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM users WHERE id = 1;

-- 显示详细执行计划和实际统计
EXPLAIN (ANALYZE, VERBOSE) SELECT * FROM users WHERE id = 1;

-- 显示所有可用信息
EXPLAIN (ANALYZE, VERBOSE, BUFFERS, SUMMARY) SELECT * FROM users WHERE id = 1;

执行计划解读 ​

1. 执行计划结构 ​

执行计划以树形结构展示,每个节点代表一个操作,父节点依赖子节点的结果。例如:

Seq Scan on users  (cost=0.00..155.00 rows=1000 width=42)
  Filter: (id = 1)
  • Seq Scan:顺序扫描操作
  • on users:操作的表名
  • cost=0.00..155.00:优化器估算的成本范围(启动成本..总成本)
  • rows=1000:估算的返回行数
  • width=42:估算的每行宽度(字节)
  • Filter: (id = 1):过滤条件

2. 常见操作符 ​

扫描操作符 ​

操作符描述优化建议
Seq Scan顺序扫描全表创建合适的索引
Index Scan使用索引查找行确保索引包含所需列
Index Only Scan只从索引中获取数据,无需回表确保索引覆盖查询所需的所有列
Bitmap Heap Scan结合位图索引的扫描适合多个索引条件的查询
Bitmap Index Scan位图索引扫描适合低选择性的索引

连接操作符 ​

操作符描述优化建议
Nested Loop嵌套循环连接适合小表连接或有索引的连接
Hash Join哈希连接适合大表连接,尤其是等值连接
Merge Join合并连接适合已排序的数据或索引

其他操作符 ​

操作符描述优化建议
Sort排序操作创建包含排序字段的索引
Aggregate聚合操作(如SUM、COUNT等)创建合适的索引,考虑使用物化视图
Limit限制返回行数创建包含排序和过滤字段的索引
Subquery Scan子查询扫描考虑重写为JOIN或CTE

3. 成本估算解读 ​

PostgreSQL优化器使用基于成本的优化(CBO),成本单位是抽象的,主要包括:

  • 启动成本:返回第一行所需的成本
  • 总成本:返回所有行所需的成本
  • 行数估算:预计返回的行数
  • 宽度估算:预计每行的字节数

例如:cost=0.00..155.00 rows=1000 width=42

  • 启动成本:0.00(立即开始返回数据)
  • 总成本:155.00
  • 预计返回行数:1000行
  • 每行预计宽度:42字节

执行计划分析案例 ​

案例1:全表扫描优化 ​

查询:

sql
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'example@example.com';

执行计划:

Seq Scan on users  (cost=0.00..155.00 rows=1 width=42) (actual time=0.023..0.023 rows=0 loops=1)
  Filter: (email = 'example@example.com'::text)
  Rows Removed by Filter: 1000
  Buffers: shared hit=4
Planning Time: 0.047 ms
Execution Time: 0.051 ms

分析:

  • 执行了全表扫描(Seq Scan)
  • 过滤了1000行,只返回0行
  • 实际执行时间0.051 ms(示例数据量小)

优化建议: 为email列创建索引:

sql
CREATE INDEX idx_users_email ON users(email);

优化后执行计划:

Index Scan using idx_users_email on users  (cost=0.29..8.30 rows=1 width=42) (actual time=0.010..0.010 rows=0 loops=1)
  Index Cond: (email = 'example@example.com'::text)
  Buffers: shared hit=2
Planning Time: 0.071 ms
Execution Time: 0.020 ms

案例2:连接操作优化 ​

查询:

sql
EXPLAIN ANALYZE SELECT o.*, u.name FROM orders o JOIN users u ON o.user_id = u.id WHERE u.age > 30;

执行计划:

Nested Loop  (cost=0.00..230.00 rows=333 width=50) (actual time=0.015..0.015 rows=0 loops=1)
  ->  Seq Scan on users u  (cost=0.00..155.00 rows=333 width=16) (actual time=0.013..0.013 rows=0 loops=1)
        Filter: (age > 30)
        Rows Removed by Filter: 1000
  ->  Seq Scan on orders o  (cost=0.00..0.20 rows=1 width=34) (actual time=0.000..0.000 rows=0 loops=0)
        Filter: (user_id = u.id)
        Rows Removed by Filter: 10
Buffers: shared hit=4
Planning Time: 0.054 ms
Execution Time: 0.030 ms

分析:

  • 使用了嵌套循环连接(Nested Loop)
  • 两个表都进行了全表扫描
  • 先扫描users表,再扫描orders表

优化建议: 为连接列和过滤列创建索引:

sql
CREATE INDEX idx_users_age ON users(age);
CREATE INDEX idx_orders_user_id ON orders(user_id);

优化后执行计划:

Nested Loop  (cost=0.56..141.06 rows=333 width=50) (actual time=0.011..0.011 rows=0 loops=1)
  ->  Index Scan using idx_users_age on users u  (cost=0.29..84.79 rows=333 width=16) (actual time=0.010..0.010 rows=0 loops=1)
        Index Cond: (age > 30)
  ->  Index Scan using idx_orders_user_id on orders o  (cost=0.27..0.17 rows=1 width=34) (actual time=0.000..0.000 rows=0 loops=0)
        Index Cond: (user_id = u.id)
Buffers: shared hit=2
Planning Time: 0.073 ms
Execution Time: 0.024 ms

案例3:排序操作优化 ​

查询:

sql
EXPLAIN ANALYZE SELECT * FROM products ORDER BY price DESC LIMIT 10;

执行计划:

Limit  (cost=18.75..18.78 rows=10 width=28) (actual time=0.025..0.026 rows=10 loops=1)
  ->  Sort  (cost=18.75..21.25 rows=1000 width=28) (actual time=0.025..0.025 rows=10 loops=1)
        Sort Key: price DESC
        Sort Method: top-N heapsort  Memory: 25kB
        ->  Seq Scan on products  (cost=0.00..15.00 rows=1000 width=28) (actual time=0.006..0.011 rows=1000 loops=1)
Buffers: shared hit=4
Planning Time: 0.045 ms
Execution Time: 0.037 ms

分析:

  • 执行了全表扫描,然后进行排序
  • 使用了top-N heapsort排序方法
  • 内存使用25kB

优化建议: 为price列创建降序索引:

sql
CREATE INDEX idx_products_price_desc ON products(price DESC);

优化后执行计划:

Limit  (cost=0.29..2.52 rows=10 width=28) (actual time=0.010..0.013 rows=10 loops=1)
  ->  Index Scan using idx_products_price_desc on products  (cost=0.29..227.29 rows=1000 width=28) (actual time=0.009..0.012 rows=10 loops=1)
Buffers: shared hit=2
Planning Time: 0.058 ms
Execution Time: 0.023 ms

高级特性 ​

1. EXPLAIN ANALYZE的高级选项 ​

sql
-- 显示缓冲区使用情况
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM users WHERE id = 1;

-- 显示详细信息和缓冲区
EXPLAIN (ANALYZE, VERBOSE, BUFFERS) SELECT * FROM users WHERE id = 1;

-- 显示查询计划的JSON格式
EXPLAIN (FORMAT JSON, ANALYZE, BUFFERS) SELECT * FROM users WHERE id = 1;

-- 关闭计时信息(减少开销)
EXPLAIN (ANALYZE, TIMING OFF) SELECT * FROM users WHERE id = 1;

2. 执行计划可视化 ​

可以使用第三方工具可视化执行计划,便于分析:

  • pgAdmin:内置执行计划可视化功能
  • pev2:PostgreSQL Explain Visualizer,Web-based工具
  • pganalyze:商业级的执行计划分析工具

3. 自动分析和统计信息 ​

PostgreSQL的自动分析守护进程会定期更新表的统计信息,优化器依赖这些信息生成准确的执行计划。

sql
-- 手动更新表统计信息
ANALYZE users;

-- 更新数据库所有表的统计信息
ANALYZE VERBOSE;

-- 查看统计信息
SELECT * FROM pg_stats WHERE tablename = 'users';

最佳实践 ​

1. 执行计划分析流程 ​

  1. 获取执行计划:使用EXPLAIN ANALYZE获取实际执行统计
  2. 识别瓶颈:找到执行计划中成本最高的节点
  3. 分析原因:了解瓶颈节点的类型和原因
  4. 制定优化方案:根据分析结果制定优化策略
  5. 验证优化效果:执行优化后再次分析执行计划

2. 常见优化策略 ​

  • 索引优化:为查询条件和连接列创建合适的索引
  • 查询重写:优化SQL语句结构,避免复杂子查询
  • 表设计优化:合理设计表结构,避免冗余列
  • 参数调整:调整PostgreSQL配置参数,如shared_buffers、work_mem等
  • 硬件优化:根据执行计划的瓶颈类型(CPU、I/O、内存)优化硬件

3. 注意事项 ​

  • 执行计划的成本估算基于统计信息,确保统计信息准确
  • 实际执行时间可能受系统负载影响,建议多次执行取平均值
  • 小表的全表扫描可能比索引扫描更高效
  • 复合索引的列顺序很重要,应将选择性高的列放在前面

常见问题处理 ​

  • 问题1:执行计划成本估算与实际执行时间不符 解决方法:

    • 更新表统计信息:ANALYZE table_name
    • 检查系统负载和资源使用情况
    • 考虑使用EXPLAIN ANALYZE获取实际执行统计
  • 问题2:优化器选择了不合适的执行计划 解决方法:

    • 调整random_page_cost或seq_page_cost参数
    • 使用查询提示(如pg_hint_plan扩展)
    • 重写SQL语句,引导优化器选择正确的执行计划
  • 问题3:索引没有被使用 解决方法:

    • 检查索引是否存在且有效
    • 检查查询条件是否与索引匹配
    • 检查索引选择性,低选择性的索引可能不被使用
    • 更新统计信息
  • 问题4:执行计划过于复杂 解决方法:

    • 分解复杂查询为多个简单查询
    • 使用CTE(公共表表达式)简化查询结构
    • 考虑使用物化视图存储中间结果

常见问题(FAQ) ​

Q1:EXPLAIN和EXPLAIN ANALYZE有什么区别? ​

A1:主要区别:

  • EXPLAIN:只显示优化器估算的执行计划,不实际执行查询
  • EXPLAIN ANALYZE:执行查询并显示实际执行统计,包括时间、行数和缓冲区使用

Q2:如何解读执行计划中的成本值? ​

A2:成本值是PostgreSQL优化器的估算,单位是抽象的:

  • 成本越低,执行计划越优
  • 启动成本:返回第一行所需的成本
  • 总成本:返回所有行所需的成本
  • 成本估算基于统计信息,可能与实际执行时间不完全一致

Q3:Index Scan和Index Only Scan有什么区别? ​

A3:主要区别:

  • Index Scan:使用索引查找行的位置,然后回表获取完整数据
  • Index Only Scan:只从索引中获取所需数据,无需回表
  • Index Only Scan性能更好,但要求索引包含查询所需的所有列

Q4:什么时候应该使用Hash Join? ​

A4:Hash Join适合以下场景:

  • 等值连接
  • 连接的两个表都较大
  • 至少有一个表可以完全放入内存
  • 没有合适的索引用于Nested Loop连接

Q5:如何优化排序操作? ​

A5:优化排序操作的方法:

  • 创建包含排序字段的索引
  • 减少排序的数据量
  • 调整work_mem参数,增加排序可用内存
  • 考虑使用集群表(CLUSTER)

Q6:如何查看表的统计信息? ​

A6:查看表统计信息的方法:

sql
-- 查看特定表的统计信息
SELECT * FROM pg_stats WHERE tablename = 'users';

-- 查看表的基本统计信息
SELECT relname, relpages, reltuples FROM pg_class WHERE relname = 'users';

Q7:执行计划中的"loops"是什么意思? ​

A7:loops表示该节点执行的次数:

  • 对于嵌套循环连接,内层节点的loops等于外层节点返回的行数
  • 高loops值可能表示连接效率低下,需要优化

Q8:如何处理慢查询? ​

A8:处理慢查询的步骤:

  1. 使用EXPLAIN ANALYZE获取执行计划
  2. 识别瓶颈节点和原因
  3. 根据瓶颈类型制定优化方案(索引优化、查询重写等)
  4. 验证优化效果
  5. 监控优化后的查询性能