全部产品
云市场

查询执行器介绍

更新时间:2019-11-20 17:19:50

基本概念

SQL 执行器是 DRDS Server 中执行逻辑层算子的组件。对于简单点查 SQL,往往可以整体下推存储层 MySQL 执行,因而感觉不到执行器的存在——MySQL 的结果经过简单的解包封包又被回传给用户。但是对于较复杂的 SQL,往往无法将SQL 中的算子全部下推,这时候就需要 DRDS 执行器做完“无法下推”的那部分计算。

例如,对于以下查询 SQL:

  1. SELECT l_orderkey, sum(l_extendedprice *(1 - l_discount)) AS revenue
  2. FROM CUSTOMER, ORDERS, LINEITEM
  3. WHERE c_mktsegment = 'AUTOMOBILE'
  4. and c_custkey = o_custkey
  5. and l_orderkey = o_orderkey
  6. and o_orderdate < '1995-03-13'
  7. and l_shipdate > '1995-03-13'
  8. GROUP BY l_orderkey;

通过 EXPLAIN 命令看到 DRDS 的执行计划:

  1. HashAgg(group="l_orderkey", revenue="SUM(*)")
  2. HashJoin(condition="o_custkey = c_custkey", type="inner")
  3. Gather(concurrent=true)
  4. LogicalView(tables="ORDERS_[0-7],LINEITEM_[0-7]", shardCount=8, sql="SELECT `ORDERS`.`o_custkey`, `LINEITEM`.`l_orderkey`, (`LINEITEM`.`l_extendedprice` * (? - `LINEITEM`.`l_discount`)) AS `x` FROM `ORDERS` AS `ORDERS` INNER JOIN `LINEITEM` AS `LINEITEM` ON (((`ORDERS`.`o_orderkey` = `LINEITEM`.`l_orderkey`) AND (`ORDERS`.`o_orderdate` < ?)) AND (`LINEITEM`.`l_shipdate` > ?))")
  5. Gather(concurrent=true)
  6. LogicalView(tables="CUSTOMER_[0-7]", shardCount=8, sql="SELECT `c_custkey` FROM `CUSTOMER` AS `CUSTOMER` WHERE (`c_mktsegment` = ?)")

如下图所示:执行时,LogicalView 的 SQL 被下发给 MySQL,而不能下推的部分(除 LogicalView 以外的算子)由 DRDS 执行器进行计算,得到最终用户 SQL 需要的结果。

x

Volcano 执行模型

DRDS 和很多数据库一样采用 Volcano 执行模型。所有算子都定义了 open()next() 等接口,算子根据执行计划组合成一棵算子树,上层算子通过调用下层算子的 next() 接口的取出结果,完成该算子的计算。最终顶层算子产生用户需要的结果并返回给客户端。

下面的例子中,假设 HashJoin 算子已经完成构建哈希表,之后,当上层的 Project 算子请求数据时,HashJoin 首先向下层 Gather 请求一批数据,然后查表得到 Join 结果,再返回给 Project 算子。

x

一些情况下,算子需要将数据全部读取并缓存在内存中,该过程被称为“物化”,例如,HashJoin 算子需要读取内表的全部数据,并在内存中构建出哈希表。其他类似的算子还有 HashAgg(聚合)、MemSort(排序)等。

内存资源是有限的,如果物化的数据量超出单条查询限制,或者使用的总内存超出 DRDS 节点内存限制,将会引起内存不足(OUT_OF_MEMORY)报错。

并行查询

并行查询(Parallel Query) 指利用多线程并行执行用户的复杂查询。该功能仅在 DRDS 标准版及企业版上提供,入门版由于硬件规格限制,不提供该项功能。

并行查询的执行计划相比原来有所改动,例如,还是上面的查询为例,它的并行执行计划如下,

  1. Gather(parallel=true)
  2. ParallelHashAgg(group="o_orderdate,o_shippriority,l_orderkey", revenue="SUM(*)")
  3. ParallelHashJoin(condition="o_custkey = c_custkey", type="inner")
  4. LogicalView(tables="ORDERS_[0-7],LINEITEM_[0-7]", shardCount=8, sql="SELECT `ORDERS`.`o_custkey`, `ORDERS`.`o_orderdate`, `ORDERS`.`o_shippriority`, `LINEITEM`.`l_orderkey`, (`LINEITEM`.`l_extendedprice` * (? - `LINEITEM`.`l_discount`)) AS `x` FROM `ORDERS` AS `ORDERS` INNER JOIN `LINEITEM` AS `LINEITEM` ON (((`ORDERS`.`o_orderkey` = `LINEITEM`.`l_orderkey`) AND (`ORDERS`.`o_orderdate` < ?)) AND (`LINEITEM`.`l_shipdate` > ?))", parallel=true)
  5. LogicalView(tables="CUSTOMER_[0-7]", shardCount=8, sql="SELECT `c_custkey` FROM `CUSTOMER` AS `CUSTOMER` WHERE (`c_mktsegment` = ?)", parallel=true)

x

可以看出,并行执行计划中 Gather 算子的位置被拉高了,这也意味者 Gather 下方的算子都会以并行方式执行,直到 Gather 时才被汇总成在一起。

执行时,Gather 下方的算子会实例化出多个执行实例,分别对应一个并行度。并行度默认等于单台机器的核心数,标准版实例默认并行度为 8,企业版实例默认并行度为 16。

执行过程的诊断分析

除了上文提到的 EXPLAIN 指令,还有几个指令能帮助我们分析性能问题:

  • EXPLAIN ANALYZE 指令用于分析 DRDS Server 中各算子执行的性能指标
  • EXPLAIN EXECUTE 指令用于输出 MySQL 的 EXPLAIN 结果(并汇总输出)

以上文提到的查询为例,我们演示一下如何分析一条查询的性能问题。

执行 EXPLAIN ANALYZE 得到以下输出(去掉了一些无关的信息):

  1. explain analyze select l_orderkey, sum(l_extendedprice *(1 - l_discount)) as revenue from CUSTOMER, ORDERS, LINEITEM where c_mktsegment = 'AUTOMOBILE' and c_custkey = o_custkey and l_orderkey = o_orderkey and o_orderdate < '1995-03-13' and l_shipdate > '1995-03-13' group by l_orderkey;
  2. HashAgg(group="o_orderdate,o_shippriority,l_orderkey", revenue="SUM(*)")
  3. ... actual time = 23.916 + 0.000, actual rowcount = 11479, actual memory = 1048576, instances = 1 ...
  4. HashJoin(condition="o_custkey = c_custkey", type="inner")
  5. ... actual time = 0.290 + 23.584, actual rowcount = 30266, actual memory = 1048576, instances = 1 ...
  6. Gather(concurrent=true)
  7. ... actual time = 0.000 + 23.556, actual rowcount = 151186, actual memory = 0, instances = 1 ...
  8. LogicalView(tables="ORDERS_[0-7],LINEITEM_[0-7]", shardCount=8, sql="SELECT `ORDERS`.`o_custkey`, `ORDERS`.`o_orderdate`, `ORDERS`.`o_shippriority`, `LINEITEM`.`l_orderkey`, (`LINEITEM`.`l_extendedprice` * (? - `LINEITEM`.`l_discount`)) AS `x` FROM `ORDERS` AS `ORDERS` INNER JOIN `LINEITEM` AS `LINEITEM` ON (((`ORDERS`.`o_orderkey` = `LINEITEM`.`l_orderkey`) AND (`ORDERS`.`o_orderdate` < ?)) AND (`LINEITEM`.`l_shipdate` > ?))")
  9. ... actual time = 0.000 + 23.556, actual rowcount = 151186, actual memory = 0, instances = 4 ...
  10. Gather(concurrent=true)
  11. ... actual time = 0.000 + 0.282, actual rowcount = 29752, actual memory = 0, instances = 1 ...
  12. LogicalView(tables="CUSTOMER_[0-7]", shardCount=8, sql="SELECT `c_custkey` FROM `CUSTOMER` AS `CUSTOMER` WHERE (`c_mktsegment` = ?)")
  13. ... actual time = 0.000 + 0.282, actual rowcount = 29752, actual memory = 0, instances = 4 ...

其中:

  • actual time 表示实际执行耗时(其中包含子算子的耗时),+ 左边表示 open (准备数据)耗时,右边表示 next (输出数据)耗时。
  • actual rowcount 表示输出的行数
  • actual memory 表示算子使用的内存空间大小(bytes)
  • instances 表示实例数,非并行查询时始终为 1,对于并行算子每个并行度对应一个实例。如果实例数不等于 1,前几项代表加总的结果。

当使用并行查询时,上述的算子耗时、输出行数等信息均为算子多个实例的加和。例如:actual time = 20,instances = 8,表示该算子有 8 个实例并行执行,平均耗时为 2.5s。

以上面的输出为例,解读如下:

  • HashAgg 算子 open 耗时为 23.916s,用于获取下层 HashJoin 的输出、并对输出的所有数据做分组和聚合。其中的 23.601s 都用在了获取了下层输出上,只有约 0.3s 用于分组聚合。
  • HashJoin 算子 open 耗时 0.290s ,用于拉取右表(下方的 Gather)数据并构建哈希表;next 耗时 23.584s,用于拉取左表数据、查询哈希表得到 Join 结果。
  • Gather 算子仅仅用于汇总多个结果集,通常代价很低,略过
  • 左侧(上方)的 LogicalView 拉取数据消耗了 23.556s,看来这里是查询的性能瓶颈
  • 右侧(下方)的 LogicalView 拉取数据消耗了 0.282s

综上,性能瓶颈在左边的 LogicalView 上。从执行计划中可以看到,它是对 ORDERS、LINEITEM 的 Join 查询,这条查询 MySQL 执行速度较慢。

我们可以通过 EXPLAIN EXECUTE 查看它的 MySQL EXPLAIN 结果:

x

不难看出,上图中,红色方框对应左边的 LogicalView 的下推查询,蓝色方框对应右边 LogicalView 的下推查询。