EXPLAIN — 显示一个语句的执行计划
EXPLAIN [ (option[, ...] ) ]statementwhereoptioncan be one of: ANALYZE [boolean] VERBOSE [boolean] COSTS [boolean] SETTINGS [boolean] GENERIC_PLAN [boolean] BUFFERS [boolean] SERIALIZE [ { NONE | TEXT | BINARY } ] WAL [boolean] TIMING [boolean] SUMMARY [boolean] MEMORY [boolean] FORMAT { TEXT | XML | JSON | YAML }
此命令显示 PostgreSQL 规划器为所给语句生成的执行计划。执行计划展示了该语句所引用的表将如何被扫描 — 通过普通顺序扫描、索引扫描等方式 — 以及在引用了多个表的情况下,将使用什么样的连接算法把来自每个输入表的所需行汇集到一起。
显示中最关键的部分是估计的语句执行代价,它是规划器对运行该语句需要多长时间的猜测(以任意的代价单位度量,但按惯例表示磁盘页面获取)。实际上会显示两个数字:能够返回第一行之前的启动代价,以及返回所有行的总代价。对大多数查询而言,总代价才是要紧的,但在诸如 EXISTS 中的子查询这样的场景下,规划器会选择最小的启动代价而不是最小的总代价(因为执行器反正会在取得一行之后停止)。此外,如果用 LIMIT 子句限制了要返回的行数,规划器会在端点代价之间做适当的插值,以估计到底哪个计划才是最便宜的。
ANALYZE 选项会导致语句被真正执行,而不仅仅是被规划。然后显示中会加入实际的运行时统计信息,包括在每个计划节点内耗费的总时间(以毫秒计)以及它实际返回的行数。这有助于查看规划器的估计值是否接近实际情况。
请记住,使用 ANALYZE 选项时语句会被真正执行。虽然 EXPLAIN 会丢弃 SELECT 本应返回的任何输出,但该语句的其他副作用会照常发生。如果你希望把 EXPLAIN ANALYZE 用在 INSERT、UPDATE、DELETE、MERGE、CREATE TABLE AS 或 EXECUTE 语句上,而又不想让该命令影响你的数据,可以使用这种方法:
BEGIN; EXPLAIN ANALYZE ...; ROLLBACK;
ANALYZE 执行该命令并显示实际的运行时间以及其他统计信息。该参数默认为 FALSE。
VERBOSE 显示有关计划的附加信息。具体而言,包括计划树中每个节点的输出列列表,用模式限定表名和函数名,总是用范围表别名标注表达式中的变量,并且总是打印显示了统计信息的每个触发器的名称。如果已经计算出查询标识符,也会将其显示出来,详见 compute_query_id。该参数默认为 FALSE。
COSTS 包括每个计划节点的估计启动代价和总代价的信息,以及估计的行数和每一行的估计宽度。该参数默认为 TRUE。
SETTINGS 包括有关配置参数的信息。具体而言,包括那些取值与内建默认值不同的、影响查询规划的选项。该参数默认为 FALSE。
GENERIC_PLAN 允许语句包含诸如 $1 之类的参数占位符,并生成一个不依赖于这些参数值的通用计划。有关通用计划以及支持参数的语句类型的详情,请参见 PREPARE。该参数不能与 ANALYZE 一起使用。默认为 FALSE。
BUFFERS 包括有关缓冲区使用情况的信息。具体而言,包括共享块的命中数、读取数、弄脏数和写出数,本地块的命中数、读取数、弄脏数和写出数,临时块的读取数和写出数,以及在启用了 track_io_timing 的情况下,读取和写入数据文件块、本地块和临时文件块所花费的时间(以毫秒计)。命中意味着避免了一次读取,因为在需要时该块已经在缓存中被找到。共享块包含来自普通表和索引的数据;本地块包含来自临时表和索引的数据;而临时块则包含用于排序、哈希、物化计划节点及类似场景的短期工作数据。被弄脏的块数表示先前未被修改、但被此查询更改过的块的数量;而被写出的块数则表示在查询处理期间被此后端从缓存中逐出的先前已弄脏块的数量。上层节点显示的块数包括其所有子节点使用的块数。在文本格式下,只会打印非零值。使用 ANALYZE 时会自动包括缓冲区信息。
SERIALIZE 包括对查询输出数据进行序列化的代价的相关信息,即把数据转换为文本或二进制格式再发送给客户端。如果数据类型输出函数的开销很大,或者必须从行外存储中取出被 TOAST 过的值,这可能会占查询正常执行所需时间的相当大一部分。EXPLAIN 的默认行为 SERIALIZE NONE 不执行这些转换。如果指定了 SERIALIZE TEXT 或 SERIALIZE BINARY,就会执行相应的转换,并且会度量这样花费的时间(除非指定了 TIMING OFF)。如果还指定了 BUFFERS 选项,那么转换过程中涉及的缓冲区访问也会被计入。但无论如何,EXPLAIN 都不会真正把结果数据发送给客户端,因此无法通过这种方式研究网络传输代价。只有当 ANALYZE 也被启用时才能启用序列化。如果书写 SERIALIZE 时没有带参数,则默认为 TEXT。
WAL 包括有关 WAL 记录生成的信息。具体而言,包括记录数、整页镜像(fpi)的数量、生成的 WAL 字节数以及 WAL 缓冲区被填满的次数。在文本格式下,只会打印非零值。该参数只能在 ANALYZE 也被启用时使用。默认为 FALSE。
TIMING 在输出中包括实际的启动时间以及在每个节点中花费的时间。在某些系统上,反复读取系统时钟的开销会显著减慢查询,因此当只需要实际的行数而不需要精确的时间时,把该参数设置为 FALSE 可能会很有用。即使通过此选项关闭了节点级的计时,整个语句的运行时间也总是会被度量。该参数只能在 ANALYZE 也被启用时使用。默认为 TRUE。
SUMMARY 在查询计划之后包括摘要信息(例如汇总后的计时信息)。使用 ANALYZE 时默认包括摘要信息,其他情况下默认不包括,但可以用此选项启用。EXPLAIN EXECUTE 中的规划时间包括从缓存中获取计划所需的时间,以及在必要时重新规划所需的时间。
MEMORY 包括查询规划阶段内存消耗的信息。具体而言,包括规划器内存结构所使用的精确存储量,以及考虑分配开销后的总内存。该参数默认为 FALSE。
FORMAT 指定输出格式,可以是 TEXT、XML、JSON 或 YAML。非文本输出包含与文本输出格式相同的信息,但对程序来说更容易解析。该参数默认为 TEXT。
boolean 指定所选的选项应当打开还是关闭。可以写 TRUE、ON 或 1 来启用选项,写 FALSE、OFF 或 0 来禁用它。也可以省略 boolean 值,此时假定为 TRUE。
statement 任何你希望查看其执行计划的 SELECT、INSERT、UPDATE、DELETE、MERGE、VALUES、EXECUTE、DECLARE、CREATE TABLE AS 或 CREATE MATERIALIZED VIEW AS 语句。
该命令的结果是对为 statement 选定的计划的文本描述,可以选择性地附上执行统计信息。Section 14.1 描述了所提供的信息。
为了让 PostgreSQL 查询规划器在优化查询时做出合理明智的决策,查询中用到的所有表的 pg_statistic 数据都应当是最新的。通常 autovacuum daemon 会自动处理好这件事。但如果某个表的内容最近发生了大量更改,你可能需要手动执行一次 ANALYZE,而不是等待自动清理追上这些更改。
为了度量执行计划中每个节点的运行时代价,EXPLAIN ANALYZE 的当前实现会给查询执行增加性能分析开销。因此,对某个查询运行 EXPLAIN ANALYZE 有时会比正常执行该查询花费的时间长得多。开销的大小取决于查询的性质以及所用的平台。最坏的情况出现在那些本身每次执行只需极少时间的计划节点上,以及那些用于获取当前时间的操作系统调用相对较慢的机器上。
展示对一个只有单个 integer 列、共 10000 行的表的简单查询的计划:
EXPLAIN SELECT * FROM foo;
QUERY PLAN
---------------------------------------------------------
Seq Scan on foo (cost=0.00..155.00 rows=10000 width=4)
(1 row)
下面是同一个查询,采用 JSON 输出格式:
EXPLAIN (FORMAT JSON) SELECT * FROM foo;
QUERY PLAN
--------------------------------
[ +
{ +
"Plan": { +
"Node Type": "Seq Scan",+
"Relation Name": "foo", +
"Alias": "foo", +
"Startup Cost": 0.00, +
"Total Cost": 155.00, +
"Plan Rows": 10000, +
"Plan Width": 4 +
} +
} +
]
(1 row)
如果存在索引,并且我们使用带有可索引 WHERE 条件的查询,EXPLAIN 可能会显示一个不同的计划:
EXPLAIN SELECT * FROM foo WHERE i = 4;
QUERY PLAN
--------------------------------------------------------------
Index Scan using fi on foo (cost=0.00..5.98 rows=1 width=4)
Index Cond: (i = 4)
(2 rows)
下面是同一个查询,但采用 YAML 格式:
EXPLAIN (FORMAT YAML) SELECT * FROM foo WHERE i='4';
QUERY PLAN
-------------------------------
- Plan: +
Node Type: "Index Scan" +
Scan Direction: "Forward"+
Index Name: "fi" +
Relation Name: "foo" +
Alias: "foo" +
Startup Cost: 0.00 +
Total Cost: 5.98 +
Plan Rows: 1 +
Plan Width: 4 +
Index Cond: "(i = 4)"
(1 row)
XML 格式就留给读者作为练习了。
下面是同一个计划,但抑制了代价估计:
EXPLAIN (COSTS FALSE) SELECT * FROM foo WHERE i = 4;
QUERY PLAN
----------------------------
Index Scan using fi on foo
Index Cond: (i = 4)
(2 rows)
下面是一个对使用聚集函数的查询给出的查询计划示例:
EXPLAIN SELECT sum(i) FROM foo WHERE i < 10;
QUERY PLAN
---------------------------------------------------------------------
Aggregate (cost=23.93..23.93 rows=1 width=4)
-> Index Scan using fi on foo (cost=0.00..23.92 rows=6 width=4)
Index Cond: (i < 10)
(3 rows)
下面是一个使用 EXPLAIN EXECUTE 显示已预备查询的执行计划的示例:
PREPARE query(int, int) AS SELECT sum(bar) FROM test
WHERE id > $1 AND id < $2
GROUP BY foo;
EXPLAIN ANALYZE EXECUTE query(100, 200);
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------
HashAggregate (cost=10.77..10.87 rows=10 width=12) (actual time=0.043..0.044 rows=10.00 loops=1)
Group Key: foo
Batches: 1 Memory Usage: 24kB
Buffers: shared hit=4
-> Index Scan using test_pkey on test (cost=0.29..10.27 rows=99 width=8) (actual time=0.009..0.025 rows=99.00 loops=1)
Index Cond: ((id > 100) AND (id < 200))
Index Searches: 1
Buffers: shared hit=4
Planning Time: 0.244 ms
Execution Time: 0.073 ms
(10 rows)
当然,这里显示的具体数字取决于相关表的实际内容。还要注意,由于规划器的改进,这些数字甚至所选的查询策略在 PostgreSQL 的不同版本之间可能会有所不同。此外,ANALYZE 命令使用随机抽样来估计数据统计信息;因此,即使表中数据的实际分布没有变化,在重新运行一次 ANALYZE 之后,代价估计也可能发生变化。
注意,前一个例子显示了一个 “定制” 计划,其针对的是 EXECUTE 中给定的那些具体参数值。我们可能还想看看参数化查询的通用计划,这可以用 GENERIC_PLAN 来做到:
EXPLAIN (GENERIC_PLAN)
SELECT sum(bar) FROM test
WHERE id > $1 AND id < $2
GROUP BY foo;
QUERY PLAN
-------------------------------------------------------------------------------
HashAggregate (cost=26.79..26.89 rows=10 width=12)
Group Key: foo
-> Index Scan using test_pkey on test (cost=0.29..24.29 rows=500 width=8)
Index Cond: ((id > $1) AND (id < $2))
(4 rows)
在这个例子中,解析器正确地推断出 $1 和 $2 应当与 id 具有相同的数据类型,因此 PREPARE 未提供参数类型信息并不是问题。在其他情况下,可能需要为参数符号显式指定类型,这可以通过对它们进行类型转换来实现,例如:
EXPLAIN (GENERIC_PLAN)
SELECT sum(bar) FROM test
WHERE id > $1::integer AND id < $2::integer
GROUP BY foo;
SQL 标准中没有定义 EXPLAIN 语句。
在 PostgreSQL 9.0 版之前使用的是下面的语法,并且目前仍然支持:
EXPLAIN [ ANALYZE ] [ VERBOSE ] statement
注意在这种语法中,选项必须严格按照所示的顺序指定。