MySQL · 源码分析 · Subquery代码分析

本文涉及的产品
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
云数据库 RDS MySQL Serverless,价值2615元额度,1个月
简介: 在上一篇介绍derived table的文章中,开头已经介绍了MySQL中子查询的基本概念,具体详见:https://ata.alibaba-inc.com/articles/221930?spm=ata.25287382.0.0.78a241676LAHnE这篇文章将重点介绍条件/投影中的子查询在MySQL中的处理,例如SELECT t1.c2, (select sum(t2.c1) from 

在上一篇介绍derived table的文章中,开头已经介绍了MySQL中子查询的基本概念,具体详见:

https://ata.alibaba-inc.com/articles/221930?spm=ata.25287382.0.0.78a241676LAHnE

这篇文章将重点介绍条件/投影中的子查询在MySQL中的处理,例如

SELECT t1.c2, (select sum(t2.c1) from t2)   <= subq1
FROM t1 
WHERE t1.c1 in (select c2 from t1);          <= subq2

其投影列中,和where条件中,各有一个子查询,这些在MySQL中都使用Item来描述,具体来说是Item_subselect

背景

首先对于子查询这个SQL construct,有相关和非相关两个类别,SQL planner在完成语法解析和语义检查后,就可以确定一个子查询是否是相关的(和外层的某些表列有相关条件),当然在后续的优化过程中,还可能再相关/非相关之间转换,先来看下MySQL中对于subquery的描述结构。

所有子查询都是Item_subselect这个表达式类的子类,其继承关系如下:

Item_subselect
  -> Item_singlerow_subselect
     标量子查询,即最多只会产生一行结果的子查询,在投影列中的必然是这种
    -> Item_maxmin_subselect
       对于ANY/ALL子查询,在某些情况下可以转换为oe $cmp$ \<max\>(SELECT ...)的形式,这时用这个类来描述
       这个类是内部变换后产生
  -> Item_exists_subselect  EXIST子查询
    -> Item_in_subselect    IN子查询
      -> Item_allany_subselect  ALL/ANY/SOME子查询,经常与 < / > 这样的运算符配合使用

以上就是MySQL中支持的所有子查询类型,每种类型都可能是相关/非相关的,类内部会有标记来描述。

而各种不同类型的子查询,在MySQL中都有哪些可能的执行方式呢,只要看Subquery_strategy这个enum就可以了:

enum class Subquery_strategy : int {
  /// Nothing decided yet
  UNSPECIFIED,
  ...
  /// 转换为semi-join
  SEMIJOIN,
  /// 子查询一定是空集,直接替换为恒false
  ALWAYS_FALSE,
  /// 转换为derived table
  DERIVED_TABLE,
  /// EXIST方式执行(外表每行,触发内层子查询执行一次)
  SUBQ_EXISTS,
  /// 子查询物化(非相关)
  SUBQ_MATERIALIZATION,
};

略过其中一些中间状态的strategy(后面会讲到),最终执行方式无非5种。

Subquery处理流程

MySQL对于一条查询语句的处理基本分为3个阶段,分别是prepare -> optimize -> execute,我们从3个阶段分别看下对于subquery的处理,相对于derived table,子查询的处理要复杂很多,后面会涉及到大量函数,但有些函数的内部细节可以跳过,只关注最为重要的整体流程即可。

Prepare阶段

这个查询的处理在经过parse,基本就构成了最初级的抽象语法树(AST),即这种SELECT_LEX_UNIT作为root的嵌套结构,prepare阶段主要完成2件事情

  1. 负责对AST对resolve,包括所有涉及的tables/columns,以及每一个查询中的表达式(Item)
  2. 基于启发式规则,完成一些query transformation,包括将子查询转换为semi-join,outer join简化为inner join,derived table展开到上层,消除常量表达式等等。。。

这一阶段的transformation是完全基于规则的,不考虑代价因素。这里专注于对subquery的处理流程:

SELECT_LEX::prepare
... 
-> remove_redundant_subquery_clauses
   如果当前SELECT_LEX是子查询,看是否可以去掉其中的一些clause:
   1. single row的标量子查询,可以去掉order by
   2. IN/EXIST子查询,可以去掉order by / distinct 
                     如果有group by但没有aggr + having + rollup + windows,可以去掉group by
-> SELECT_LEX::resolve_subquery, 如果当前是子查询
  -> 对于IN/EXIST subselect,判断subquery是否可以转换为semi-join/anti-join,设置strategy为 CANDIDATE_FOR_SEMIJOIN
     FROM ot WHERE ot.x IN (SELECT y FROM it1)
     =>
     FROM ot semi-join it on ot.x = it.y
     这里会有很严苛的大量判断条件,极简的概括就是,只有SPJ的subquery,可以转换为semi/anti join
  
  -> 对于IN/EXIST subselect,如果无法转semi-join,尝试转为derived_table,设置strategy为 CANDIDATE_FOR_DERIVED_TABLE
     FROM ot WHERE ot.x IN (SELECT y FROM it1, it2)
     =>
     FROM ot LEFT JOIN (SELECT DISTINCT y FROM it1, it2) AS derived ON ot.x = derived.y
     WHERE derived.y IS NOT NULL
     同样大量条件判断,但与上面不同的是,如果当前是IN子查询,是允许其中包含group by + aggregation的
  
  -> 如果以上都不成立,调用select_transformer,做可能的变换(除一些特殊情况,都转换为EXISTS)
    -> Item_singlerow_subselect::select_transformer
       满足如下特定条件:
       子查询没有union + 没有table + 只有一个投影列 + 没有where + 没有having
       整个subquery可以被消除,直接用唯一的投影列替换掉子查询
    -> Item_in_subselect::select_transformer
      -> select_in_like_transformer 适用于IN/ALL/ANY/SOME子查询
        -> 创建Item_in_optimizer,这是一个辅助子查询执行的封装类,内部会处理左右NULL值时的计算
           具体细节请看Item_in_optimizer class注释
        -> 设置strategy为 CANDIDATE_FOR_IN2EXISTS_OR_MAT,表示尚不确定其执行方式是EXISTS还是物化
        -> 如果left_expr只有1列,调用single_value_transformer
          -> 在特定情况下,可以将ANY/ALL转换为MIN/MAX子查询
             是 >/>=/</<= 的运算符 + 非相关子查询 + 所在条件会将UNKNOWN视为FALSE的情况下可以转换
             没有group by + aggregation + 没有having + 没有windows
             1. 满足这些条件时,会构造一个新的Item_singlerow_subselect,但投影列是MIN/MAX的计算
             2. 不满足条件,构造一个Item_maxmin_subselect对象,替换原有IN子查询表达式   
          -> 无法转换,做如下一系列操作,来为转换为EXIST做准备
             1. 创建m_injected_left_expr,是一个Item_ref,指向IN子查询的左表达式(外层相关表达式)
             2. 创建In2exists_info结构,这个对象用来辅助做IN -> EXIST的转换
             3. 生成pushed_cond_guards数组,辅助对相关条件的两侧出现的NULL值的处理
          -> single_value_in_to_exists_transformer
             这个函数主要是把外层相关条件注入到子查询中,但根据子查询中是否有聚集,注入方式稍有不同
             1. 如果子查询中有group by/having/sum/window:
             -> 创建必要的Item_ref_null_helper/Item_func_trig_cond,放在having_cond里,在EXISTS的执行中使用,辅助left/right null值的处理
             2. 没有聚集
             -> 创建 Item_is_not_null_test/Item_func_trig_cond,放到where_cond/having_cond中
             具体变换详见函数注释和实现,这里不再深入
        -> 如果left_expr有多列,就变成一个row而不是一个标量,调用row_value_transformer
           内部处理逻辑其实和single value的基本一致,只是变成了对多个value的处理
    -> Item_exist_subselect没有实现select_transformer,不做任何处理

-> flatten_subqueries,对于之前收集的可以转换为semi-join/anti-join/derived_table的subquery,尝试转换
  遍历每个收集的subquery_item
  -> CANDIDATE_FOR_DERIVED_TABLE,调用 transform_table_subquery_to_join_with_derived,转换为derived_table
    -> strategy从 CANDIDATE_FOR_DERIVED_TABLE 变为 DERIVED_TABLE
    -> transform_subquery_to_derived
    ...
  -> CANDIDATE_FOR_SEMIJOIN
    -> strategy从 CANDIDATE_FOR_SEMIJOIN 变为 SEMIJOIN
    -> convert_subquery_to_semijoin
    ... 
  -> 对于sj_candidates中无法做前两种的,只能转为EXISTS
    -> 调用select_transformer,和前面逻辑相同

这里略过了transform_subquery_to_derived和convert_subquery_to_semijoin这两个函数的实现细节,内部还是比较复杂的,篇幅原因这里就不介绍了。

到这里针对子查询的主要变换逻辑就结束了,涉及的代码还是比较复杂的,有很多来回merge,替换item,添加item等操作,但其实主体的处理思路是比较清晰的:

  1. 对子查询内部化简
  2. 判断是否可能转换为semi-join,收集
  3. 判断是否可能转换为derived table,收集
  4. 都不行的,为转换为EXISTS做好准备
  5. 对上面收集的subquery做相应变换

完成这些变换后,所有subquery中,只剩下标记为CANDIDATE_FOR_IN2EXISTS_OR_MAT的subquery item,其执行方式在优化期确定。

Optimize阶段

优化期间会基于CBO估算的代价,决定CANDIDATE_FOR_IN2EXISTS_OR_MAT的子查询,最终是物化执行还是EXISTS方式执行。优化的逻辑很多,这里只介绍针对subquery的部分。

JOIN::optimize
...
-> JOIN::make_join_plan
   最主要的CBO优化函数,包括确定semi-join执行策略,join ordering,子查询执行方式...
  -> 当前是子查询,调用JOIN::decide_subquery_strategy 
     对于 CANDIDATE_FOR_IN2EXISTS_OR_MAT 的子查询, 基于代价决定其执行方式:exists或者materialize
    -> compare_costs_of_subquery_strategies
      -> Item_in_subselect::subquery_allows_materialization,硬性限制,无法做物化
      -> calculate_materialization_costs 物化的代价
      -> calculate_subquery_executions exists子查询的执行次数
      对比cost_mat和cost_exists
      如果是exist执行,finalize_exists_transform
      -> 只是非空判断,加上limit 1
      如果是materialize执行,finalize_materialization_transform
      -> remove_in2exists_conds 去掉在resolve阶段,为exist转换注入的额外cond
      -> 创建 subselect_hash_sj_engine (物化执行一定使用这个engine)
      -> subselect_hash_sj_engine::setup 用unit的投影列做初始化
         在leader上,对应item由于是后优化(在pq_finalize_shared_strategy之前),因此没有PQ_PUSHDOWN_SHARED标记,只有worker有!
        -> 正常创建Query_result_union,调用 create_result_table 创建物化的临时表,保存在Query_result_union::table上 
        -> 在table上建索引,要构建对应QEP_TAB/TABLE/TABLE_REF对象,便于后续读取
      -> pq_finalize_shared_strategy ,判断这个物化子查询是否可以shared方式下推到worker:  
         外层是并行的且子查询本身是非相关的,且已经决定要下推了,这里处理了IN_SUBS/ANY_SUBS/ALL_SUBS!!
         对于SINGLEROW_SUBS,add_subselects_to_template 里设置了PQ_PUSHDOWN_SHARED
...
-> JOIN::replace_index_subquery ,针对一种特殊情况,把Item_in_subselect的SUBQ_EXISTS策略,改为用 subselect_indexsubquery_engine 这个执行方式,这样本层subquery直接优化完成
...

Execute阶段

执行期间的逻辑就比较简单了,针对之前分析的几种执行方式,总结如下:

  1. semi-join/anti-join/derived table,由于都转换为join,原始的item对象已经没用
  2. 物化执行,EXISTS执行,都会由所在的条件/投影列的计算作为入口,调用Item_subselect::exec方法来执行
Item_subselect::exec
-> 对于前面提到的单表index lookup方式,调用subselect_indexsubquery_engine::exec
  -> ExecuteExistsQuery
     这是一个通用方法,用来执行一个返回boolen标量的子查询,传入的是在子查询优化时生成的root iterator
     对于单表lookup,这个iterator就是内层表的ref iterator
     返回结果保持在所属item的value字段中,供外层读取
-> 对于物化执行,调用subselect_hash_sj_engine::exec
  -> ExecuteExistsQuery
     也是类似的流程,但这里root iterator是表示物化操作的MaterializeIterator
    -> MaterializeIterator::Init 完成物化操作
      -> instantiate_tmp_table 创建引擎层的实际table对象,对应Query_result_union::table
      -> 遍历m_query_blocks_to_materialize这个数组,针对其中每个query block,调用MaterializeQueryBlock
        -> 每个query block对象也包含一个subquery_iterator,是对应子查询优化后的root iterator,触发执行流程,结果写入到物化table中
    -> MaterializeIterator::Read 读取物化结果表,触发后续执行流程
-> 对于EXISTS执行,调用SubqueryWithResult::exec
  -> SELECT_LEX_UNIT::exec 完成执行,返回标量的boolean结果传递到外层

总结

到这里对于subquery的大体处理流程已经讲完了,可以发现相对于derived table,subquery的处理流程复杂度要更高些,尤其集中在prepare阶段做的transformation,这里介绍的粒度仍然是比较粗的,可以作为outline,供感兴趣的同学可以进一步研究源码时参考。

相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
存储 SQL 监控
MySQL · 源码分析 · 8.0 原子DDL的实现过程续
之前的一篇月报MySQL · 源码分析 · 原子DDL的实现过程对MySQL8.0的原子DDL的背景以及使用的一些关键数据结构进行了阐述,同时也以CREATE TABLE为例介绍了Server层和Storage层统一系统表后如何创建一张新表进行了介绍。
1930 0
|
SQL Oracle 关系型数据库
MySQL · 源码分析 · Derived table代码分析
在具体介绍MySQL的derived table之前,先介绍一下子查询的概念。在MySQL中,包含2种类型的子查询:From字句中的子查询,例如select * from (select * from t1) tt;tt是一个抽象的表概念,内部就是一个子查询,在PG的概念中叫做sublink,MySQL则叫做derived table、view其他位置的子查询,如投影列中、条件中、having中,
320 0
MySQL · 源码分析 · Derived table代码分析
|
Prometheus 监控 Cloud Native
mysql exporter源码分析
通过对MySQL Exporter整体进行分析,实现一个自定义的demo收集,并进行采集的整合
487 0
|
SQL NoSQL 关系型数据库
MySQL · 源码分析 · MySQL Range (Min-Max Tree)结构分析
概述条件查询被广泛的使用在SQL查询中,复杂条件是否能在执行过程中被优化,比如恒为true或者false的条件,可以合并的条件。另外,由于索引是MySQL访问数据的基本方式,已经追求更快的访问方式,SARGable这个概念已经被我们遗忘了,因为他已经成为默认必要的方法(Search ARGument ABLE)。MySQL如何组织复杂条件并计算各个Ranges所影响到的对应可以使用的索引的代价和使
654 0
|
SQL 关系型数据库 MySQL
MySQL 子查询优化源码分析
# 子查询定义 在一个完整的查询语句中包含的子查询块被称为子查询。通常情况下,我们可以将出现在SELECT、WHERE和HAVING语法中的子查询块称为嵌套子查询,出现在FROM语法后的子查询块称为内联视图或派生表。 本篇文章将会结合源码介绍在MySQL中针对子查询的几种优化策略。 # 子查询在执行计划中的表示 ![temp.jpg](https://ata2-img.oss-cn
305 0
MySQL 子查询优化源码分析
|
SQL NoSQL 算法
MYSQL SUBQUERY执行过程
尝试从源码层面分析子查询在mysql内部的处理过程
404 0
|
关系型数据库 索引
MySQL · 源码分析 · 聚合函数(Aggregate Function)的实现过程
--- title: MySQL · 源码分析 · 聚合函数(Aggregate Function)的实现过程 author: 道客 --- ## 总览 聚合函数(Aggregate Function)顾名思义,就是将一组数据进行统一计算,常常用于分析型数据库中,当然在应用中是非常重要不可或缺的函数计算方式。比如我们常见的COUNT/AVG/SUM/MIN/MAX等等。本文主要分析下
1820 0
|
关系型数据库 MySQL 数据库
MySQL · 源码分析 · Innodb缓冲池刷脏的多线程实现
简介 为了提高性能,大多数的数据库在操作数据时都不会直接读写磁盘,而是中间经过缓冲池,将要写入磁盘的数据先写入到缓冲池里,然后在某个时刻后台线程把修改的数据刷写到磁盘上。MySQL的InnoDB引擎也使用缓冲池来缓存从磁盘读取或修改的数据页,如果当前数据库需要操作的数据集比缓冲池中的空闲页面大的话,当前缓冲池中的数据页就必须进行脏页淘汰,以便腾出足够的空闲页面供当前的查询使用。
1441 0
|
MySQL 关系型数据库 索引
MySQL · 源码分析 · binlog crash recovery
前言 本文主要介绍binlog crash recovery 的过程 假设用户使用 InnoDB 引擎,sync_binlog=1 使用 MySQL 5.7.20 版本进行分析 crash recovery 过程中,binlog 需要保证: 所有已提交事务的binlog已存在 所有未提交...
2491 0
|
22小时前
|
关系型数据库 MySQL Linux
Linux CentOs7 安装Mysql(5.7和8.0版本)密码修改 超详细教程
Linux CentOs7 安装Mysql(5.7和8.0版本)密码修改 超详细教程