PostgreSQL

关注公众号 jb51net

关闭
首页 > 数据库 > PostgreSQL > PostgreSQL EXPLAIN使用

PostgreSQL EXPLAIN 的深入解析与应用实例小结

作者:光蛋

文章介绍了PostgreSQL的查询优化器如何为SQL查询生成和选择合适的查询计划,以及如何使用EXPLAIN命令查看和理解这些计划,最后总结了使用EXPLAIN时的注意事项和常见误区,帮助读者更好地理解和优化PostgreSQL查询性能,感兴趣的朋友跟随小编一起看看吧

PostgreSQL 对每一个收到的 SQL 查询都会生成一个查询计划。选择合适的计划,是决定查询性能的关键因素之一。PostgreSQL 内置了一个复杂的查询优化器(planner),负责在众多可行计划中挑选“代价最低”的一个。

要查看优化器为某个查询生成的计划,可以使用 EXPLAIN 命令。读懂查询计划是一项需要经验的技能,本文将带你了解 EXPLAIN 的基础用法和常见计划节点的含义。

本文示例基于 PostgreSQL 9.3 开发版的回归测试数据库,且已事先执行过 VACUUM ANALYZE。你在自己环境中运行时,代价和行数估计可能会略有不同,这是正常现象。

一、EXPLAIN 基础

查询计划本质上是一棵“计划节点树”。

EXPLAIN 会为每个节点输出一行,显示节点类型和优化器对该节点执行代价的估计。最上面一行是整个计划的总代价估计,优化器的目标就是最小化这个值。

1. 最简单的例子

EXPLAIN SELECT * FROM tenk1;

典型输出:

Seq Scan on tenk1  (cost=0.00..458.00 rows=10000 width=244)

这表示优化器选择了**顺序扫描(Seq Scan)**整个表。括号中的四个数字含义是:

  1. 启动代价(start-up cost) :在开始输出第一行之前需要花费的代价,比如排序节点需要先完成排序。
  2. 总代价(total cost) :假设该节点执行到完成(读取所有可用行)的总代价。
  3. 输出行数(rows) :预计该节点会输出的行数。
  4. 行宽度(width) :预计每行的平均宽度(字节)。

代价单位是 PostgreSQL 内部的“虚构单位”,默认以“磁盘页面读取”为基准:seq_page_cost = 1.0,其他代价参数都相对它来设定。

2. 代价是如何计算的?

tenk1 为例,假设:

SELECT relpages, reltuples FROM pg_class WHERE relname = 'tenk1';

得到:

顺序扫描的代价计算公式大致为:

总代价 ≈ 页面读取代价 + 处理每行的 CPU 代价

= relpages * seq_page_cost + reltuples * cpu_tuple_cost

使用默认参数(seq_page_cost = 1.0, cpu_tuple_cost = 0.01):

358 * 1.0 + 10000 * 0.01 = 358 + 100 = 458

这与 EXPLAIN 输出中的 cost=0.00..458.00 相吻合。

二、WHERE 条件与过滤

当查询中加入 WHERE 条件时,计划会发生变化。

1. 简单过滤:仍然顺序扫描

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 7000;

输出:

Seq Scan on tenk1  (cost=0.00..483.00 rows=7001 width=244)
  Filter: (unique1 < 7000)

2. 更严格的条件:使用索引

当条件更严格时,优化器可能会选择索引扫描:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100;

输出:

Bitmap Heap Scan on tenk1  (cost=5.07..229.20 rows=101 width=244)
  Recheck Cond: (unique1 < 100)
  ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0)
        Index Cond: (unique1 < 100)

这里使用了位图索引扫描 + 位图堆扫描的两步计划:

  1. 子节点 Bitmap Index Scan:在索引 tenk1_unique1 上找到满足 unique1 < 100 的行的位置,生成一个位图。
  2. 父节点 Bitmap Heap Scan:根据位图去表中抓取对应的行,并再次检查条件(Recheck Cond)。

虽然随机访问表行比顺序扫描更昂贵,但因为只访问少量页面,总体代价仍然低于全表扫描。

3. 多条件:索引条件 vs 过滤条件

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND stringu1 = 'xxx';

输出:

Bitmap Heap Scan on tenk1  (cost=5.04..229.43 rows=1 width=244)
  Recheck Cond: (unique1 < 100)
  Filter: (stringu1 = 'xxx'::name)
  ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0)
        Index Cond: (unique1 < 100)

三、索引扫描 vs 位图扫描

PostgreSQL 有多种索引访问方式,常见的有:

1. 索引扫描(Index Scan)

EXPLAIN SELECT * FROM tenk1 WHERE unique1 = 42;

输出:

Index Scan using tenk1_unique1 on tenk1  (cost=0.29..8.30 rows=1 width=244)
  Index Cond: (unique1 = 42)

2. 位图扫描(Bitmap Scan)

位图扫描适合需要返回中等数量行的场景:

一般来说:

四、排序与 LIMIT

1. 显式排序

EXPLAIN SELECT * FROM tenk1 ORDER BY unique1;

输出:

Sort  (cost=1109.39..1134.39 rows=10000 width=244)
  Sort Key: unique1
  ->  Seq Scan on tenk1  (cost=0.00..445.00 rows=10000 width=244)

2. 增量排序(Incremental Sort)

如果排序键有前缀已经有序,PostgreSQL 可能使用增量排序:

EXPLAIN SELECT * FROM tenk1 ORDER BY four, ten LIMIT 100;

输出:

Limit  (cost=521.06..538.05 rows=100 width=244)
  ->  Incremental Sort  (cost=521.06..2220.95 rows=10000 width=244)
        Sort Key: four, ten
        Presorted Key: four
        ->  Index Scan using index_tenk1_on_four on tenk1  (cost=0.29..1510.08 rows=10000 width=244)

3. LIMIT 的影响

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000 LIMIT 2;

输出:

Limit  (cost=0.29..14.48 rows=2 width=244)
  ->  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..71.27 rows=10 width=244)
        Index Cond: (unique2 > 9000)
        Filter: (unique1 < 100)

注意:

五、连接(Join)计划

连接是复杂查询中最关键的部分之一,PostgreSQL 支持多种连接算法:嵌套循环(Nested Loop)、哈希连接(Hash Join)、归并连接(Merge Join)。

1. 嵌套循环连接(Nested Loop)

EXPLAIN SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2;

输出:

Nested Loop  (cost=4.65..118.62 rows=10 width=488)
  ->  Bitmap Heap Scan on tenk1 t1  (cost=4.36..39.47 rows=10 width=244)
        Recheck Cond: (unique1 < 10)
        ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0)
              Index Cond: (unique1 < 10)
  ->  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.91 rows=1 width=244)
        Index Cond: (unique2 = t1.unique2)

结构说明:

适合场景:

2. 哈希连接(Hash Join)

当内层表较大时,哈希连接往往更高效:

EXPLAIN SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2;

输出:

Hash Join  (cost=230.47..713.98 rows=101 width=488)
  Hash Cond: (t2.unique2 = t1.unique2)
  ->  Seq Scan on tenk2 t2  (cost=0.00..445.00 rows=10000 width=244)
  ->  Hash  (cost=229.20..229.20 rows=101 width=244)
        ->  Bitmap Heap Scan on tenk1 t1  (cost=5.07..229.20 rows=101 width=244)
              Recheck Cond: (unique1 < 100)
              ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0)
                    Index Cond: (unique1 < 100)

执行步骤:

  1. 构建阶段:扫描 tenk1 t1,构建以 unique2 为键的哈希表。
  2. 探查阶段:扫描 tenk2 t2,对每一行在哈希表中查找匹配的 unique2

适合场景:

3. 归并连接(Merge Join)

归并连接要求两个输入都按连接键排序:

EXPLAIN SELECT *
FROM tenk1 t1, onek t2
WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2;

输出:

Merge Join  (cost=198.11..268.19 rows=10 width=488)
  Merge Cond: (t1.unique2 = t2.unique2)
  ->  Index Scan using tenk1_unique2 on tenk1 t1  (cost=0.29..656.28 rows=101 width=244)
        Filter: (unique1 < 100)
  ->  Sort  (cost=197.83..200.33 rows=1000 width=244)
        Sort Key: t2.unique2
        ->  Seq Scan on onek t2  (cost=0.00..148.00 rows=1000 width=244)

适合场景:

六、EXPLAIN ANALYZE:查看真实执行情况

EXPLAIN ANALYZE实际执行查询,并在计划中加入真实执行时间和行数:

EXPLAIN ANALYZE SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2;

典型输出片段:

Nested Loop  (cost=4.65..118.62 rows=10 width=488) (actual time=0.128..0.377 rows=10 loops=1)
  ->  Bitmap Heap Scan on tenk1 t1  (cost=4.36..39.47 rows=10 width=244) (actual time=0.057..0.121 rows=10 loops=1)
        Recheck Cond: (unique1 < 10)
        ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.024..0.024 rows=10 loops=1)
              Index Cond: (unique1 < 10)
  ->  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.91 rows=1 width=244) (actual time=0.021..0.022 rows=1 loops=10)
        Index Cond: (unique2 = t1.unique2)
Planning time: 0.181 ms
Execution time: 0.501 ms

新增字段说明:

对于被多次执行的节点(如嵌套循环的内层):

1. 额外统计信息

某些节点会输出额外信息,例如排序和哈希:

EXPLAIN ANALYZE SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;

排序节点输出:

Sort  (cost=717.34..717.59 rows=101 width=488) (actual time=7.761..7.774 rows=100 loops=1)
  Sort Key: t1.fivethous
  Sort Method: quicksort  Memory: 77kB

哈希节点输出:

Hash  (cost=229.20..229.20 rows=101 width=244) (actual time=0.659..0.659 rows=100 loops=1)
  Buckets: 1024  Batches: 1  Memory Usage: 28kB

这些信息有助于判断:

2. 过滤与索引重检查

EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE ten < 7;

输出:

Seq Scan on tenk1  (cost=0.00..483.00 rows=7000 width=244) (actual time=0.016..5.107 rows=7000 loops=1)
  Filter: (ten < 7)
  Rows Removed by Filter: 3000

对于 GiST 等“有损”索引:

EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @> polygon '(0.5,2.0)';

可能输出:

Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=32) (actual time=0.062..0.062 rows=0 loops=1)
  Index Cond: (f1 @> '((0.5,2))'::polygon)
  Rows Removed by Index Recheck: 1

3. BUFFERS 选项

EXPLAIN (ANALYZE, BUFFERS) 可以查看 I/O 相关统计:

EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000;

输出中会包含:

Buffers: shared hit=15

这有助于判断查询的性能瓶颈是否在磁盘 I/O。

七、数据修改语句的 EXPLAIN ANALYZE

EXPLAIN ANALYZE 也适用于 INSERT / UPDATE / DELETE / MERGE

BEGIN;
EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 < 100;
ROLLBACK;

输出结构:

Update on tenk1  (cost=5.08..230.08 rows=0 width=0) (actual time=3.791..3.792 rows=0 loops=1)
  ->  Bitmap Heap Scan on tenk1  (cost=5.08..230.08 rows=102 width=10) (actual time=0.069..0.513 rows=100 loops=1)
        Recheck Cond: (unique1 < 100)
        Heap Blocks: exact=90
        ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.05 rows=102 width=0) (actual time=0.036..0.037 rows=300 loops=1)
              Index Cond: (unique1 < 100)

注意:

八、使用注意事项与常见误区

九、总结

读懂 EXPLAIN 是优化 PostgreSQL 查询性能的基础能力。本文介绍了:

到此这篇关于PostgreSQL EXPLAIN 的深入解析与应用的文章就介绍到这了,更多相关PostgreSQL EXPLAIN使用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文