PostgreSql的Explain命令详解

http://toplchx.iteye.com/blog/2091860
使用EXPLAIN
PostgreSQL为每一个收到的查询设计一个查询规划。选择正确的匹配查询结构和数据属性的规划对执行效率是相当重要要的,因此系统包含一个复杂的规划器来试图选择好的规划。你可使用EXPLAIN命令查看查询规划器建立的任何查询。阅读查询规划是一门艺术,须要掌握必定的经验,本节试图涵盖一些基础知识。
如下的例子来自PostgreSQL 9.3开发版。
EXPLAIN基础
查询规划是以规划为节点的树形结构。树的最底节点是扫描节点:他返回表中的原数据行。
不一样的表有不一样的扫描节点类型:顺序扫描,索引扫描和位图索引扫描。
也有非表列源,如VALUES子句并设置FROM返回,他们有本身的扫描类型。
若是查询须要关联,聚合,排序或其余操做,会在扫描节点之上增长节点执行这些操做。一般有不仅一种可能的方式作这些操做,因此可能出现不一样的节点类型。
EXPLAIN的输出是每一个树节点显示一行,内容是基本节点类型和执行节点的消耗评估。可能会楚翔其余行,从汇总行节点缩进显示节点的其余属性。第一行(最上节点的汇总行)是评估执行计划的总消耗,这个值越小越好。
下面是一个简单的例子:
Sql代码   收藏代码
  1. EXPLAIN SELECT * FROM tenk1;  
  2.   
  3.                          QUERY PLAN  
  4. -------------------------------------------------------------  
  5.  Seq Scan on tenk1  (cost=0.00..458.00 rows=10000 width=244)  
由于这个查询没有WHERE子句,因此必须扫描表中的全部行,因此规划器选择使用简单的顺序扫描规划。括号中的数字从左到右依次是:
  • 评估开始消耗。这是能够开始输出前的时间,好比排序节点的排序的时间。
  • 评估总消耗。假设查询从执行到结束的时间。有时父节点可能中止这个过程,好比LIMIT子句。
  • 评估查询节点的输出行数,假设该节点执行结束。
  • 评估查询节点的输出行的平均字节数。
这个消耗的计算依赖于规划器的设置参数,这里的例子都是在默认参数下运行。
须要知道的是:上级节点的消耗包括其子节点的消耗。这个消耗值只反映规划器关心的内容,通常这个消耗不包括将数据传输到客户端的时间。
评估的行数不是执行和扫描查询节点的数量,而是节点返回的数量。它一般会少于扫描数量,由于有WHERE条件会过滤掉一些数据。理想状况顶级行数评估近似于实际返回的数量
回到刚才的例子,表tenk1有10000条数据分布在358个磁盘页,评估时间是(磁盘页*seq_page_cost)+(扫描行*cpu_tuple_cost)。默认seq_page_cost是1.0,cpu_tuple_cost是0.01,因此评估值是(358 * 1.0) + (10000 * 0.01) = 458
 
如今咱们将查询加上WHERE子句:
Sql代码   收藏代码
  1. EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 7000;  
  2.   
  3.                          QUERY PLAN  
  4. ------------------------------------------------------------  
  5.  Seq Scan on tenk1  (cost=0.00..483.00 rows=7001 width=244)  
  6.    Filter: (unique1 < 7000)  
查询节点增长了“filter”条件。这意味着查询节点为扫描的每一行数据增长条件检查,只输入符合条件数据。评估的输出记录数由于where子句变少了,可是扫描的数据仍是10000条,因此消耗没有减小,反而增长了一点cup的计算时间。
这个查询实际输出的记录数是7000,可是评估是个近似值,屡次运行可能略有差异,这中状况能够经过ANALYZE命令改善。
 
如今再修改一下条件
Sql代码   收藏代码
  1. EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100;  
  2.   
  3.                                   QUERY PLAN  
  4. ------------------------------------------------------------------------------  
  5.  Bitmap Heap Scan on tenk1  (cost=5.07..229.20 rows=101 width=244)  
  6.    Recheck Cond: (unique1 < 100)  
  7.    ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0)  
  8.          Index Cond: (unique1 < 100)  
查询规划器决定使用两步规划:首先子查询节点查看索引找到符合条件的记录索引,而后外层查询节点将这些记录从表中提取出来。分别提取数据的成本要高于顺序读取,但由于不须要读取全部磁盘页,因此总消耗比较小。(其中Bitmap是系统排序的一种机制)
 
如今,增长另外一个查询条件:
Sql代码   收藏代码
  1. EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND stringu1 = 'xxx';  
  2.   
  3.                                   QUERY PLAN  
  4. ------------------------------------------------------------------------------  
  5.  Bitmap Heap Scan on tenk1  (cost=5.04..229.43 rows=1 width=244)  
  6.    Recheck Cond: (unique1 < 100)  
  7.    Filter: (stringu1 = 'xxx'::name)  
  8.    ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0)  
  9.          Index Cond: (unique1 < 100)  
增长的条件stringu1='xxx'减小了输出记录数的评估,但没有减小时间消耗,应为系统仍是要查询相同数量的记录。请注意stringu1不是索引条件。
 若是在不一样的字段上有独立的索引,规划器可能选择使用AND或者OR组合索引:
Sql代码   收藏代码
  1. EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000;  
  2.   
  3.                                      QUERY PLAN  
  4. -------------------------------------------------------------------------------------  
  5.  Bitmap Heap Scan on tenk1  (cost=25.08..60.21 rows=10 width=244)  
  6.    Recheck Cond: ((unique1 < 100) AND (unique2 > 9000))  
  7.    ->  BitmapAnd  (cost=25.08..25.08 rows=10 width=0)  
  8.          ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0)  
  9.                Index Cond: (unique1 < 100)  
  10.          ->  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0)  
  11.                Index Cond: (unique2 > 9000)  
 这个查询条件的两个字段都有索引,索引不须要filre。
 
下面咱们来看看LIMIT的影响:
Sql代码   收藏代码
  1. EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000 LIMIT 2;  
  2.   
  3.                                      QUERY PLAN  
  4. -------------------------------------------------------------------------------------  
  5.  Limit  (cost=0.29..14.48 rows=2 width=244)  
  6.    ->  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..71.27 rows=10 width=244)  
  7.          Index Cond: (unique2 > 9000)  
  8.          Filter: (unique1 < 100)  
这条查询的where条件和上面的同样,只是增长了LIMIT,因此不是全部数据都须要返回,规划器改变了规划。在索引扫描节点总消耗和返回记录数是运行玩查询以后的数值,但Limit节点预期时间消耗是15,因此总时间消耗是15.增长LIMIT会使启动时间小幅增长(0.25->0.29)。
 
来看一下经过索引字段的表链接:
Sql代码   收藏代码
  1. EXPLAIN SELECT *  
  2. FROM tenk1 t1, tenk2 t2  
  3. WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2;  
  4.   
  5.                                       QUERY PLAN  
  6. --------------------------------------------------------------------------------------  
  7.  Nested Loop  (cost=4.65..118.62 rows=10 width=488)  
  8.    ->  Bitmap Heap Scan on tenk1 t1  (cost=4.36..39.47 rows=10 width=244)  
  9.          Recheck Cond: (unique1 < 10)  
  10.          ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0)  
  11.                Index Cond: (unique1 < 10)  
  12.    ->  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.91 rows=1 width=244)  
  13.          Index Cond: (unique2 = t1.unique2)  
这个规划中有一个内链接的节点,它有两个子节点。节点摘要行的缩进反映了规划树的结构。最外层是一个链接节点,子节点是一个Bitmap扫描。外部节点位图扫描的消耗和记录数如同咱们使用SELECT...WHERE unique1 < 10,由于这时t1.unique2 = t2.unique2还不相关。接下来为每个从外部节点获得的记录运行内部查询节点。这里外部节点获得的数据的t1.unique2值是可用的,因此咱们获得的计划和SELECT...WHEREt2.unique2=constant的状况相似。(考虑到缓存的因素评估的消耗可能要小一些)
外部节点的消耗加上循环内部节点的消耗(39.47+10*7.91)再加一点CPU时间就获得规划的总消耗。
 
再看一个例子:
Sql代码   收藏代码
  1. EXPLAIN SELECT *  
  2. FROM tenk1 t1, tenk2 t2  
  3. WHERE t1.unique1 < 10 AND t2.unique2 < 10 AND t1.hundred < t2.hundred;  
  4.   
  5.                                          QUERY PLAN  
  6. ---------------------------------------------------------------------------------------------  
  7.  Nested Loop  (cost=4.65..49.46 rows=33 width=488)  
  8.    Join Filter: (t1.hundred < t2.hundred)  
  9.    ->  Bitmap Heap Scan on tenk1 t1  (cost=4.36..39.47 rows=10 width=244)  
  10.          Recheck Cond: (unique1 < 10)  
  11.          ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0)  
  12.                Index Cond: (unique1 < 10)  
  13.    ->  Materialize  (cost=0.29..8.51 rows=10 width=244)  
  14.          ->  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..8.46 rows=10 width=244)  
  15.                Index Cond: (unique2 < 10)  
条件t1.hundred<t2.hundred不在tenk2_unique2索引中,因此这个条件出如今链接节点中。这将减小链接节点的评估输出记录数,但不会改变子节点的扫描数。
注意此次规划器选择使用Meaterialize节点,将条件加入内部节点,这觉得着内部节点的索引扫描只作一次,即便嵌套循环须要读取这些数据10次,Meterialize节点将数据保存在内存中,每次循环都从内存中读取数据。
 
若是咱们稍微改变一下查询,会看到彻底不一样的规划:
Sql代码   收藏代码
  1. EXPLAIN SELECT *  
  2. FROM tenk1 t1, tenk2 t2  
  3. WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2;  
  4.   
  5.                                         QUERY PLAN  
  6. ------------------------------------------------------------------------------------------  
  7.  Hash Join  (cost=230.47..713.98 rows=101 width=488)  
  8.    Hash Cond: (t2.unique2 = t1.unique2)  
  9.    ->  Seq Scan on tenk2 t2  (cost=0.00..445.00 rows=10000 width=244)  
  10.    ->  Hash  (cost=229.20..229.20 rows=101 width=244)  
  11.          ->  Bitmap Heap Scan on tenk1 t1  (cost=5.07..229.20 rows=101 width=244)  
  12.                Recheck Cond: (unique1 < 100)  
  13.                ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0)  
  14.                      Index Cond: (unique1 < 100)  
这里规划器选择使用hash join,将一个表的数据存入内存中的哈希表,而后扫描另外一个表并和哈希表中的每一条数据进行匹配。
注意缩进反应的规划结构。在tenk1表上的bitmap扫描结果做为Hash节点的输入创建哈希表。而后Hash Join节点读取外层子节点的数据,再循环检索哈希表的数据。
 
另外一个可能的链接类型是merge join:
 
Sql代码   收藏代码
  1. EXPLAIN SELECT *  
  2. FROM tenk1 t1, onek t2  
  3. WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2;  
  4.   
  5.                                         QUERY PLAN  
  6. ------------------------------------------------------------------------------------------  
  7.  Merge Join  (cost=198.11..268.19 rows=10 width=488)  
  8.    Merge Cond: (t1.unique2 = t2.unique2)  
  9.    ->  Index Scan using tenk1_unique2 on tenk1 t1  (cost=0.29..656.28 rows=101 width=244)  
  10.          Filter: (unique1 < 100)  
  11.    ->  Sort  (cost=197.83..200.33 rows=1000 width=244)  
  12.          Sort Key: t2.unique2  
  13.          ->  Seq Scan on onek t2  (cost=0.00..148.00 rows=1000 width=244)  
Merge Join须要已经排序的输入数据。在这个规划中按正确顺序索引扫描tenk1的数据,可是对onek表执行排序和顺序扫描,由于须要在这个表中查询多条数据。由于索引扫描须要访问不连续的磁盘,因此索引扫描多条数据时会频繁使用排序顺序扫描(Sequential-scan-and-sort)。
有一种方法能够看到不一样的规划,就是强制规划器忽略任何策略。例如,若是咱们不相信排序顺序扫描(sequential-scan-and-sort)是最好的办法,咱们能够尝试这样的作法:
Sql代码   收藏代码
  1. SET enable_sort = off;  
  2.   
  3. EXPLAIN SELECT *  
  4. FROM tenk1 t1, onek t2  
  5. WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2;  
  6.   
  7.                                         QUERY PLAN  
  8. ------------------------------------------------------------------------------------------  
  9.  Merge Join  (cost=0.56..292.65 rows=10 width=488)  
  10.    Merge Cond: (t1.unique2 = t2.unique2)  
  11.    ->  Index Scan using tenk1_unique2 on tenk1 t1  (cost=0.29..656.28 rows=101 width=244)  
  12.          Filter: (unique1 < 100)  
  13.    ->  Index Scan using onek_unique2 on onek t2  (cost=0.28..224.79 rows=1000 width=244)  
显示测结果代表,规划器认为索引扫描比排序顺序扫描消耗高12%。固然下一个问题就是规划器的评估为何是正确的。咱们能够经过EXPLAIN ANALYZE进行考察。
 
EXPLAIN ANALYZE
经过EXPLAIN ANALYZE能够检查规划器评估的准确性。使用ANALYZE选项,EXPLAIN实际运行查询,显示真实的返回记录数和运行每一个规划节点的时间,例如咱们能够获得下面的结果:
Sql代码   收藏代码
  1. EXPLAIN ANALYZE SELECT *  
  2. FROM tenk1 t1, tenk2 t2  
  3. WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2;  
  4.   
  5.                                                            QUERY PLAN  
  6. ---------------------------------------------------------------------------------------------------------------------------------  
  7.  Nested Loop  (cost=4.65..118.62 rows=10 width=488) (actual time=0.128..0.377 rows=10 loops=1)  
  8.    ->  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)  
  9.          Recheck Cond: (unique1 < 10)  
  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)  
  11.                Index Cond: (unique1 < 10)  
  12.    ->  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)  
  13.          Index Cond: (unique2 = t1.unique2)  
  14.  Total runtime: 0.501 ms  
注意,实际时间(actual time)的值是已毫秒为单位的实际时间,cost是评估的消耗,是个虚拟单位时间,因此他们看起来不匹配。
一般最重要的是看评估的记录数是否和实际获得的记录数接近。在这个例子里评估数彻底和实际同样,但这种状况不多出现。
 
某些查询规划可能执行屡次子规划。好比以前提过的内循环规划(nested-loop),内部索引扫描的次数是外部数据的数量。在这种状况下,报告显示循环执行的总次数、平均实际执行时间和数据条数。这样作是为了和评估值表示方式一至。由循环次数和平均值相乘获得总消耗时间。
 
某些状况EXPLAIN ANALYZE会显示额外的信息,好比sort和hash节点的时候:
Sql代码   收藏代码
  1. EXPLAIN ANALYZE SELECT *  
  2. FROM tenk1 t1, tenk2 t2  
  3. WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;  
  4.   
  5.                                                                  QUERY PLAN  
  6. --------------------------------------------------------------------------------------------------------------------------------------------  
  7.  Sort  (cost=717.34..717.59 rows=101 width=488) (actual time=7.761..7.774 rows=100 loops=1)  
  8.    Sort Key: t1.fivethous  
  9.    Sort Method: quicksort  Memory: 77kB  
  10.    ->  Hash Join  (cost=230.47..713.98 rows=101 width=488) (actual time=0.711..7.427 rows=100 loops=1)  
  11.          Hash Cond: (t2.unique2 = t1.unique2)  
  12.          ->  Seq Scan on tenk2 t2  (cost=0.00..445.00 rows=10000 width=244) (actual time=0.007..2.583 rows=10000 loops=1)  
  13.          ->  Hash  (cost=229.20..229.20 rows=101 width=244) (actual time=0.659..0.659 rows=100 loops=1)  
  14.                Buckets: 1024  Batches: 1  Memory Usage: 28kB  
  15.                ->  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)  
  16.                      Recheck Cond: (unique1 < 100)  
  17.                      ->  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)  
  18.                            Index Cond: (unique1 < 100)  
  19.  Total runtime: 8.008 ms  
排序节点(Sort)显示排序类型(通常是在内存仍是在磁盘)和使用多少内存。哈希节点(Hash)显示哈希桶和批数以及使用内存的峰值。
 
另外一种额外信息是过滤条件过滤掉的记录数:
Sql代码   收藏代码
  1. EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE ten < 7;  
  2.   
  3.                                                QUERY PLAN  
  4. ---------------------------------------------------------------------------------------------------------  
  5.  Seq Scan on tenk1  (cost=0.00..483.00 rows=7000 width=244) (actual time=0.016..5.107 rows=7000 loops=1)  
  6.    Filter: (ten < 7)  
  7.    Rows Removed by Filter: 3000  
  8.  Total runtime: 5.905 ms  
这个值在join节点上尤为有价值。"Rows Removed"只有在过滤条件过滤掉数据时才显示。
 
相似条件过滤的状况也会在"lossy"索引扫描时发生,好比这样一个查询,一个多边形含有的特定的点:
Sql代码   收藏代码
  1. EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @> polygon '(0.5,2.0)';  
  2.   
  3.                                               QUERY PLAN  
  4. ------------------------------------------------------------------------------------------------------  
  5.  Seq Scan on polygon_tbl  (cost=0.00..1.05 rows=1 width=32) (actual time=0.044..0.044 rows=0 loops=1)  
  6.    Filter: (f1 @> '((0.5,2))'::polygon)  
  7.    Rows Removed by Filter: 4  
  8.  Total runtime: 0.083 ms  
规划器认为(正确的)这样的表过小以致于不须要索引扫描,因此采用顺序扫描全部行经行条件检查。
可是,若是咱们强制使用索引扫描,将会看到:
Sql代码   收藏代码
  1. SET enable_seqscan TO off;  
  2.   
  3. EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @> polygon '(0.5,2.0)';  
  4.   
  5.                                                         QUERY PLAN  
  6. --------------------------------------------------------------------------------------------------------------------------  
  7.  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)  
  8.    Index Cond: (f1 @> '((0.5,2))'::polygon)  
  9.    Rows Removed by Index Recheck: 1  
  10.  Total runtime: 0.144 ms  
这里咱们能够看到索引返回一条候选数据,但被过滤条件拒绝。这是由于GiST索引在多边形包含检测上是松散的"lossy":它实际返回哪些和多边形交叠的数据,而后咱们还须要针对这些数据作包含检测。
 
EXPLAIN还有BUFFERS选项能够和ANALYZE一块儿使用,来获得更多的运行时间分析:
Sql代码   收藏代码
  1. EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000;  
  2.   
  3.                                                            QUERY PLAN  
  4. ---------------------------------------------------------------------------------------------------------------------------------  
  5.  Bitmap Heap Scan on tenk1  (cost=25.08..60.21 rows=10 width=244) (actual time=0.323..0.342 rows=10 loops=1)  
  6.    Recheck Cond: ((unique1 < 100) AND (unique2 > 9000))  
  7.    Buffers: shared hit=15  
  8.    ->  BitmapAnd  (cost=25.08..25.08 rows=10 width=0) (actual time=0.309..0.309 rows=0 loops=1)  
  9.          Buffers: shared hit=7  
  10.          ->  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)  
  11.                Index Cond: (unique1 < 100)  
  12.                Buffers: shared hit=2  
  13.          ->  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)  
  14.                Index Cond: (unique2 > 9000)  
  15.                Buffers: shared hit=5  
  16.  Total runtime: 0.423 ms   
Buffers提供的数据能够帮助肯定哪些查询是I/O密集型的。
 
请注意EXPLAIN ANALYZE实际运行查询,任何实际影响都会发生。若是要分析一个修改数据的查询又不想改变你的表,你可使用roll back命令进行回滚,好比:
Sql代码   收藏代码
  1. BEGIN;  
  2.   
  3. EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 < 100;  
  4.   
  5.                                                            QUERY PLAN  
  6. --------------------------------------------------------------------------------------------------------------------------------  
  7.  Update on tenk1  (cost=5.07..229.46 rows=101 width=250) (actual time=14.628..14.628 rows=0 loops=1)  
  8.    ->  Bitmap Heap Scan on tenk1  (cost=5.07..229.46 rows=101 width=250) (actual time=0.101..0.439 rows=100 loops=1)  
  9.          Recheck Cond: (unique1 < 100)  
  10.          ->  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)  
  11.                Index Cond: (unique1 < 100)  
  12.  Total runtime: 14.727 ms  
  13.   
  14. ROLLBACK;  
当查询是INSERT,UPDATE或DELETE命令时,在顶级节点是实施对表的变动。在这下面的节点实行定位旧数据计算新数据的工做。因此咱们看到同样的bitmap索引扫描,并返回给Update节点。值得注意的是虽然修改数据的节点可能须要至关长的运行时间(在这里它消耗了大部分的时间),规划器却没有再评估时间中添加任何消耗,这是由于更新工做对于任何查询规划都是同样的,因此并不影响规划器的决策。
 
EXPLAIN ANALYZE的"Total runtime"包括执行启动和关闭时间,以及运行被激发的任何处触发器的时间,但不包括分析、重写或规划时间。执行时间包括BEFORE触发器,但不包括AFTER触发器,由于AFTER是在查询运行结束以后才触发的。每一个触发器(不管BEFORE仍是AFTER)的时间也会单独显示出来。注意,延迟的触发器在事务结束前都不会被执行,因此EXPLAIN ANALYZE不会显示。
相关文章
相关标签/搜索