本文分享自华为云社区《【华为云MySQL技术专栏】MySQL 派生表合并优化的原理和实现》,作者:GaussDB 数据库。

引言

MySQL是一种流行的开源关系型数据库管理系统,广泛应用于各种Web应用程序和企业系统中。随着数据量的增加和查询复杂度的提高,优化SQL查询性能变得至关重要。派生表(Derived Table)是SQL查询中常用的一种技术,通过在主查询中嵌套子查询来实现更复杂的数据处理。然而,派生表的使用有时会导致系统的性能瓶颈。

为了解决这一问题,MySQL引入了派生表合并优化(Derived Table Merging Optimization)。本文将详细介绍派生表合并优化的原理及在MySQL中的实现。

何为派生表?

派生表是一个临时表,它是由子查询的结果集生成并在主查询中使用。简单来讲,就是将FROM子句中出现的检索结果集当成一张表,比如 FROM一个SELECT构造的子查询,这个子查询就是一个派生表;SELECT一个视图,这个视图就是一个派生表;SELECT一个WITH构造的临时表(Common table expression,CTE),这个CTE表就是一个派生表。如下图举例所示:

图1 子查询语句样例

MySQL优化器处理派生表有两种策略:

 
第一种,将派生表物化为一个临时表;
第二种,将派生表合并到外查询块中。
 
派生表物化为一个临时表,可能会引发性能问题,如下情况:
  • 大数据量子查询:派生表的结果集可能非常大,导致内存消耗和磁盘I/O增加。
  • 复杂查询:多层嵌套查询或包含多个派生表的查询,会使优化器难以选择最佳执行计划。
  • 不可索引:派生表的结果集是临时的,无法直接使用索引进行优化。
为了解决这些问题,MySQL 引入了派生表合并优化。

派生表合并优化原理

派生表合并优化的核心思想是将派生表的子查询合并到主查询中,从而避免生成临时表。具体来说就是,优化器会尝试将派生表的子查询展开,并直接嵌入到主查询的执行计划中。这样可以减少临时表的使用,降低内存和磁盘I/O的负担,从而提高查询性能。

下文将通过案例对派生表合并优化进行详细说明。

1.案例分析

创建如下两张表:

CREATE TABLE `departments` (
`id` int NOT NULL,
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
CREATE TABLE `employees` (
`id` int NOT NULL,
`name` varchar(50) DEFAULT NULL,
`department_id` int DEFAULT NULL,
`salary` decimal(10,2) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;

对于下述的查询语句:

SELECT t1.department_id, t2.name, t1.total_salary
FROM
(SELECT department_id, SUM(salary) total_salary
FROM employees GROUP BY department_id) t1
JOIN
(SELECT id, name
FROM departments
WHERE name='Human Resources') t2
ON t1.department_id = t2.id;

关闭optimizer_switch(优化器开关)的derived_merge选项,对应的执行计划如下:

+----+-------------+-------------+------------+------+---------------+-------------+---------+------------------+------+----------+-----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra
|+----+-------------+-------------+------------+------+---------------+-------------+---------+------------------+------+----------+-----------------+
| 1 | PRIMARY | <derived2> | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 100.00 | Using where |
| 1 | PRIMARY | <derived3> | NULL | ref | <auto_key0> | <auto_key0> | 4 | t1.department_id | 2 | 100.00 | NULL |
| 3 | DERIVED | departments | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using where |
| 2 | DERIVED | employees | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using temporary |
+----+-------------+-------------+------------+------+---------------+-------------+---------+------------------+------+----------+-----------------+
4 rows in set, 1 warning (0.01 sec)

select_type列出现两行DERIVED类型, 说明派生表没有合并,派生表会物化为临时表。

执行EXPLAIN ANALYZE进一步分析,可知两个子查询都是物化为临时表后,再执行JOIN。

EXPLAIN: -> Nested loop inner join  (actual time=0.304..0.308 rows=1 loops=1)
-> Table scan on t2 (cost=2.73 rows=2) (actual time=0.003..0.003 rows=1 loops=1)
-> Materialize (cost=0.55 rows=1) (actual time=0.163..0.164 rows=1 loops=1)
-> Filter: (departments.`name`='Human Resources') (cost=0.55 rows=1) (actual time=0.103..0.125 rows=1 loops=1)
-> Table scan on departments (cost=0.55 rows=3) (actual time=0.095..0.114 rows=3 loops=1)
-> Index lookup on t1 using <auto_key0> (department_id=t2.id) (actual time=0.004..0.006 rows=1 loops=1)
-> Materialize (actual time=0.137..0.139 rows=1 loops=1)
-> Table scan on <temporary> (actual time=0.001..0.003 rows=3 loops=1)
-> Aggregate using temporary table (actual time=0.102..0.104 rows=3 loops=1)
-> Table scan on employees (cost=0.65 rows=4) (actual time=0.040..0.056 rows=4 loops=1)
1 row in set (0.00 sec)

开启optimizer_switch(优化器开关)的derived_merge选项,对应的执行计划如下:

+----+-------------+-------------+------------+------+---------------+-------------+---------+---------------------+------+----------+-----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+------+---------------+-------------+---------+---------------------+------+----------+-----------------+
| 1 | PRIMARY | departments | NULL | ALL | PRIMARY | NULL | NULL | NULL | 1 | 100.00 | Using where |
| 1 | PRIMARY | <derived2> | NULL | ref | <auto_key0> | <auto_key0> | 5 | test.departments.id | 2 | 100.00 | NULL |
| 2 | DERIVED | employees | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using temporary |
+----+-------------+-------------+------------+------+---------------+-------------+---------+---------------------+------+----------+-----------------+
3 rows in set, 1 warning (0.00 sec)

从执行计划可以看出,select_type列上只有一行为DERIVED类型,说明发生了派生表合并。

执行EXPLAIN ANALYZE进一步分析,employees表上的子查询仍然会被物化为临时表。departments表上的子查询(派生表)进行了合并优化,departments表直接与临时表t1进行JOIN。

EXPLAIN: -> Nested loop inner join (actual time=0.271..0.295 rows=1 loops=1)
-> Filter: (departments.`name` = 'Human Resources') (cost=0.55 rows=1) (actual time=0.103..0.122 rows=1 loops=1)
-> Table scan on departments (cost=0.55 rows=3) (actual time=0.095..0.112 rows=3 loops=1)
-> Index lookup on t1 using <auto_key0> (department_id=departments.id) (actual time=0.005..0.007 rows=1 loops=1)
-> Materialize (actual time=0.164..0.166 rows=1 loops=1)
-> Table scan on <temporary> (actual time=0.002..0.004 rows=3 loops=1)
-> Aggregate using temporary table (actual time=0.114..0.117 rows=3 loops=1)
-> Table scan on employees (cost=0.65 rows=4) (actual time=0.044..0.065 rows=4 loops=1)
1 row in set (0.00 sec)

对比derived_merge选项开启和关闭的两个执行计划可知,开启派生表合并优化特性后,departments表上的子查询(派生表)不再物化为临时表,而是合并到了父查询,进而简化了执行计划,并提高了执行效率。

另外,也可以发现,并不是所有派生表都可以合并优化,比如,案例中的employees表上的子查询(派生表),因为含有聚合函数,就无法进行合并优化。

2.应用场景限制

如下场景中派生表合并优化是无效的:

1)派生表中含有聚合函数,或者含有DISTINCT、GROUP BY、HAVING这些分组子句。比如,案例中的派生表t1包含了聚合函数和GROUP BY分组就无法合并优化。

2)派生表的SELECT列表中有子查询,也就是标量子查询。比如:

select *
from (select stuno,
course_no,
(select course_name
from course c
where c.course_no = a.course_no) as course_name,
score
from score a) b
where b.stuno = 1;

因为派生表b的select 列表中有标量子查询,无法合并,会被物化。

3)分配了用户变量。比如:

select (@i := @i + 1) as rownum, stuno, course_no, course_name, score
from ((select a.stuno, a.course_no, b.course_name, a.score
from score a
left join course b
on a.course_no = b.course_no) dt, (select (@i := 0) num) c)
where stuno = 1;

上面这个例子使用用户变量的形式给记录加了行号,不能合并。

4)如果合并会导致外查询块中超过61张基表的连接访问,优化器会选择物化派生表。

5)UNION或UNION ALL。比如:

select id, c1from (select id, c1
from t1
union
select id, c1 from t2) dt
where dt.id = 1;

因为派生表dt有union操作,无法合并,会被物化。

6)对于视图而言,创建视图时如果指定了ALGORITHM=TEMPTABLE,它会阻止合并,这个属性的优先级比优化器开关的优先级要高。

7)派生表中含LIMIT子句,因为合并会导致结果集改变。比如:

select * from (select id,c1 from t1 limit 10) a where a. id=1;

8)只引用了字面量值。比如:

select * from (select '1' as c1, 2 as c2 ) a;

源码分析

1.背景知识

我们使用的MySQL代码版本号为8.0.22。在介绍派生表代码实现之前,先了解下MySQL描述一条查询的逻辑语法树结构,有4个较为核心的类:

SELECT_LEX_UINT

对于一个query expression的描述结构,其中可以包含union/union all等多个query block的集合运算,同时SELECT_LEX_UNIT也根据query的结构形成递归包含关系。

SELECT_LEX

对于一个query block的描述结构,就是我们最为熟悉SPJ(选择Selection、投影Projection、连接Join) + group by + order by + select list... 这样的一个查询块,一个SELECT_LEX_UNIT中可能包含多个SELECT_LEX,而SELECT_LEX内部则也可能嵌套包含其他SELECT_LEX_UNIT。

Item

对于expression的描述结构,例如on条件、where条件、投影列等,都是用这个类来描述一个个表达式的,Item系统是MySQL SQL层代码中最为复杂的子系统之一,其构成了表达式树。

TABLE_LIST

对于表信息的描述结构。TABLE_LIST不仅仅针对SQL表达式中的物理表,也可以表示其他类型的表,例如视图、临时表、派生表等。此外,TABLE_LIST类还用于处理别名和连接等信息。

TABLE_LIST类是MySQL查询处理的核心部分,涵盖了SQL表达式中的各种表类型。以案例中的SQL查询语句为例,在派生表合并优化前,其对应的类实例映射关系如下:

图2 派生表合并优化前的SQL语句

图3 派生表合并优化前的逻辑语法树

图2为SQL表达式,图3为MySQL处理后对应的逻辑语法树。图2颜色涵盖的SQL语句范围与图3相同颜色的类实例一一对应。比如,图2米黄色涵盖了整条SELECT语句(query block),也就对应着图3的SELECT_LEX1实例;图2最外层的浅灰色包含了米黄色区域,代表整条SQL语句(query expression),对应着图3的SELECT_LEX_UINT1实例(不涉及UNION操作,SELECT_LEX_UINT1只包含SELECT_LEX1,即一个SELECT_LEX实例)。

图2中用括号圈起来的部分,就是一个SELECT_LEX_UNIT,而每个SELECT toke开始的一个query block,就是一个SELECT_LEX,而在外层的SELECT_LEX中,会嵌套子查询,用一个SELECT_LEX_UNIT描述,子查询中可以是任意查询形态,再包含多个SELECT_LEX,从而形成SELECT_LEX_UNIT -> SELECT_LEX -> SELECT_LEX_UNIT -> SELECT_LEX ... 这种相互嵌套的结构。

最外层的 query block(SELECT_LEX1)有两个派生表(t1、t2)。t1 和 t2 通过 derived 指针分别指子查询 query expression(SELECT_LEX_UINT3、SELECT_LEX_UINT2)。

2. 代码实现

MySQL主要在prepare阶段处理派生表的合并优化,详细的函数调用和处理过程如下:

-> Sql_cmd_dml::prepare
-> Sql_cmd_select::prepare_inner
-> SELECT_LEX::prepare
顶层 query block 的处理,全局入口
-> SELECT_LEX::resolve_placeholder_tables
处理query block中的第一个 derived table
-> TABLE_LIST::resolve_derived
-> 创建Query_result_union对象,在执行derived子查询时,用来向临时表里写入结果数据
-> 调用内层嵌套SELECT_LEX_UNIT::prepare,对derived table对应的子查询做递归处理
-> SELECT_LEX::prepare
-> 判断derived中的子查询是否允许merge到外层,当满足如下任一条件时,“有可能”可以merge到外层:
1. derived table属于最外层查询
2. 属于最外层的子查询之中,且query是一个SELECT查询
-> SELECT_LEX::resolve_placeholder_tables 嵌套处理derived table这个子查询内部的derived table...
... 处理query block中其他的各个组件,包括condition/group by/rollup/distinct/order by...
-> SELECT_LEX::transform_scalar_subqueries_to_join_with_derived
... 一系列对query block(Item中的)处理,略过
-> SELECT_LEX::apply_local_transforms 做最后的一些查询变换(针对最外层query block)
1. 简化join,把嵌套的join表序列尽可能展开,去掉无用的join,outer join转inner join等
2. 对分区表做静态剪枝
-> SELECT_LEX::push_conditions_to_derived_tables(针对最外层query block)
外 query block 中与 derived table 相关的条件会推入到派生表中
-> 至此derived table对应的子查询部分resolve完成
-> TABLE_LIST::is_mergeable
-> SELECT_LEX_UNIT::is_mergeable
判断当前 derived table 是否可以merge到外层,要同时满足如下的要求:(只支持最简单的SPJ查询)
1. derived table query expression 没有union
2. derived table query block 没有聚合/窗口函数+group by + 没有having + 没有distinct + 有table + 没有window + 没有limit
-> SELECT_LEX::merge_derived,确定可以展开到外层后,执行 merge_derived 动作
-> 再做一系列的检查看是否可以merge
1. 外层query block是否允许merge,例如CREATE VIEW/SHOW CREATE这样的命令,不允许做merge
2. 基于启发式,检查derived子查询的投影列是否有子查询,有则不做merge
3. 如果外层有straight_join,而derived子查询中有semi-join/anti-join,则不允许merge
4. 外层表的数量达到MySQL能处理的最大值
-> 通过检查后,开始merge
1. 把内层join列表合并到外层中
2. 把where条件与外层的where条件做AND组合
3. 把投影列合并到外层投影列中
-> 对于不能展开的,采用物化方式执行,setup_materialized_derived
处理query block中的其它 derived table,...
-> resolve_placeholder_tables 处理完成
顶层 query block 的其它处理 ...

案例中的SQL语句经过上面的派生表的合并优化处理后,其对应的映射关系如下:

图4 派生表合并优化后的SQL语句

图5 派生表合并优化后的逻辑语法树

对比合并优化前,有如下变化:(图4的SQL语句已基于图5的逻辑语法树等价变换)

1)派生表t2所指向的内部 query expression(SELECT_LEX_UINT2/SELECT_LEX2)已消除。

2)SELECT_LEX2中的物理表departments上移至外部query block(SELECT_LEX1)的JOIN运算中。

3)SELECT_LEX2中的WHERE条件合并到SELECT_LEX1。

4)SELECT_LEX1中针对派生表t2的投影,替换为物理表departments。

原理证明

前文描述了MySQL派生表合并优化的具体实现,那么,如何从原理上证明该优化方法的正确性呢?可以尝试根据关系代数定理对其进行论证。

先简化场景,假设有两个表,一个是主查询(主表)R,一个是派生表D。在没有合并优化之前,查询可能是这样的形式:

1)外层查询从派生表中选择数据:σ条件1(D)
2)派生表D是从另一个或多个表导出的结果,通过一定的操作如选择σ条件2、投影π属性或连接⋈等得到。

不考虑具体实现的复杂性,让我们通过一个简单查询的例子来说明外层查询和派生表合并的效果。假设派生表D是从主表R通过选择操作产生的:D = σ条件2(R),而外层查询又对D进行选择:σ条件1(D)。

根据关系代数的选择的叠加律(σa(σb(R)) = σa ∧ b(R)),可以合并这两个选择操作为一个操作,直接作用在主表R上:σ条件1 ∧ 条件2(R)。

这样,外层查询和派生表D就被合并成了一个直接对原始表R进行操作的查询,省去了创建和访问派生表D的开销。

对于更复杂的派生表,它们可能通过多个操作,如连接、投影和选择,从一个或多个表导出。针对这样的情况,基于关系代数的性质,比如选择的叠加律和交换律、投影的结合律等,通过相应的关系代数变换,所有这些操作的组合都可以被重写为直接作用于原始表上的一系列操作,也就证明了MySQL的这一优化方式是有效的。

总结

本文从一个案例出发梳理了MySQL派生表合并优化的流程实现和优化原理,并对优化前后同一条SQL语句在代码层面的类实例映射关系进行了对比。MySQL派生表合并的代码实现细节比较多,篇幅有限,不再赘述,希望本文能够作为一个参考,帮助感兴趣的读者进一步研究这部分源码。

点击关注,第一时间了解华为云新鲜技术~

MySQL派生表合并优化的原理和实现的更多相关文章

  1. MySQL派生表(derived)优化一例

    1.什么是派生表derived 关键字:子查询–>在From后where前的子查询 mysql; +----+-------------+------------+------+-------- ...

  2. MySQL 派生表(Derived Table) Merge Optimization

    本文将通过演示告诉你:MySQL中派生表(Derived Table)是什么?以及MySQL对它的优化. Background 有如下一张表: mysql> desc city; +------ ...

  3. mysql多表合并为一张表

    有人提出要将4张表合并成一张.数据量比较大,有4千万条数据.有很多重复数据,需要对某一列进行去重. 数据量太大的话,可以看我另外一篇:http://www.cnblogs.com/magmell/p/ ...

  4. mysql两表合并,对一列数据进行处理

    加班一时爽,一直加班~一直爽~  欢迎收看http://www.996.icu/ 今天弄了下MySQL中两表合并的并且要处理一列数据,这列数据原来都是小写字母,处理时将这列数据改成驼峰命名的~~ 基本 ...

  5. Mysql多表合并以及连接问题

    目的 1.为了备战过两天的面试,我又重新给孙老师的课件看了一遍,学累了,就写写自己的新的体会,和遇到的问题,来进行一个记录,这是知识产出的过程,据说可以帮助我学习,看视频什么的都是被动学习,不进行及时 ...

  6. 数据库~Mysql派生表注意的几点~关于百万数据的慢查询问题

    基础概念 派生表是从SELECT语句返回的虚拟表.派生表类似于临时表,但是在SELECT语句中使用派生表比临时表简单得多,因为它不需要创建临时表的步骤. 术语:*派生表*和子查询通常可互换使用.当SE ...

  7. mysql 单表索引优化

    建表语句 CREATE TABLE IF NOT EXISTS `article` ( `id` INT(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMEN ...

  8. MySQL的表的优化和列类型的选择

    列选择原则: 1:字段类型优先级 整型 > date,time  >  enum,char>varchar > blob 列的特点分析: 整型: 定长,没有国家/地区之分,没有 ...

  9. mysql大表如何优化

    作者:哈哈链接:http://www.zhihu.com/question/19719997/answer/81930332来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处 ...

  10. Mysql学习总结(17)——MySQL数据库表设计优化

    1.选择优化的数据类型 MySQL支持很多种不同的数据类型,并且选择正确的数据类型对于获得高性能至关重要.不管选择何种类型,下面的简单原则都会有助于做出更好的选择: (1).更小通常更好 一般来说,要 ...

随机推荐

  1. AIRIOT物联网低代码平台如何配置三菱PLC驱动?

    三菱PLC驱动配置使用三菱Melsec协议(MC协议)从三菱PLC读取数据,仅支持以太网方式.三菱PLC都可以通过此协议访问,但是需要对PLC进行设置. AIRIOT物联网低代码平台如何配置三菱PLC ...

  2. Java中双括号初始化是个什么操作

    最近在阅读Mybatis源码的时候,看到了一种原来很少见到的语法: public class RichType { ... private List richList = new ArrayList( ...

  3. 使用Docker快速安装Redis

    1.使用docker命令下一个redis的镜像 docker pull redis 2.创建 redis 的 data 目录和 conf 目录 1. cd /home/fengsir/redis 2. ...

  4. k8s——daemonset

    daemonset 为每一个匹配的node都部署一个守护进程 # daemonset node:type=logs daemonset 选择节点 - nadeSelector: 只调度到匹配指定的la ...

  5. vue3 Suspense

    在Vue.js 3中,Suspense 是一个用于处理异步组件的特殊组件,它允许你在等待异步组件加载时展示备用内容.这对于优化用户体验.处理懒加载组件或异步数据获取时非常有用.Suspense 的主要 ...

  6. vue3项目安装依赖报错 npm ERR! code ERESOLVE

    vue3项目安装依赖报错 npm ERR! code ERESOLVE npm ERR! ERESOLVE could not resolve npm ERR! npm ERR! While reso ...

  7. margin的用法 清除默认样式 display属性值 块状元素 内联元素 行内块元素

    margin的用法: 1,margin是在元素的宽高以外的 2,作用:控制元素之间的位置关系 3,margin不能改变盒子本身大小的 4,单一一个方向设置margin值: margin-left    ...

  8. kettle从入门到精通 第五十六课 ETL之kettle Microsoft Excel Output

    1.9.4 版本的kettle中有两个Excel输出,Excel输出和Microsoft Excel输出.前者只支持xls格式,后者支持xls和xlsx两种格式,本节课主要讲解步骤Microsoft ...

  9. System.lineSeparator()行分隔符的用法

    System.lineSeparator()具体含义 从JDK的源码中,可以看出:它是从JDK1.7之后开始有的这个方法. 在UNIX系统下,System.lineSeparator()方法返回&qu ...

  10. 【译】Visual Studio 17.10 发布了新版扩展管理器

    我们将更新的扩展管理器带给所有用户!在过去的一年里,我们已经将更新后的扩展管理器作为可选的预览功能提供,并一直期待您的反馈.基于您令人难以置信的反馈,我们现在准备从 Visual Studio 17. ...