《Mysql是怎样运行的》读书笔记三
《Mysql是怎样运行的》读书笔记三
一、两个表的连接-连接的原理
连接的本质其核心在于从各个连接表中提取出所有记录进行匹配处理,并整合到结果集中供用户查询。

1.1连接的过程
将若干张表进行联结时,在没有附加限制条件下(即不限制条件),这些表之间的联结操作会产生一个巨大的笛卡尔积数据集
- 单表过滤条件
- 涉及多表的条件
SELECT * FROM t1, t2 WHERE t1.m1 > 1 AND t1.m1 = t2.m2 AND t2.n2 < 'd';
这个连接查询的大致执行过程:
选择第一个需要查询的表,并将其定义为驱动表(采用代价最低的方式进行单表查询)。
对于上一步生成的结果集中的每一行数据,在对应的情况下需要对t2表进行查找匹配。

1.2 内连接和外连接
即使在被驱动表中没有对应的记录的情况下,在结果集中仍会包含所有驱动物体中的记录信息。依据这种思路将数据划分为两个部分:一个是作为主库的数据源(驱动表),另一个则是辅助库的数据源(被驱动表)。
在执行外连接操作时,默认情况下不会将驱动表的所有记录注入最终的结果集中。
在某些情况下(如数据匹配不成功)会包含这些记录,在其他情况下则会排除。
那么该如何处理呢?我们可以根据需求将过滤条件分为两类:使用where子句指定明确的匹配条件...
- where子句的过滤条件
无论是inner join还是outer join,在应用WHERE子句进行筛选时(即应用过滤条件时),符合条件的记录会被包含在结果集中;不符合条件的记录则不会被包含。
- on子句的过滤条件
在外连接驱动表中的记录中,在不满足被驱动表中匹配 ON 子句过滤条件的情况下,在查询结果中仍然会包含这些记录,并且这些被处理过的数据会将各个字段设置为 NULL 值。
在内联结中使用WHERE子句与ON子句具有等价性。因此,在执行内联结时无需强制指定ON子句。在内联结操作中,驱动表与被驱动表的位置可以相互调换,并不会影响最终查询结果。在外联结中交换主从关系时需谨慎处理。
1.3 连接的原理
1.3.1嵌套循环连接
SELECT * FROM t1, t2 WHERE t1.m1 > 1 AND t1.m1 = t2.m2 AND t2.n2 < 'd';
对于这条含有两个表的内连接的SQL 查询的步骤:
- 通过与驱动表相关的过滤条件筛选后,在计算成本最低的前提下采用单表访问方法来执行对驱动表的单表查询。
- 将上一步骤所得结果集中每一条记录逐一对应到被驱动表中进行匹配查询。
当有三个表进行连接时,在步骤2中产生的结果集充当新的驱动数据源。第三个表格随后成为了被动的数据接收方。

什么是嵌套循环连接?在数据库设计中,在驱动表中进行一次查询时,在这种情况下,然而,在这种情况下,被驱动表可能会被多次访问。其访问次数主要由对驱动表执行单表查询所得结果集中记录的数量以及相应的连接方式决定。也被认为是最简单但效率最低的一种常见的数据库连接查询方法。
1.3.2 使用索引加快连接速度
在嵌套循环连接的步骤2中, 可能会反复查询被驱动表. 如果这些查询操作全部采用逐一扫描的方式执行, 则可能导致大量的重复查询.
SELECT * FROM t1, t2 WHERE t1.m1 > 1 AND t1.m1 = t2.m2 AND t2.n2 < 'd';
解决方案:给被驱动表 搜索条件的列加 索引
- 为m2字段创建索引,在回表时只需判断t2.n2 < 'd'即可。
- 为n2字段建立索引,则采用范围(Range)访问法。
1.3.3 基于块的嵌套循环连接
基于先前的两种方案实施后发现,在实际运行过程中仍存在较多的数据调用频率问题。具体而言,在处理的数据规模极大且缺乏有效的索引机制的情况下,则必须通过多次磁盘读取操作来获取相关信息。这将导致显著的I/O开销,并严重影响系统的整体性能表现。因此我们亟需探索一种能够有效降低对频繁调用数据源干扰的技术路径
Join Buffer:
在将被驱动表的数据加载至内存的过程中,系统会一次性地处理多条驾驶表中的记录进行匹配,从而大幅降低从磁盘加载被驾驶表数据所需的重复计算.此时生成并建立了一个用于临时存储这些数据的Join Buffer,该Buffer在执行连接查询前即被预先申请并分配了一定大小的内存空间,以便将这些来自多个驾驶表结果集中的相关数据快速整合到其中.

请注意:驱动表的记录并不会将所有列放入 join buffer 中;相反地,在这种情况下只会包含查询列表中的列以及过滤条件中的列。
二、Mysql基于成本的优化
mysql的成本主要有下面两个方面组成
- I/O成本
常用的数据库管理工具MyISAM、InnoDB这些数据库引擎都是将数据与索引存储在磁盘上;当我们试图查询表中的记录时,必须先加载相关数据或索引至内存后再进行操作;这个从磁盘加载至内存的过程消耗的时间即为I/O成本。
- CPU成本
获取并判断记录是否符合预设的搜索标准以及对多个数据集合进行排序处理所消耗的时间指标即为CPU成本
2.1基于成本的优化步骤
在前面提到过的一点是关于二级索引和回表的。这些内容实际上是查询优化器应该承担起的一部分工作。为了进一步强调这一点,我们再次复述如下:
根据搜索条件,找出所有可能使用的索引
计算全表扫描的代价
计算使用不同索引执行查询的代价
对比各种执行方案的代价,找出成本最低的那一个
主要分为:不同索引执行查询的代价、连接查询的成本
对于连接查询而言,在这种特定情况下已经固定了驱动表与被驱动表的关系。我们仅限于探讨内部连接查询的情况,在这种情况下优化器需评估不同驱动表选择所带来的总执行成本,并据此制定最经济的执行策略。此外还需要比较各备选方案的成本效益后确定最优路径,并据此构建相应的执行计划以实现最佳性能表现
三、Mysql基于规则的优化(含子查询优化)
在日常工作中,我们不得不编写一些低效且耗时的SQL语句,Mysql制定了将这些低效SQL转化为高效执行形式的优化规则,这一过程被称为查询优化或查询转换技术,下面我们将深入探讨几种关键的查询优化策略
3.1条件化简
- 移除不必要的括号
((a = 5 AND b = c) OR ((a > c) AND (c < 5)))
#查询优化器会将这些括号干掉
(a = 5 and b = c) OR (a > c AND c < 5)
- 常量传递 (和AND连接)
a = 5 AND b > a
被转化为
a = 5 AND b > 5
为什么Or不可以进行常量传递?
a=5 Or b>a #a的条件不一定为a=5
- 移除没用的条件
对于一些明显永远为 TRUE 或者 FALSE 的表达式,优化器会移除掉它们
(a < 1 and b = b) OR (a = 6 OR 5 != 5)
#优化为
(a < 1 and TRUE) OR (a = 6 OR FALSE)
最终
a < 1 OR a = 6
- 表达式计算
在执行查询之前,在表达式仅包含常量的情况下(即当表达式中只有常量时),其值将被提前计算。
a = 5 + 1 化简为 a=6;
请记住,在第一篇文章中讨论过查询优化器对于某些情况的具体处理方式时指出:对于某个字段如果以单独的形式作为运算符参与运算的情况(例如,在函数内部或更为复杂的运算中),该系统不会对其进行任何简化处理。
ABS(a) > 5
-a < -8 //不会被化简
so 最好让索引列以单独的形式出现在表达式中
- Having子句和Where子句合并
当查询语句未使用诸如SUM、MAX等聚合函数且未涉及GROUP BY子 clause时,优化器通常会将Having子 clause与Where子 clause合并为一个条件。
- 常量表检测
在mysql有两种查询速度特别快
- 在查询的表中没有记录存在或仅存在一条记录。
- 采用主键对应的精确匹配和唯一二级索引对应的精确匹配作为搜索条件来检索特定的表。
有这两种方式查询的表称为 常量表(const tables)
- 外连接消除
探讨一个问题:内连接为何在效率上优于外连接?主键-外键关联结构在内外两个方向上都可以实现互换性。这种特性使得数据库系统在执行内部查询时能够更加灵活高效。
之前已经介绍过,在外连接操作中执行主键约束时的情况:当主键约束无法通过ON子句指定过滤条件来关联到目标表中的相应行时,主记录仍然会被包含在结果集中,并且其对应的目标行字段值将被设置为NULL;而在内连接操作中使用了ON子句指定过滤条件的情况下,则会排除那些无法满足约束条件的情况。
那么Innodb是如何消除外连接的?
用到where条件过滤,凡是不符合WHERE子句中条件记录都不会参与连接。
只要我们在搜索条件中设置关于被驱动表相关字段的值不为 NULL ,这样就会导致外连接中在被驱动表中无法找到符合 ON 子句条件的相关记录不再包含在最终的结果集中。换句话说,在这种情况下:外连接与内连接之间并无明显差异。
SELECT * FROM t1 LEFT JOIN t2 ON t1.m1 = t2.m2 WHERE t2.n2 IS NOT NULL;
此时这个外连接可以转化为内连接
SELECT * FROM t1 INNER JOIN t2 ON t1.m1 = t2.m2 WHERE t2.m2 = 2;
空值拒绝
被称为"空值拒绝"(NAT)条件的WHERE子句所涉及的驱动表中的列均为非NULL值的情况称之为"空值拒绝"(NAT)策略。**当一个WHERE子句满足"空值拒绝"(NAT)策略时,在外联接与内联接之间是可以实现相互转换的。这种转换带来的便利在于使得查询优化器能够评估不同表间关联关系的各种可能组合及其对应的计算开销,并最终选择具有最低总成本的最佳关联方式来进行数据操作。
3.2子查询优化
3.2.1 按返回的结果集区分子查询
- 标量子查询:只返回一个单一值的子查询称之为标量子查询
SELECT * FROM t1 WHERE m1 = (SELECT MIN(m2) FROM t2);
- 行子查询: 返回一个记录的子查询,这条记录含有多个列
SELECT * FROM t1 WHERE (m1, n1) = (SELECT m2, n2 FROM t2 LIMIT 1);
- 列子查询:返回某个列的数据,列的数据需要包含多条记录
SELECT * FROM t1 WHERE m1 IN (SELECT m2 FROM t2);
- 表子查询:是子查询的结果既包含很多条记录,又包含很多个列
SELECT * FROM t1 WHERE (m1, n1) IN (SELECT m2, n2 FROM t2);
3.2.2 按与外层查询关系来区分子查询
- 不相关子查询:若子查询能够独立执行并产出结果,不受外围查询数据的影响
- 相关子查询:当子查询的运行过程受制于外围查询的结果时
#相关子查询
SELECT * FROM t1 WHERE m1 IN (SELECT m2 FROM t2 WHERE n1 = n2);
3.2.3 子查询在布尔表达式中的使用
子查询的主要用途在于充当逻辑判断条件应用于WHERE或ON子句中
* 使用 = 、 > 、 < 、 >= 、 <= 、 <> 、 != 、 <=> 作为布尔表达式的操作符
操作数 操作符(子查询),这里的子查询只能为 标量子查询或 行子查询
- 【NOT】IN/ANY/SOME/ALL子查询
对于列子查询和表子查询而言,在关系操作中它们的结果集通常会返回大量数据项。
这些返回的数据项在本质上构成了一个集合。
- IN 或者 NOT IN
> SELECT * FROM t1 WHERE (m1, n2) IN (SELECT m2, n2 FROM t2);
>
>
>
> ```
>
>
> * ANY/SOME (ANY和SOME语义一致)
>
SELECT * FROM t1 WHERE m1 > ANY(SELECT m2 FROM t2);
* ALL
> SELECT * FROM t1 WHERE m1 > ALL(SELECT m2 FROM t2);
>
>
>
> ```
>
>
* EXISTS子查询
有时候我们仅仅需要判断子查询结果集是否有记录,而不在乎他的记录具体形式,可以把EXISTS或 NOT EXISTS放在子查询语句之前
SELECT * FROM t1 WHERE EXISTS (SELECT 1 FROM t2);
##### 3.2.4子查询注意事项
1.
子查询必须用小括号括起来
SELECT SELECT m1 FROM t1; #报错
2.
在Select 子句的子查询必须为标量子查询
SELECT (SELECT m1, n1 FROM t1); #报错
3.
在想要得到标量子查询或者行子查询,但又不能保证子查询的结果集只有一条记录时,应该使用 LIMIT 1 语句来限制记录数量。
4.
**对于[NOT] IN/ANY/SOME/ALL子查询来说,子查询中不允许有LIMIT,下列子查询出现的语句也是多余**
1. Order By子句
2. DISTINCT语句
3. 没有聚合函数以及 Having子句的Group子句
5.
**不允许在一条语句中删改某个表的记录的同时还对该表进行子查询**
DELETE FROM t1 WHERE m1 < (SELECT MAX(m1) FROM t1);
#### 3.3 子查询在Mysql中的执行过程
##### 3.3.1 标量子查询、行子查询的执行方式
* 对于不相关的标量子查询 或 行子查询的查询语句,MYSQL会分别独立的执行外层查询和子查询,相当于两个单表查询
* 对于相关子查询(标量或行子查询),其执行方式:
* 从外层查询获取一条记录
* 从上一步骤得到记录找出子查询涉及到的值,进行子查询
##### 3.3.2 IN 子查询优化
对于不相关子查询
如果子查询的结果集中的记录数很少,那么把子查询和外层查询分别看成两个单独的单表查询
如果子查询的结果集太多内存放不下。对于外查询而言,子查询结果集太多,意味着IN子句的参数比较多,导致
无法有效使用索引,只能对外层查询进行全表扫描
由于 IN 子句中的参数太多,这会导致检测一条记录是否符合和 IN 子句
中的参数匹配花费的时间太长。
###### 临时表的提出:
对于上述IN子查询(不相关子查询) 出现子查询结果集太多,Mysql设计了临时表。不直接将不相关子查询的结果当作外层查询的参数,而是将该结果集写入一个临时表里。
**写入临时表的过程:**
1. 临时表的列就是子查询结果集中的列
2. 写入临时表的记录会被去重
3. 一般子查询的结果集不会大的离谱,所以为他建立基于内存使用Memory存储引擎的临时表,并为该表建立哈希索引(是hash索引,不是B+树索引),如果确实太大,超过了指定的阈值,临时表转而使用基于磁盘的存储引擎来保持记录,此时就用到B+索引
###### 物化的概念
Mysql将子查询结果集中的记录保存到临时表的过程称之为物化,我们就把那个存储子查询结果集的临时表称之为 物化表 。
###### 物化表转为连接
将子查询的结果集物化之后,其实等价于一个内连接查询,外层查询的表和物化表的内连接查询。这样查询优化器又可以对内连接进行优化,通过评估不同连接顺序需要的成本,选取成本最低的那种查询方式进行查询
##### 将子查询转化为半连接(Semi-join)
子查询转化为连接,需要将子查询的结果集进行物化(每次查询都会有建立临时表的成本)。为此Mysql有设法能不能不进行物化将子查询转化为连接呢?
SELECT * FROM s1
WHERE key1 IN (SELECT common_field FROM s2 WHERE key3 = 'a');
上面sql执行和下面联合查询的执行很类似
SELECT s1.* FROM s1 INNER JOIN s2
ON s1.key1 = s2.common_field
WHERE s2.key3 = 'a';
但是还是有区别:我们不能保证s1表的某条记录,在s2表中有多少条记录满足`s1.key1 = s2.common_field` 这个筛选条件:
情况一:对于 s1 表的某条记录来说, s2 表中没有任何记录满足 s1.key1 = s2.common_field 这个条件,那么该记录自然也不会加入到最后的结果集。
情况二:对于 s1 表的某条记录来说, s2 表中有且只有记录满足 s1.key1 = s2.common_field 这个条件,那么该记录会被加入最终的结果集。
情况三:对于 s1 表的某条记录来说, s2 表中至少有2条记录满足 s1.key1 = s2.common_field 这个条件,那么该记录会被多次加入最终的结果集。
>
>
> 对于上面的子查询,我们子关系s1的key1 在s2表是否存在记录满足`s1.key1 = s2.common_field`这个条件,而不关心具体有多少条记录与其匹配,又因为有情况三的存在,所以我们上面俩个sql并不完全等价。
>
>
**这里Mysql提出了`半连接`:表s1与表s2进行半连接,指对于s1表的某条记录来说,我们只关心在s2表中是否存在与之匹配的记录是否存在,而不关心具体有多少条记录与之匹配**
SELECT 所有字段 从s₁半联结s₂ 通过s₁.key₁等于s₂.common_field WHERE key₃等于'a';
实际上相当于
SELECT * 从s₁ WHERE key₁属于(SELECT common_field 从s₂ WHERE key₃等于'a')
注:半连接semi-join 只是在Mysql内部采用的一种子查询方式,并没面向用户提供
##### 半连接的实现方式
1. Table pullout(子查询中的表上拉)
当子查询的查询列表处只有 主键或者唯一索引列时,可以把子查询中的表上拉到外层查询中,并把子查询中的搜索条件合并到外层查询的搜索条件中
SELECT * FROM s1
WHERE key 位置 is selected from the subquery that retrieves key 位置 from table source_1 where source_key equals 'a';
This query is equivalent to:
select all columns from the inner join of tables source_1 and source_2 where the matching on position 位置 is performed on position 位置; where condition is that source_table_3 equals 'a';
2. DuplicateWeedout execution strategy(重复值消除)
对于查询
SELECT * FROM s1
WHERE key1 IN (SELECT common_field FROM s2 WHERE key3 = 'a');
#转换为半连接后, s1表中的一条记录在s2 表中可能存在多条匹配的记录, 索引该条记录可能会被多次添加到最后的结果集中, 为了消除重复, 可以建立一个临时表
CREATE TABLE tmp (
id PRIMARY KEY
);
某条 s1 表中的记录要加入结果集时,就首先把这条记录的 id 值加入到这个临时表里,如果添加成功,说明之前这条 s1 表中的记录并没有加入最终的结果集,现在把该记录添加到最终的结果集;如果添加失败,说明这条之前这条 s1 表中的记录已经加入过最终的结果集,这里直接把它丢弃就好了,这种使用临时表消除 semi-join 结果集中的重复值的方式称之为 DuplicateWeedout 。
3. LooseScan execution strategy (松散索引扫描)
SELECT * FROM s1
WHERE key3 IN (SELECT key1 FROM s2 WHERE key1 > 'a' AND key1 < 'b');
转换为内连接后,将子查询的表作为 驱动表的话,执行过程如下:

* 驱动表idx_key1二级索引值为‘aa’的记录有三条,只需取第一条值到s1表中查找s1.key3=‘aa’ 对应的记录。一次类推,其它值相同的二级索引,也只需要去第一条记录的值到s1表匹配。这种方式称为(松散索引扫描)
4.
Semi-join Materialization execution strategy
即上面的物化子查询结果集
5.
FirstMatch execution strategy(首次匹配策略)
FirstMatch 作为最原始的半连接的执行方式,就是先取一条外层查询中的记录,然后到子查询的表中寻找符合匹配条件的记录,**如果能找到一条,则将该外层查询的记录放入最终的结果集并且停止查找匹配 更多的记录**
##### 对于相关子查询 (In子查询)
* 也可以转换为半连接查询,上边介绍的table pullout(前提是子查询的查询列表处于主键或唯一二级索引)、复制消除、松散索引扫描、首次匹配 都可以使用
* 唯独 物化子查询结果集不能,因为相关子查询并不是一个独立的查询
##### 半连接(semi-join)的适用条件
1. 子查询必须是和IN语句组成的布尔表达式,并在外层查询的WHERE或ON子句中出现
2. 外层查询也可以有其他的搜索条件,只不过和 IN 子查询的搜索条件必须使用 AND 连接起来。
3. 该子查询必须是一个单一的查询,不能是由若干查询由 UNION 连接起来的形式。
4. 该子查询不能包含 GROUP BY 或者 HAVING 语句或者聚集函数。
##### 半连接(semi-join)的不适用条件
1. 外层查询的WHERE条件中有其他搜索条件与IN子查询组成的布尔表达式使用 OR 连接起来
2. 使用 NOT IN 而不是 IN 的情况
3. 在 SELECT 子句中的IN子查询的情况
4. 子查询中包含 GROUP BY 、 HAVING 或者聚集函数的情况
5. 子查询中包含 UNION 的情况
如果IN子查询不符合转换为Semi-join的条件,查询优化器会执行两种策略
1.
先将子查询物化之后再执行查询
SELECT * FROM s1
WHERE key1 NOT IN (SELECT common_field FROM s2 WHERE key3 = 'a')
虽然用不了semi-join,但物化后,执行效率还是显著增强
2.
执行IN to EXISTS转换
外层表达式存在于子查询的结果集中
#### 3.3.3 ANY/ALL子查询优化

#### 3.3.4 【NOT】EXISTS子查询的执行
如果 [NOT] EXISTS 子查询是不相关子查询,可以先执行子查询,得出该 [NOT] EXISTS 子查询的结果是 TRUE 还 是 FALSE ,并重写原先的查询语句
SELECT * FROM s1
WHERE EXISTS (SELECT 1 FROM s2 WHERE key1 = 'a')
OR key2 > 100;
因为这个语句里的子查询是不相关子查询,所以优化器会首先执行该子查询,假设该EXISTS子查询的结果为
TRUE ,那么接着优化器会重写查询为:
SELECT * FROM s1
WHERE TRUE OR key2 > 100
#### 3.3.5 对于派生表的优化
派生表:把子查询放在外层的FROM子句后,那么这个子查询的结果就像对于一个派生表
Select all columns from a subquery that selects the id and key3 fields from table s2 where key1 is 'a', aliasing this result as a derived table called derived_s1. Finally, select rows where the alias for key3 equals 'a'.
对于派生表的优化,Mysql有两种策略
1.
将派生表物化
>
>
> 此时涉及到一个延迟物化的策略,也就是在查询中正真使用到派生表的时候才回去尝试物化派生表。
>
>
2.
将派生表和外层的表合并,即查询重写为没有派生表的形式
