MySQL 子查询优化源码分析

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: # 子查询定义 在一个完整的查询语句中包含的子查询块被称为子查询。通常情况下,我们可以将出现在SELECT、WHERE和HAVING语法中的子查询块称为嵌套子查询,出现在FROM语法后的子查询块称为内联视图或派生表。 本篇文章将会结合源码介绍在MySQL中针对子查询的几种优化策略。 # 子查询在执行计划中的表示 ![temp.jpg](https://ata2-img.oss-cn

子查询定义

在一个完整的查询语句中包含的子查询块被称为子查询。通常情况下,我们可以将出现在SELECT、WHERE和HAVING语法中的子查询块称为嵌套子查询,出现在FROM语法后的子查询块称为内联视图或派生表。

本篇文章将会结合源码介绍在MySQL中针对子查询的几种优化策略。

子查询在执行计划中的表示

temp.jpg

Semijoin/Antijoin

对于表示是否存在语义的查询语句,在语法上表示为IN/=ANY/EXISTS,优化器会尝试转换为semijoin/antijoin进行优化。与普通join会将左表和右表的记录连接在一起不同,semijoin/antijoin仅关心右表中是否存在可以与左表记录连接的记录,而返回左表记录。

在prepare阶段,优化器会首先检查当前查询是否可以转换为semijoin/antijoin的条件(由于antijoin是semijoin的相反,在代码层面也是一块处理的,所以之后的论述以semijoin为主),这部分代码在SELECT_LEX::resolve_subquery中,具体的条件总结如下:

  1. 子查询必须是谓词IN/=ANY/EXISTS的一部分,并且出现在WHERE或ON语法的最高层,可以被包含在AND表达式中。
  1. 必须是单个查询块,不带有UNION。
  2. 不包含HAVING语法。
  3. 不包含任何聚合函数。
  4. 不包含LIMIT语法。
  5. 外查询语句没有使用STRAIGHT_JOIN语法。

如果满足条件,将会把当前谓词加入到外查询的SELECT_LEX::sj_candidates中作为semijon的备选。

由于优化器对查询块的处理是一种递归的方式,在完成对子查询的判断之后,在外层查询的prepare阶段,会调用SELECT_LEX::flatten_subqueries函数完成子查询到semijoin的最终转换,这个过程在整个查询的生命周期只会发生一次,且不可逆。在SQL语法上等价为:

从一个带有备选semijoin子查询判断条件的查询块:
    SELECT ...
    FROM ot, ...
    WHERE oe IN (SELECT ie FROM it1 ... itN WHERE subq_where) AND outer_where
转换为:
    SELECT ...
    FROM ot SEMI JOIN (it1 ... itN), ...
    WHERE outer_where AND subq_where AND oe=ie

为了实现上述过程,需要进行以下步骤:

  1. 创建SEMI JOIN (it1 ... itN)语以部分,并加入到外层查询块的执行计划中。
  2. 将子查询的WHERE条件以及JOIN条件,加入到父查询的WHERE条件中。
  3. 将子查询谓词从父查询的判断谓词中消除。

具体的伪代码如下:

SELECT_LEX::flatten_subqueries()
     /* Semijoin flattening is bottom-up. Indeed, we have this execution flow,
        for SELECT#1 WHERE X IN (SELECT #2 WHERE Y IN (SELECT#3)) :

        SELECT_LEX::prepare() (select#1)
           -> fix_fields() on IN condition
               -> SELECT_LEX::prepare() on subquery (select#2)
                   -> fix_fields() on IN condition
                        -> SELECT_LEX::prepare() on subquery (select#3)
                        <- SELECT_LEX::prepare()
                   <- fix_fields()
                   -> flatten_subqueries: merge #3 in #2
                   <- flatten_subqueries
               <- SELECT_LEX::prepare()
           <- fix_fields()
           -> flatten_subqueries: merge #2 in #1

        Note that flattening of #(N) is done by its parent JOIN#(N-1), because
        there are cases where flattening is not possible and only the parent can
        know.*/
   |--子查询层层嵌套中采用bottom-up的方式去展开。在fix_fields()的过程中依次从里往外。仅支持IN和EXISTS的子查询,且内层的sj_candidates为空。
   |--由于在WHERE条件同一层可能存在多个可以展开的子查询判断,首先会计算优先级来决定semijoin展开顺序:
      1. 依赖外层查询的子查询优先于不相关子查询。
      2. 有着更多表的子查询优先于更少表的子查询。
      3. 顺序上先计算的子查询优先于后计算的。
   |--semijoin子查询不能和antijoin子查询相互嵌套。
   |--判断子查询的WHERE条件是否为常量。
      如果判断条件永远为FALSE,那么子查询结果永远为空。该情况下,可以将子查询直接清除,不用转换成semijoin。
   |--替换外层查询的WHERE条件中子查询判断的条件
      1. 子查询内条件并不永远为FALSE,或者永远为FALSE的情况下,需要改写为antijoin(antijoin情况下,子查询结果永远为空,外层查询条件永远通过)。
         此时将条件改为永远为True。
      2. 子查询永远为FALSE,且不是antijoin。那么将外层查询中的条件改成永远为False。
      /* 子查询判断条件可能为IN/=ANY/EXISTS,或者对应的否定。参数为Item_exists_subselect *。
         The following transformations are performed:

         1. IN/=ANY predicates on the form:

         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 ...]

         are transformed into:

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

         Notice that the inner-cond may contain correlated and non-correlated
         expressions. Further transformations will analyze and break up such
         expressions.

         2. EXISTS predicates on the form:

         SELECT ...
         FROM ot1 ... otN
         WHERE EXISTS (SELECT expressions
                       FROM it1 ... itK
                       [WHERE inner-cond])
          [AND outer-cond]
         [GROUP BY ...] [HAVING ...] [ORDER BY ...]

         are transformed into:

         SELECT ...
         FROM (ot1 ... otN) SJ (it1 ... itK)
                            [ON inner-cond]
         [WHERE outer-cond]
         [GROUP BY ...] [HAVING ...] [ORDER BY ...]
         
         3. Negated EXISTS predicates on the form:

         SELECT ...
         FROM ot1 ... otN
         WHERE NOT EXISTS (SELECT expressions
                       FROM it1 ... itK
                       [WHERE inner-cond])
          [AND outer-cond]
         [GROUP BY ...] [HAVING ...] [ORDER BY ...]

         are transformed into:

         SELECT ...
         FROM (ot1 ... otN) AJ (it1 ... itK)
                            [ON inner-cond]
         [WHERE outer-cond AND is-null-cond(it1)]
         [GROUP BY ...] [HAVING ...] [ORDER BY ...]

         where AJ means "antijoin" and is like a LEFT JOIN; and is-null-cond is
         false if the row of it1 is "found" and "not_null_compl" (i.e. matches
         inner-cond).
         
         4. Negated IN predicates on the form:

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

         are transformed into:

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

         5. The cases 1/2 (respectively 3/4) above also apply when the predicate is
         decorated with IS TRUE or IS NOT FALSE (respectively IS NOT TRUE or IS FALSE).*/
   |--SELECT_LEX::convert_subquery_to_semijoin() // 将当前查询块中包含的子查询判断转换成TABLE_LIST中的semijoin嵌套,antijoin也在里面完成。
     |--生成一个新的semijoin嵌套的TABLE_LIST表
     |--TABLE_LIST::merge_underlying_tables() // 将子查询中潜在的表合并到上述join表中
     |--将子查询的叶子表插入到当前查询块的叶子表后面,重新设置子查询的叶子表的序号和依赖的外表。将子查询的叶子表重置。
     |--如果是outer join的话,在join链表中传递可空性。
     |--SELECT_LEX::decorrelate_condition()
       |--将内层子查询中的关联条件去关联化,这些条件被加入到semijoin的列表里。这些条件必须是确定的,仅支持简单判断条件或者由简单判断条件组成的AND条件。
       |--decorrelate_equality()
         |--判断左右条件是否仅依赖于内外层表,将其表达式分别加入到semijoin内外表的表达式列表中。
     |--decorrelate_join_conds() // 解关联内层查询的join条件
     |--Item_cond_and::fix_after_pullout() // 将子查询的WHERE条件上拉,更新使用表的信息
     |--SELECT_LEX::build_sj_cond() // 根据semijoin的条件列表创建AND条件,如果有条件为常量True,则去除该条件;如果常量为False,则整个条件都去除。
     |--将创建出来的semijoin条件加入到外层查询的WHERE条件中

物化执行 or 迭代式循环执行

对于不能采用semijoin/antijoin执行的存在式语义的子查询,在MySQL源码的表示含义下,会做IN->EXISTS的转换,其实本质是在物化执行和迭代式循环执行中做选择。IN语法代表非相关子查询仅执行一次,将查询结果物化成临时表,之后需要结果时候就去物化表中查找;EXISTS代表对于外表的每一条记录,子查询都会执行一次,是迭代式循环执行。

MySQL会在prepare阶段尝试做IN->EXISTS的转换,然后在optimize阶段,比较IN or EXISTS执行的代价,最后根据代价决定采用哪种执行策略完成最终转换。

在prepare阶段IN->EXISTS的转换主要是将IN语法的左表达式与右表达式中子查询的输出列对应组合,加入到子查询的WHERE或者HAVING条件中,在SQL语义上表示为:

outer_expr IN (SELECT inner_expr FROM ... WHERE subquery_where)
转换为:
EXISTS (SELECT 1 FROM ... WHERE subquery_where AND outer_expr=inner_expr)

这一过程主要发生在Item_in_subselect::single_value_in_to_exists_transformer中,详细过程为:

/* 通过判断条件注入将IN语法转换为EXISTS语法
   向子查询中注入额外的判断条件,并将子查询标记为关联子查询。*/
|--Item_in_subselect::single_value_in_to_exists_transformer()
  |--如果子查询包含聚合函数、窗口函数、GROUP语法、HAVING语法,将判断条件加入到HAVING语法中。
  |--如果我们想区分NULL和False的结果的话,将这个条件封装到触发器中。
     SELECT ie FROM ... HAVING subq_having AND
               trigcond(oe $cmp$ ref_or_null_helper<ie>)
  |--创建指向子查询唯一列的Item_ref_null_helper对象,与之前注入的左表达式Item_ref共同创建比较表达式
  |--如果子查询的第一个列为包含聚合列的表达式,那么WHERE和HAVING语法中可能通过不同的Item_ref引用到这个Item,存入到Item_sum::ref_by数组中
  |--and_items() // 加入到HAVING条件中
|--如果不包含聚合函数、窗口函数、GROUP语法、HAVING语法,将判断条件加入WHERE语句中
  |--如果不需要区分NULL与False的结果:
     SELECT 1 FROM ... WHERE (oe $cmp$ ie) AND subq_where
  |--如果需要区分上述结果的差别,使用触发器
     SELECT 1 FROM ...
     WHERE subq_where AND trigcond((oe $cmp$ ie) OR (ie IS NULL))
           HAVING trigcond(@<is_not_null_test@>(ie))
  |--其他,单个查询块,没有表及上述语法,直接用条件表达式在外查询中替代

总结

以上就是MySQL中针对子查询所做的大部分优化和转换的工作,代码分析基于MySQL 8.0.19版本。

参考:https://dev.mysql.com/doc/refman/8.0/en/subquery-optimization.html

相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
19天前
|
SQL 关系型数据库 MySQL
深入解析MySQL的EXPLAIN:指标详解与索引优化
MySQL 中的 `EXPLAIN` 语句用于分析和优化 SQL 查询,帮助你了解查询优化器的执行计划。本文详细介绍了 `EXPLAIN` 输出的各项指标,如 `id`、`select_type`、`table`、`type`、`key` 等,并提供了如何利用这些指标优化索引结构和 SQL 语句的具体方法。通过实战案例,展示了如何通过创建合适索引和调整查询语句来提升查询性能。
118 9
|
2月前
|
SQL 关系型数据库 MySQL
大厂面试官:聊下 MySQL 慢查询优化、索引优化?
MySQL慢查询优化、索引优化,是必知必备,大厂面试高频,本文深入详解,建议收藏。关注【mikechen的互联网架构】,10年+BAT架构经验分享。
大厂面试官:聊下 MySQL 慢查询优化、索引优化?
|
2天前
|
SQL 存储 关系型数据库
MySQL秘籍之索引与查询优化实战指南
最左前缀原则。不冗余原则。最大选择性原则。所谓前缀索引,说白了就是对文本的前几个字符建立索引(具体是几个字符在建立索引时去指定),比如以产品名称的前 10 位来建索引,这样建立起来的索引更小,查询效率更快!
41 22
 MySQL秘籍之索引与查询优化实战指南
|
24天前
|
缓存 关系型数据库 MySQL
MySQL 索引优化以及慢查询优化
通过本文的介绍,希望您能够深入理解MySQL索引优化和慢查询优化的方法,并在实际应用中灵活运用这些技术,提升数据库的整体性能。
61 18
|
13天前
|
存储 关系型数据库 MySQL
10个案例告诉你mysql不使用子查询的原因
大家好,我是V哥。上周与朋友讨论数据库子查询问题,深受启发。为此,我整理了10个案例,详细说明如何通过优化子查询提升MySQL性能。主要问题包括性能瓶颈、索引失效、查询优化器复杂度及数据传输开销等。解决方案涵盖使用EXISTS、JOIN、IN操作符、窗口函数、临时表及索引优化等。希望通过这些案例,帮助大家在实际开发中选择更高效的查询方式,提升系统性能。关注V哥,一起探讨技术,欢迎点赞支持!
|
23天前
|
缓存 关系型数据库 MySQL
MySQL 索引优化以及慢查询优化
通过本文的介绍,希望您能够深入理解MySQL索引优化和慢查询优化的方法,并在实际应用中灵活运用这些技术,提升数据库的整体性能。
22 7
|
22天前
|
缓存 关系型数据库 MySQL
MySQL 索引优化与慢查询优化:原理与实践
通过本文的介绍,希望您能够深入理解MySQL索引优化与慢查询优化的原理和实践方法,并在实际项目中灵活运用这些技术,提升数据库的整体性能。
57 5
|
2月前
|
关系型数据库 MySQL Java
MySQL索引优化与Java应用实践
【11月更文挑战第25天】在大数据量和高并发的业务场景下,MySQL数据库的索引优化是提升查询性能的关键。本文将深入探讨MySQL索引的多种类型、优化策略及其在Java应用中的实践,通过历史背景、业务场景、底层原理的介绍,并结合Java示例代码,帮助Java架构师更好地理解并应用这些技术。
57 2
|
13天前
|
存储 Oracle 关系型数据库
数据库传奇:MySQL创世之父的两千金My、Maria
《数据库传奇:MySQL创世之父的两千金My、Maria》介绍了MySQL的发展历程及其分支MariaDB。MySQL由Michael Widenius等人于1994年创建,现归Oracle所有,广泛应用于阿里巴巴、腾讯等企业。2009年,Widenius因担心Oracle收购影响MySQL的开源性,创建了MariaDB,提供额外功能和改进。维基百科、Google等已逐步替换为MariaDB,以确保更好的性能和社区支持。掌握MariaDB作为备用方案,对未来发展至关重要。
39 3
|
13天前
|
安全 关系型数据库 MySQL
MySQL崩溃保险箱:探秘Redo/Undo日志确保数据库安全无忧!
《MySQL崩溃保险箱:探秘Redo/Undo日志确保数据库安全无忧!》介绍了MySQL中的三种关键日志:二进制日志(Binary Log)、重做日志(Redo Log)和撤销日志(Undo Log)。这些日志确保了数据库的ACID特性,即原子性、一致性、隔离性和持久性。Redo Log记录数据页的物理修改,保证事务持久性;Undo Log记录事务的逆操作,支持回滚和多版本并发控制(MVCC)。文章还详细对比了InnoDB和MyISAM存储引擎在事务支持、锁定机制、并发性等方面的差异,强调了InnoDB在高并发和事务处理中的优势。通过这些机制,MySQL能够在事务执行、崩溃和恢复过程中保持
42 3