【MySQL·8.0·源码】subquery 子查询处理分析(二)

引文

【MySQL·8.0·源码】subquery 子查询处理分析(一)中,已经介绍了 MySQL 子查询的语法树形式,并简单介绍了非相关 scalar 子查询的一些处理流程,本文将继续介绍更多子查询的处理流程。

本文后续以 “分析(一)” 来代替 “【MySQL·8.0·源码】subquery 子查询处理分析(一)”

假如对子查询不做任何优化处理,那么子查询应该怎么执行呢?

先来看一个 IN 子查询

Q1>: select * from t1 where a in (select pk from t2);

显然,先执行子查询,将 t2 的所有 pk 都查询出来,然后以所有的 pk 作为查询条件,检索 t1 中满足条件的记录返回即可。

再来看一个 Exists 子查询

Q2>: select * from t3 where exists (select * from t2 where t2.b=t3.a);

在 “分析(一)” 中已经知道其等同于

select * from t3 where WHERE 0 < (select count(*) from t2 where t2.b=t3.a)

所以只要检索遍历 t3 表记录,然后用 t3 一行行的 t3.a 值去充当 t2 的 select count(*) 条件,不为 0 则返回 t3 记录,否则跳过并继续下一行即可。

到此,IN 子查询和 Exists 子查询的大致执行过程已经清楚,那么就此执行会有什么问题?

  1. 假如 Q1 中,t2 表非常大,而 t1 表只有几行记录,也要遍历完 t2 表,代价很高
  2. 假如 Q2 中,t3 表非常大,而 t2 表满足条件的记录只有几行记录,也要遍历完所有的 t3 表,代价也很高

有没有发现,上述问题都是子查询按着严格的表的先后执行顺序引来的,那么如果子查询是否可以不按着严格的先后执行顺序来执行?

这就引申出了对子查询的各种优化策略。

去重策略

总统上,为了打破子查询的固有执行顺序,只有将子查询转换为 JOIN 操作,
但转换为 JOIN 操作后会引来什么问题? 例如:

Q3> select * from t1 where a in (select b from t2);

转换为 JOIN 之后

Q4> select * from t1 join t2 where t1.a = t2.b;

假如
t1 有 2 条记录,t1(a,b):(1,1),(2,2)
t2 有 4 条记录,t2(a,b):(1,1),(1,1),(2,2),(2,2)

则转换为 JOIN 之后结果为:

select * from t1 join t2 where t1.a = t2.b;
+------+------+------+------+
| a    | b    | a    | b    |
+------+------+------+------+
|    1 |    1 |    1 |    1 |
|    2 |    2 |    2 |    2 |
|    1 |    1 |    1 |    1 |
|    2 |    2 |    2 |    2 |
+------+------+------+------+
4 rows in set (0.00 sec)

而子查询结果为:

select * from t1 where a in (select b from t2);
+------+------+
| a    | b    |
+------+------+
|    1 |    1 |
|    2 |    2 |
+------+------+

可以看到,如果做 JOIN 操作,可能导致问题:

  1. Q4 查询结果的 schema 比子查询 Q3 多,Q3 只需要 t1 表对应的列;
  2. Q4 查询结果可能会比子查询 Q3 更多,原因在于子查询 t2 有重复的记录
Q5> select b from t2;
+------+
| b    |
+------+
|    1 |
|    2 |
|    1 |
|    2 |
+------+

为了解决第 1 个问题,需要保证外表的 select list 中的 “*” 要在转成 JOIN 之前展开或者把 JOIN 换为 Semi-Join 即可,即表明只需要返回外表关系的 select list;
为了解决第 2 个问题,需要对结果进行去重 duplicate remove 即可,去重也可以选择对子查询去重,也可以选择对外表结果集去重:

  • 对子查询去重即去除 Q5 中重复的记录,即只返回 1,2,而不是 1,2,1,2,即可得到正确结果;
  • 对外表结果去重即去除 Q4 中重复的记录也能得到正确结果

MySQL 对子查询的优化策略总是围绕着这些点进行的

MySQL 优化策略

在 MySQL 中,对子查询的优化策略可以总结为:

  • Transform to join derived table
  • Transform to semi-join
    • Pullout as simple JOIN
    • FirstMatch
    • LooseScan
    • DuplicateWeedout
    • Materialization
      • MaterializeLookup
      • MaterializeScan
  • Exists
    • Transform IN to Exists
    • Exists/Not Exists
  • Materialize
    • MaterializeLookup

首先,对于特定的子查询,MySQL 支持将其子查询谓词转换为派生表来做 JOIN 操作
其次会尽可能尝试转换为 semi-join 来优化表的驱动顺序
MySQL 有一系列相关的开关进行控制(optimizer_switch)

subquery_to_derived=on;
semijoin=on;
loosescan=on
firstmatch=on
duplicateweedout=on
subquery_materialization_cost_based=on

Transform

在子查询诸多优化策略确定之前,在 prepare 阶段,需要将语法树进行 rewrite 改写,改写会改变整个
父子查询 query_expression 和 query_block 的关系,也可能将 where 和 on 条件进行整合重组,整个
带子查询的 Transform 大致处理流程为:

例如有查询语句:
SELECT #1 WHERE X IN (SELECT #2 WHERE Y IN (SELECT#3)) :

Query_block::prepare()(select#1-> Query_block::setup_conds() 在父查询块 IN 条件上执行 Item_exists_subselect::fix_fields()
        -> Query_block::prepare() 处理子查询(select#2-> 在IN条件上执行 fix_fields()
                  -> Query_block::prepare() 处理子查询(select#3<- Query_block::prepare()
             <-fix_fields() 中结束
             -> flatten_subqueries: 将 #3 合并到 #2<- 在 flatten_subqueries 中结束
         <- Query_block::prepare()
     <-fix_fields() 中结束
     -> flatten_subqueries: 将 #2 合并到 #1

从父查询块在解析条件到子查询条件时,不断递归到子查询块的 prepare 工作,再不断深入子查询条件解析,
如果可能,进一步递归到子查询 in 条件的下一个子查询的解析中。
在这里插入图片描述
整个工作自顶而下,从父查询一直到最底层子查询
然后自下往上的进行子查询相关优化的判断,可能则进行自底而上的进行 merge 合并,达到去除子查询并简化查询块的目的

Transform to join derived table

转换形式为

FROM [tables] WHERE ... AND/OR oe IN (SELECT ie FROM it) ...
变为
FROM (tables) LEFT JOIN (SELECT DISTINCT ie FROM it) AS derived
              ON oe = derived.ie WHERE ... AND/OR derived.ie IS NOT NULL ...

例如,对于上述的 Q3 转换为 join derived table 为:

select t1.* from t1 left join (select distinct b from t2) as derived 
                             on t1.a = derived.b where derived.b is not null;

对于 derived table 的 Transform,子查询需要满足以下条件,才能称为候选者

 Item_exists_subselect::is_derived_candidate(THD *thd)
  • 开关已开启,optimizer_switch = ‘subquery_to_derived=on’
  • 子查询是一个简单的查询(非集合运算,例如 union/intersect/except,非带有括号嵌套查询)
  • 如果是 [Not] Exists 子查询,子查询必须不带 group 或者 windows 函数
  • 子查询谓词是
    • 在 Where 子句中,非 On 子句
    • 子查询谓词必须直接链接在根条件谓词 AND 或 OR 之下
  • 父查询块必须支持接受 semi-join(如果父查询是 update/delete 是不支持的)
  • 父查询块必须存在基表,因为转换之后的 derived 是通过 left join 连接到父查询块基表后的
  • 父查询块必须未禁止 semi-join 操作
  • 如果是 IN 谓词,IN 谓词的左操作数必须是确定性的(例如,非 random 函数)
  • not in/not = ,左右谓词可能为 NULLs 的都不允许做 semi-join

Transform

Query_block::transform_table_subquery_to_join_with_derived()

对于 derived table 转换,查询块会创建一个派生表添加到外部查询块上,派生表的名称一般为 “derived_%d_%d”,
后面两个数字为查询块的 id,例如把 select #3 子查询创建一个派生表添加到 select #2 查询块上,名称为 “derived_2_3”。

同时,子查询 IN 表达式会被替换为关联条件,例如 select #2 上的 Item_in_subselect 替换为 Item_func_eq(t2.a,derived_2_3.Name_exp_1)。
在这里插入图片描述

Transform to semi-join

还是以 Q3 为例,转换为 semi-join 为:

select t1.* from t1 semi join t2 where t1.a = t2.b;

对于 semi join 的 Transform,子查询需要满足以下条件,才能称为候选者

Item_exists_subselect::is_semijoin_candidate()
  • 启用了 semi join,optimizer_switch = ‘semijoin=on’
  • 子查询是简单的查询块(非集合运算,例如 union/intersect/except,非带有括号嵌套查询)
  • 子查询没有 group 聚集(显式的或隐式的)
  • 子查询没有 having
  • 子查询没有 window 窗口函数
  • 子查询谓词在 ON/WHERE 子句中,并且在根 AND 谓词下
  • 父查询块接受 semi-join(如果父查询是 update/delete 是不支持的)
  • 子查询不是不存在基表的查询,例如:select 1
  • 父子查询均无 straight join
  • 父查询块未禁止 semi-join
  • IN 谓词左侧是确定性的
  • not in/not = ,左右谓词可能为 NULLs 的都不允许做 semi-join
  • 引擎要么支持 anti-join,要么不是 anti-join
  • 如果存在 OFFSET,则需从第一行开始,LIMIT 不为 0

Transform

Query_block::convert_subquery_to_semijoin()

对于 semi join 转换,查询块会不断通过调用 convert_subquery_to_semijoin() 尝试将查询块的子查询谓词(比如说 IN 谓词)转换为
Table_ref 的半连接嵌套查询,

转换形式为:

  • IN
  SELECT ...
  FROM ot1 ... otN
  WHERE (oe1, ... oeM) IN (SELECT ie1, ..., ieM
                           FROM it1 ... itK
                          [WHERE inner-cond])
   [AND outer-cond]
  [GROUP BY ...] [HAVING ...] [ORDER BY ...]

会被转换为

  SELECT ...
  FROM (ot1 ... otN) SJ (it1 ... itK)
                     ON (oe1, ... oeM) = (ie1, ..., ieM)
                        [AND inner-cond]
  [WHERE outer-cond]
  [GROUP BY ...] [HAVING ...] [ORDER BY ...]
  • EXISTS
  SELECT ...
  FROM ot1 ... otN
  WHERE EXISTS (SELECT expressions
                FROM it1 ... itK
                [WHERE inner-cond])
   [AND outer-cond]
  [GROUP BY ...] [HAVING ...] [ORDER BY ...]

会被转换为

  SELECT ...
  FROM (ot1 ... otN) SJ (it1 ... itK)
                     [ON inner-cond]
  [WHERE outer-cond]
  [GROUP BY ...] [HAVING ...] [ORDER BY ...]
  • 同理 NOT IN、NOT EXISTS 会被转换为 Anti Join

用图示来展示之前 Q3 的转换如下在这里插入图片描述

Transform to exists

最终,当子查询无法称为 derived table 或 semi join 的候选者时,只能对子查询做 IN->Exists 转换查询
遍历查询块的子查询谓词,调用子查询谓词对应的 transform() 函数进行转换

Item_exists_subselect::transformer()
	Item_in_subselect::transformer()
	Item_allany_subselect::transformer()
	Item_in_subselect::transformer()
	Item_allany_subselect::transformer()

在 Item_in_subselect::transformer() 中,会做一些规则性的转换,例如:

  • 非表的 IN、NOT IN 子查询并且不带 group by、where、和 having 子句
    例如:

    select * from t1 where a  in (select 3)
    

    IN 子查询会被转换为 where t1.a = 3

  • 单列子查询,子查询左侧操作数为单列
    单列子查询会调用 Item_in_subselect::single_value_transformer,以尝试是否可能将子查询转换为 scalar 查询,形如:

    oe $cmp$ (SELECT ie FROM ... WHERE subq_where ... HAVING subq_having)
    

    可以尝试转换为:

    - oe $cmp$ (SELECT MAX(...) )  // 由 Item_singlerow_subselect 处理
    - oe $cmp$ \<max\>(SELECT ...)   // 由 Item_maxmin_subselect 处理
    

    如果不能转换,则使用 Item_in_optimzer 类处理子查询

  • 多列子查询,子查询左侧操作数为多列
    多列子查询会调用 Item_in_subselect::row_value_transformer(),根据规则对多列 IN/ALL/ANY 子查询做一定的重写,形如:

      (oe1, oe2, ...)  $cmp$ (SELECT ie1, ie2, ... FROM ... WHERE subq_where ...
                              HAVING subq_having)
    

    转换为类似形如:

    (l1, l2, l3) IN (SELECT v1, v2, v3 ... WHERE where) =>
          EXISTS (SELECT ... WHERE where and
                                   (l1 = v1 or is null v1) and
                                   (l2 = v2 or is null v2) and
                                   (l3 = v3 or is null v3)
                             HAVING is_not_null_test(v1) and
                                    is_not_null_test(v2) and
                                    is_not_null_test(v3))
    

    在 where 条件中,为每个左操作创建等值连接条件

对于子查询做完一定的 Transform 之后,就会赋予子查询临时性的执行策 Subquery_strategy::CANDIDATE_FOR_IN2EXISTS_OR_MAT,
即在 IN2EXISTS 转换或 Materialization 两种策略之中二选一,
然后在 optimize 阶段的 JOIN::make_join_plan()->JOIN::decide_subquery_strategy() 中最终决定走 Materialization 或者 EXISTS 执行策略。

  • 9
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

抡着鼠标扛大旗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值