谓词下推

本文档介绍 TiDB 逻辑优化规则中的谓词下推规则,旨在让读者对谓词下推形成理解,并了解常见的谓词下推适用及不适用的场景。

谓词下推将查询语句中的过滤表达式计算尽可能下推到距离数据源最近的地方,以尽早完成数据的过滤,进而显著地减少数据传输或计算的开销。

示例

以下通过一些例子对谓词下推优化进行说明,其中示例 1、2、3 为谓词下推适用的案例,示例 4、5、6 为谓词下推不适用的案例。

示例 1: 谓词下推到存储层

1. create table t(id int primary key, a int);
2. explain select * from t where a < 1;
3. +-------------------------+----------+-----------+---------------+--------------------------------+
4. | id                      | estRows  | task      | access object | operator info                  |
5. +-------------------------+----------+-----------+---------------+--------------------------------+
6. | TableReader_7           | 3323.33  | root      |               | data:Selection_6               |
7. | └─Selection_6           | 3323.33  | cop[tikv] |               | lt(test.t.a, 1)                |
8. |   └─TableFullScan_5     | 10000.00 | cop[tikv] | table:t       | keep order:false, stats:pseudo |
9. +-------------------------+----------+-----------+---------------+--------------------------------+
10. 3 rows in set (0.00 sec)

在该查询中,将谓词 a < 1 下推到 TiKV 上对数据进行过滤,可以减少由于网络传输带来的开销。

示例 2: 谓词下推到存储层

1. create table t(id int primary key, a int not null);
2. explain select * from t where a < substring('123', 1, 1);
3. +-------------------------+----------+-----------+---------------+--------------------------------+
4. | id                      | estRows  | task      | access object | operator info                  |
5. +-------------------------+----------+-----------+---------------+--------------------------------+
6. | TableReader_7           | 3323.33  | root      |               | data:Selection_6               |
7. | └─Selection_6           | 3323.33  | cop[tikv] |               | lt(test.t.a, 1)                |
8. |   └─TableFullScan_5     | 10000.00 | cop[tikv] | table:t       | keep order:false, stats:pseudo |
9. +-------------------------+----------+-----------+---------------+--------------------------------+

该查询与示例 1 中的查询生成了完成一样的执行计划,这是因为谓词 a < substring('123', 1, 1) 的 substring 的入参均为常量,因此可以提前计算,进而简化得到等价的谓词 a < 1。进一步的,可以将 a < 1 下推至 TiKV 上。

示例 3: 谓词下推到 join 下方

1. create table t(id int primary key, a int not null);
2. create table s(id int primary key, a int not null);
3. explain select * from t join s on t.a = s.a where t.a < 1;
4. +------------------------------+----------+-----------+---------------+--------------------------------------------+
5. | id                           | estRows  | task      | access object | operator info                              |
6. +------------------------------+----------+-----------+---------------+--------------------------------------------+
7. | HashJoin_8                   | 4154.17  | root      |               | inner join, equal:[eq(test.t.a, test.s.a)] |
8. | ├─TableReader_15(Build)      | 3323.33  | root      |               | data:Selection_14                          |
9. | │ └─Selection_14             | 3323.33  | cop[tikv] |               | lt(test.s.a, 1)                            |
10. | │   └─TableFullScan_13       | 10000.00 | cop[tikv] | table:s       | keep order:false, stats:pseudo             |
11. | └─TableReader_12(Probe)      | 3323.33  | root      |               | data:Selection_11                          |
12. |   └─Selection_11             | 3323.33  | cop[tikv] |               | lt(test.t.a, 1)                            |
13. |     └─TableFullScan_10       | 10000.00 | cop[tikv] | table:t       | keep order:false, stats:pseudo             |
14. +------------------------------+----------+-----------+---------------+--------------------------------------------+
15. 7 rows in set (0.00 sec)

在该查询中,将谓词 t.a < 1 下推到 join 前进行过滤,可以减少 join 时的计算开销。

此外,这条 SQL 执行的是内连接,且 ON 条件是 t.a = s.a,可以由 t.a < 1 推导出谓词 s.a < 1,并将其下推至 join 运算前对 s 表进行过滤,可以进一步减少 join 时的计算开销。

示例 4: 存储层不支持的谓词无法下推

1. create table t(id int primary key, a varchar(10) not null);
2. desc select * from t where truncate(a, " ") = '1';
3. +-------------------------+----------+-----------+---------------+---------------------------------------------------+
4. | id                      | estRows  | task      | access object | operator info                                     |
5. +-------------------------+----------+-----------+---------------+---------------------------------------------------+
6. | Selection_5             | 8000.00  | root      |               | eq(truncate(cast(test.t.a, double BINARY), 0), 1) |
7. | └─TableReader_7         | 10000.00 | root      |               | data:TableFullScan_6                              |
8. |   └─TableFullScan_6     | 10000.00 | cop[tikv] | table:t       | keep order:false, stats:pseudo                    |
9. +-------------------------+----------+-----------+---------------+---------------------------------------------------+

在该查询中,存在谓词 truncate(a, " ") = '1'。

从 explain 结果中可以看到,该谓词没有被下推到 TiKV 上进行计算,这是因为 TiKV coprocessor 中没有对 truncate 内置函数进行支持,因此无法将其下推到 TiKV 上。

示例 5: 外连接中内表上的谓词不能下推

1. create table t(id int primary key, a int not null);
2. create table s(id int primary key, a int not null);
3. explain select * from t left join s on t.a = s.a where s.a is null;
4. +-------------------------------+----------+-----------+---------------+-------------------------------------------------+
5. | id                            | estRows  | task      | access object | operator info                                   |
6. +-------------------------------+----------+-----------+---------------+-------------------------------------------------+
7. | Selection_7                   | 10000.00 | root      |               | isnull(test.s.a)                                |
8. | └─HashJoin_8                  | 12500.00 | root      |               | left outer join, equal:[eq(test.t.a, test.s.a)] |
9. |   ├─TableReader_13(Build)     | 10000.00 | root      |               | data:TableFullScan_12                           |
10. |   │ └─TableFullScan_12        | 10000.00 | cop[tikv] | table:s       | keep order:false, stats:pseudo                  |
11. |   └─TableReader_11(Probe)     | 10000.00 | root      |               | data:TableFullScan_10                           |
12. |     └─TableFullScan_10        | 10000.00 | cop[tikv] | table:t       | keep order:false, stats:pseudo                  |
13. +-------------------------------+----------+-----------+---------------+-------------------------------------------------+
14. 6 rows in set (0.00 sec)

在该查询中,内表 s 上存在谓词 s.a is null。

从 explain 中可以看到,该谓词没有被下推到 join 前进行计算,这是因为外连接在不满足 on 条件时会对内表填充 NULL,而在该查询中 s.a is null 用来对 join 后的结果进行过滤,如果将其下推到 join 前在内表上进行过滤,则下推前后不等价,因此不可进行下推。

示例 6: 谓词中包含用户变量时不能下推

1. create table t(id int primary key, a char);
2. set @a = 1;
3. explain select * from t where a < @a;
4. +-------------------------+----------+-----------+---------------+--------------------------------+
5. | id                      | estRows  | task      | access object | operator info                  |
6. +-------------------------+----------+-----------+---------------+--------------------------------+
7. | Selection_5             | 8000.00  | root      |               | lt(test.t.a, getvar("a"))      |
8. | └─TableReader_7         | 10000.00 | root      |               | data:TableFullScan_6           |
9. |   └─TableFullScan_6     | 10000.00 | cop[tikv] | table:t       | keep order:false, stats:pseudo |
10. +-------------------------+----------+-----------+---------------+--------------------------------+
11. 3 rows in set (0.00 sec)

在该查询中,表 t 上存在谓词 a < @a,其中 @a 为值为 1 的用户变量。

从 explain 中可以看到,该谓词没有像示例 2 中一样,将谓词简化为 a < 1 并下推到 TiKV 上进行计算。这是因为,用户变量 @a 的值可能会某些场景下在查询过程中发生改变,且 TiKV 对于用户变量 @a 的值不可知,因此 TiDB 不会将 @a 替换为 1,且不会下推至 TiKV 上进行计算。

一个帮助理解的例子如下:

1. create table t(id int primary key, a int);
2. insert into t values(1, 1), (2,2);
3. set @a = 1;
4. select id, a, @a:=@a+1 from t where a = @a;
5. +----+------+----------+
6. | id | a    | @a:=@a+1 |
7. +----+------+----------+
8. |  1 |    1 | 2        |
9. |  2 |    2 | 3        |
10. +----+------+----------+
11. 2 rows in set (0.00 sec)

可以从在该查询中看到,@a 的值会在查询过程中发生改变,因此如果将 a = @a 替换为 a = 1 并下推至 TiKV,则优化前后不等价。