19. ClustrixDB 执行计划解读

 

 

EXPLAIN语句用于显示ClustrixDB查询优化器(也称为Sierra)如何执行INSERT、SELECT、UPDATE和DELETE语句。EXPLAIN的输出有三列:

  1. Operation - 完成一项任务的内部操作员
  2. Est. Cost - 估计成本是与执行操作所需的挂钟时间成比例的度量
  3. Est. Rows - Sierra认为操作员将输出的估计行数

它们将执行计划描述为实现声明性SQL语句的物理计划。在大多数情况下,EXPLAIN的输出中的每一行都代表一个单独的操作,该操作收集输入,或者处理在接下来的行中缩进一级的输入。换句话说,大多数explain语句可以在缩进程度最高的语句首先执行,而整个执行过程则是缩进程度最低的语句。

 

它们将执行计划描述为实现声明性SQL语句的物理计划。在大多数情况下,EXPLAIN的输出中的每一行都代表一个单独的操作,该操作收集输入,或者处理在接下来的行中缩进一级的输入。换句话说,大多数explain语句可以在缩进程度最高的语句首先执行,而整个执行过程则是缩进程度最低的语句。

 

创建数据

为了演示EXPLAIN输出,我们将通过一个定义和使用数据库来跟踪客户和销售给他们的产品的练习。此示例仅用于说明,并不一定是为应用程序设计数据的好方法——应用程序的良好数据模型将取决于您的业务需求和使用模式。这个数据模型关注的是关系,而不是完整的数据一致性模型。

我们将从这个基本数据模型开始。(下载这里使用的脚本。)

sql> CREATE TABLE customers (
         c_id INTEGER AUTO_INCREMENT
       , name VARCHAR(100) 
       , address VARCHAR(100)
       , city VARCHAR(50)
       , state CHAR(2)
       , zip CHAR(10)
       , PRIMARY KEY c_pk (c_id)
      ) /*$ SLICES=3 */;
sql> CREATE TABLE products (
         p_id INTEGER AUTO_INCREMENT
       , name VARCHAR(100)
       , price DECIMAL(5,2)
       , PRIMARY KEY p_pk (p_id)
      ) /*$ SLICES=3 */;
sql> CREATE TABLE orders (
         o_id INTEGER AUTO_INCREMENT
       , c_id INTEGER
       , created_on DATETIME
       , PRIMARY KEY o_pk (o_id)
       , KEY c_fk (c_id)
       , CONSTRAINT FOREIGN KEY c_fk (c_id) REFERENCES customers (c_id)
      ) /*$ SLICES=3 */;
sql> CREATE TABLE order_items (
         oi_id INTEGER AUTO_INCREMENT
       , o_id INTEGER
       , p_id INTEGER
       , PRIMARY KEY oi_pk (oi_id)
       , KEY o_fk (o_id)
       , KEY p_fk (p_id)
       , CONSTRAINT FOREIGN KEY order_fk (o_id) REFERENCES orders (o_id)
       , CONSTRAINT FOREIGN KEY product_fk (p_id) REFERENCES products (p_id)
      )  /*$ SLICES=3 */;

After populating the database, there are 1,000 customers, 100 products, 4,000 orders and around 10,000 order_items.

 

查看执行计划

让我们从一个简单的查询开始,它为我们提供了关于所有客户的所有信息。

sql> EXPLAIN SELECT * FROM customers; 
+------------------------------------------------------+-----------+-----------+
| Operation                                            | Est. Cost | Est. Rows |
+------------------------------------------------------+-----------+-----------+
| stream_combine                                       |    712.70 |   1000.00 |
|   index_scan 1 := customers.__idx_customers__PRIMARY |    203.90 |    333.33 |
+------------------------------------------------------+-----------+-----------+

通常,可以先从最里面的缩进读取explain输出,然后按照自己的方式向下缩进,最后在输出的第一行结束。读取上面的解释输出后,首先发生的事情是对索引客户执行index_scan操作。主键索引,并将名称“1”分配给读取的结果。在本例中,不再使用该名称。请注意,尽管关系中有1,000个客户,但估计行数大约是333。这是因为每个index_scan读取的是分布在集群中的数据子集,我们称之为片。在这个关系的模式中,有三个片,所以三个index_scan操作并行运行,以收集客户信息。三个index_scan操作的输出被传递给stream_combine操作符,顾名思义,该操作符将把流组合成一个流,以便将其传递给客户机。stream_combine操作符的工作方式是简单地将第一个输入的全部内容复制到单个流输出中,然后继续操作,直到所有流都被合并。

让我们看看如果向查询添加限制会发生什么。

sql>  EXPLAIN SELECT * FROM customers LIMIT 10;  
+----------------------------------------------------------+-----------+-----------+
| Operation                                                | Est. Cost | Est. Rows |
+----------------------------------------------------------+-----------+-----------+
| row_limit LIMIT := param(0)                              |    615.70 |     10.00 |
|   stream_combine                                         |    615.70 |     30.00 |
|     row_limit LIMIT := param(0)                          |    203.90 |     10.00 |
|       index_scan 1 := customers.__idx_customers__PRIMARY |    203.90 |    333.33 |
+----------------------------------------------------------+-----------+-----------+

这里的执行计划与前面添加row_limit操作符时的执行计划基本相同。row_limit操作符接收输入流,并在满足限制(和偏移量)后关闭输入流。由于有三个并行流,Sierra将row_limit操作符的副本“下推”到每个index_scan流,因为不需要从每个片读取超过10行。合并流之后,我们再次限制输出,以便客户机获得请求的10行。

假设我们想对结果进行排序。

sql> EXPLAIN SELECT * FROM customers ORDER BY c_id;
+------------------------------------------------------+-----------+-----------+
| Operation                                            | Est. Cost | Est. Rows |
+------------------------------------------------------+-----------+-----------+
| stream_merge KEYS=[(1 . "c_id") ASC]                 |    816.70 |   1000.00 |
|   index_scan 1 := customers.__idx_customers__PRIMARY |    203.90 |    333.33 |
+------------------------------------------------------+-----------+-----------+

这个计划类似于未排序的版本,除了这次有一个stream_merge来合并结果,而不是stream_combine。stream_merge操作符的工作方式是根据所提供的顺序将所有传入流中的下一行拉入输出流。在本例中,c_id列的顺序是升序的,因此stream_merge将弹出所有流中比较最小的行。

在ClustrixDB集群中,数据通常是散列分布在各个节点上的,由于stream_combine返回最先到达的数据,因此结果可能与没有分布的数据库不同,并且总是按顺序读取数据。例如:

sql> SELECT * FROM customers LIMIT 10; 
+------+---------------------+--------------+-------------+-------+-------+
| c_id | name                | address      | city        | state | zip   |
+------+---------------------+--------------+-------------+-------+-------+
|    1 | Chanda Nordahl      | 4280 Maple   | Greenville  | WA    | 98903 |
|    2 | Dorinda Tomaselli   | 8491 Oak     | Centerville | OR    | 97520 |
|    9 | Minerva Donnell     | 4644 1st St. | Springfield | WA    | 98613 |
|   21 | Chanda Nordahl      | 5090 1st St. | Fairview    | OR    | 97520 |
|    4 | Dorinda Hougland    | 8511 Pine    | Springfield | OR    | 97477 |
|    6 | Zackary Velasco     | 6296 Oak     | Springfield | OR    | 97286 |
|   11 | Tennie Soden        | 7924 Maple   | Centerville | OR    | 97477 |
|    3 | Shawnee Soden       | 4096 Maple   | Ashland     | WA    | 98035 |
|   24 | Riley Soden         | 7470 1st St. | Greenville  | WA    | 98613 |
|   12 | Kathaleen Tomaselli | 8926 Maple   | Centerville | OR    | 97477 |
+------+---------------------+--------------+-------------+-------+-------+

重复这个查询可能会得到不同的结果。通过在语句中添加ORDER By子句,我们可以确保得到一致的结果。为了使事情更有趣,我们还将更改从升序到降序的顺序。

sql> EXPLAIN SELECT * FROM customers ORDER BY c_id DESC LIMIT 10; 
+------------------------------------------------------------------+-----------+-----------+
| Operation                                                        | Est. Cost | Est. Rows |
+------------------------------------------------------------------+-----------+-----------+
| row_limit LIMIT := param(0)                                      |    622.70 |     10.00 |
|   stream_merge KEYS=[(1 . "c_id") DSC]                           |    622.70 |     30.00 |
|     row_limit LIMIT := param(0)                                  |    203.90 |     10.00 |
|       index_scan 1 := customers.__idx_customers__PRIMARY REVERSE |    203.90 |    333.33 |
+------------------------------------------------------------------+-----------+-----------+

我们可以看到从这个执行计划,并行数据库将第一次读到主索引和在所有可用的反向片index_scan运营商停止阅读10行发现使用row_limit操作符后,合并这些溪流通过选择最大价值从每个流c_id stream_merge算子,最后限制,通过重复应用10行row_limit算子。

 

JOIN执行计划

到目前为止,我们一直在研究单个关系读取。Sierra的工作之一是比较不同连接顺序的成本,并选择成本最低的计划。此查询将为order_items中的每一行生成订单id、产品名称和价格。

       
1.   | sql> EXPLAIN SELECT o_id, name, price FROM orders o NATURAL JOIN order_items NATURAL JOIN products;                         
2.   +-------------------------------------------------------------------------------+-----------+-----------+
3.   | Operation                                                                     | Est. Cost | Est. Rows |
4.   +-------------------------------------------------------------------------------+-----------+-----------+
5.   | nljoin                                                                        |  95339.90 |   9882.00 |
6.   |   nljoin                                                                      |  50870.90 |   9882.00 |
7.   |     stream_combine                                                            |     82.70 |    100.00 |
8.   |       index_scan 3 := products.__idx_products__PRIMARY                        |     23.90 |     33.33 |
9.   |     nljoin                                                                    |    507.88 |     98.82 |
10. |       index_scan 2 := order_items.p_fk, p_id = 3.p_id                         |     63.19 |     98.82 |
11. |       index_scan 2 := order_items.__idx_order_items__PRIMARY, oi_id = 2.oi_id |      4.50 |      1.00 |
12. |   index_scan 1 := orders.__idx_orders__PRIMARY, o_id = 2.o_id                 |      4.50 |      1.00 |
13. +-------------------------------------------------------------------------------+-----------+-----------+

这个计划稍微复杂一点,需要更多一点的解释来看看发生了什么。

  1. 给定缩进,我们可以推断index_scan将首先发生。在解释的输出,我们可以看到p_id index_scan中发现产品的主键的8号线时使用阅读p_fk指数order_items oi_id是10号线时使用阅读第11行order_items主键索引。实际上,产品是通过stream_combine操作符收集的,而order_items信息是通过order_items的nljoin收集的。p_fk和order_items主键索引。
  2. nljoin操作符是一个嵌套循环连接,它实现了关系等连接。
  3. 产品stream_combine和order_items nljoin的输出然后在另一个nljoin中联接。
  4. order_items。o_id用于读取订单,所有结果都放在最后的nljoin中。

查看最终nljoin中的估计行让我们知道,在这个特定的数据集中,Sierra认为大约有9882个order_items行。

Stage Operation Lookup/Scan representation Lookup/Scan Key Run on Node
1 Lookup and Forward __idx_products__PRIMARY none (all nodes with slices) The node where the query begins
 
2.1 Index Scan __idx_products__PRIMARY None, all rows Nodes with slices of __idx_products__PRIMARY
2.2 Lookup and Forward p_fk p_id = 3.p_id same
 
3.1 Index Scan p_fk p_id = 3.p_id Nodes with slices of p_fk
3.2 Join     same
3.3 Lookup and Forward __idx_order_items__PRIMARY oi_id = 2.oi_id same
 
4.1 Index Scan __idx_order_items__PRIMARY oi_id = 2.oi_id Nodes with slices of __idx_order_items__PRIMARY
4.2  Join     same
4.3  Lookup and Forward __idx_orders__PRIMARY o_id = 2.o_id same
 
5.1 Index Scan __idx_orders__PRIMARY o_id = 2.o_id Nodes with slices of __idx_orders__PRIMARY
5.2 Join      
5.3 Lookup and Forward GTM none - single GTM node  
 
6 Return to user     The node where the query began

 

ClustrixDB使用两阶段锁定(2PL)作为并发控制来保证可串行化。在事务中,Sierra将为写操作和更新操作计划锁。首先,我们将研究一个简单的更新,它在大于10时将price的值增加1。

sql> EXPLAIN UPDATE products SET price = price + 1 WHERE price > 10;
+-------------------------------------------------------------------------+-----------+-----------+
| Operation                                                               | Est. Cost | Est. Rows |
+-------------------------------------------------------------------------+-----------+-----------+
| table_update products                                                   |   1211.58 |     81.00 |
|   compute expr0 := (1.price + param(0))                                 |   1211.58 |     81.00 |
|     filter (1.price > param(1))                                         |   1210.50 |     81.00 |
|       nljoin                                                            |   1208.70 |     90.00 |
|         pk_lock "products" exclusive                                    |    803.70 |     90.00 |
|           stream_combine                                                |     83.70 |     90.00 |
|             filter (1.price > param(1))                                 |     24.57 |     30.00 |
|               index_scan 1 := products.__idx_products__PRIMARY          |     23.90 |     33.33 |
|         index_scan 1 := products.__idx_products__PRIMARY, p_id = 1.p_id |      4.50 |      1.00 |
+-------------------------------------------------------------------------+-----------+-----------+

在此查询计划中:

  1. 我们使用index_scan读取产品主键索引,并将输出发送到“下推”过滤器,该过滤器丢弃每片价格不大于10的行。
  2. 然后,将这些输出与stream_combine组合在一起,并将该流分布在整个集群中,使用pk_lock操作符对找到的行获取独占主键锁。
  3. 然后,我们可以使用找到的p_id并使用另一个index_scan读取主键索引。
  4. 由于在第一个index_scan中找到的行可能在读取该行并获取锁后发生了价格变化,所以将再次应用过滤器。
  5. 匹配的行被发送给计算price的新值的计算操作符,而新行被发送给写入新值的table_update操作符。

在某些情况下,为每一行修改单独的行锁比简单地获取单个表锁并修改所有符合条件的行要昂贵得多。Sierra优化器将考虑在计划探索期间使用表锁而不是行锁,并选择成本最低的计划。在本例中,100行通常太小,不需要使用表锁,但是如果Sierra选择使用表锁,则计划如下所示。

sql> EXPLAIN UPDATE products SET price = price + 1; 
+------------------------------------------------------------+-----------+-----------+
| Operation                                                  | Est. Cost | Est. Rows |
+------------------------------------------------------------+-----------+-----------+
| table_locks 1                                              |   8084.03 |    100.00 |
|   table_update products                                    |     84.03 |    100.00 |
|     stream_combine                                         |     84.03 |    100.00 |
|       compute expr0 := (1.price + param(0))                |     24.34 |     33.33 |
|         table_lock "products" exclusive                    |     23.90 |     33.33 |
|           index_scan 1 := products.__idx_products__PRIMARY |     23.90 |     33.33 |
+------------------------------------------------------------+-----------+-----------+

有趣的是,index_scan看起来像是table_lock的输入。情况并非如此,因为表锁将在读取之前获得。有了这一点,我们可以看到计划:

  1. 用index_scan读取关系中的所有行。
  2. 使用compute将价格增加1。
  3. 使用stream_combine将这些结果合并到单个流中。
  4. 将输出发送到table_update以写入新值。

table_lock操作符是Sierra的一个辅助操作符,它具有一种启发式的方法,可以在其他更新被阻塞的情况下,平衡相对便宜的单锁,从而在该事务期间消耗壁钟时间。

 

使用索引来提高性能

到目前为止,我们只检查了读取主键索引来获得结果。我们可以通过添加对给定工作负载有意义的索引来改变这一点。例如,假设我们有一个业务流程,如果我们将客户信息按邮政编码排序并合并成小块,那么该业务流程的工作效果会更好。要获得这些信息:

sql> EXPLAIN SELECT name, address, city, state, zip FROM customers ORDER BY zip LIMIT 10 OFFSET 0;
+----------------------------------------------------------+-----------+-----------+
| Operation                                                | Est. Cost | Est. Rows |
+----------------------------------------------------------+-----------+-----------+
| row_limit LIMIT := param(0)                              |   2632.70 |     10.00 |
|   sigma_sort KEYS=[(1 . "zip") ASC]                      |   2632.70 |   1000.00 |
|     stream_combine                                       |    712.70 |   1000.00 |
|       index_scan 1 := customers.__idx_customers__PRIMARY |    203.90 |    333.33 |
+----------------------------------------------------------+-----------+-----------+

它读取主键索引,组合结果,然后将这些行发送给sigma_sort操作符。sigma_sort操作符根据需要在内存或存储中构建一个临时容器,以便对邮政编码找到的行进行排序。一旦所有的结果都被排序,它们就会被传递给row_limit操作符来执行限制和偏移量。

如果我们按顺序读取邮政编码,而不是读取所有的行,对邮政编码进行排序,然后返回下一批10行,那么我们可以显著提高这里的性能。为此,我们在customers.zip上添加一个索引,并查看Sierra如何更改执行计划。

sql> ALTER TABLE customers ADD INDEX (zip); 
sql> EXPLAIN SELECT name, address, city, state, zip FROM customers ORDER BY zip LIMIT 10 OFFSET 0;
+---------------------------------------------------------------------+-----------+-----------+
| Operation                                                           | Est. Cost | Est. Rows |
+---------------------------------------------------------------------+-----------+-----------+
| msjoin KEYS=[(1 . "zip") ASC]                                       |    674.70 |     10.00 |
|   row_limit LIMIT := param(0)                                       |    622.70 |     10.00 |
|     stream_merge KEYS=[(1 . "zip") ASC]                             |    622.70 |     30.00 |
|       row_limit LIMIT := param(0)                                   |    203.90 |     10.00 |
|         index_scan 1 := customers.zip                               |    203.90 |    333.33 |
|   index_scan 1 := customers.__idx_customers__PRIMARY, c_id = 1.c_id |      4.50 |      1.00 |
+---------------------------------------------------------------------+-----------+-----------+ 

这里,查询优化器选择:

  1. 使用index_scan操作符并行读取所有片上的custom.zip索引。
  2. 使用“下推”row_limit操作符限制结果。
  3. 合并这些结果并使用stream_merge操作符保持顺序。
  4. 使用另一个row_limit限制合并的结果。
  5. 使用zip索引中找到的c_id来读取该行的其余部分。
  6. 使用msjoin操作符执行等连接。

msjoin操作符是一个“合并排序嵌套循环连接”,它类似于nljoin,但在连接期间保留排序顺序。请注意,在此计划中,zip索引的排序顺序将被读取,并在整个计划中始终保持不变,从而消除了创建sigma容器来对结果进行排序的需要。换句话说,这个计划在执行过程中将所有结果流化,这在读取数百万行数据时是一个重要的考虑因素。

 

 

聚合

在使用关系数据库时,另一个常见的任务是筛选大数据来计算总和、平均值、最小值或最大值。这些查询是通过向语句中添加一个GROUP by子句来执行的,该子句声明希望如何聚合数据。ClustrixDB还实现了对GROUP BY的MySQL扩展,允许在输出列中包含非聚合列。如果GROUP BY columns和非聚合列之间没有一对一的关系,那么非聚合列的值将是该行中的一个值,不过没有定义返回哪个值。因为我们的数据中有zip和state之间的一对一映射,所以我们可以生成一个结果集来为我们生成映射。

sql> EXPLAIN SELECT zip, state FROM customers GROUP BY zip; 
+--------------------------------------------------------+-----------+-----------+
| Operation                                              | Est. Cost | Est. Rows |
+--------------------------------------------------------+-----------+-----------+
| sigma_distinct_combine KEYS=((1 . "zip"))              |   1303.90 |   1000.00 |
|   sigma_distinct_partial KEYS=((1 . "zip"))            |    203.90 |   1000.00 |
|     index_scan 1 := customers.__idx_customers__PRIMARY |    203.90 |    333.33 |
+--------------------------------------------------------+-----------+-----------+

这个查询将:

  1. 首先,执行index_scan并将输出发送给sigma_distinct_partial操作符。
  2. sigma_distinct_partial操作符,它在读取同一节点上的键的不同值上产生一行输出。
  3. 然后将这些不同的值发送给sigma_distinct_combine操作符,该操作符将对发起查询的节点上的键执行相同的不同操作。

对于更实际的聚合,让我们假设我们想要查找每个客户下了多少订单以及该客户的姓名。

sql> EXPLAIN SELECT c.name, COUNT(*) FROM orders o NATURAL JOIN customers c GROUP BY o.c_id; 
+-------------------------------------------------------------------------------+-----------+-----------+
| Operation                                                                     | Est. Cost | Est. Rows |
+-------------------------------------------------------------------------------+-----------+-----------+
| hash_aggregate_combine GROUPBY((1 . "c_id")) expr1 := countsum((0 . "expr1")) |  12780.38 |   4056.80 |
|   hash_aggregate_partial GROUPBY((1 . "c_id")) expr1 := count((0 . "expr0"))  |   7100.87 |   4056.80 |
|     compute expr0 := param(0)                                                 |   7100.87 |   4056.80 |
|       nljoin                                                                  |   7046.78 |   4056.80 |
|         stream_combine                                                        |    712.70 |   1000.00 |
|           index_scan 2 := customers.__idx_customers__PRIMARY                  |    203.90 |    333.33 |
|         index_scan 1 := orders.c_fk, c_id = 2.c_id                            |      6.33 |      4.06 |
+-------------------------------------------------------------------------------+-----------+-----------+

在这个计划:

  1. 首先是客户主键的index_scan并与stream_combine组合,然后使用c_id读取订单。带有另一个index_scan的c_fk索引。
  2. 这些结果在我们读取订单的节点上连接。使用nljoin操作符建立c_fk索引,并使用同一个节点上的hash_aggregate_partial操作符进行分组和计数。
  3. 然后,将结果发送到原始节点上的hash_aggregate_combine操作符,以获得最终的组和计数,然后再将行返回给用户。

 

总结

希望这是对ClustrixDB Sierra查询优化器的解释输出的充分介绍,您可以使用该优化器检查您自己的查询。有关可能出现在EXPLAIN中的操作符的完整列表,请参考Planner操作符列表。有关Sierra如何进行查询优化的更多信息,请参见分布式数据库架构中的查询优化器。

 

猜你喜欢

转载自www.cnblogs.com/yuxiaohao/p/11984494.html
今日推荐