前几天在看官方文档的时候,看到关于 dependent subquery 和 uncacheable subquery 的一句说明,被搞的神魂颠倒的,各种百度、谷歌也没有找到能够解释清楚这句话是什么意思的,去折腾了几天 5.7.35 版本的源码,对于 subquery、dependent subquery、uncacheable subquery 的执行过程,多了一些了解。

官方文档的说明是这样的:

DEPENDENT SUBQUERY evaluation differs from UNCACHEABLE SUBQUERY evaluation. For DEPENDENT SUBQUERY, the subquery is re-evaluated only once for each set of different values of the variables from its outer context. For UNCACHEABLE SUBQUERY, the subquery is re-evaluated for each row of the outer context.

我理解的意思大概是这样的:

dependent subquery 和 uncacheable subquery 的执行过程不一样。dependent subquery,对于外层查询的每一组变量的不同值,子查询只执行一次。uncacheable subquery,对于外层查询的每一行,子查询都会执行一次。

以前,我对于 dependent subquery 和 uncacheable subquery 的执行过程的理解是一样的:
对于外层查询的每一行,都会把子查询中使用到的外层查询的字段替换为具体的值,去执行一次子查询。

看了官方文档的说明之后,把我搞懵了,怎么也理解不了文档中那句话的意思,折腾完源码之后,发现了它们之间的不一样,接下来就说说 subquery、dependent subquery、uncacheable subquery 这 3 类子查询的执行过程。

进入正题之前,还有一点要唠叨一下,本文所讲的内容,是以 optimizer_switch='semijoin=off' 以及 optimizer_switch='materialization=on' 为前提的,为什么呢?

因为,子查询有 5 种优化策略

  1. 子查询表上拉(table pullout)
  2. 重复值消除(duplicate weedout)
  3. 首次匹配(first match)
  4. 松散扫描(loose scan)
  5. 物化

第 1 ~ 4 种优化策略,子查询都不再是子查询了,会和外层查询组成一个连接(JOIN)查询,只有第 5 种优化策略,子查询还是那个子查询。

使用 optimizer_switch='semijoin=off' 就是把第 2 ~ 4 种优化策略给禁用了,使用 optimizer_switch='materialization=on' 就是把第 5 种优化策略启用了(当然,这个选项在 MySQL 5.7.35 中默认是开启的,其它版本没试过),至于第 1 种策略,本文的示例 SQL 中都不满足使用这种优化策略。

1. subquery

select * from t1 where t1.i1 in (select i1 from t3 where id in (3, 666, 990, 887, 76) and i2 > 16384
);
+----+-------------+-------+------------+-------+---------------+---------+---------+--------+------+----------+-------------+
| id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref    | rows | filtered | Extra       |
+----+-------------+-------+------------+-------+---------------+---------+---------+--------+------+----------+-------------+
| 1  | PRIMARY     | t1    | <null>     | ALL   | <null>        | <null>  | <null>  | <null> | 8    | 100.0    | Using where |
| 2  | SUBQUERY    | t3    | <null>     | range | PRIMARY       | PRIMARY | 4       | <null> | 5    |  33.33   | Using where |
+----+-------------+-------+------------+-------+---------------+---------+---------+--------+------+----------+-------------+

从 explain 结果可以看到,上面的 SQL 中子查询的执行方式 SUBQUERY,接下来我们通过 optimizer_trace 看一下它的大体执行方案:

{"join_preparation": {"select#": 1,"steps": [{"join_preparation": {"select#": 2,"steps": [{"IN_uses_bisection": true},{"expanded_query": "/* select#2 */ select `t3`.`i1` from `t3` where ((`t3`.`id` in (3,666,990,887,76)) and (`t3`.`i2` > 16384))"},{"transformation": {"select#": 2,"from": "IN (SELECT)","to": "semijoin","chosen": false}},{"transformation": {"select#": 2,"from": "IN (SELECT)","to": "EXISTS (CORRELATED SELECT)","chosen": true,"evaluating_constant_where_conditions": []}}]}},{"expanded_query": "/* select#1 */ select `t1`.`id` AS `id`,`t1`.`str1` AS `str1`,`t1`.`str2` AS `str2`,`t1`.`str3` AS `str3`,`t1`.`str4` AS `str4`,`t1`.`i1` AS `i1`,`t1`.`type` AS `type`,`t1`.`i2` AS `i2`,`t1`.`blob1` AS `blob1` from `t1` where <in_optimizer>(`t1`.`i1`,<exists>(/* select#2 */ select `t3`.`i1` from `t3` where ((`t3`.`id` in (3,666,990,887,76)) and (`t3`.`i2` > 16384) and (<cache>(`t1`.`i1`) = `t3`.`i1`))))"}]}
}

上面的 JSON 是 查询准备阶段 的 trace,从中可以看到,没有使用半连接进行子查询优化("from": "IN (SELECT)", "to": "semijoin", "chosen": false),因为我通过设置 set optimizer_switch='semijoin=off' 把半连接优化给关掉了。

"from": "IN (SELECT)", "to": "EXISTS (CORRELATED SELECT)", "chosen": true 表示 IN 子查询被转换成了 EXISTS 相关子查询。

{"transformation": {"select#": 2,"from": "IN (SELECT)","to": "materialization","chosen": true,"unknown_key_1": {"creating_tmp_table": {"tmp_table_info": {"row_length": 5,"key_length": 4,"unique_constraint": false,"location": "memory (heap)","row_limit_estimate": 3355443}}}}
}

上面的 JSON 是 查询优化阶段 的 trace,从中可以看到,使用了物化(materialization)来优化子查询。

说完了大体的执行方案,接下来详细描述一下子查询执行的具体过程:

第 1 步

外层查询从存储引擎读取一条记录

第 2 步

where <in_optimizer>(`t1`.`i1`,<exists>(/* select#2 */ select `t3`.`i1` from `t3` where ((`t3`.`id` in (3,666,990,887,76)) and (`t3`.`i2` > 16384) and (<cache>(`t1`.`i1`) = `t3`.`i1`))))

这是上面的 SQL 语句在查询准备阶段处理之后,WHERE 条件变成的样子,可以看到变成 exists 了,t1.i1 in (子查询) 变成了 ((t1.i1) = t3.i1) 下推到子查询中了。

外层查询从存储引擎读取到第 1 条记录时,会对外层查询的 WHERE 条件进行求值,此时就会第一次执行子查询(注意:此时,外层查询的字段 t1.i1 就已经有具体的值了,这个在后面执行子查询时会用到这个字段的具体值)。

因为会使用物化表来存储子查询的结果集,所以,在第一次执行时,会把子查询符合条件的记录存储到物化表中。

对于子查询中查出来的每一条记录,会判断记录是否匹配 ((t3.id in (3,666,990,887,76)) and (t3.i2 > 16384) 这两个查询条件,匹配就插入到物化表,不匹配就忽略。

第 3 步

子查询结果集物化完成之后,会循环读取物化表的每一条记录,判断记录是否匹配 ((t1.i1) = t3.i1) 这个查询条件,不匹配就继续读取物化表的下一条记录,匹配则执行第 4 步

可以看到子查询物化使用了个小技巧,在物化过程中,并没有使用下推到子查询的 ((t1.i1) = t3.i1) 条件去过滤记录,而是等到物化完成之后,从物化表中读取记录时,再用这个条件去过滤一遍物化表中的记录,这样就能做到子查询只物化一次。

另外,下推到子查询的条件中,前面有个 <cache>,这也是一个优化,假设外层查询有连续 2 条或者多条记录的 i1 字段的值是 55,那么对于第 1 条值为 55 的记录,会去物化表中遍历每条记录,判断是否匹配 t1.i1 = t3.i1 这个条件,如果物化表中有一条记录匹配这个条件,说明 exists 为 true,跳转到第 4 步继续执行,如果物化表中的记录全都不匹配这个条件,exists = false,跳转到第 1 步继续执行。对于第 2 条及以后的(注意是连续的)值为 55 的记录,不会再去遍历物化表了,而是直接根据第 1 条值为 55 的记录的结果作为外层查询当前记录的 exists 子查询的结果(相当于是缓存的作用了,也可以参考下 shell 的 uniq 命令的实现逻辑)。

第 4 步

判断外层查询的记录是否匹配外层查询中其它的 WHERE 条件,如果匹配,则把记录发送给客户端;如果不匹配,继续读取外层查询的下一条记录。

所以,整体 SQL 的执行过程,就是先执行第 1、2、3、4 步,读取外层查询第 1 条记录,然后循环读取外层查询的第 2 条及以后的记录,执行第 1、3、4 步(相比之下,少了第 2 步物化这个过程),直到外层查询符合条件的记录全部读取完成,结束 SQL 的执行。

2. uncacheable subquery

select * from t1 where t1.i1 in (select i1 from t3 where id in (3, 666, 990, 887, 76) and i2 > rand() * 100
) and t1.str1 > 'abc';
+----+----------------------+-------+------------+-------+---------------+---------+---------+--------+------+----------+-------------+
| id | select_type          | table | partitions | type  | possible_keys | key     | key_len | ref    | rows | filtered | Extra       |
+----+----------------------+-------+------------+-------+---------------+---------+---------+--------+------+----------+-------------+
| 1  | PRIMARY              | t1    | <null>     | ALL   | <null>        | <null>  | <null>  | <null> | 8    | 33.33    | Using where |
| 2  | UNCACHEABLE SUBQUERY | t3    | <null>     | range | PRIMARY       | PRIMARY | 4       | <null> | 5    | 33.33    | Using where |
+----+----------------------+-------+------------+-------+---------------+---------+---------+--------+------+----------+-------------+

上面的 SQL 中子查询的执行方式为 UNCACHEABLE SUBQUERY(因为存在 i2 > rand() * 100 这个查询条件)。

uncacheable subquery 类型的子查询,执行过程和 subquery 类型的子查询是一样的(也是上面 subquery 小节中的第 1、2、3、4 步),不同的地方在于:

  • 包含 uncacheable subquery 类型的子查询的整条 SQL 是不能使用 Query Cache 的(Query Cache 在 MySQL 8.0 之后已经被移除了)
  • uncacheable subquery 类型的子查询,像 i2 > rand() * 100 这样导致子查询 uncacheable 的条件不能使用索引条件下推,也会对 group by 子段有一些影响(这个先忽略吧)

目前发现的不同点,也就是上面这些吧,所以其实可以简单粗暴的认为 uncacheable subquery 和 subquery 基本上是一样的。

3. dependent subquery

其实上面列举的关于 uncacheable subquery 和 subquery 的两个 SQL,都是属于相关子查询,而本节要讲的 dependent subquery 也是相关子查询,只是执行方式不一样而已。

dependent subquery 类型的子查询,执行起来简单粗暴,有两种场景下会使用:

  1. 作为 in 子查询的兜底的执行逻辑,就是在子查询的 5 种优化策略都不能使用的情况下,in 子查询转换为 exists 子查询后,会使用 dependent subquery 方式执行
  2. SQL 中直接把子查询直接写成 exists 相关子查询(就是本小节要讲的这种情况)

本文前面两小节关于 uncacheable subquery 和 subquery 的两个 SQL,如果这样设置:set optimizer_switch='semijoin=off'set optimizer_switch='materialization=off',这两条 SQL 的执行方式也会变成 dependent subquery

select * from t1 where exists (select i1 from t3 where id in (3, 666, 990, 887, 76) and i2 > 16384 and t1.i1 = t3.i1
) and t1.str1 > 's5';
+----+--------------------+-------+------------+-------+---------------+---------+---------+--------+------+----------+-------------+
| id | select_type        | table | partitions | type  | possible_keys | key     | key_len | ref    | rows | filtered | Extra       |
+----+--------------------+-------+------------+-------+---------------+---------+---------+--------+------+----------+-------------+
| 1  | PRIMARY            | t1    | <null>     | ALL   | <null>        | <null>  | <null>  | <null> | 8    | 33.33    | Using where |
| 2  | DEPENDENT SUBQUERY | t3    | <null>     | range | PRIMARY       | PRIMARY | 4       | <null> | 5    |  3.33    | Using where |
+----+--------------------+-------+------------+-------+---------------+---------+---------+--------+------+----------+-------------+

从 explain 的结果可以看到,上面的 SQL 中子查询的执行方式为 DEPENDENT SUBQUERY,接下来通过 optimizer_trace 看一下它的执行过程:

{"join_execution": {"select#": 1,"steps": [{ // 这是第 1 个"subselect_execution": {"select#": 2,"steps": [{"join_execution": {"select#": 2,"steps": []}}]}},// 中间省略了 8 个 subselect_execution 结构{ // 这是第 10 个"subselect_execution": {"select#": 2,"steps": [{"join_execution": {"select#": 2,"steps": []}}]}}]}
}

上面的 JSON 中,最外层的 join_execution 表示执行了一次外层查询,subselect_execution 表示执行子查询(每执行一次子查询都对应一个 subselect_execution),subselect_execution 里面的 join_execution,表示是真正执行子查询的 SQL 去原表里查数据。下面我们来对比一下使用物化方式执行子查询的 join_execution:

{"join_execution": {"select#": 1,"steps": [{ // 这是第 1 个"subselect_execution": {"select#": 2,"steps": [{"join_execution": {"select#": 2,"steps": []}}]}},{ // 这是第 2 个"subselect_execution": {"select#": 2,"steps": []}},// 中间省略了 7 个 subselect_execution 结构{ // 这是第 10 个"subselect_execution": {"select#": 2,"steps": []}}]}
}

对比一下可以看到,使用物化方式执行的子查询,只有第一个 subselect_execution 里面有一个 join_execution,其它的 subselect_execution 里面都没有,这就是因为第 1 次执行子查询时,去原表里读取记录并物化,第 2 次及以后都直接从物化表里读取记录,并没有去原表里查询记录。

从上面的对比可知,dependent subquery 的执行效率是比较低的,因为每次都要去原表里读数据,外层查询有多少条符合条件的记录,子查询就要执行多少次,特别是单次执行子查询时间长,执行次数又多的时候,这是很要命的。这也就是为什么 dependent subquery 是作为 in 子查询的兜底逻辑了(不到万不得已是不会使用的)。

接下来还是总结一下 dependent subquery 的执行过程:

第 1 步

外层查询从存储引擎读取一条记录。

第 2 步

把子查询中使用的外层查询字段替换为具体的值,然后执行子查询,判断子查询中的记录对于 exists 条件是否为 true,不为 true,则外层查询的记录被跳过,回到第 1 步;如果为 true,跳到第 3 步继续执行。

第 3 步

判断外层查询中其它的 WHERE 条件是否匹配,不匹配则回到第 1 步;匹配则把记录发送给客户端。

重复执行第 1、2、3 步,直到读取完外层查询中符合条件的所有记录,结束 SQL 的执行。

4. 提升 SQL 性能小技巧

以前面小节 dependent subquery 的 SQL 为例:

select * from t1 where exists (select i1 from t3 where id in (3, 666, 990, 887, 76) and i2 > 16384 and t1.i1 = t3.i1
) and t1.str1 > 's5';

WHERE 条件中有 2 个条件:exists (子查询)、t1.str1 > ‘s5’,我的 t1 表里有 10 条记录,子查询就会执行 10 次。

上面的 SQL,外层查询没有使用索引,所以执行过程是这样的:

读取外层查询的一条记录,执行子查询后如果 exists 为 false,就继续读取外层查询的下一条记录,不会判断外层查询的当前记录是否匹配 t1.str1 > ‘s5’;如果 exists 为 true,然后再判断外层查询当前记录是否匹配 t1.str1 > ‘s5’,不匹配则继续读取外层查询的下一条记录,匹配则把记录发送给客户端。

这样就存在一个问题了,判断 t1.str1 > ‘s5’ 的成本肯定要比执行 exists 子查询的成本低,执行子查询后,exists 为 true 时,外层记录如果不满足 t1.str1 > ‘s5’ 这个条件,子查询就白白的执行了,浪费了时间和性能。

如果像下面这样修改一下 SQL 语句:

select * from t1 where t1.str1 > 's5' and exists (select i1 from t3 where id in (3, 666, 990, 887, 76) and i2 > 16384 and t1.i1 = t3.i1
);

t1 表里还是有 10 条记录,满足 t1.str1 > ‘s5’ 的只有 4 条记录,经过 t1.str1 > ‘s5’ 的筛选之后,只有 4 条外层查询的记录会触发子查询,这样性能就提升了 60% 了,还是很可观的。

基于此,总结一个小技巧:

在写一个包含 in、exists 子查询,并且 select_type = subquery、dependent subquery、uncacheable subquery、union、union all、dependent union 的 SQL 时,把其它简单 WHERE 条件放在前面(如:t1.str1 > 's5'),包含子查询的 WHERE 条件放在后面,可以减少子查询的执行次数,提升性能。

原文链接

MySQL subquery、dependent subquery、uncacheable subquery 是怎么执行的?相关推荐

  1. MySQL入门 (九) : 子查询 Subquery

    1 一个叙述中的查询叙述 子查询(subquery)是一种很常见的应用,不论是查询.新增.修改或删除都有可能出现. 子查询是一个放在左右刮号中的「SELECT」叙述,而这个查询叙述会放在另一个SQL叙 ...

  2. MySQL调优(八):查缺补漏(mysql的锁机制,读写分离,执行计划详解,主从复制原理)

    mysql的锁机制 1.MySQL锁的基本介绍 ​ 锁是计算机协调多个进程或线程并发访问某一资源的机制.在数据库中,除传统的 计算资源(如CPU.RAM.I/O等)的争用以外,数据也是一种供许多用户共 ...

  3. mysql存储过程执行两边_麻烦问下,为什么我的mysql存储过程一次调用,会被执行了两次?...

    你的位置: 问答吧 -> MySQL -> 问题详情 麻烦问下,为什么我的mysql存储过程一次调用,会被执行了两次? mysql版本是 mysql-community-5.1.45 系统 ...

  4. mysql五补充部分:SQL逻辑查询语句执行顺序

    mysql五补充部分:SQL逻辑查询语句执行顺序一 SELECT语句关键字的定义顺序 二 SELECT语句关键字的执行顺序 三 准备表和数据 四 准备SQL逻辑查询测试语句 五 执行顺序分析 一 SE ...

  5. MySQL数据库:创建一个表,并执行插入、修改和删除数据操作

    一: 展示数据库:mysql> show databases; 二: 创建数据库:create database company;使用所创建的数据库:mysql> use company; ...

  6. mysql 命令行 结束编辑_MySql命令行执行多行命令编辑时非常有用的命令

    在MySql命令行下进行多行命令编辑是很正常的一个操作,但有的时候,我们在执行多行命令编辑的时候,难免会有各种失误,一旦到此时,我们往往不得其法,其实了解一下MySql命令行的一些参数和提示的信息将会 ...

  7. 【MySQL专题】MySQL中一条SQL是如何被执行的?---来自于令狐的独孤九剑

    [MySQL专题]MySQL中一条SQL是如何被执行的? 前言 ​ 大家好,我是令狐,欢迎来到令狐的独孤九剑**[MySQL专题]**这节课跟大家聊一聊MySQL中一条SQL是如何被执行的. MySQ ...

  8. mysql 优化器_mysql之优化器、执行计划、简单优化

    mysql> explain select * from employees.employees limit 1\G; *************************** 1. row ** ...

  9. Mysql基础(十六):explain命令SQL执行计划

    目录 写在前面 1.通过explain命令得到的SQL执行计划(1) 2.通过explain命令得到的SQL执行计划(2) 3.通过explain命令得到的SQL执行计划(3) 4.通过explain ...

最新文章

  1. 如何理解Memory leak
  2. 一个模型击溃12种AI造假,各种GAN与Deepfake都阵亡
  3. 国家计划统筹布局哪些人工智能创新平台?
  4. Object C学习笔记22-#define 用法
  5. C# 学生成绩管理系统 完整版
  6. python设计个人简历_python软件工程师 个人简历模板
  7. [Matlab]函数表达式运算
  8. 信安教程第二版-第18章网络安全测评技术与标准
  9. SharePoint【Site Definition 系列】-- 04. 相对快捷地创建List Definition的Schema.xml文件
  10. c语言 int转bool,C语言的布尔类型(_Bool)【转】
  11. 【转】What is an entity system framework for game development?
  12. 乾颐堂现任明教教主(2014年课程)TCPIP协议详解卷一 第六节课笔记
  13. 微信小程序如何使用阿里巴巴矢量图标库彩色图标
  14. iOS 第三方库插件和知名博客
  15. 微型计算机cpu组成部分组成部分的功能,微处理器的组成及其各部分的功能?
  16. 基于C++ Qt实现的红色警戒3修改器
  17. ST7920 12864液晶图片取模,显示你想显示的图片。
  18. 重庆计算机应用高级工程师,重庆高级园林工程师职称评审条件
  19. Vmware安装CensOS6.4
  20. linux iscsi 发起程序,设置iSCSI的发起程序(客户端)(三)

热门文章

  1. 关于单片机控制用字库显示LCD12864汉字
  2. Xcode苹果iOS硬件编程实现iPhone XR打开或关闭电灯
  3. Unity UI前显示可操控三维模型
  4. js和jQuery调用搜狐IP地址查询接口
  5. 【前端初学者】【CSS笔记】之定位:相对定位、绝对定位、固定定位、静态定位及粘性定位(上)
  6. full join 全连接
  7. Qt模仿VS停靠窗口(一)
  8. bootstrap使用教程
  9. Python基础知识——字典dict
  10. 移动平均(Moving Average)