From ffbf3cfd7a627a85e84ce239b0db27880cee117f Mon Sep 17 00:00:00 2001 From: Yiding Cui Date: Wed, 20 May 2020 21:21:57 +0800 Subject: [PATCH 01/11] perf-tuning: add docs for subquery optimizations --- correlated-subquery-optimization.md | 55 ++++++++++++++++++++++ subquery-optimization.md | 73 ++++++++++++++++++++++++++++- 2 files changed, 127 insertions(+), 1 deletion(-) diff --git a/correlated-subquery-optimization.md b/correlated-subquery-optimization.md index 6efaf95781c0..707e574a7f4f 100644 --- a/correlated-subquery-optimization.md +++ b/correlated-subquery-optimization.md @@ -4,3 +4,58 @@ category: performance --- # 关联子查询去关联 + +在[子查询相关的优化](/subquery-optimization.md)中我们介绍了当没有关联列时,TiDB是如何处理子查询的。由于关联子查询解除关联依赖是一个比较复杂的工作,我们这里介绍一些简单的场景以及这个优化规则的适用范围。 + +以 `select * from t1 where t1.a < (select sum(t2.a) from t2 where t2.b = t1.b)` 为例,这里子查询 `t1.a < (select sum(t2.a) from t2 where t2.b = t1.b)` 中涉及了关联列上的条件 `t2.b=t1.b`,不过恰好由于这是一个等值条件,因此我们可以将其等价的改写为 `select t1.* from t1, (select b, sum(a) sum_a from t2 group by b) t2 where t1.b = t2.b and t1.a < t2.sum_a;`这样一个关联子查询就被重新改写为 `JOIN` 的形式。 + +TiDB 之所以要进行这样的改写,是因为关联子查询每次子查询执行时都是要和它的外部查询结果绑定的。在上面的例子中如果 `t1.a` 有一千万个值,那这个子查询就要被重复执行一千万次,因为 `t2.b=t1.b` 这个条件会随着 `t1.a` 值的不同而发生变化。当我们通过一些手段将关联依赖解除后,这个子查询就只需要被执行一次了。 + +同时这种改写的弊端在于在关联没有被解除时,我们是可以使用关联列上的索引的,也就是说虽然这个子查询可能被重复执行多次,但是每次都可以使用索引过滤数据。而解除关联的变换上,通常是会导致关联列的位置发生改变而导致虽然子查询只被执行了一次,但是单次执行的时间会比没有解除关联时的单次执行时间长。因此在外部的值比较少的情况下,不解除关联依赖反而可能对执行性能更优帮助。我们可以通过[优化规则及表达式下推的黑名单](/blacklist-control-plan.md)中关闭`子查询去关联`优化规则的方式来关闭这个优化。 + +``` +mysql> create table t1(a int, b int); +Query OK, 0 rows affected (0.01 sec) + +mysql> create table t2(a int, b int, index idx(b)); +Query OK, 0 rows affected (0.02 sec) + +mysql> explain select * from t1 where t1.a < (select sum(t2.a) from t2 where t2.b = t1.b); ++----------------------------------+----------+-----------+---------------+-----------------------------------------------------------------------------------------+ +| id | estRows | task | access object | operator info | ++----------------------------------+----------+-----------+---------------+-----------------------------------------------------------------------------------------+ +| HashJoin_11 | 9990.00 | root | | inner join, equal:[eq(test.t1.b, test.t2.b)], other cond:lt(cast(test.t1.a), Column#7) | +| ├─HashAgg_23(Build) | 7992.00 | root | | group by:test.t2.b, funcs:sum(Column#8)->Column#7, funcs:firstrow(test.t2.b)->test.t2.b | +| │ └─TableReader_24 | 7992.00 | root | | data:HashAgg_16 | +| │ └─HashAgg_16 | 7992.00 | cop[tikv] | | group by:test.t2.b, funcs:sum(test.t2.a)->Column#8 | +| │ └─Selection_22 | 9990.00 | cop[tikv] | | not(isnull(test.t2.b)) | +| │ └─TableFullScan_21 | 10000.00 | cop[tikv] | table:t2 | keep order:false, stats:pseudo | +| └─TableReader_15(Probe) | 9990.00 | root | | data:Selection_14 | +| └─Selection_14 | 9990.00 | cop[tikv] | | not(isnull(test.t1.b)) | +| └─TableFullScan_13 | 10000.00 | cop[tikv] | table:t1 | keep order:false, stats:pseudo | ++----------------------------------+----------+-----------+---------------+-----------------------------------------------------------------------------------------+ +mysql> insert into mysql.opt_rule_blacklist values("decorrelate"); +Query OK, 1 row affected (0.00 sec) + +mysql> admin reload opt_rule_blacklist; +Query OK, 0 rows affected (0.00 sec) + +mysql> explain select * from t1 where t1.a < (select sum(t2.a) from t2 where t2.b = t1.b); ++----------------------------------------+----------+-----------+------------------------+------------------------------------------------------------------------------+ +| id | estRows | task | access object | operator info | ++----------------------------------------+----------+-----------+------------------------+------------------------------------------------------------------------------+ +| Projection_10 | 10000.00 | root | | test.t1.a, test.t1.b | +| └─Apply_12 | 10000.00 | root | | CARTESIAN inner join, other cond:lt(cast(test.t1.a), Column#7) | +| ├─TableReader_14(Build) | 10000.00 | root | | data:TableFullScan_13 | +| │ └─TableFullScan_13 | 10000.00 | cop[tikv] | table:t1 | keep order:false, stats:pseudo | +| └─MaxOneRow_15(Probe) | 1.00 | root | | | +| └─HashAgg_27 | 1.00 | root | | funcs:sum(Column#10)->Column#7 | +| └─IndexLookUp_28 | 1.00 | root | | | +| ├─IndexRangeScan_25(Build) | 10.00 | cop[tikv] | table:t2, index:idx(b) | range: decided by [eq(test.t2.b, test.t1.b)], keep order:false, stats:pseudo | +| └─HashAgg_17(Probe) | 1.00 | cop[tikv] | | funcs:sum(test.t2.a)->Column#10 | +| └─TableRowIDScan_26 | 10.00 | cop[tikv] | table:t2 | keep order:false, stats:pseudo | ++----------------------------------------+----------+-----------+------------------------+------------------------------------------------------------------------------+ + +``` + +上面的样例中展示了打开和关闭优化规则前后执行计划的不同。其中在关闭优化规则的情况下我们可以在 `IndexRangeScan_25(Build)` 的 `operator info` 中看到 `range: decided by [eq(test.t2.b, test.t1.b)]`,便是关联依赖未被接触时,可以使用关联条件进行索引范围查询的展示。 \ No newline at end of file diff --git a/subquery-optimization.md b/subquery-optimization.md index f38bbaf84572..cbf4ff789f10 100644 --- a/subquery-optimization.md +++ b/subquery-optimization.md @@ -3,4 +3,75 @@ title: 子查询相关的优化 category: performance --- -# 子查询相关的优化 \ No newline at end of file +# 子查询相关的优化 + +通常我们遇到的会是如下情况的子查询: + +- `NOT IN (SELECT ... FROM ...)` +- `NOT EXISTS (SELECT ... FROM ...)` +- `IN (SELECT ... FROM ..)` +- `EXISTS (SELECT ... FROM ...)` +- `... >/>=/ ANY (SELECT ... FROM ...)` + +对于这种情况我们可以将 `ALL` 或者 `ANY` 用 `MAX` 以及 `MIN` 来代替。不过由于在表为空时,`MAX(EXPR)` 以及 `MIN(EXPR)` 的结果会为 NULL,其表现形式和 `EXPR` 是有 `NULL` 值的结果一样。以及外部表达式结果为 `NULL` 时也会影响表达式的最终结果,因此这里完整的改写会是如下的形式: + +- `t.id < all(select s.id from s)` 会被改写为 `t.id < min(s.id) and if(sum(s.id is null) != 0, null, true)`。 +- `t.id < any (select s.id from s)` 会被改写为 `t.id < max(s.id) or if(sum(s.id is null) != 0, null, false)`。 + +## `... != ANY (SELECT ... FROM ...)` + +对于这种情况,当子查询中不同值的各种只有一种的话,那只要和这个值对比就可以了,如果子查询中不同值的个数多余1个,那么必然会有不相等的情况出现。因此这样的子查询可以采取如下的改写手段。 + +- `select * from t where t.id != any (select s.id from s)` 会被改写为 `select t.* from t, (select s.id, count(distinct s.id) as cnt_distinct from s) where (t.id != s.id or cnt_distinct > 1)` + +## `... = ALL (SELECT ... FROM ...)` + +对于这种情况,当子查询中不同值的个数多于一种的话,那么这个表达式的结果必然为假。因此这样的子查询在 TiDB 中会改写为如下的形式。 + +- `select * from t where t.id = all (select s.id from s)` 会被改写为 `select t.* from t, (select s.id, count(distinct s.id) as cnt_distinct from s) where (t.id = s.id and cnt_distinct <= 1)` + +## `... IN (SELECT ... FROM ...)` + +对于这种情况,我们会将其改写为 `IN` 的子查询改写为 `SELECT ... FROM ... GROUP ...` 的形式然后将 `IN` 改写为普通的 `JOIN` 的形式。 +如 `select * from t1 where t1.a in (select t2.a from t2)` 会被改写为 `select t1.* from t1, (select distinct(a) a from t2) t2 where t1.a = t2.a` 的形式。同时这里的 `DISTINCT` 可以在 `t2.a` 具有 `UNIQUE` 属性时被自动消去。 + +``` +mysql> explain select * from t1 where t1.a in (select t2.a from t2); ++------------------------------+---------+-----------+------------------------+----------------------------------------------------------------------------+ +| id | estRows | task | access object | operator info | ++------------------------------+---------+-----------+------------------------+----------------------------------------------------------------------------+ +| IndexJoin_12 | 9990.00 | root | | inner join, inner:TableReader_11, outer key:test.t2.a, inner key:test.t1.a | +| ├─HashAgg_21(Build) | 7992.00 | root | | group by:test.t2.a, funcs:firstrow(test.t2.a)->test.t2.a | +| │ └─IndexReader_28 | 9990.00 | root | | index:IndexFullScan_27 | +| │ └─IndexFullScan_27 | 9990.00 | cop[tikv] | table:t2, index:idx(a) | keep order:false, stats:pseudo | +| └─TableReader_11(Probe) | 1.00 | root | | data:TableRangeScan_10 | +| └─TableRangeScan_10 | 1.00 | cop[tikv] | table:t1 | range: decided by [test.t2.a], keep order:false, stats:pseudo | ++------------------------------+---------+-----------+------------------------+----------------------------------------------------------------------------+ +``` + +这个改写会在 `IN` 子查询相对较小,而外部查询相对较大时产生更好的执行性能。因为不经过改写的情况下,我们无法使用以 t2 为驱动表的 `index join`。同时这里的弊端便是当改写删成的聚合无法被自动消去且 `t2` 表比较大时,反而会影响查询的性能。目前 TiDB 中使用 [tidb\_opt\_insubq\_to\_join\_and\_agg](/tidb-specific-system-variables.md#tidb_opt_insubq_to_join_and_agg) 变量来控制这个优化的打开与否。当遇到不合适这个优化的情况可以手动关闭。 + + +## `EXISTS` 子查询以及 `... >/>=/ create table t1(a int); +mysql> create table t2(a int); +mysql> insert into t2 values(1); +mysql> explain select * from t where exists (select * from t2); ++------------------------+----------+-----------+---------------+--------------------------------+ +| id | estRows | task | access object | operator info | ++------------------------+----------+-----------+---------------+--------------------------------+ +| TableReader_12 | 10000.00 | root | | data:TableFullScan_11 | +| └─TableFullScan_11 | 10000.00 | cop[tikv] | table:t | keep order:false, stats:pseudo | ++------------------------+----------+-----------+---------------+--------------------------------+ + +``` \ No newline at end of file From a3bc1e30128e3e99a026bfb91ad6b1f1c6034b5d Mon Sep 17 00:00:00 2001 From: TomShawn <41534398+TomShawn@users.noreply.github.com> Date: Thu, 21 May 2020 13:37:35 +0800 Subject: [PATCH 02/11] Update subquery-optimization.md --- subquery-optimization.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/subquery-optimization.md b/subquery-optimization.md index cbf4ff789f10..9528df2aec21 100644 --- a/subquery-optimization.md +++ b/subquery-optimization.md @@ -57,7 +57,6 @@ mysql> explain select * from t1 where t1.a in (select t2.a from t2); 这个改写会在 `IN` 子查询相对较小,而外部查询相对较大时产生更好的执行性能。因为不经过改写的情况下,我们无法使用以 t2 为驱动表的 `index join`。同时这里的弊端便是当改写删成的聚合无法被自动消去且 `t2` 表比较大时,反而会影响查询的性能。目前 TiDB 中使用 [tidb\_opt\_insubq\_to\_join\_and\_agg](/tidb-specific-system-variables.md#tidb_opt_insubq_to_join_and_agg) 变量来控制这个优化的打开与否。当遇到不合适这个优化的情况可以手动关闭。 - ## `EXISTS` 子查询以及 `... >/>=/ explain select * from t where exists (select * from t2); | └─TableFullScan_11 | 10000.00 | cop[tikv] | table:t | keep order:false, stats:pseudo | +------------------------+----------+-----------+---------------+--------------------------------+ -``` \ No newline at end of file +``` From 511db06c31b6b8b980195c35bafa4d733663ae1f Mon Sep 17 00:00:00 2001 From: TomShawn <41534398+TomShawn@users.noreply.github.com> Date: Thu, 21 May 2020 13:37:42 +0800 Subject: [PATCH 03/11] Update subquery-optimization.md --- subquery-optimization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subquery-optimization.md b/subquery-optimization.md index 9528df2aec21..40758b504fbe 100644 --- a/subquery-optimization.md +++ b/subquery-optimization.md @@ -17,7 +17,7 @@ category: performance 子查询默认会以[理解 TiDB 执行计划](/query-execution-plan.md)中提到的 `semi join` 作为默认的执行方式,同时对于一些特殊的子查询,TiDB 会做一些逻辑上的替换使得查询可以获得更好的执行性能 -## `... < ALL (SELECT ... FROM ...)` 或者 ` ... > ANY (SELECT ... FROM ...)` +## `... < ALL (SELECT ... FROM ...)` 或者 `... > ANY (SELECT ... FROM ...)` 对于这种情况我们可以将 `ALL` 或者 `ANY` 用 `MAX` 以及 `MIN` 来代替。不过由于在表为空时,`MAX(EXPR)` 以及 `MIN(EXPR)` 的结果会为 NULL,其表现形式和 `EXPR` 是有 `NULL` 值的结果一样。以及外部表达式结果为 `NULL` 时也会影响表达式的最终结果,因此这里完整的改写会是如下的形式: From 0870d0aa96552b46aaa79d3675f4cc0eef7b7526 Mon Sep 17 00:00:00 2001 From: Yiding Cui Date: Mon, 25 May 2020 11:48:10 +0800 Subject: [PATCH 04/11] address comments --- correlated-subquery-optimization.md | 43 +++++++++++++++++------------ 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/correlated-subquery-optimization.md b/correlated-subquery-optimization.md index 707e574a7f4f..135dc6a09281 100644 --- a/correlated-subquery-optimization.md +++ b/correlated-subquery-optimization.md @@ -5,22 +5,27 @@ category: performance # 关联子查询去关联 -在[子查询相关的优化](/subquery-optimization.md)中我们介绍了当没有关联列时,TiDB是如何处理子查询的。由于关联子查询解除关联依赖是一个比较复杂的工作,我们这里介绍一些简单的场景以及这个优化规则的适用范围。 +## 介绍 -以 `select * from t1 where t1.a < (select sum(t2.a) from t2 where t2.b = t1.b)` 为例,这里子查询 `t1.a < (select sum(t2.a) from t2 where t2.b = t1.b)` 中涉及了关联列上的条件 `t2.b=t1.b`,不过恰好由于这是一个等值条件,因此我们可以将其等价的改写为 `select t1.* from t1, (select b, sum(a) sum_a from t2 group by b) t2 where t1.b = t2.b and t1.a < t2.sum_a;`这样一个关联子查询就被重新改写为 `JOIN` 的形式。 +在[子查询相关的优化](/subquery-optimization.md)中介绍了当没有关联列时,TiDB是如何处理子查询的。由于关联子查询解除关联依赖是一个比较复杂的工作,本文档中会介绍一些简单的场景以及这个优化规则的适用范围。 -TiDB 之所以要进行这样的改写,是因为关联子查询每次子查询执行时都是要和它的外部查询结果绑定的。在上面的例子中如果 `t1.a` 有一千万个值,那这个子查询就要被重复执行一千万次,因为 `t2.b=t1.b` 这个条件会随着 `t1.a` 值的不同而发生变化。当我们通过一些手段将关联依赖解除后,这个子查询就只需要被执行一次了。 +以 `select * from t1 where t1.a < (select sum(t2.a) from t2 where t2.b = t1.b)` 为例,这里子查询 `t1.a < (select sum(t2.a) from t2 where t2.b = t1.b)` 中涉及了关联列上的条件 `t2.b=t1.b`,不过恰好由于这是一个等值条件,因此可以将其等价的改写为 `select t1.* from t1, (select b, sum(a) sum_a from t2 group by b) t2 where t1.b = t2.b and t1.a < t2.sum_a;`这样一个关联子查询就被重新改写为 `JOIN` 的形式。 -同时这种改写的弊端在于在关联没有被解除时,我们是可以使用关联列上的索引的,也就是说虽然这个子查询可能被重复执行多次,但是每次都可以使用索引过滤数据。而解除关联的变换上,通常是会导致关联列的位置发生改变而导致虽然子查询只被执行了一次,但是单次执行的时间会比没有解除关联时的单次执行时间长。因此在外部的值比较少的情况下,不解除关联依赖反而可能对执行性能更优帮助。我们可以通过[优化规则及表达式下推的黑名单](/blacklist-control-plan.md)中关闭`子查询去关联`优化规则的方式来关闭这个优化。 +TiDB 之所以要进行这样的改写,是因为关联子查询每次子查询执行时都是要和它的外部查询结果绑定的。在上面的例子中如果 `t1.a` 有一千万个值,那这个子查询就要被重复执行一千万次,因为 `t2.b=t1.b` 这个条件会随着 `t1.a` 值的不同而发生变化。当通过一些手段将关联依赖解除后,这个子查询就只需要被执行一次了。 -``` -mysql> create table t1(a int, b int); -Query OK, 0 rows affected (0.01 sec) +## 限制 + +这种改写的弊端在于在关联没有被解除时,优化器是可以使用关联列上的索引的,也就是说虽然这个子查询可能被重复执行多次,但是每次都可以使用索引过滤数据。而解除关联的变换上,通常是会导致关联列的位置发生改变而导致虽然子查询只被执行了一次,但是单次执行的时间会比没有解除关联时的单次执行时间长。因此在外部的值比较少的情况下,不解除关联依赖反而可能对执行性能更优帮助。这时可以通过[优化规则及表达式下推的黑名单](/blacklist-control-plan.md)中关闭`子查询去关联`优化规则的方式来关闭这个优化。 -mysql> create table t2(a int, b int, index idx(b)); -Query OK, 0 rows affected (0.02 sec) +## 样例 -mysql> explain select * from t1 where t1.a < (select sum(t2.a) from t2 where t2.b = t1.b); +``` +create table t1(a int, b int); +create table t2(a int, b int, index idx(b)); +explain select * from t1 where t1.a < (select sum(t2.a) from t2 where t2.b = t1.b); +``` + +``` +----------------------------------+----------+-----------+---------------+-----------------------------------------------------------------------------------------+ | id | estRows | task | access object | operator info | +----------------------------------+----------+-----------+---------------+-----------------------------------------------------------------------------------------+ @@ -34,13 +39,18 @@ mysql> explain select * from t1 where t1.a < (select sum(t2.a) from t2 where t2. | └─Selection_14 | 9990.00 | cop[tikv] | | not(isnull(test.t1.b)) | | └─TableFullScan_13 | 10000.00 | cop[tikv] | table:t1 | keep order:false, stats:pseudo | +----------------------------------+----------+-----------+---------------+-----------------------------------------------------------------------------------------+ -mysql> insert into mysql.opt_rule_blacklist values("decorrelate"); -Query OK, 1 row affected (0.00 sec) -mysql> admin reload opt_rule_blacklist; -Query OK, 0 rows affected (0.00 sec) +``` -mysql> explain select * from t1 where t1.a < (select sum(t2.a) from t2 where t2.b = t1.b); +上面是优化生效的情况,可以看到 `HashJoin_11` 是一个普通的 `inner join`。 + +``` +insert into mysql.opt_rule_blacklist values("decorrelate"); +admin reload opt_rule_blacklist; +explain select * from t1 where t1.a < (select sum(t2.a) from t2 where t2.b = t1.b); +``` + +``` +----------------------------------------+----------+-----------+------------------------+------------------------------------------------------------------------------+ | id | estRows | task | access object | operator info | +----------------------------------------+----------+-----------+------------------------+------------------------------------------------------------------------------+ @@ -55,7 +65,6 @@ mysql> explain select * from t1 where t1.a < (select sum(t2.a) from t2 where t2. | └─HashAgg_17(Probe) | 1.00 | cop[tikv] | | funcs:sum(test.t2.a)->Column#10 | | └─TableRowIDScan_26 | 10.00 | cop[tikv] | table:t2 | keep order:false, stats:pseudo | +----------------------------------------+----------+-----------+------------------------+------------------------------------------------------------------------------+ - ``` -上面的样例中展示了打开和关闭优化规则前后执行计划的不同。其中在关闭优化规则的情况下我们可以在 `IndexRangeScan_25(Build)` 的 `operator info` 中看到 `range: decided by [eq(test.t2.b, test.t1.b)]`,便是关联依赖未被接触时,可以使用关联条件进行索引范围查询的展示。 \ No newline at end of file +在执行了关闭规划规则的语句后,可以在 `IndexRangeScan_25(Build)` 的 `operator info` 中看到 `range: decided by [eq(test.t2.b, test.t1.b)]`,便是关联依赖未被解除时,可以使用关联条件进行索引范围查询的展示。 \ No newline at end of file From 5eccb007bcef02773872a943a7530acc27014ddc Mon Sep 17 00:00:00 2001 From: Yiding Cui Date: Mon, 25 May 2020 11:50:37 +0800 Subject: [PATCH 05/11] Update subquery-optimization.md Co-authored-by: Ran --- subquery-optimization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subquery-optimization.md b/subquery-optimization.md index 40758b504fbe..001314cfdf41 100644 --- a/subquery-optimization.md +++ b/subquery-optimization.md @@ -26,7 +26,7 @@ category: performance ## `... != ANY (SELECT ... FROM ...)` -对于这种情况,当子查询中不同值的各种只有一种的话,那只要和这个值对比就可以了,如果子查询中不同值的个数多余1个,那么必然会有不相等的情况出现。因此这样的子查询可以采取如下的改写手段。 +对于这种情况,当子查询中不同值的各种只有一种的话,那只要和这个值对比就即可。如果子查询中不同值的个数多于 1 个,那么必然会有不相等的情况出现。因此这样的子查询可以采取如下的改写手段: - `select * from t where t.id != any (select s.id from s)` 会被改写为 `select t.* from t, (select s.id, count(distinct s.id) as cnt_distinct from s) where (t.id != s.id or cnt_distinct > 1)` From 8f2bc54fb8c134e34ec385146980d3f29caeabde Mon Sep 17 00:00:00 2001 From: Ran Date: Mon, 25 May 2020 13:55:27 +0800 Subject: [PATCH 06/11] update format --- correlated-subquery-optimization.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/correlated-subquery-optimization.md b/correlated-subquery-optimization.md index 135dc6a09281..72a5a5ddb246 100644 --- a/correlated-subquery-optimization.md +++ b/correlated-subquery-optimization.md @@ -5,17 +5,19 @@ category: performance # 关联子查询去关联 -## 介绍 +[子查询相关的优化](/subquery-optimization.md)中介绍了当没有关联列时,TiDB 是如何处理子查询的。由于关联子查询解除关联依赖是一个比较复杂的工作,本文档中会介绍一些简单的场景以及这个优化规则的适用范围。 -在[子查询相关的优化](/subquery-optimization.md)中介绍了当没有关联列时,TiDB是如何处理子查询的。由于关联子查询解除关联依赖是一个比较复杂的工作,本文档中会介绍一些简单的场景以及这个优化规则的适用范围。 +## 介绍 -以 `select * from t1 where t1.a < (select sum(t2.a) from t2 where t2.b = t1.b)` 为例,这里子查询 `t1.a < (select sum(t2.a) from t2 where t2.b = t1.b)` 中涉及了关联列上的条件 `t2.b=t1.b`,不过恰好由于这是一个等值条件,因此可以将其等价的改写为 `select t1.* from t1, (select b, sum(a) sum_a from t2 group by b) t2 where t1.b = t2.b and t1.a < t2.sum_a;`这样一个关联子查询就被重新改写为 `JOIN` 的形式。 +以 `select * from t1 where t1.a < (select sum(t2.a) from t2 where t2.b = t1.b)` 为例,这里子查询 `t1.a < (select sum(t2.a) from t2 where t2.b = t1.b)` 中涉及了关联列上的条件 `t2.b=t1.b`,不过恰好由于这是一个等值条件,因此可以将其等价的改写为 `select t1.* from t1, (select b, sum(a) sum_a from t2 group by b) t2 where t1.b = t2.b and t1.a < t2.sum_a;`。这样,一个关联子查询就被重新改写为 `JOIN` 的形式。 -TiDB 之所以要进行这样的改写,是因为关联子查询每次子查询执行时都是要和它的外部查询结果绑定的。在上面的例子中如果 `t1.a` 有一千万个值,那这个子查询就要被重复执行一千万次,因为 `t2.b=t1.b` 这个条件会随着 `t1.a` 值的不同而发生变化。当通过一些手段将关联依赖解除后,这个子查询就只需要被执行一次了。 +TiDB 之所以要进行这样的改写,是因为关联子查询每次子查询执行时都是要和它的外部查询结果绑定的。在上面的例子中,如果 `t1.a` 有一千万个值,那这个子查询就要被重复执行一千万次,因为 `t2.b=t1.b` 这个条件会随着 `t1.a` 值的不同而发生变化。当通过一些手段将关联依赖解除后,这个子查询就只需要被执行一次了。 ## 限制 -这种改写的弊端在于在关联没有被解除时,优化器是可以使用关联列上的索引的,也就是说虽然这个子查询可能被重复执行多次,但是每次都可以使用索引过滤数据。而解除关联的变换上,通常是会导致关联列的位置发生改变而导致虽然子查询只被执行了一次,但是单次执行的时间会比没有解除关联时的单次执行时间长。因此在外部的值比较少的情况下,不解除关联依赖反而可能对执行性能更优帮助。这时可以通过[优化规则及表达式下推的黑名单](/blacklist-control-plan.md)中关闭`子查询去关联`优化规则的方式来关闭这个优化。 +这种改写的弊端在于,在关联没有被解除时,优化器是可以使用关联列上的索引的。也就是说,虽然这个子查询可能被重复执行多次,但是每次都可以使用索引过滤数据。而解除关联的变换上,通常是会导致关联列的位置发生改变而导致虽然子查询只被执行了一次,但是单次执行的时间会比没有解除关联时的单次执行时间长。 + +因此,在外部的值比较少的情况下,不解除关联依赖反而可能对执行性能更优帮助。这时可以通过[优化规则及表达式下推的黑名单](/blacklist-control-plan.md)中关闭`子查询去关联`优化规则的方式来关闭这个优化。 ## 样例 @@ -67,4 +69,4 @@ explain select * from t1 where t1.a < (select sum(t2.a) from t2 where t2.b = t1. +----------------------------------------+----------+-----------+------------------------+------------------------------------------------------------------------------+ ``` -在执行了关闭规划规则的语句后,可以在 `IndexRangeScan_25(Build)` 的 `operator info` 中看到 `range: decided by [eq(test.t2.b, test.t1.b)]`,便是关联依赖未被解除时,可以使用关联条件进行索引范围查询的展示。 \ No newline at end of file +在执行了关闭规划规则的语句后,可以在 `IndexRangeScan_25(Build)` 的 `operator info` 中看到 `range: decided by [eq(test.t2.b, test.t1.b)]`,便是关联依赖未被解除时,可以使用关联条件进行索引范围查询的展示。 From d090c03731c78b3ce5d3d3efd059d250492144fb Mon Sep 17 00:00:00 2001 From: Ran Date: Mon, 25 May 2020 14:14:19 +0800 Subject: [PATCH 07/11] update format --- subquery-optimization.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/subquery-optimization.md b/subquery-optimization.md index 001314cfdf41..4dff734e3665 100644 --- a/subquery-optimization.md +++ b/subquery-optimization.md @@ -13,13 +13,13 @@ category: performance - `EXISTS (SELECT ... FROM ...)` - `... >/>=/ ANY (SELECT ... FROM ...)` -对于这种情况我们可以将 `ALL` 或者 `ANY` 用 `MAX` 以及 `MIN` 来代替。不过由于在表为空时,`MAX(EXPR)` 以及 `MIN(EXPR)` 的结果会为 NULL,其表现形式和 `EXPR` 是有 `NULL` 值的结果一样。以及外部表达式结果为 `NULL` 时也会影响表达式的最终结果,因此这里完整的改写会是如下的形式: +对于这种情况,可以将 `ALL` 或者 `ANY` 用 `MAX` 以及 `MIN` 来代替。不过由于在表为空时,`MAX(EXPR)` 以及 `MIN(EXPR)` 的结果会为 `NULL`,其表现形式和 `EXPR` 是有 `NULL` 值的结果一样。以及外部表达式结果为 `NULL` 时也会影响表达式的最终结果,因此这里完整的改写会是如下的形式: - `t.id < all(select s.id from s)` 会被改写为 `t.id < min(s.id) and if(sum(s.id is null) != 0, null, true)`。 - `t.id < any (select s.id from s)` 会被改写为 `t.id < max(s.id) or if(sum(s.id is null) != 0, null, false)`。 @@ -32,7 +32,7 @@ category: performance ## `... = ALL (SELECT ... FROM ...)` -对于这种情况,当子查询中不同值的个数多于一种的话,那么这个表达式的结果必然为假。因此这样的子查询在 TiDB 中会改写为如下的形式。 +对于这种情况,当子查询中不同值的个数多于一种的话,那么这个表达式的结果必然为假。因此这样的子查询在 TiDB 中会改写为如下的形式: - `select * from t where t.id = all (select s.id from s)` 会被改写为 `select t.* from t, (select s.id, count(distinct s.id) as cnt_distinct from s) where (t.id = s.id and cnt_distinct <= 1)` @@ -55,7 +55,7 @@ mysql> explain select * from t1 where t1.a in (select t2.a from t2); +------------------------------+---------+-----------+------------------------+----------------------------------------------------------------------------+ ``` -这个改写会在 `IN` 子查询相对较小,而外部查询相对较大时产生更好的执行性能。因为不经过改写的情况下,我们无法使用以 t2 为驱动表的 `index join`。同时这里的弊端便是当改写删成的聚合无法被自动消去且 `t2` 表比较大时,反而会影响查询的性能。目前 TiDB 中使用 [tidb\_opt\_insubq\_to\_join\_and\_agg](/tidb-specific-system-variables.md#tidb_opt_insubq_to_join_and_agg) 变量来控制这个优化的打开与否。当遇到不合适这个优化的情况可以手动关闭。 +这个改写会在 `IN` 子查询相对较小,而外部查询相对较大时产生更好的执行性能。因为不经过改写的情况下,我们无法使用以 t2 为驱动表的 `index join`。同时这里的弊端便是,当改写删成的聚合无法被自动消去且 `t2` 表比较大时,反而会影响查询的性能。目前 TiDB 中使用 [tidb\_opt\_insubq\_to\_join\_and\_agg](/tidb-specific-system-variables.md#tidb_opt_insubq_to_join_and_agg) 变量来控制这个优化的打开与否。当遇到不合适这个优化的情况可以手动关闭。 ## `EXISTS` 子查询以及 `... >/>=/ Date: Mon, 25 May 2020 15:19:02 +0800 Subject: [PATCH 08/11] Apply suggestions from code review Co-authored-by: Ran --- subquery-optimization.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/subquery-optimization.md b/subquery-optimization.md index 4dff734e3665..b591ed1f3432 100644 --- a/subquery-optimization.md +++ b/subquery-optimization.md @@ -13,7 +13,7 @@ category: performance - `EXISTS (SELECT ... FROM ...)` - `... >/>=/ Date: Mon, 25 May 2020 15:28:39 +0800 Subject: [PATCH 09/11] address comment --- correlated-subquery-optimization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/correlated-subquery-optimization.md b/correlated-subquery-optimization.md index 72a5a5ddb246..ba36fd547ff9 100644 --- a/correlated-subquery-optimization.md +++ b/correlated-subquery-optimization.md @@ -69,4 +69,4 @@ explain select * from t1 where t1.a < (select sum(t2.a) from t2 where t2.b = t1. +----------------------------------------+----------+-----------+------------------------+------------------------------------------------------------------------------+ ``` -在执行了关闭规划规则的语句后,可以在 `IndexRangeScan_25(Build)` 的 `operator info` 中看到 `range: decided by [eq(test.t2.b, test.t1.b)]`,便是关联依赖未被解除时,可以使用关联条件进行索引范围查询的展示。 +在执行了关闭规划规则的语句后,可以在 `IndexRangeScan_25(Build)` 的 `operator info` 中看到 `range: decided by [eq(test.t2.b, test.t1.b)]`,这部分信息就是关联依赖未被解除时,TiDB 可以使用关联条件进行索引范围查询的展示。 From 33bdfd1096e106c38f4383be3bb7c4c45edc2bca Mon Sep 17 00:00:00 2001 From: lilin90 Date: Mon, 25 May 2020 17:08:17 +0800 Subject: [PATCH 10/11] Update wording and format --- correlated-subquery-optimization.md | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/correlated-subquery-optimization.md b/correlated-subquery-optimization.md index ba36fd547ff9..7f42fa658d12 100644 --- a/correlated-subquery-optimization.md +++ b/correlated-subquery-optimization.md @@ -1,13 +1,14 @@ --- title: 关联子查询去关联 +summary: 了解如何给关联子查询解除关联。 category: performance --- # 关联子查询去关联 -[子查询相关的优化](/subquery-optimization.md)中介绍了当没有关联列时,TiDB 是如何处理子查询的。由于关联子查询解除关联依赖是一个比较复杂的工作,本文档中会介绍一些简单的场景以及这个优化规则的适用范围。 +[子查询相关的优化](/subquery-optimization.md)中介绍了当没有关联列时,TiDB 是如何处理子查询的。由于为关联子查询解除关联依赖比较复杂,本文档中会介绍一些简单的场景以及这个优化规则的适用范围。 -## 介绍 +## 简介 以 `select * from t1 where t1.a < (select sum(t2.a) from t2 where t2.b = t1.b)` 为例,这里子查询 `t1.a < (select sum(t2.a) from t2 where t2.b = t1.b)` 中涉及了关联列上的条件 `t2.b=t1.b`,不过恰好由于这是一个等值条件,因此可以将其等价的改写为 `select t1.* from t1, (select b, sum(a) sum_a from t2 group by b) t2 where t1.b = t2.b and t1.a < t2.sum_a;`。这样,一个关联子查询就被重新改写为 `JOIN` 的形式。 @@ -21,13 +22,15 @@ TiDB 之所以要进行这样的改写,是因为关联子查询每次子查询 ## 样例 -``` +{{< copyable "sql" >}} + +```sql create table t1(a int, b int); create table t2(a int, b int, index idx(b)); explain select * from t1 where t1.a < (select sum(t2.a) from t2 where t2.b = t1.b); ``` -``` +```sql +----------------------------------+----------+-----------+---------------+-----------------------------------------------------------------------------------------+ | id | estRows | task | access object | operator info | +----------------------------------+----------+-----------+---------------+-----------------------------------------------------------------------------------------+ @@ -44,15 +47,19 @@ explain select * from t1 where t1.a < (select sum(t2.a) from t2 where t2.b = t1. ``` -上面是优化生效的情况,可以看到 `HashJoin_11` 是一个普通的 `inner join`。 +上面是优化生效的情况,可以看到 `HashJoin_11` 是一个普通的 `inner join`。 -``` +接下来,关闭关联规则: + +{{< copyable "sql" >}} + +```sql insert into mysql.opt_rule_blacklist values("decorrelate"); admin reload opt_rule_blacklist; explain select * from t1 where t1.a < (select sum(t2.a) from t2 where t2.b = t1.b); ``` -``` +```sql +----------------------------------------+----------+-----------+------------------------+------------------------------------------------------------------------------+ | id | estRows | task | access object | operator info | +----------------------------------------+----------+-----------+------------------------+------------------------------------------------------------------------------+ @@ -69,4 +76,4 @@ explain select * from t1 where t1.a < (select sum(t2.a) from t2 where t2.b = t1. +----------------------------------------+----------+-----------+------------------------+------------------------------------------------------------------------------+ ``` -在执行了关闭规划规则的语句后,可以在 `IndexRangeScan_25(Build)` 的 `operator info` 中看到 `range: decided by [eq(test.t2.b, test.t1.b)]`,这部分信息就是关联依赖未被解除时,TiDB 可以使用关联条件进行索引范围查询的展示。 +在执行了关闭关联规则的语句后,可以在 `IndexRangeScan_25(Build)` 的 `operator info` 中看到 `range: decided by [eq(test.t2.b, test.t1.b)]`。这部分信息就是关联依赖未被解除时,TiDB 使用关联条件进行索引范围查询的显示结果。 From f252aae34fc72102b89694827a5b268ad2bdb2ab Mon Sep 17 00:00:00 2001 From: lilin90 Date: Mon, 25 May 2020 17:18:00 +0800 Subject: [PATCH 11/11] Update format --- subquery-optimization.md | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/subquery-optimization.md b/subquery-optimization.md index b591ed1f3432..73463c21d75e 100644 --- a/subquery-optimization.md +++ b/subquery-optimization.md @@ -1,11 +1,14 @@ --- title: 子查询相关的优化 +summary: 了解子查询相关的优化。 category: performance --- # 子查询相关的优化 -通常我们遇到的会是如下情况的子查询: +本文主要介绍子查询相关的优化。 + +通常会遇到如下情况的子查询: - `NOT IN (SELECT ... FROM ...)` - `NOT EXISTS (SELECT ... FROM ...)` @@ -13,7 +16,7 @@ category: performance - `EXISTS (SELECT ... FROM ...)` - `... >/>=/}} + +```sql +explain select * from t1 where t1.a in (select t2.a from t2); ``` -mysql> explain select * from t1 where t1.a in (select t2.a from t2); + +```sql +------------------------------+---------+-----------+------------------------+----------------------------------------------------------------------------+ | id | estRows | task | access object | operator info | +------------------------------+---------+-----------+------------------------+----------------------------------------------------------------------------+ @@ -61,16 +69,20 @@ mysql> explain select * from t1 where t1.a in (select t2.a from t2); 当前对于这种场景的子查询,当它不是关联子查询时,TiDB 会在优化阶段提前展开它,将其直接替换为一个结果集直接判断结果。如下图中,`EXISTS` 会提前在优化阶段被执行为 `TRUE`,从而不会在最终的执行结果中看到它。 +{{< copyable "sql" >}} + +```sql +create table t1(a int); +create table t2(a int); +insert into t2 values(1); +explain select * from t where exists (select * from t2); ``` -mysql> create table t1(a int); -mysql> create table t2(a int); -mysql> insert into t2 values(1); -mysql> explain select * from t where exists (select * from t2); + +```sql +------------------------+----------+-----------+---------------+--------------------------------+ | id | estRows | task | access object | operator info | +------------------------+----------+-----------+---------------+--------------------------------+ | TableReader_12 | 10000.00 | root | | data:TableFullScan_11 | | └─TableFullScan_11 | 10000.00 | cop[tikv] | table:t | keep order:false, stats:pseudo | +------------------------+----------+-----------+---------------+--------------------------------+ - ```