DeepSeek总结的postgresql EXPLAIN 的其他超能力

原文地址:https://richyen.com/postgres/2026/03/23/explain_options.html

EXPLAIN 的其他超能力

2026年3月23日 ·


引言

大多数使用 PostgreSQL 的人最终都会学到两个用于查询调优的命令:EXPLAINEXPLAIN ANALYZE

EXPLAIN 展示规划器选择的执行计划,而 EXPLAIN ANALYZE 会实际运行查询并添加运行时统计信息。对于大多数调优任务来说,这已经提供了丰富的信息。

但许多人没有意识到的是,EXPLAIN 还有几个其他选项,可以使故障排查变得容易得多。在某些情况下,它们能回答 EXPLAIN ANALYZE 本身无法回答的问题。

本文将介绍其中几个不太为人所知的选项。


BUFFERS:数据来自哪里?

性能分析过程中一个常见的问题是:数据来自共享缓冲区(缓存)、磁盘还是临时缓冲区?这时 BUFFERS 选项就派上用场了。输出看起来像这样:

sql 复制代码
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM mytable WHERE id = 123;
[...]
  Index Scan using mytable_pkey on mytable
  Buffers: shared hit=5 read=2
[...]

在上面的例子中,我们看到:

  • shared hit -- 已在缓存中的页面(即缓存命中)
  • shared read -- 从磁盘读取的页面(即缓存未命中)

请注意,此上下文中的缓冲区是 8 KB 的内存块(大多数存储系统的标准块大小)。

这在尝试判断性能问题是否与冷缓存、过多磁盘读取或内存不足(即缓存过于拥挤,无法容纳正在处理的所有数据)有关时非常有用。

特别是对于索引扫描,此信息可以确认一个本应对索引友好的查询是否实际上正在将表的大部分内容拉入内存。


MEMORY:规划器使用的内存

这是 PostgreSQL 18 中引入的一个新功能。它与 BUFFERS 的不同之处在于,它跟踪的是查询规划阶段消耗的内存量,而不是执行阶段。输出会出现在 EXPLAIN 输出的底部,如下所示:

sql 复制代码
EXPLAIN (ANALYZE, TIMING OFF)
SELECT * FROM mytable WHERE id = 123;
[...]
 Planning:
   Buffers: shared hit=36 read=1
   Memory: used=63kB  allocated=64kB

WAL:产生了多少日志?

另一个容易被忽略的有用选项是 WAL

sql 复制代码
EXPLAIN (ANALYZE, WAL)
INSERT INTO mytable SELECT * FROM staging_table;
[...]
  WAL: records=100, fpi=5, bytes=45000

在上面的例子中,records 是生成的 WAL 记录数,fpi 指写入的完整页镜像(自上次检查点以来首次修改的页数),bytes 是查询产生的总 WAL 流量。这在调查写入密集型工作负载时非常有用,包括批量加载、大型更新、索引创建和高复制流量。


SETTINGS:提醒我环境是什么样子?

有时,即使 SQL 完全相同,查询在两台服务器上的行为也会不同。或者你可能在运行查询之前在本地修改了一些参数(例如 work_mem)。为了正确理解环境差异对查询的影响,SETTINGS 选项有时会很有用:

sql 复制代码
EXPLAIN (SETTINGS)
SELECT * FROM mytable WHERE id = 123;
[...]
Settings: effective_cache_size = '48GB', effective_io_concurrency = '200', enable_partitionwise_aggregate = 'on', enable_partitionwise_join = 'on', max_parallel_workers = '16', max_parallel_workers_per_gather = '4', temp_buffers = '1MB', search_path = 'public'

VERBOSE:查看规划器的完整叙述

另一个有用的选项是 VERBOSE,它会打印额外信息,如内部列引用、展开的目标列表和模式限定的对象:

sql 复制代码
postgres=# EXPLAIN (ANALYZE) SELECT * FROM pgbench_accounts a JOIN pgbench_branches b ON b.bid=a.bid ORDER BY 2 DESC;
                                                                          QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------
 Nested Loop  (cost=0.12..3898.14 rows=100000 width=461) (actual time=0.216..32.597 rows=100000.00 loops=1)
   Join Filter: (b.bid = a.bid)
   Buffers: shared hit=1642
   ->  Index Scan Backward using pgbench_branches_pkey on pgbench_branches b  (cost=0.12..8.14 rows=1 width=364) (actual time=0.095..0.102 rows=1.00 loops=1)
         Index Searches: 1
         Buffers: shared hit=2
   ->  Seq Scan on pgbench_accounts a  (cost=0.00..2640.00 rows=100000 width=97) (actual time=0.024..9.732 rows=100000.00 loops=1)
         Buffers: shared hit=1640
 Planning Time: 0.346 ms
 Execution Time: 40.078 ms
(10 rows)

postgres=# EXPLAIN (ANALYZE, VERBOSE) SELECT * FROM pgbench_accounts a JOIN pgbench_branches b ON b.bid=a.bid ORDER BY 2 DESC;
                                                                             QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Nested Loop  (cost=0.12..3898.14 rows=100000 width=461) (actual time=0.225..32.869 rows=100000.00 loops=1)
   Output: a.aid, a.bid, a.abalance, a.filler, b.bid, b.bbalance, b.filler
   Join Filter: (b.bid = a.bid)
   Buffers: shared hit=1642
   ->  Index Scan Backward using pgbench_branches_pkey on public.pgbench_branches b  (cost=0.12..8.14 rows=1 width=364) (actual time=0.183..0.190 rows=1.00 loops=1)
         Output: b.bid, b.bbalance, b.filler
         Index Searches: 1
         Buffers: shared hit=2
   ->  Seq Scan on public.pgbench_accounts a  (cost=0.00..2640.00 rows=100000 width=97) (actual time=0.026..9.756 rows=100000.00 loops=1)
         Output: a.aid, a.bid, a.abalance, a.filler
         Buffers: shared hit=1640
 Planning Time: 0.547 ms
 Execution Time: 40.228 ms
(13 rows)

虽然看起来可能有些杂乱,但它在诊断以下问题时非常有用:

  • 视图展开
  • 规则重写
  • 复杂的查询转换

组合选项

EXPLAIN 的真正力量来自于将选项组合在一起。例如:

sql 复制代码
EXPLAIN (ANALYZE, BUFFERS, WAL, SETTINGS)
SELECT * FROM mytable WHERE id = 123;

这将产生一个计划,显示执行时间、缓存使用情况、WAL 生成以及影响规划器的配置参数。

在许多情况下,这能更全面地描绘出数据库内部正在发生的事情。

请注意,其中许多选项也可以在数据库配置中作为 auto_explain 的参数启用。


结论

EXPLAIN ANALYZE 功能强大,但该功能还提供了许多用于理解查询行为的额外工具。这些附加选项可以提供关于内存使用、磁盘活动、WAL 生成以及 工具 开销的宝贵见解。在排查棘手的性能问题时,这些选项可以揭示基本执行计划可能隐藏的细节。

相关推荐
weixin_464307636 分钟前
QT智能指针
java·数据库·qt
王仲肖36 分钟前
PostgreSQL VACUUM 与 AUTOVACUUM 深度解析
数据库·postgresql
电商API&Tina39 分钟前
电商数据采集API接口||合规优先、稳定高效、数据精准
java·javascript·数据库·python·json
lifewange1 小时前
SQL 中 IN 和 AND 可以搭配使用么?
数据库·sql
博语小屋2 小时前
I/O 多路转接之epoll
运维·服务器·数据库
问道飞鱼2 小时前
【大模型学习】LangGraph 深度解析:定义、功能、原理与实践
数据库·学习·大模型·工作流
DJ斯特拉2 小时前
黑马点评技术汇总(四)缓存雪崩 && 缓存击穿
数据库·缓存
lzhdim3 小时前
SQL 入门 7:SQL 聚合与分组:函数、GROUP BY 与 ROLLUP
java·服务器·数据库·sql·mysql
lifewange3 小时前
INSERT INTO ... SELECT ...
数据库·sql
Uso_Magic3 小时前
SQLSERVER__EXPLAIN 常用分析案例。
服务器·数据库·sql