目录
二、TopN 下推过 Join 的情况(排序规则仅依赖于外表中的列)
概念
谓词下推 Predicate Pushdown(PPD), 顾名思义,就是把过滤算子(就是你在 sql语句里面写的 where语句),尽可能地放在执行计划靠前的地方,好处就是尽早地过滤到不必要的数据,后续流程都节省了计算量,从而优化了性能。
谓词下推概念中的谓词指返回bool值即true和false的函数,或是隐式转换为bool的函数,比如:like ,is null,in,exists,=,!、= 这些返回bool值的函数。
逻辑算子介绍
在写具体的优化规则之前,先简单介绍查询计划里面的一些逻辑算子。
- DataSource 这个就是数据源,也就是表,select * from t 里面的 t。
- Selection 选择,例如 select xxx from t where xx = 5 里面的 where 过滤条件。
- Projection 投影, select c from t 里面的取 c 列是投影操作。
- Join 连接, select xx from t1, t2 where t1.c = t2.c 就是把 t1 t2 两个表做 Join。
select b from t1, t2 where t1.c = t2.c and t1.a > 5
变成逻辑查询计划之后,t1 t2 对应的 DataSource,负责将数据捞上来。上面接个 Join 算子,将两个表的结果按 t1.c = t2.c 连接,再按 t1.a > 5 做一个 Selection 过滤,最后将 b 列投影。下图是未经优化的表示:
- Sort 就是 select xx from xx order by 里面的 order by。
- Aggregation,在 select sum(xx) from xx group by yy 中的 group by 操作,按某些列分组。分组之后,可能带一些聚合函数,比如 Max/Min/Sum/Count/Average 等。
- Apply 这个是用来做子查询的。
为什么要谓词下推
在大数据领域,影响程序性能主要原因并不是数据量的大小,而是数据倾斜,通过谓词下推可以使程序提前过滤部分数据,降低Join等一系列操作的数据量级,尽可能避免因数据倾斜导致程序性能问题。
谓词下推在Join中的应用
那么这两类不同的条件,在外连接查询中是否都会下推呢?不是的,请看下面详细的描述信息。
一、Inner Join 下推情况说明
1. Join Predicate情况如下
inner join的结果集是左表和右表都要满足条件,所以inner join condtion中的条件都是可以下推的,比如下面的查询
SELECT
*
FROM
LEFT_TABLE LT
INNER JOIN RIGHT_TABLE RT ON LT.ID = RT.ID
AND LT.ID = 1
或
SELECT
*
FROM
LEFT_TABLE LT
INNER JOIN RIGHT_TABLE RT ON LT.ID = RT.ID
AND RT.ID = 1
Oracle进行谓词下推后的计划如下图所示
相当于优化后的查询
SELECT
*
FROM
( SELECT * FROM LEFT_TABLE WHERE ID = 1 ) LT
INNER JOIN ( SELECT * FROM RIGHT_TABLE WHERE ID = 1 ) RT ON LT.ID = RT.ID
计划如下图所示
上面可以看到SQL的语义是等价了,并且在Inner Join的Join Predicate的场景下进行了谓词下推。
2.Where Predicate情况如下
inner join的结果集是左表和右表都要满足条件,所以inner join后的条件都是可以下推的,比如下面的查询
SELECT
*
FROM
LEFT_TABLE LT
INNER JOIN RIGHT_TABLE RT ON LT.ID = RT.ID
WHERE
LT.ID = 1
或
SELECT
*
FROM
LEFT_TABLE LT
INNER JOIN RIGHT_TABLE RT ON LT.ID = RT.ID
WHERE
RT.ID = 1
Oracle进行谓词下推后的计划如下图所示
相当于优化后的查询
SELECT
*
FROM
( SELECT * FROM LEFT_TABLE WHERE ID = 1 ) LT
INNER JOIN ( SELECT * FROM RIGHT_TABLE WHERE ID = 1 ) RT ON LT.ID = RT.ID
计划如下图所示
可以看到对于inner join来说,不管是在join中计算的谓词表达式还是在join后计算的谓词表达式都是可以下推的。
二、Left Join下推情况说明
1. Join Predicate情况如下
left join 由于左表是保留表,所有join condition 中的左表条件会失效,右表的条件可以下推。
1.1 Join Predicate左表的情况如下
比如下面的查询
SELECT
*
FROM
LEFT_TABLE LT
LEFT JOIN RIGHT_TABLE RT ON LT.ID = RT.ID
AND LT.ID = 1
计算结果如下:
Oracle的计划如下图所示
通过计划可以看到,Join上面的条件是没有下推的。为什么没有下推我们可以推导一下。
假如下推了,相当于优化的SQL如下
SELECT
*
FROM
( SELECT * FROM LEFT_TABLE WHERE ID = 1 ) LT
LEFT JOIN ( SELECT * FROM RIGHT_TABLE ) RT ON LT.ID = RT.ID
计算结果如下:
可以看到两个SQL执行的结果完全不一致,那么为什么,我们看下LEFT JOIN的语义 LEFT JOIN 关键字会从左表那里返回所有的行,即使在右表中没有匹配的行。如果右表中没有匹配的行则右表补NULL。上面优化后的SQL,左表因为过滤导致左表只输出一行和右表做join,所以导致结果不对。
1.2 Join Predicate右表的情况如下
比如下面的查询
SELECT
*
FROM
LEFT_TABLE LT
LEFT JOIN RIGHT_TABLE RT ON LT.ID = RT.ID
AND RT.ID = 1
计算结果如下:
Oracle的计划如下图所示
通过计划可以看到,Oracle进行了谓词下推优化,把Join上面的条件下推了。
相当于优化的SQL如下
SELECT
*
FROM
( SELECT * FROM LEFT_TABLE ) LT
LEFT JOIN ( SELECT * FROM RIGHT_TABLE WHERE ID = 1) RT ON LT.ID = RT.ID
计算结果如下:
Oracle的计划如下图所示
可以看到两个SQL执行的结果完全一致。
2.Where Predicate情况如下
2.1 Where Predicate左表的情况如下
比如下面的查询
SELECT
*
FROM
LEFT_TABLE LT
LEFT JOIN RIGHT_TABLE RT ON LT.ID = RT.ID
WHERE
LT.ID = 1
计算结果如下:
Oracle的计划如下图所示
通过计划可以看到,Oracle进行了谓词下推优化,把谓词条件下推了。
相当于优化的SQL如下
SELECT
*
FROM
( SELECT * FROM LEFT_TABLE WHERE ID = 1 ) LT
LEFT JOIN ( SELECT * FROM RIGHT_TABLE WHERE ID = 1) RT ON LT.ID = RT.ID
计算结果如下:
Oracle的计划如下图所示
可以看到两个SQL执行的结果完全一致。
2.2 Where Predicate右表的情况如下
比如下面的查询
SELECT
*
FROM
LEFT_TABLE LT
LEFT JOIN RIGHT_TABLE RT ON LT.ID = RT.ID
WHERE
RT.ID = 1
计算结果如下:
Oracle的计划如下图所示
通过计划可以看到,Oracle进行了谓词下推优化,把Join上面的谓词条件下推了。
相当于优化的SQL如下
SELECT
*
FROM
( SELECT * FROM LEFT_TABLE WHERE ID = 1) LT
LEFT JOIN ( SELECT * FROM RIGHT_TABLE WHERE ID = 1) RT ON LT.ID = RT.ID
计算结果如下:
Oracle的计划如下图所示
可以看到两个SQL执行的结果完全一致。
三、Right Join下推情况说明
1. Join Predicate情况如下
RIGHT JOIN 由于右表是保留表,所有join condition 中的右表条件会失效,左表的条件可以下推。
1.1 Join Predicate左表的情况如下
比如下面的查询
SELECT
*
FROM
LEFT_TABLE LT
RIGHT JOIN RIGHT_TABLE RT ON LT.ID = RT.ID
AND LT.ID = 1
计算结果如下:
Oracle的计划如下图所示
通过计划可以看到,Oracle进行了谓词下推优化,把Join上面的条件下推了。
相当于优化的SQL如下
SELECT
*
FROM
( SELECT * FROM LEFT_TABLE WHERE ID = 1 ) LT
RIGHT JOIN ( SELECT * FROM RIGHT_TABLE ) RT ON LT.ID = RT.ID
计算结果如下:
Oracle的计划如下图所示
由此可知,两个SQL的语义是等价的。
1.2 Join Predicate右表的情况如下
比如下面的查询
SELECT
*
FROM
LEFT_TABLE LT
RIGHT JOIN RIGHT_TABLE RT ON LT.ID = RT.ID
AND RT.ID = 1
计算结果如下:
Oracle的计划如下图所示
通过计划可以看到,Join上面的条件是没有下推的。为什么没有下推我们可以推导一下
假如下推了,相当于优化的SQL如下
SELECT
*
FROM
( SELECT * FROM LEFT_TABLE ) LT
RIGHT JOIN ( SELECT * FROM RIGHT_TABLE WHERE ID = 1 ) RT ON LT.ID = RT.ID
计算结果如下:
可以看到两个SQL执行的结果完全不一致,那么为什么,我们看下RIGHT JOIN的语义 RIGHT JOIN 关键字会右表那里返回所有的行,即使在左表中没有匹配的行。如果左表中没有匹配的行则左表补NULL。上面优化后的SQL,右表因为过滤导致右表只输出一行和左表做join,所以导致结果不对。
2.Where Predicate情况如下
2.1 Where Predicate左表的情况如下
比如下面的查询
SELECT
*
FROM
LEFT_TABLE LT
RIGHT JOIN RIGHT_TABLE RT ON LT.ID = RT.ID
WHERE
LT.ID = 1
计算结果如下:
Oracle的计划如下图所示
通过计划可以看到,Oracle进行了谓词下推优化,把谓词条件下推了。
相当于优化的SQL如下
SELECT
*
FROM
( SELECT * FROM LEFT_TABLE WHERE ID = 1 ) LT
RIGHT JOIN ( SELECT * FROM RIGHT_TABLE WHERE ID = 1 ) RT ON LT.ID = RT.ID
计算结果如下:
Oracle的计划如下图所示
可以看到两个SQL执行的结果完全一致。
2.2 Where Predicate右表的情况如下
比如下面的查询
SELECT
*
FROM
LEFT_TABLE LT
RIGHT JOIN RIGHT_TABLE RT ON LT.ID = RT.ID
WHERE
RT.ID = 1
计算结果如下:
Oracle的计划如下图所示
通过计划可以看到,Oracle进行了谓词下推优化。
相当于优化的SQL如下
SELECT
*
FROM
( SELECT * FROM LEFT_TABLE WHERE ID = 1) LT
RIGHT JOIN ( SELECT * FROM RIGHT_TABLE WHERE ID = 1) RT ON LT.ID = RT.ID
计算结果如下:
Oracle的计划如下图所示
可以看到两个SQL执行的结果完全一致。
四、Full Join下推情况说明
语义: 只要其中某个表存在匹配,FULL JOIN 关键字就会返回行。
1. Join Predicate情况如下
1.1 Join Predicate左表的情况如下
比如下面的查询
SELECT
*
FROM
LEFT_TABLE LT
FULL JOIN RIGHT_TABLE RT ON LT.ID = RT.ID
AND LT.ID = 1
计算结果如下:
Oracle的计划如下图所示
通过计划可以看到,Oracle没有做谓词下推优化的优化。我们推导下看Oracle为什么没有做优化。
假如把Join上面的谓词下推了,如下面的SQL
SELECT
*
FROM
( SELECT * FROM LEFT_TABLE WHERE ID = 1 ) LT
FULL JOIN ( SELECT * FROM RIGHT_TABLE ) RT ON LT.ID = RT.ID
计算结果如下:
Oracle的计划如下图所示
可以看到两个SQL执行的结果完全不一致,那么为什么,我们看下FULL JOIN的语义 只要其中某个表存在匹配,FULL JOIN 关键字就会返回行。不能匹配的一侧补充NULL。相当于两个表全输出。当谓词下推后相当于左表只有一行数据和右表做Full Join所以结果不对。
1.2 Join Predicate右表的情况如下
同上
2.Where Predicate情况如下
2.1 Where Predicate左表的情况如下
比如下面的查询
SELECT
*
FROM
LEFT_TABLE LT
FULL JOIN RIGHT_TABLE RT ON LT.ID = RT.ID
WHERE
LT.ID = 1
计算结果如下:
Oracle的计划如下图所示
通过计划可以看到,Oracle做了谓词下推优化的优化。优化后的SQL相当于
SELECT
*
FROM
( SELECT * FROM LEFT_TABLE WHERE ID = 1 ) LT
FULL JOIN ( SELECT * FROM RIGHT_TABLE WHERE ID = 1 ) RT ON LT.ID = RT.ID
计算结果如下:
Oracle的计划如下图所示
两个SQL等价,执行的结果一致。
2.2 Where Predicate右表的情况如下
同上
根据上面的情况总结的表格
Push:谓词下推,可以理解为被优化
Not Push:谓词没有下推,可以理解为没有被优化
谓词下推在Parquet中的应用
Apache Parquet是Hadoop生态圈中一种新型列式存储格式,它可以兼容Hadoop生态圈中大多数计算框架(Mapreduce、Spark等),被多种查询引擎支持(Hive、Impala、Drill等),并且它是语言和平台无关的。Parquet最初是由Twitter和Cloudera合作开发完成并开源,2015年5月从Apache的孵化器里毕业成为Apache顶级项目。
Parquet最初的灵感来自Google于2010年发表的Dremel论文,文中介绍了一种支持嵌套结构的存储格式,并且使用了列式存储的方式提升查询性能。
文件格式如下
在数据访问的过程中,我们可以把谓词条件直接下推给Parquet, Parquet可以利用每一个row group生成的统计信息进行过滤,这部分信息包括该Column Chunk的最大值、最小值和空值个数。通过这些统计值和该列的过滤条件可以判断该Row Group是否需要扫描。另外Parquet未来还会增加诸如Bloom Filter和Index等优化数据,更加有效的完成谓词下推。
聚合下推
SQL 中经常使用聚合函数利用一组输入值来计算单个结果。最常用的聚合函数是 AVG、COUNT、MAX、MIN 和 SUM。可以将这些聚合下推到数据源级别,以提升性能。性能提升主要表现在两个领域:
- 节点之间的网络 IO 显著减少。
- 使用每一个节点的计算能力,从而提高整个集群的计算能力。
一、简单聚合函数下推
比如下面的查询
SELECT SUM(LT.SALARY) FROM LEFT_TABLE_AGG LT;
计算结果如下:
Oracle的计划如下图所示
如果进行聚合下推优化后的SQL相当于
SELECT SUM(LT.SALARY)
FROM (SELECT SUM(SALARY) AS SALARY FROM LEFT_TABLE_AGG) LT;
计算结果如下:
Oracle的计划如下图所示
如果LEFT_TABLE_AGG表特别大,每一个节点上面存储一部分数据,这样可以高效的利用每一个节点的计算性能。并且如果分组列重复值比较多,可以提前聚合在一起,降低了网络IO。
二、Join场景下的聚合函数下推
比如下面的查询
SELECT
AVG( LT.SALARY ),
LT.ID
FROM
LEFT_TABLE_AGG LT
JOIN RIGHT_TABLE_AGG RT ON LT.ID = RT.ID
GROUP BY
LT.ID ;
计算结果如下:
Oracle的计划如下图所示
如果进行聚合下推优化后的SQL相当于
SELECT
AVG(LT.SALARY),
LT.ID
FROM
(SELECT AVG(SALARY) AS SALARY, ID as ID FROM LEFT_TABLE_AGG GROUP BY ID) LT
JOIN RIGHT_TABLE_AGG RT ON LT.ID = RT.ID
GROUP BY
LT.ID ;
计算结果如下:
Oracle的计划如下图所示
两个SQL等价,执行的结果一致。
也不是所有的情况都适合做聚合下推的,比如说当分组Key是唯一值时(group by 后面的列),使用聚合下推相当于多算了一遍聚合函数,从而浪费性能,这种就是基于规则的优化(RBO)所没有办法处理的情况,为了解决这种情况产生了一种优化规则叫做基于代价的优化(CBO)后续会讲。
Join下推
说到Join下推给其他计算节点,就不得不提一个数据结构了,他就是Bloom Filter。
Bloom Filter是一种空间效率很高的随机数据结构,它利用位数组很简洁地表示一个集合,并能判断一个元素是否属于这个集合。Bloom Filter的这种高效是有一定代价的:在判断一个元素是否属于某个集合时,有可能会把不属于这个集合的元素误认为属于这个集合(false positive)。因此,Bloom Filter不适合那些“零错误”的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter通过极少的错误换取了存储空间的极大节省。
Join下推说白了是使用bloomfilter对参与join的表进行过滤,减少实际参与join的数据量。Bloom Filter使用位数组来实现过滤,初始状态下位数组每一位都为 0,如下图所示:
假如此时有一个集合S = {x1,x2,...,xn},Bloom Filter使用k个独立的hash函数,分别将集合中的每一个元素映射到{1,…,m}的范围。对于任何一个元素,被映射到的数字作为对应的位数组的索引,该位会被置为1。比如元素x1被hash函数映射到数字8,那么位数组 的第8位就会被置为1。下图中集合S只有两个元素x和y,分别被3个hash函数进行映射,映射到的位置分别为(1,4,8)和(5,6,10),对 应的位会被置为1:
现在假如要判断另一个元素是否是在此集合中,只需要被这3个hash函数进行映射,查看对应的位置是否有0存在,如果有的话,表 示此元素肯定不存在于这个集合,否则有可能存在。下图所示就表示z肯定不在集合{x,y}中:
为了更好地说明整个过程,这里使用一个SQL示例对Join下推算法进行完整讲解,SQL:select item.name,order.* from order,item where order.item_id = item.id and item.category = ‘book’,其中order为订单表,item为商品表,两张表根据商品id字段 进行join,该SQL意为取出商品类别为书籍的所有订单详情。假设商品类型为书籍的商品并不多,join算法因此确定为broadcast hash join。整个流程如下图所示:
Step 1:将item表的join字段(item.id)经过多个hash函数映射处理为一个bloomfilter;
Step 2:将映射好的bloomfilter分别广播到order表的所有partition上,准备进行过滤;
Step 3:以Partition2为例,存储进程(比如DataNode进程)将order表中join列(order.item_id)数据一条一条读出来,使用 bloomfilter进行过滤。淘汰该订单数据不是书籍相关商品的订单,这条数据直接跳过;否则该条订单数据有可能是待检索订单,将 该行数据全部扫描出来;
Step 4:将所有未被bloomfilter过滤掉的订单数据,通过本地socket通信发送到计算进程(impalad);
Step 5:再将所有书籍商品数据广播到所有Partition节点与step4所得订单数据进行真正的hashjoin操作,得到最终的选择结果。
通过谓词( bloomfilter)下推将一些过滤条件下推到存储进程,直接让存储进程将数据过滤掉。这样的好处显而易见,过滤的越早,数据量越少,序列化开销、网络开销、计算开销这一系列都会减少,性能自然会提高。
Order By Limit下推
在分布式查询中,将 limit 下推到数据源节点往往有更好的性能,因为可以减少数据的返回(网络传输)以TiDB为例。
SQL 中的 LIMIT 子句在 TiDB 查询计划树中对应 Limit 算子节点,ORDER BY 子句在查询计划树中对应 Sort 算子节点,此外,我们会将相邻的 Limit 和 Sort 算子组合成 TopN 算子节点,表示按某个排序规则提取记录的前 N 项。从另一方面来说,Limit 节点等价于一个排序规则为空的 TopN 节点。
和谓词下推类似,TopN(及 Limit,下同)下推将查询计划树中的 TopN 计算尽可能下推到距离数据源最近的地方,以尽早完成数据的过滤,进而显著地减少数据传输或计算的开销。
一、下推到存储层 Coprocessor
create table t(id int primary key, a int not null);
explain select * from t order by a limit 10;
显示的计划如下:
+----------------------------+----------+-----------+---------------+--------------------------------+
| id | estRows | task | access object | operator info |
+----------------------------+----------+-----------+---------------+--------------------------------+
| TopN_7 | 10.00 | root | | test.t.a, offset:0, count:10 |
| └─TableReader_15 | 10.00 | root | | data:TopN_14 |
| └─TopN_14 | 10.00 | cop[tikv] | | test.t.a, offset:0, count:10 |
| └─TableFullScan_13 | 10000.00 | cop[tikv] | table:t | keep order:false, stats:pseudo |
+----------------------------+----------+-----------+---------------+--------------------------------+
4 rows in set (0.00 sec)
在该查询中,将 TopN 算子节点下推到 TiKV 上对数据进行过滤,每个 Coprocessor 只向 TiDB 传输 10 条记录。在 TiDB 将数据整合后,再进行最终的过滤。
二、TopN 下推过 Join 的情况(排序规则仅依赖于外表中的列)
create table t(id int primary key, a int not null);
create table s(id int primary key, a int not null);
explain select * from t left join s on t.a = s.a order by t.a limit 10;
显示的计划如下:
+----------------------------------+----------+-----------+---------------+-------------------------------------------------+
| id | estRows | task | access object | operator info |
+----------------------------------+----------+-----------+---------------+-------------------------------------------------+
| TopN_12 | 10.00 | root | | test.t.a, offset:0, count:10 |
| └─HashJoin_17 | 12.50 | root | | left outer join, equal:[eq(test.t.a, test.s.a)] |
| ├─TopN_18(Build) | 10.00 | root | | test.t.a, offset:0, count:10 |
| │ └─TableReader_26 | 10.00 | root | | data:TopN_25 |
| │ └─TopN_25 | 10.00 | cop[tikv] | | test.t.a, offset:0, count:10 |
| │ └─TableFullScan_24 | 10000.00 | cop[tikv] | table:t | keep order:false, stats:pseudo |
| └─TableReader_30(Probe) | 10000.00 | root | | data:TableFullScan_29 |
| └─TableFullScan_29 | 10000.00 | cop[tikv] | table:s | keep order:false, stats:pseudo |
+----------------------------------+----------+-----------+---------------+-------------------------------------------------+
8 rows in set (0.01 sec)
在该查询中,TopN 算子的排序规则仅依赖于外表 t 中的列,可以将 TopN 下推到 Join 之前进行一次计算,以减少 Join 时的计算开销。除此之外,TiDB 同样将 TopN 下推到了存储层中。
三、TopN 不能下推过 Join 的情况
create table t(id int primary key, a int not null);
create table s(id int primary key, a int not null);
explain select * from t join s on t.a = s.a order by t.id limit 10;
显示的计划如下:
+-------------------------------+----------+-----------+---------------+--------------------------------------------+
| id | estRows | task | access object | operator info |
+-------------------------------+----------+-----------+---------------+--------------------------------------------+
| TopN_12 | 10.00 | root | | test.t.id, offset:0, count:10 |
| └─HashJoin_16 | 12500.00 | root | | inner join, equal:[eq(test.t.a, test.s.a)] |
| ├─TableReader_21(Build) | 10000.00 | root | | data:TableFullScan_20 |
| │ └─TableFullScan_20 | 10000.00 | cop[tikv] | table:s | keep order:false, stats:pseudo |
| └─TableReader_19(Probe) | 10000.00 | root | | data:TableFullScan_18 |
| └─TableFullScan_18 | 10000.00 | cop[tikv] | table:t | keep order:false, stats:pseudo |
+-------------------------------+----------+-----------+---------------+--------------------------------------------+
6 rows in set (0.00 sec)
TopN 无法下推过 Inner Join。以上面的查询为例,如果先 Join 得到 100 条记录,再做 TopN 可以剩余 10 条记录。而如果在 TopN 之前就过滤到剩余 10 条记录,做完 Join 之后可能就剩下 5 条了,导致了结果的差异。
同理,TopN 无法下推到 Outer Join 的内表上。在 TopN 的排序规则涉及多张表上的列时,也无法下推,如 t.a+s.a。只有当 TopN 的排序规则仅依赖于外表上的列时,才可以下推。
四、TopN 转换成 Limit 的情况
create table t(id int primary key, a int not null);
create table s(id int primary key, a int not null);
explain select * from t left join s on t.a = s.a order by t.id limit 10;
显示的计划如下:
+----------------------------------+----------+-----------+---------------+-------------------------------------------------+
| id | estRows | task | access object | operator info |
+----------------------------------+----------+-----------+---------------+-------------------------------------------------+
| TopN_12 | 10.00 | root | | test.t.id, offset:0, count:10 |
| └─HashJoin_17 | 12.50 | root | | left outer join, equal:[eq(test.t.a, test.s.a)] |
| ├─Limit_21(Build) | 10.00 | root | | offset:0, count:10 |
| │ └─TableReader_31 | 10.00 | root | | data:Limit_30 |
| │ └─Limit_30 | 10.00 | cop[tikv] | | offset:0, count:10 |
| │ └─TableFullScan_29 | 10.00 | cop[tikv] | table:t | keep order:true, stats:pseudo |
| └─TableReader_35(Probe) | 10000.00 | root | | data:TableFullScan_34 |
| └─TableFullScan_34 | 10000.00 | cop[tikv] | table:s | keep order:false, stats:pseudo |
+----------------------------------+----------+-----------+---------------+-------------------------------------------------+
8 rows in set (0.00 sec)
在上面的查询中,TopN 首先推到了外表 t 上。然后因为它要对 t.id 进行排序,而 t.id 是表 t 的主键,可以直接按顺序读出 (keep order:true),从而省略了 TopN 中的排序,将其简化为 Limit。
结论
下推技术是基于规则优化(RBO)的一种方式。他的含义是某些SQL满足某些条件下,转成一种更加高效的利用集群计算性能的执行方式来执行,比如把过滤条件下推给数据源,数据源先进行数据的过滤,来减少数据量来降低网络IO和计算开销的一种方式。但是这种优化方式往往无法知道表中数据的具体特征,有些场景下生成的计划不是高效的可能还影响性能,比如前文提到的如果我们把聚合下推了,用户表中分组Key是唯一值,相当于多算了一次聚合。针对于这样的场景往往需要另一种优化方式基于代价的优化(CBO)后续会讲到。下推的场景很多,受本人水平的影响本文仅仅举了部分例子,还有更多场景需要大家去探索。
参考资料
- https://pingcap.com/docs-cn/dev/
- https://en.wikipedia.org/wiki/Bloom_filter
- http://mysql.taobao.org/monthly/
- http://parquet.apache.org
- 《分布式数据库系统原理》
- 《数据库查询优化器的艺术》
本文相关数据表的创建语句
drop table left_table
create table left_table(id int, name varchar(20))
insert into left_table values(1, 'Dog')
insert into left_table values(2, 'Cat')
insert into left_table values(3, 'Pig')
insert into left_table values(null,null)
drop table right_table
create table right_table(id int, name varchar(20))
insert into right_table values(1, 'Squirrel')
insert into right_table values(2, 'Rabbit')
insert into right_table values(4, 'Whale')
insert into right_table values(null,null)
create table left_table_agg(id int, salary int)
insert into left_table_agg values(1,1)
insert into left_table_agg values(1,2)
insert into left_table_agg values(1,3)
insert into left_table_agg values(2,4)
insert into left_table_agg values(3,5)
insert into left_table_agg values(null,null)
create table right_table_agg(id int, salary int)
insert into right_table_agg values(1,11)
insert into right_table_agg values(1,21)
insert into right_table_agg values(1,31)
insert into right_table_agg values(2,41)
insert into right_table_agg values(4,51)
insert into right_table_agg values(null,null)

微信公众号名称:技术茶馆
微信公众号ID : Night_ZW