【数据库内核】基于规则优化之谓词下推

目录

概念

逻辑算子介绍

为什么要谓词下推

谓词下推在Join中的应用

一、Inner Join 下推情况说明

二、Left Join下推情况说明

三、Right Join下推情况说明

四、Full Join下推情况说明

谓词下推在Parquet中的应用

聚合下推

一、简单聚合函数下推

二、Join场景下的聚合函数下推

Join下推

Order By Limit下推

一、下推到存储层 Coprocessor

二、TopN 下推过 Join 的情况(排序规则仅依赖于外表中的列)

三、TopN 不能下推过 Join 的情况

四、TopN 转换成 Limit 的情况

结论

参考资料


概念

 

谓词下推 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 = 1SELECT  * 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 = 1SELECT  * 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)后续会讲到。下推的场景很多,受本人水平的影响本文仅仅举了部分例子,还有更多场景需要大家去探索。

 

参考资料

  1. https://pingcap.com/docs-cn/dev/
  2. https://en.wikipedia.org/wiki/Bloom_filter
  3. http://mysql.taobao.org/monthly/
  4. http://parquet.apache.org
  5. 《分布式数据库系统原理》
  6. 《数据库查询优化器的艺术》

 

本文相关数据表的创建语句

drop table left_tablecreate 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_tablecreate 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
  • 7
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值