ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
# 14.1\. 使用`EXPLAIN` PostgreSQL对每个查询产生一个_查询规划_。 为匹配查询结构和数据属性选择正确的规划对性能绝对有关键性的影响。 因此系统包含了一个复杂的规划器用于寻找最优的规划。 你可以使用[EXPLAIN](#calibre_link-575)命令察看规划器为每个查询生成的查询规划是什么。 阅读查询规划是一门值得专门写一厚本教程的学问,但是这部分试图掩盖这些基本信息。 本节的例子是从数据库执行`VACUUM ANALYZE`之后的回归测试中提取的,使用9.3开发源。 如果你尝试自己的例子,你应该可以得到类似结果,但你的估计成本及行数可能会略有不同,因为 `ANALYZE`的统计数据是随机样本,而不是确切的,并且因为成本本身有点依赖于平台。 该示例使用`EXPLAIN`的缺省"文本"输出格式, 它结构紧凑,便于人们阅读。如果你想为进一步分析提供`EXPLAIN`输出给程序, 你应该使用它的机器可读的输出格式之一(XML, JSON, or YAML)来代替。 ## 14.1.1\. `EXPLAIN`基础 查询规划的结构是一个_规划节点_的树。最底层的节点是表扫描节点: 它们从表中返回原始数据行。不同的表访问模式有不同的扫描节点类型: 顺序扫描、索引扫描、位图索引扫描。 也有非表行来源,如`VALUES`子句和`FROM`中的设置返回函数, 其中有他们自己的扫描节点类型。 如果查询需要连接、聚集、排序、或者对原始行的其它操作, 那么就会在扫描节点之上有其它额外的节点。并且,做这些操作通常都有多种方法, 因此在这些位置也有可能出现不同的节点类型。`EXPLAIN`给规划树中每个节点都输出一行, 显示基本的节点类型和规划器为执行这个规划节点预计的开销值。 其他行可能会出现,从节点的汇总行缩进,以显示节点的附加属性。 第一行(最上层的汇总行节点)是对该规划的总执行开销的预计;这个数值就是规划器试图最小化的数值 这里是一个简单的例子,只是用来显示输出会有些什么内容: ``` EXPLAIN SELECT * FROM tenk1; QUERY PLAN ------------------------------------------------------------- Seq Scan on tenk1 (cost=0.00..458.00 rows=10000 width=244) ``` 由于此查询没有`WHERE`子句,它必须扫描所有表的行,所以规划器已经选择使用一个简单的顺序扫描计划。 括号中引用的数值是(从左到右): * 预计的启动开销。在输出扫描开始之前消耗的时间,也就是在一个排序节点里执行排序的时间。 * 预计总开销。这是假设所规定的,计划节点运行完成,即所有可用行被检索。 在实践中一个节点的父节点可能会很快停止读取所有可用的行(参见`LIMIT`下面的例子)。 * 预计这个规划节点输出的行数。同样,只执行到完成为止。 * 预计这个规划节点的行平均宽度(以字节计算)。 开销是用规划器根据成本参数(参见节[Section 18.7.2](#calibre_link-1200))捏造的单位来衡量的, 习惯上以磁盘页面抓取为单位。 也就是[seq_page_cost](#calibre_link-1201)将被按照习惯设为`1.0`(一次顺序的磁盘页面抓取), 其它开销参数将参照它来设置。本节的例子都假定这些参数使用默认值。 有一点很重要:一个上层节点的开销包括它的所有子节点的开销。还有一点也很重要: 这个开销只反映规划器关心的东西,尤其是没有把结果行传递给客户端的时间考虑进去, 这个时间可能在实际的总时间里占据相当重要的分量,但是被规划器忽略了, 因为它无法通过修改规划来改变:我们相信,每个正确的规划都将输出同样的记录集。 `行`值有一些小技巧,因为它不是规划节点处理/扫描过的行数, 而是节点发射数目。通常会少于扫描数,正如应用于此节点上的任意`WHERE`子句条件的过滤结果。通常而言, 顶层的行预计会接近于查询实际返回、更新、删除的行数。 回到我们的例子: ``` EXPLAIN SELECT * FROM tenk1; QUERY PLAN ------------------------------------------------------------- Seq Scan on tenk1 (cost=0.00..458.00 rows=10000 width=244) ``` 这些数字的获得非常直截了当。如果你这样做: ``` SELECT relpages, reltuples FROM pg_class WHERE relname = 'tenk1'; ``` 你会发现`tenk1`有 358 磁盘页面和 10000 行。 估计成本作为(磁盘页面读取*[seq_page_cost](#calibre_link-1201))+(行扫描*[cpu_tuple_cost](#calibre_link-1178))被计算。默认情况下, `seq_page_cost`是1.0,`cpu_tuple_cost`是0.01, 因此估计成本为(358 * 1.0) + (10000 * 0.01) = 458。 现在让我们修改查询并增加一个`WHERE`条件: ``` EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 7000; QUERY PLAN ------------------------------------------------------------ Seq Scan on tenk1 (cost=0.00..483.00 rows=7001 width=244) Filter: (unique1 < 7000) ``` 请注意`EXPLAIN`输出显示`WHERE`子句当作一个"filter"条件附属于顺序扫描计划节点。 这意味着规划节点为它扫描的每一行检查该条件,并且只输出符合条件的行。 预计的输出行数降低了,因为有`WHERE`子句。不过,扫描仍将必须访问所有 10000 行, 因此开销没有降低;实际上它还增加了一些(确切的说,通过10000 * [cpu_operator_cost](#calibre_link-735))以反映检查`WHERE`条件的额外CPU时间。 这条查询实际选择的行数是7000 ,但是预计的`行数`只是个大概。如果你试图重复这个试验, 那么你很可能得到不同的预计。还有,这个预计会在每次`ANALYZE`命令之后改变, 因为`ANALYZE`生成的统计是从该表中随机抽取的样本计算的。 把查询限制条件改得更严格一些: ``` EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100; QUERY PLAN ------------------------------------------------------------------------------ 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) ``` 这里,规划器决定使用两步的规划:最底层的规划节点访问一个索引,找出匹配索引条件的行的位置, 然后上层规划节点真实地从表中抓取出那些行。独立地抓取数据行比顺序地读取它们的开销高很多, 但是因为并非所有表的页面都被访问了,这么做实际上仍然比一次顺序扫描开销要少。 使用两层规划的原因是因为上层规划节点把索引标识出来的行位置在读取它们之前按照物理位置排序, 这样可以最小化独立抓取的开销。节点名称里面提到的"bitmap"是进行排序的机制。 现在让我们添加另外一个条件到`WHERE`子句: ``` EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND stringu1 = 'xxx'; QUERY PLAN ------------------------------------------------------------------------------ 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) ``` 新增的条件`stringu1 = 'xxx'`减少了预计的输出行,但是没有减少开销, 因为我们仍然需要访问相同的行。 请注意,`stringu1`子句不能当做一个索引条件使用(因为这个索引只是在`unique1`列上有)。 它被当做一个从索引中检索出的行的过滤器来使用。 因此开销实际上略微增加了一些以反映这个额外的检查。 在某些情况下规划区更加喜欢"simple"索引扫描规划: ``` EXPLAIN SELECT * FROM tenk1 WHERE unique1 = 42; QUERY PLAN ----------------------------------------------------------------------------- Index Scan using tenk1_unique1 on tenk1 (cost=0.29..8.30 rows=1 width=244) Index Cond: (unique1 = 42) ``` 在这种规划类型中,表的数据行是以索引顺序抓取的,这样就令读取它们的开销更大, 但是这里的行少得可怜,因此对行位置的额外排序并不值得。最常见的就是看到这种规划类型只抓取一行, 以及那些要求`ORDER BY`条件匹配索引顺序的查询。因为那时候没有多余的排序步骤是必要的以满足`ORDER BY`。 如果在`WHERE`里面使用的好几个字段上都有索引,那么规划器可能会使用索引的AND或OR的组合: ``` EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000; QUERY PLAN ------------------------------------------------------------------------------------- Bitmap Heap Scan on tenk1 (cost=25.08..60.21 rows=10 width=244) Recheck Cond: ((unique1 < 100) AND (unique2 > 9000)) -> BitmapAnd (cost=25.08..25.08 rows=10 width=0) -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=101 width=0) Index Cond: (unique1 < 100) -> Bitmap Index Scan on tenk1_unique2 (cost=0.00..19.78 rows=999 width=0) Index Cond: (unique2 > 9000) ``` 但是这么做要求访问两个索引,因此与只使用一个索引,而把另外一个条件只当作过滤器相比, 这个方法未必是更优。如果你改变涉及的范围,你会看到规划器相应地发生变化。 下面是一个例子,显示`LIMIT`的影响: ``` EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000 LIMIT 2; QUERY PLAN ------------------------------------------------------------------------------------- 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) ``` 这是上面相同的查询,但我们增加了`LIMIT`,以致于不是所有的行需要被检索,并且规划关于该怎么做改变了主意, 请注意,索引扫描节点的总成本和行数被显示,好像它是运行完毕的。然而,限制节点预计在提取这些行的仅仅五分之一后停止, 所以其总成本只有五分之一之多,这就是实际的预算费用查询。该计划优于增加一个限制节点到 先前的计划,因为该限制无法避免支付位图扫描的启动成本,所以总成本将超过使用这种方法的25个单位的东西。 让我们试着使用我们上面讨论的字段连接两个表: ``` EXPLAIN SELECT * FROM tenk1 t1, tenk2 t2 WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2; QUERY PLAN -------------------------------------------------------------------------------------- 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) ``` 在这个规划中,我们有两个表扫描的作为输入或者子节点的嵌套循环连接节点, 节点摘要行的缩进反映规划树结构。 连接的第一个,或者"outer",子节点就是类似于我们之前看到的位图扫描。 其成本和行数是一样的,正如我们从`SELECT ... WHERE unique1 &lt; 10`获得。 因为我们只能在那个节点上应用`WHERE` clause `unique1 &lt; 10`。`t1.unique2 = t2.unique2` 子句还没有任何关系。因此它不影响外层扫描的行计数。 嵌套循环连接节点将运行它的第二部分, 或者"inner"子节点一次从外部子节点获得每一行。 从目前的外层行获得的值可以被插入到内扫描。这儿,从外层行中获得的`t1.unique2`是可用的。 这样我们就得到一个计划和成本,并且类似于我们上面看到的简单的`SELECT ... WHERE t2.unique2 =` `_constant_`的情况。 (估计费用实际上比上面看到的低一点,在`t2`上可重复的索引扫描期间,作为期望发生的高速缓存结果)。 以外层扫描的开销为基础设置循环节点的开销,加上每个外层行的一个重复(这里是10 * 7.87), 然后再加上连接处理需要的一点点CPU时间。 在这个例子里,连接的输出行数与两个扫描的行数的乘积相同,但通常并不是这样的, 因为通常你会有提及两个表的`WHERE`子句,因此它只能应用于连接(join)点, 而不能影响两个关系的输入扫描。 这里有一个例子: ``` EXPLAIN SELECT * FROM tenk1 t1, tenk2 t2 WHERE t1.unique1 < 10 AND t2.unique2 < 10 AND t1.hundred < t2.hundred; QUERY PLAN --------------------------------------------------------------------------------------------- Nested Loop (cost=4.65..49.46 rows=33 width=488) Join Filter: (t1.hundred < t2.hundred) -> 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) -> Materialize (cost=0.29..8.51 rows=10 width=244) -> Index Scan using tenk2_unique2 on tenk2 t2 (cost=0.29..8.46 rows=10 width=244) Index Cond: (unique2 < 10) ``` 条件`t1.hundred &lt; t2.hundred`不能 在`tenk2_unique2`索引中被测试,因此它被应用在 连接节点。这减少了连接节点的预计输出行数, 但不改变任何一个输入扫描。 注意,这里的规划器已经选择"具体化"连接内部关系, 通过放在规划节点上面。这也就是说,`t2`索引扫描将执行一次,尽管 嵌套循环连接节点需要读取数据十次,来自外部关系的每一行。 实现节点将数据保存在存储器中,因为它被读取,然后从存储器每个后续过程中返回每一个数据。 当与外部联接时,你可能会看到带有附属"Join Filter"以及纯"Filter"条件的连接计划节点。 连接过滤条件来自于外部连接的`ON`子句,因此 这样的行失败了,连接过滤条件仍然可以作为非扩展行发出。 但一个纯过滤条件可以在外连接规则之后被应用 因此这个行为无条件地删除行。在内连接中 这些过滤器类型之间没有语义差异。 如果我们改变查询的选择性,我们可能会得到一个非常不同的连接计划: ``` EXPLAIN SELECT * FROM tenk1 t1, tenk2 t2 WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2; QUERY PLAN ------------------------------------------------------------------------------------------ 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) ``` 在这里,规划器选择使用一个哈希联接,表中的行被输入到内存中的哈希表中,在此之后,其他 表被扫描并且哈希表进行探测以匹配每一行。 再次注意如何缩进来反映规划结构:在`tenk1`上的位图 扫描是输入到哈希节点,它构造哈希表。这之后返回哈希连接节点,其内容是从它的外部子计划中读取每一行并且搜索每一个哈希表。 另一种可能的连接类型是合并连接,在这里说明: ``` EXPLAIN SELECT * FROM tenk1 t1, onek t2 WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2; QUERY PLAN ------------------------------------------------------------------------------------------ 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) ``` 合并连接要求其输入的数据在连接键上进行排序。在这种 规划中`tenk1`数据是通过使用索引扫描访问正确顺序的行来进行排序。 但顺序扫描和排序是`onek`的首选,因为有该表上被访问的更多行。 (顺序扫描和排序为排序行数而频繁进行索引扫描,因为通过索引扫描需要不连续的磁盘访问) 找另外一个规划的方法是通过设置每种规划类型的允许/禁止开关(在[Section 18.7.1](#calibre_link-1202)里描述), 强制规划器抛弃它认为优秀的(扫描)策略。这个工具目前比较原始,但很有用。又见[Section 14.3](#calibre_link-1189)。 例如,如果我们不相信顺序扫描和排序对于前面例子中处理表`onek`是最好的方式,我们可以尝试 ``` SET enable_sort = off; EXPLAIN SELECT * FROM tenk1 t1, onek t2 WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2; QUERY PLAN ------------------------------------------------------------------------------------------ Merge Join (cost=0.56..292.65 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) -> Index Scan using onek_unique2 on onek t2 (cost=0.28..224.79 rows=1000 width=244) ``` 这表明规划期认为通过索引扫描排序`onek`比顺序扫描和排序更昂贵约12%。 当然,接下来的问题是它是否是对的。我们可以使用`EXPLAIN ANALYZE`调查,正如下面所讨论的: ## 14.1.2\. `EXPLAIN ANALYZE` 我们可以用`EXPLAIN`的`ANALYZE`检查规划器的估计值的准确性。 这个命令实际上执行该查询然后显示每个规划节点内实际运行时间的和以及单纯`EXPLAIN`显示的估计开销。 比如,我们可以像下面这样获取一个结果: ``` EXPLAIN ANALYZE SELECT * FROM tenk1 t1, tenk2 t2 WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2; QUERY PLAN --------------------------------------------------------------------------------------------------------------------------------- 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) Total runtime: 0.501 ms ``` 请注意"actual time"数值是以真实时间的毫秒计的,而`cost`估计值是以任意磁盘抓取的单元计的; 因此它们很可能不一致。我们要关心的事是两组比值是否一致。 通常最重要的事情是看是否估计行数相当接近于现实。在这个例子中, 估计都是完全正确的,但是这是相当不寻常的做法。 在一些查询规划里,一个子规划节点很可能运行多次。比如,在上面的嵌套循环的规划里, 内层的索引扫描对每个外层行执行一次。在这种情况下,`loops`报告该节点执行的总数目, 而显示的实际时间和行数目是每次执行的平均值。这么做的原因是令这些数字与开销预计显示的数字具有可比性。 要乘以`loops`值才能获得在该节点花费的总时间。在上面的例子中,我们共需要0.220毫秒来执行`tenk2`的索引扫描。 在某些情况下`EXPLAIN ANALYZE`显示超出规划节点执行时间和行数的额外执行统计数据。 例如,排序和哈希节点提供额外的信息: ``` EXPLAIN ANALYZE SELECT * FROM tenk1 t1, tenk2 t2 WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous; QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------------------- 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 Join (cost=230.47..713.98 rows=101 width=488) (actual time=0.711..7.427 rows=100 loops=1) Hash Cond: (t2.unique2 = t1.unique2) -> Seq Scan on tenk2 t2 (cost=0.00..445.00 rows=10000 width=244) (actual time=0.007..2.583 rows=10000 loops=1) -> 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 -> Bitmap Heap Scan on tenk1 t1 (cost=5.07..229.20 rows=101 width=244) (actual time=0.080..0.526 rows=100 loops=1) Recheck Cond: (unique1 < 100) -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=101 width=0) (actual time=0.049..0.049 rows=100 loops=1) Index Cond: (unique1 < 100) Total runtime: 8.008 ms ``` 排序节点显示使用的排序方法(特别是,排序是否在内存或磁盘上)以及所需的内存或磁盘空间量。 哈希节点显示哈希桶数量以及批处理用于哈希表的内存峰值数。 (如果批处理数大于1,也将有参与磁盘空间使用情况,但不在这显示。 另一种类型的附加信息是通过过滤条件删除行数: ``` EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE ten < 7; QUERY PLAN --------------------------------------------------------------------------------------------------------- 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 Total runtime: 5.905 ms ``` 这些计数对于应用在连接节点的过滤条件特别有价值。 "删除行"只出现在扫描行,或者连接节点的情况下的潜在连接对, 通过过滤条件被拒绝。 类似的情况,过滤条件产生"lossy"的索引扫描。 例如,考虑多边形这个搜索包含的具体点: ``` EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @> polygon '(0.5,2.0)'; QUERY PLAN ------------------------------------------------------------------------------------------------------ Seq Scan on polygon_tbl (cost=0.00..1.05 rows=1 width=32) (actual time=0.044..0.044 rows=0 loops=1) Filter: (f1 @> '((0.5,2))'::polygon) Rows Removed by Filter: 4 Total runtime: 0.083 ms ``` 规划器认为(很正确)这种表太小而干扰索引扫描,所以我们有一个纯顺序扫描, 其中所有行通过过滤条件被拒绝。但是,如果我们强制 使用索引扫描,我们看到: ``` SET enable_seqscan TO off; EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @> polygon '(0.5,2.0)'; QUERY PLAN -------------------------------------------------------------------------------------------------------------------------- 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 Total runtime: 0.144 ms ``` 在这里,我们可以看到,索引返回一个候选行,这是 通过索引条件的重新检查被拒绝。这是因为 GiST索引对于多边形封闭测试是"lossy":它实际上 返回带有重叠目标多边形的行,然后我们在那些行上进行确切的封闭测试。 `EXPLAIN`有`BUFFERS`选项,`ANALYZE`以获得更多的运行时间统计: ``` EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000; QUERY PLAN --------------------------------------------------------------------------------------------------------------------------------- Bitmap Heap Scan on tenk1 (cost=25.08..60.21 rows=10 width=244) (actual time=0.323..0.342 rows=10 loops=1) Recheck Cond: ((unique1 < 100) AND (unique2 > 9000)) Buffers: shared hit=15 -> BitmapAnd (cost=25.08..25.08 rows=10 width=0) (actual time=0.309..0.309 rows=0 loops=1) Buffers: shared hit=7 -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=101 width=0) (actual time=0.043..0.043 rows=100 loops=1) Index Cond: (unique1 < 100) Buffers: shared hit=2 -> Bitmap Index Scan on tenk1_unique2 (cost=0.00..19.78 rows=999 width=0) (actual time=0.227..0.227 rows=999 loops=1) Index Cond: (unique2 > 9000) Buffers: shared hit=5 Total runtime: 0.423 ms ``` 通过`BUFFERS`提供的数字帮助辨识查询的哪些部分大多是I/O密集型。 请记住,因为`EXPLAIN ANALYZE`实际运行查询, 任何副作用还是一样会发生,即使无论什么结果查询可能的输出都将被丢弃而 赞成输出`EXPLAIN`的数据。 如果要分析一个数据修改的查询,而无需改变你的表,你可以回滚命令到后面,例如: ``` BEGIN; EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 < 100; QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------- Update on tenk1 (cost=5.07..229.46 rows=101 width=250) (actual time=14.628..14.628 rows=0 loops=1) -> Bitmap Heap Scan on tenk1 (cost=5.07..229.46 rows=101 width=250) (actual time=0.101..0.439 rows=100 loops=1) Recheck Cond: (unique1 < 100) -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=101 width=0) (actual time=0.043..0.043 rows=100 loops=1) Index Cond: (unique1 < 100) Total runtime: 14.727 ms ROLLBACK; ``` 如该示例中,当查询是`INSERT`,`UPDATE`或者`DELETE`命令时, 申请表变化的实际工作是由顶层插入、更新、或删除规划节点完成的。 这个节点下的规划节点进行定位旧的行和/或计算新数据。 所以上面,我们看到了已经看到的相同排序的位表扫描, 并且其输出被传递给存储更新行的更新节点。 值得一提的是,虽然修改数据的节点可以采取大量的运行时间(在这里,它消耗了大部分的共享的时间), 规划器目前不添加任何东西的成本来估计说明这项工作。这是因为要做的工作是同样为了每一个正确的查询规划, 因此它不影响规划决定。 `EXPLAIN ANALYZE`显示的`Total runtime`包括执行器启动和关闭的时间, 以及被激发的任何触发器运行时间。但它不包括分析、重写、规划的时间。 执行`BEFORE`触发器花费的时间,如果有的话,包括在为相关插入,更新或删除节点的时间内, 但执行`AFTER`触发器的时间花费并不计算在内, 因为整个规划完成之后,才触发`AFTER`触发器。 单独显示每个触发器花费的总时间(`BEFORE` 或者`AFTER`)。 需要注意的是延迟约束触发器直到事务结束将不会执行,因而不会通过`EXPLAIN ANALYZE`显示。 ## 14.1.3\. 警告 有两个显著方式测量运行时间,通过 `EXPLAIN ANALYZE`偏离相同查询的正常执行。 首先,由于没有输出行被传递到客户端, 不包含网络传输成本和I/O转换费用。其次,通过`EXPLAIN ANALYZE`增加的测量开销是巨大的, 特别是在慢的`gettimeofday()`操作系统调用机器上。您可以使用 [pg_test_timing](#calibre_link-1203)工具来测量您系统上的定时开销。 `EXPLAIN`的结果除了在你实际测试的情况之外不能推导出其它的情况; 比如,在一个小得像玩具的表上的结果不能适用于大表。规划器的开销计算不是线性的, 因此它很可能对大些或者小些的表选择不同的规划。一个极端的例子是一个只占据一个磁盘页面的表, 在这样的表上,不管它有没有索引可以使用,你几乎都总是得到顺序扫描规划。 规划器知道不管在任何情况下它都要进行一个磁盘页面的读取, 所以再扩大几个磁盘页面读取以查找索引是没有意义的。(我们可以从`polygon_tbl`上面的例子中看到) 在某些情况中,实际与估计值不能很好的匹配,但没有什么是真的错了。 出现这样的一个情况,当规划节点执行是由`LIMIT`或类似效果而短暂停止 。例如,我们以前使用`LIMIT`查询。 ``` EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000 LIMIT 2; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------- Limit (cost=0.29..14.71 rows=2 width=244) (actual time=0.177..0.249 rows=2 loops=1) -> Index Scan using tenk1_unique2 on tenk1 (cost=0.29..72.42 rows=10 width=244) (actual time=0.174..0.244 rows=2 loops=1) Index Cond: (unique2 > 9000) Filter: (unique1 < 100) Rows Removed by Filter: 287 Total runtime: 0.336 ms ``` 为索引扫描节点的估计成本和行数都被显示。即使它是运行完毕的。但现实是请求行运行两个之后限制节点停止, 所以实际的行数只有2和,并且运行时间低于成本估算。这不是估计错误,由于只有一个差异估计和真实值显示。 合并连接也有可混淆粗心的测量产品。 合并连接将停止读取一个输入,如果它用尽了其他输入,并且输入端的下一个关键值大于其他输入的最后一个关键值; 在这种情况下,就不可能有更多的匹配,所以不需要扫描第一个输入的其余部分。 这会导致无法读取所有子节点,有些像那些提到的`LIMIT`结果。 此外,如果外部(第一)子节点包含重复键值的行,内部(第二个)子节点被备份并重新扫描行匹配键值的部分。 `EXPLAIN ANALYZE`计算同一内部行的重复部分,好像他们是真正的附加行。 当有许多外部重复部分,为了内部子规划节点比真实内部关系行数足够大,则报告的实际行数。 BitmapAnd和BitmapOr节点总是报告自己的实际行数为零,由于实施限制。