查询执行器介绍
基本概念
SQL 执行器是 DRDS Server 中执行逻辑层算子的组件。对于简单点查 SQL,往往可以整体下推存储层 MySQL 执行,因而感觉不到执行器的存在——MySQL 的结果经过简单的解包封包又被回传给用户。但是对于较复杂的 SQL,往往无法将SQL 中的算子全部下推,这时候就需要 DRDS 执行器做完“无法下推”的那部分计算。
例如,对于以下查询 SQL:
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;
通过 EXPLAIN 命令看到 DRDS 的执行计划:
HashAgg(group="l_orderkey", revenue="SUM(*)")
HashJoin(condition="o_custkey = c_custkey", type="inner")
Gather(concurrent=true)
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` > ?))")
Gather(concurrent=true)
LogicalView(tables="CUSTOMER_[0-7]", shardCount=8, sql="SELECT `c_custkey` FROM `CUSTOMER` AS `CUSTOMER` WHERE (`c_mktsegment` = ?)")
如下图所示:执行时,LogicalView 的 SQL 被下发给 MySQL,而不能下推的部分(除 LogicalView 以外的算子)由 DRDS 执行器进行计算,得到最终用户 SQL 需要的结果。
Volcano 执行模型
DRDS 和很多数据库一样采用 Volcano 执行模型。所有算子都定义了 open()
、next()
等接口,算子根据执行计划组合成一棵算子树,上层算子通过调用下层算子的 next()
接口的取出结果,完成该算子的计算。最终顶层算子产生用户需要的结果并返回给客户端。
下面的例子中,假设 HashJoin 算子已经完成构建哈希表,之后,当上层的 Project 算子请求数据时,HashJoin 首先向下层 Gather 请求一批数据,然后查表得到 Join 结果,再返回给 Project 算子。
一些情况下,算子需要将数据全部读取并缓存在内存中,该过程被称为“物化”,例如,HashJoin 算子需要读取内表的全部数据,并在内存中构建出哈希表。其他类似的算子还有 HashAgg(聚合)、MemSort(排序)等。
内存资源是有限的,如果物化的数据量超出单条查询限制,或者使用的总内存超出 DRDS 节点内存限制,将会引起内存不足(OUT_OF_MEMORY)报错。
并行查询
并行查询(Parallel Query) 指利用多线程并行执行用户的复杂查询。该功能仅在 DRDS 标准版及企业版上提供,入门版由于硬件规格限制,不提供该项功能。
并行查询的执行计划相比原来有所改动,例如,还是上面的查询为例,它的并行执行计划如下,
Gather(parallel=true)
ParallelHashAgg(group="o_orderdate,o_shippriority,l_orderkey", revenue="SUM(*)")
ParallelHashJoin(condition="o_custkey = c_custkey", type="inner")
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)
LogicalView(tables="CUSTOMER_[0-7]", shardCount=8, sql="SELECT `c_custkey` FROM `CUSTOMER` AS `CUSTOMER` WHERE (`c_mktsegment` = ?)", parallel=true)
可以看出,并行执行计划中 Gather 算子的位置被拉高了,这也意味者 Gather 下方的算子都会以并行方式执行,直到 Gather 时才被汇总成在一起。
执行时,Gather 下方的算子会实例化出多个执行实例,分别对应一个并行度。并行度默认等于单台机器的核心数,标准版实例默认并行度为 8,企业版实例默认并行度为 16。
执行过程的诊断分析
除了上文提到的 EXPLAIN 指令,还有几个指令能帮助我们分析性能问题:
EXPLAIN ANALYZE
指令用于分析 DRDS Server 中各算子执行的性能指标EXPLAIN EXECUTE
指令用于输出 MySQL 的 EXPLAIN 结果(并汇总输出)
以上文提到的查询为例,我们演示一下如何分析一条查询的性能问题。
执行 EXPLAIN ANALYZE
得到以下输出(去掉了一些无关的信息):
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;
HashAgg(group="o_orderdate,o_shippriority,l_orderkey", revenue="SUM(*)")
... actual time = 23.916 + 0.000, actual rowcount = 11479, actual memory = 1048576, instances = 1 ...
HashJoin(condition="o_custkey = c_custkey", type="inner")
... actual time = 0.290 + 23.584, actual rowcount = 30266, actual memory = 1048576, instances = 1 ...
Gather(concurrent=true)
... actual time = 0.000 + 23.556, actual rowcount = 151186, actual memory = 0, instances = 1 ...
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` > ?))")
... actual time = 0.000 + 23.556, actual rowcount = 151186, actual memory = 0, instances = 4 ...
Gather(concurrent=true)
... actual time = 0.000 + 0.282, actual rowcount = 29752, actual memory = 0, instances = 1 ...
LogicalView(tables="CUSTOMER_[0-7]", shardCount=8, sql="SELECT `c_custkey` FROM `CUSTOMER` AS `CUSTOMER` WHERE (`c_mktsegment` = ?)")
... 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 结果:
不难看出,上图中,红色方框对应左边的 LogicalView 的下推查询,蓝色方框对应右边 LogicalView 的下推查询。
版权声明
本文仅代表作者观点,不代表本站立场。
本文系作者授权发表,未经许可,不得转载。
评论