MySQL-refman-8.0
目录
1.3.1 索引合并交集访问算法(Index Merge Intersection Access Algorithm)
1.3.2 索引合并联合访问算法(Index Merge Union Access Algorithm)
1.3.3 索引合并排序并集访问算法(Index Merge Sort-Union Access Algorithm)
1.3.4 影响索引合并优化(Influencing Index Merge Optimization)
1.5 引擎条件下推优化(Engine Condition Pushdown Optimization)
1.7.1 Nested-Loop Join Algorithm
1.7.2 Block Nested-Loop Join Algorithm
-inf:负无穷大 -infinity
+inf:正无穷大 +infinity
数据库应用程序的核心逻辑是通过SQL语句来执行的,无论是通过解释器直接发布,还是通过API在幕后提交。本节中的调优指南有助于加速所有类型的MySQL应用程序。该指南涵盖了读写数据的SQL操作,一般的SQL操作的幕后开销,以及在数据库监控等特定场景中使用的操作。
一、优化SELECT语句
查询,以SELECT语句的形式,执行数据库中的所有查找操作。调整这些语句是首要任务,无论是为动态网页实现亚秒的响应时间,还是减少数小时的时间来生成大量的夜间报告。
除了SELECT语句外,查询的调优技术也适用于构造,如CREATE TABLE……AS SELECT,INSERT INTO...SELECT,以及在删除语句中的WHERE语句子句。
这些语句具有额外的性能考虑因素,因为它们将写操作与面向读的查询操作结合起来。
NDB集群支持连接下推优化,即将合格的连接完整地发送到NDB集群数据节点,在这些数据节点中可以在这些节点之间分布并并行执行。有关此优化的更多信息,请参考NDB下推连接的条件。
优化查询的主要考虑事项是:
- 让一个执行慢的SELECT...WHERE查询速度更快,首先要检查的是是否可以添加索引。
对WHERE子句中使用的列设置索引,以加快评估、过滤和最终的最终检索。
为了避免浪费磁盘空间,请构建一组小型索引,以加快应用程序中使用的许多相关查询。
索引对于使用joins和foreign keys等特性的引用不同表的查询尤其重要。
可以使用 EXPLAIN 语句来确定为选择使用哪些索引。
可参考第8.3.1节“MySQL如何使用索引”和第8.8.1节“使用解释优化查询”。 - 隔离和调整查询的任何部分,例如需要花费过多时间的函数调用。根据查询的结构方式,可以对结果集中的每一行调用一次函数,甚至对表中的每一行调用一次函数,这极大地放大了任何低效率。
- 在查询中最小化全表扫描的数量,特别是对于大表。
- 通过定期使用ANALYZE TABLE语句保持表统计的最新,因此优化器具有构建高效执行计划所需的信息。
- 了解每个表中特定于存储引擎的调优技术、索引技术和配置参数。
InnoDB和MyISAM都有一套用于支持和维持查询中的高性能的指导方针。
有关详细信息,可参考8.5.6节“优化InnoDB查询”和第8.6.1节“优化MyISAM查询”。 - 可以使用第8.5.3节“优化InnoDB只读事务”中的技术来优化InnoDB表的单个查询事务。
- 避免使用让优化器难以理解的方式转换查询,特别是当优化器自动执行一些相同的转换时。
- 如果性能问题不能通过基本准则之一解决,请通过阅读EXPLAIN计划和调整索引、WHERE子句、join连接子句等索引来调查特定查询的内部细节。
(当你达到一定的专业水平时,阅读EXPLAIN计划可能是你对每个查询的第一步。) - 调整MySQL用于缓存的内存区域的大小和属性。
通过有效使用InnoDB缓冲池、MyISAM键缓存和MySQL查询缓存,重复查询运行得更快,因为结果是在第二次和以后从内存中检索的。 - 即使对于使用缓存内存区域快速运行的查询,仍然可以进一步优化,以便它们需要更少的缓存内存,从而使您的应用程序更具可扩展性。可伸缩性意味着应用程序可以处理更多的同步用户、更大的请求等等,而不会出现性能的大幅下降。
- 处理锁定问题,即查询的速度可能会受到其他会话同时访问表的影响。
1.1 WHERE 语句优化
这里讨论可以为处理WHERE子句而进行的优化。这些示例使用SELECT语句,但对删除和更新语句中的WHERE子句也适用相同的优化。
提示
因为在MySQL上工作的优化器正在进行中,所以这里并没有记录到MySQL执行的所有优化。
您可能会试图重写查询,让算术运算更快,同时牺牲可读性。因为MySQL会自动执行类似的优化,所以您通常可以避免这项工作,并将查询以更容易理解和可维护的形式进行。由MySQL执行的一些优化如下:
- 删除不必要的圆括号:
((a AND b) AND c OR (((a AND b) AND (c AND d))))
-> (a AND b AND c) OR (a AND b AND c AND d)
- 常数合并:
(a<b AND b=c) AND a=5
-> b>5 AND b=c AND a=5
- 常量状态清除:
(b>=5 AND b=5) OR (b=6 AND 5=5) OR (b=7 AND 5=6)
-> b=5 OR b=6
在MySQL 8.0.14及以后的版本中,这发生在准备期间,而不是在优化阶段,这有助于简化连接。有关更多信息和示例,请参考第8.2.1.9节“Outer Join连接优化”。
- 索引使用的常量表达式只计算一次
- 从MySQL8.0.16开始,检查常数值类型的列,或删除无效或过期值:
# CREATE TABLE t (c TINYINT UNSIGNED NOT NULL);
SELECT * FROM t WHERE c ≪ 256;
-≫ SELECT * FROM t WHERE 1;
有关更多信息,请参考第8.2.1.14节,“常量折叠优化”。
- 在没有WHERE的单表上的COUNT(*)是直接从MyISAM和MEMORY表的表信息中检索的。
当只使用一个表时,对任何非空的表达式也会这样做。 - 尽早的检测无效的常量表达式。
MySQL快速检测到一些选择语句是不可能的,并且不返回任何行。 - 如果不使用GROUP BY或聚合函数(COUNT()、MIN()等),则HAVING与WHERE合并。
- 对于join连接中的每个表,构造了一个更简单的WHERE,以快速获得表的计算WHERE,并尽快跳过行。
- 在查询中的任何其他表之前,将先读取所有常量表。常量表是以下任一项:
- 一个空表或一个包含一行的表
- 与PRIMARY KEY或PRIMARY KEY或一个UNIQUE上的WHERE子句一起使用的表,其中所有索引部分都与常量表达式进行比较,并定义为NOT NULL。
以下所有表都用作常量表:
SELECT * FROM t WHERE primary_key=1;
SELECT * FROM t1,t2
WHERE t1.primary_key=1 AND t2.primary_key=t1.id;
- 通过尝试所有的可能性,可以找到加入表的最佳连接组合。
如果ORDER BY和GROUP BY子句中的所有列都来自同一个表,则在连接时首先首选该表。 - 如果存在ORDER BY子句和不同的GROUP BY子句,或者ORDER BY或GROUP BY包含来自连接队列中第一个表以外的表的列,则将创建一个临时表。
- 如果你使用SQL_SMALL_RESULT修改器,则MySQL将使用内存中的临时表。
- 查询每个表索引,并使用最佳索引,除非优化器认为使用表扫描更有效。
从前,使用扫描是基于最佳索引是否超过表的30%,但一个固定的百分比不再决定使用索引或扫描之间的选择。优化器现在更加复杂,并基于其他因素进行估计,如表大小、行数和I/O块大小。 - 在某些情况下,MySQL可以从索引中读取行。如果索引中使用的所有列都是数字的,则仅使用索引树来解析查询。
- 在输出每一行之前,跳过那些与HAVING子句不匹配的行。
一些非常快的查询的例子:
SELECT COUNT(*) FROM tbl_name;
SELECT MIN(key_part1),MAX(key_part1) FROM tbl_name;
SELECT MAX(key_part2) FROM tbl_name
WHERE key_part1=constant;
SELECT ... FROM tbl_name
ORDER BY key_part1,key_part2,... LIMIT 10;
SELECT ... FROM tbl_name
ORDER BY key_part1 DESC, key_part2 DESC, ... LIMIT 10;
MySQL仅使用索引树解析以下查询,假设索引列是数字的:
SELECT key_part1,key_part2 FROM tbl_name WHERE key_part1=val;
SELECT COUNT(*) FROM tbl_name
WHERE key_part1=val1 AND key_part2=val2;
SELECT MAX(key_part2) FROM tbl_name GROUP BY key_part1;
以下查询使用索引按排序顺序检索行,而不需要单独的排序传递:
SELECT ... FROM tbl_name
ORDER BY key_part1,key_part2,... ;
SELECT ... FROM tbl_name
ORDER BY key_part1 DESC, key_part2 DESC, ... ;
1.2 范围优化
范围访问方法使用单个索引,来检索包含在一个或多个索引值间隔内的表行的子集。
它可以用于单部分或多部分的索引。
以下部分描述了优化器使用范围访问的条件。
- 单部分索引的范围访问方法
- 多部分索引的范围访问方法
- 多值比较的平等范围优化
- 跳过扫描范围访问方法
- 行构造函数表达式的范围优化
- 限制范围优化的内存使用
1.2.1 单部分索引的范围访问方法
对于单部分索引,索引值区间可以方便地用WHERE子句中的相应条件表示,表示为范围条件,而不是“区间”。
单个部分索引的范围条件的定义如下:
- 对于BTREE和HASH索引,当使用=、<=>、IN()、ISNULL或IS NOT NULL运算符时,关键部分与常值的比较是一个范围条件。
- 此外,对于BTREE索引,当使用>、<、>=、<=、之间、!=或<>操作符时,关键部分与常量值的比较是一个范围条件,如果LIKE的参数是不以通配符开头的常量字符串,则是LIKE比较。
- 对于所有的索引类型,与OR或AND组合的多个范围条件形成了一个范围条件。
上述描述中的“常数值”是指下列情况之一:
- 从查询字符串中获得的一个常量
- 来自同一连接的常量或系统表的列
- 一个不相关的子查询的结果
- 完全由上述类型的子表达式组成的任何表达式
以下是WHERE子句中的一些具有范围条件的查询示例:
SELECT * FROM t1
WHERE key_col > 1
AND key_col < 10;
SELECT * FROM t1
WHERE key_col = 1
OR key_col IN (15,18,20);
SELECT * FROM t1
WHERE key_col LIKE 'ab%'
OR key_col BETWEEN 'bar' AND 'foo';
在优化器的常数传播阶段,一些非·常数值可以被转换为常数。
MySQL尝试从每个可能的子句WHERE索引提取范围条件。
在提取过程中,去掉不能用于构造范围条件的条件,合并产生重叠范围的条件,去除产生空范围的条件。
思考以下语句,其中 key1 是有索引的列,而 nonkey 没有索引:
SELECT * FROM t1 WHERE
(key1 < 'abc' AND (key1 LIKE 'abcde%' OR key1 LIKE '%b')) OR
(key1 < 'bar' AND nonkey = 4) OR
(key1 < 'uux' AND key1 > 'z');
key1的提取过程如下:
1. 从where语句开始
(key1 < 'abc' AND (key1 LIKE 'abcde%' OR key1 LIKE '%b')) OR
(key1 < 'bar' AND nonkey = 4) OR
(key1 < 'uux' AND key1 > 'z')
2. 删除 nonkey = 4 和 key1 LIKE '%b',因为它们不能用于范围扫描。
删除它们的正确方法是用TRUE替换它们,这样我们在进行范围扫描时就不会错过任何匹配的行。
用TRUE替换它们:
(key1 < 'abc' AND (key1 LIKE 'abcde%' OR TRUE)) OR
(key1 < 'bar' AND TRUE) OR
(key1 < 'uux' AND key1 > 'z')
3. 将总是为TRUE或FALSE的条件折叠起来
替换这些带有常量字段的条件后:
(key1 < 'abc' AND TRUE) OR (key1 < 'bar' AND TRUE) OR (FALSE)
删除不必要的TRUE和FALSE常数,得到:
(key1 < 'abc') OR (key1 < 'bar')
4. 将重叠的区间合并为一个区间,将得到用于范围扫描的最终条件:
(key1 < 'bar')
一般来说(如这个例子所示),范围扫描的条件限制比WHERE子句要小。
MySQL执行一个额外的检查,以过滤出满足范围条件但不满足完整的WHERE子句的行。
范围条件提取算法可以处理任意深度的嵌套AND/OR构造,其输出结果不依赖于条件在WHERE子句中出现的顺序。
MySQL不支持为空间索引的 range范围访问方法合并多个范围。
要解决此限制,可以使用具有相同 SELECT语句的 UNION,只要将每个空间结果放在不同的SELECT中。
1.2.2 多部分索引的范围访问方法
多部分指标的范围条件是单部分指数的范围条件的扩展。
多部分索引上的范围条件将索引行限制在一个或几个关键元组间隔内。
关键元组区间在一组关键元组上定义,使用索引的排序。
例如,思考一个被定义为 key1(key_part1, key_part2, key_part3)的多部分索引,以及按key顺序列出的以下key元组:
key_part1 key_part2 key_part3
NULL 1 'abc'
NULL 1 'xyz'
NULL 2 'foo'
1 1 'abc'
1 1 'xyz'
1 2 'abc'
2 1 'aaa'
条件key_part1 = 1定义了这个区间:
(1,-inf,-inf) <= (key_part1,key_part2,key_part3) < (1,+inf,+inf)
该区间覆盖了前面数据集中的第4、第5和第6个元组,可由范围访问方法使用。
相比之下,条件key_part3 = 'abc'没有定义单个时间间隔,并且不能被范围访问方法使用。
以下描述将更详细地说明范围条件如何在多部分索引中工作。
- 对于HASH索引,可以使用包含相同值的每个区间。这意味着只能针对以下形式的条件产生时间间隔:
key_part1 cmp const1 AND key_part2 cmp const2 AND ... AND key_partN cmp constN;
这里,const1,const2,……都是常数,cmp是=、<=>或IS NULL比较运算符之一,这些条件涵盖了所有索引部分。(也就是说,有N个条件,N部分指数的每个部分都有一个。)
例如,以下是三部分HASH索引的范围条件:key_part1 = 1 AND key_part2 IS NULL AND key_part3 = 'foo'
有关被认为是常量的定义,可参考:单部分索引的范围访问方法(1.2.1)。
- 对于BTREE索引,区间可能使用了结合AND的条件,其中每个条件使用=、<=>、ISNULL、>、<、>=、<=、!=、<>之间,或 LIKE 'pattern'(其中 'pattern'不以通配符开始)。
只要能够确定一个包含匹配条件的所有行的单个键元组(如果使用<>或 !=,则使用两个区间),就可以使用一个区间。
只要比较操作符使用=、<=>或ISNULL,优化程序将尝试使用其他关键部件来确定区间。
如果运算符是 >、<、>=、<=、!=、<>、BETWEEN或LIKE,则优化器使用它,但不考虑其他关键部分。
对于下面的表达式,优化器使用第一次比较中的=。
它还使用了第二次比较中的>=,但没有考虑进一步的关键部分,也没有使用第三次比较来进行区间构造:
对应单区间为:key_part1 = 'foo' AND key_part2 >= 10 AND key_part3 > 10
创建的区间可能比初始条件包含更多的行。('foo',10,-inf) < (key_part1,key_part2,key_part3) < ('foo',+inf,+inf)
例如,前面的区间包括不满足原始条件的值(“foo”,11,0)。 - 如果覆盖区间内包含的行集结合了OR区间的条件,它们将形成一个覆盖包含在其区间的并集中的一组行的条件。
如果这些条件与AND结合,它们将形成一个条件,覆盖包含在它们区间的交集中的一组行。
例如,对于在两部分索引上的此条件:
这个区间为:(key_part1 = 1 AND key_part2 < 2) OR (key_part1 > 5)
在本例中,第一行上的区间使用一个关键部分作为左界,使用两个关键部分作为右界。(1,-inf) < (key_part1,key_part2) < (1,2) (5,-inf) < (key_part1,key_part2)
第二行上的区间只使用一个关键部分。
EXPLAIN 输出中的key_len列表示所使用的键前缀的最大长度。
在某些情况下,key_len可能表示使用了一个关键部分,但这可能不是您所期望的。
假设key_part1和key_part2可以为NULL。
然后,key_len列将显示以下条件下的两个关键部分长度:
但是,实际上,条件是这样转换成这样的:key_part1 >= 1 AND key_part2 < 2
有关如何执行优化以组合或消除单部分索引上的区间条件的区间描述,可参考:单个部分索引的范围访问方法(1.2.1)。key_part1 >= 1 AND key_part2 IS NOT NULL
有关如何执行优化以组合或消除多部分索引上的区间条件的区间描述,可参考:多个部分索引的范围访问方法(1.2.2)
1.2.3 多值比较的平等范围优化
思考以下表达式,其中col_name是一个索引列:
col_name IN(val1, ..., valN)
col_name = val1 OR ... OR col_name = valN
如果col_name等于几个值中的任意一个,则每个表达式都为true。
这些比较是相等的范围比较(其中“范围”是单个值)。
优化器估计读取相等范围比较的限定行的成本如下:
- 如果在col_name上有一个唯一的索引,那么每个范围的行估计值是1,因为最多有一行可以有给定的值。
- 否则,col_name上的任何索引都是非唯一的,优化器可以通过深入到索引或索引统计数据来估计每个范围的行计数
使用索引下钻,优化器在范围的边界处进行下钻,并使用范围内的行数作为估计值。
例如,表达式col_name IN(10,20,30)有三个相等的范围,优化器在每个范围内进行两次下钻以生成一个行估计。每对下钻都产生具有给定值的行数的估计数。
索引下钻提供了准确的行估计,但是随着表达式中比较值数量的增加,优化器需要更长的时间来生成行估计。索引统计数据的使用不如索引下钻数据准确,但允许对大值列表进行更快的行估计。
eq_range_index_dive_limit系统变量使您能够配置优化器从一个行估计策略切换到另一个行估计策略时的值数。为了允许使用索引下钻来比较最多N个相等范围,将eq_range_index_dive_limit设置为N+1。要禁用统计数据并始终使用索引下钻而不管N,请将eq_range_index_dive_limit设置为0。
要更新表的索引统计信息以获得最佳估计值,请使用ANALYZE TABLE命令。
在MySQL8.0之前,除了使用eq_range_index_dive_limit系统变量外,没有办法跳过使用索引下钻来估计索引的有用性。在MySQL8.0中,对于满足所有这些条件的查询,可以跳过索引下钻:
- 单表查询,而不是多表join连接
- 存在一个单索引 FORCE INDEX 索引提示。其想法是:如果强制使用索引,就不能从执行下钻索引的额外开销中获得任何好处。
- 索引不是唯一的,而且不是一个 FULLTEXT索引
- 不存在子查询
- 不存在 DISTINCT,GROUP BY,或ORDER BY语句
对于 EXPLAIN FOR CONNECTION,如果跳过下钻索引,输出更改如下:
- 对于传统的输出,其行和过滤后的值均为NULL
- 对于JSON输出,rows_examined_per_scan和 rows_produced_per_join没有出现,skip_index_dive_due_to_force是true,成本计算不准确。
如果跳过索引下钻,没有FOR CONNECTION,EXPLAIN的输出不会改变。
在执行了一个被跳过的索引下钻的查询后,INFORMATION_SCHEMA.OPTIMIZER_TRACE对应的行包含了一个skipped_due_to_force_inedx的 index_dives_for_range_access值。
After execution of a query for which index dives are skipped, the corresponding row in theINFORMATION_SCHEMA.OPTIMIZER_TRACE table contains an index_dives_for_range_access value of skipped_due_to_force_index .
1.2.4 跳过范围访问方法
思考以下场景:
CREATE TABLE t1 (f1 INT NOT NULL, f2 INT NOT NULL, PRIMARY KEY(f1, f2));
INSERT INTO t1 VALUES
(1,1), (1,2), (1,3), (1,4), (1,5),
(2,1), (2,2), (2,3), (2,4), (2,5);
INSERT INTO t1 SELECT f1, f2 + 5 FROM t1;
INSERT INTO t1 SELECT f1, f2 + 10 FROM t1;
INSERT INTO t1 SELECT f1, f2 + 20 FROM t1;
INSERT INTO t1 SELECT f1, f2 + 40 FROM t1;
ANALYZE TABLE t1;
EXPLAIN SELECT f1, f2 FROM t1 WHERE f2 > 40;
- 跳过第一个索引部分 f1(索引前缀)的不同值。
- 对其余索引部分上的 f2 > 40条件的每个不同的前缀值执行子范围扫描。
对于前面显示的数据集,该算法是如下操作的:
- 获取第一个关键部分的第一个不同值( f1 = 1)
- 基于第一和第二关键部分( f1 = 1 和 f2 > 40)构建范围
- 执行范围扫描
- 获取第一个关键部分的下一个不同的值( f1 = 2)
- 基于第一和第二个关键部分构建范围( f1 = 2 和 f2 > 40)
- 执行范围扫描
使用此策略减少访问的行数,因为MySQL跳过了不符合每个构造范围的行。
此跳过扫描访问方法适用于以下条件:
- 表T至少有一个复合指数与括号内形式的关键部分([A_1,...,A_k,]B_1,...,B_m,C[,D_1,...,D_n])。关键部分A和D可以为空,但B和C必须为非空。
- 该查询只引用了一个表。
- 该查询没有使用GROUP BY或DISTINCT
- 该查询仅引用索引中的列
- A_1,...,A_k上的断言必须是相等的断言,并且它们必须是常数。这包括 IN()操作。
- 查询必须是连接查询;也就是说,一个AND或OR条件:(cond1(key_part1) OR cond2(key_part1)) AND (cond1(key_part2) OR ...) AND...
- 在C上必须有一个范围条件。
- 允许使用D列上的条件。D上的条件必须与C上的范围条件相结合。
跳过扫描的使用如下所示:
Using index for skip scan 在 'Extra'中表示使用了松散索引跳过扫描访问方法。
使用跳过扫描是指示在优化器跟踪输出的一个“skip ecan”元素的此形式:
"skip_scan_range": {
"type": "skip_scan",
"index": index_used_for_skip_scan,
"key_parts_used_for_access": [key_parts_used_for_access],
"range": [range]
}
您还可以看到一个“best_skip_scan_summary”元素。
如果选择跳过扫描作为最佳范围访问变量,则写入“chosen_range_access_summary”。|
如果选择跳过扫描作为总体最佳访问方法,则存在一个“best_access_path”元素。
跳过扫描的使用取决于 optimizer_switch系统变量的skip_scan标志的值。
请参考第8.9.2节,“Switchable可切换优化”。
默认情况下,此标志已打开。要禁用它,请将skip_scan设置为off。
除了使用optimizer_switch系统变量来控制跳过扫描会话范围内的优化器的使用之外,MySQL还支持优化器提示,以在每个语句的基础上影响优化器。
请参考第8.9.3节,“优化器提示”。
1.2.5 行构造函数表达式的范围优化
优化器能够将范围扫描访问方法应用于此形式的查询:
SELECT ... FROM t1 WHERE ( col_1, col_2 ) IN (( 'a', 'b' ), ( 'c', 'd' ));
以前,要使用范围扫描,必须将查询写为:
SELECT ... FROM t1 WHERE ( col_1 = 'a' AND col_2 = 'b' )
OR ( col_1 = 'c' AND col_2 = 'd' );
要使优化器使用范围扫描,查询必须满足以下条件:
- 使用了IN(),而不是用 NOT IN()
- 在IN()的左侧,行构造函数只包含列引用
- 在IN()的右侧,行构造函数只包含运行时常量,这些常量是在执行期间绑定到常量的文字或本地列引用
- 在IN()的右侧,有多个行构造函数。
有关优化器和行构造函数的更多信息,请参阅第8.2.1.22节,“行构造函数表达式优化”
1.2.7 限制范围优化的内存使用
- 值为0表示“无限制”。
- 当值大于0时,优化器将跟踪在考虑范围访问方法时所消耗的内存。
如果即将超过指定的限制,则放弃范围访问方法,转而考虑其他方法,包括全表扫描。
这可能不那么理想。
如果发生这种情况,则将发生以下警告(其中N是当前的range_optimizer_max_mem_size值):
Warning 3170 Memory capacity of N bytes for 'range_optimizer_max_mem_size' exceeded.
Range optimization was not done for this query.
- 对于 UPDATE和DELETE语句,如果优化器返回到完整的表扫描,并且启用了sql_safe_updates系统变量,则会发生错误而不是警告,因为实际上没有使用键来确定要修改哪些行。有关详细信息,请参考使用安全更新模式(--safe-updates)。
对于超过可用范围优化内存且优化器退回到较差最优计划的单个查询,增加range_optimizer_max_mem_size值可能会提高性能。
要估计处理范围表达式所需的内存量,请使用以下准则:
- 对于以下一个简单的查询,其中有一个范围访问方法的候选键,每个与OR组合的结果使用大约230个字节:
SELECT COUNT(*) FROM t WHERE a=1 OR a=2 OR a=3 OR .. . a=N;
- 类似地,对于以下查询,与AND组合的每个结果将使用大约125个字节:
SELECT COUNT(*) FROM t WHERE a=1 AND b=1 AND c=1 ... N;
- 对于具有IN()的查询:
SELECT COUNT(*) FROM t WHERE a IN (1,2, ..., M) AND b IN (1,2, ..., N);
IN()列表中的每个文字值都算作与OR结合的结果。
如果有两个IN()列表,则与OR组合的结果的数量是每个列表中文字值的数量的乘积。
因此,在前一例中,与OR组合的结果数为M×N。
1.3 索引合并优化
索引合并访问方法检索具有多个范围扫描的行,并将其结果合并为一个行。此访问方法仅合并来自单个表的索引扫描,而不是跨多个表的扫描。合并可以产生其底层扫描的联合、交集或联合交集。
可能使用索引合并的示例查询:
SELECT * FROM tbl_name WHERE key1 = 10 OR key2 = 20;
SELECT * FROM tbl_name
WHERE (key1 = 10 OR key2 = 20) AND non_key = 30;
SELECT * FROM t1, t2
WHERE (t1.key1 IN (1,2) OR t1.key2 LIKE 'value%')
AND t2.key1 = t1.some_col;
SELECT * FROM t1, t2
WHERE t1.key1 = 1
AND (t2.key1 = t1.some_col OR t2.key2 = t1.some_col2);
提示
索引合并优化算法有以下已知的局限性:
- 如果你的查询有一个复杂的WHERE子句具有深度AND/OR嵌套,并且MySQL没有选择最优计划,请尝试使用以下转换
( x AND y ) OR z => ( x OR z ) AND ( y OR z )( x OR y ) AND z => ( x AND z ) OR ( y AND z )- 索引合并不适用于全文索引
在 EXPLAIN输出中,索引合并方法在type列中显示为index_merge。在这种情况下,key列包含所使用的索引列表,而 key_len包含这些索引的最长关键部分的列表。
索引合并访问方法有几种算法,它们显示在 EXPLAIN输出的 Extra字段中:
- Using intersect(...)
- Using union(...)
- Using sort_union(...)
优化器根据各种可用选项的成本估计,在不同可能的索引合并算法和其他访问方法之间进行选择。
- 索引合并交集访问算法(Index Merge Intersection Access Algorithm)
- 索引合并集合访问算法(Index Merge Union Access Algorithm)
- 索引合并排序并集访问算法(Index Merge Sort-Union Access Algorithm)
- 影响索引合并优化(Influencing Index Merge Optimization)
1.3.1 索引合并交集访问算法(Index Merge Intersection Access Algorithm)
此访问算法适用于将WHERE子句转换为与AND组合的不同键上的多个范围条件,且每个条件都是以下条件之一:
- 此形式的N个部分表达式,其中索引恰好有N个部分(即,涵盖所有索引部分):
key_part1 = const1 AND key_part2 = const2 ... AND key_partN = constN - 在InnoDB表的主键上的任何范围条件。
例如:
SELECT * FROM innodb_table
WHERE primary_key < 10 AND key_col1 = 20;
SELECT * FROM tbl_name
WHERE key1_part1 = 1 AND key1_part2 = 2 AND key2 = 2;
索引合并交集算法对所有使用的索引执行同时扫描,并生成从合并的索引扫描接收到的行序列的交集。
如果查询中使用的所有列都被所使用的索引覆盖,则不会检索完整的表行(在本例中,解释输出包含在额外字段中使用索引)。下面是此类查询的一个示例:
SELECT COUNT(*) FROM t1 WHERE key1 = 1 AND key2 = 1;
如果使用过的索引不覆盖查询中使用的所有列,则只有在满足所有使用过的键的范围条件时才会检索完整的行。
如果合并的条件之一是InnoDB表的主键上的条件,则它不用于行检索,而是用于过滤出使用其他条件检索的行。
1.3.2 索引合并联合访问算法(Index Merge Union Access Algorithm)
该算法的准则与索引合并交集算法的准则相似。
该算法适用于表的WHERE子句转换为多个范围条件,每个条件都是以下条件之一:
- 此形式的N个部分表达式,其中索引恰好有N个部分(即,涵盖所有索引部分):
key_part1 = const1 AND key_part2 = const2 ... AND key_partN = constN - 在InnoDB表的主键上的任何范围条件
- 索引合并交集算法适用的一个条件。
示例:
SELECT * FROM t1
WHERE key1 = 1 OR key2 = 2 OR key3 = 3;
SELECT * FROM innodb_table
WHERE (key1 = 1 AND key2 = 2)
OR (key3 = 'foo' AND key4 = 'bar') AND key5 = 5;
1.3.3 索引合并排序并集访问算法(Index Merge Sort-Union Access Algorithm)
当WHERE子句被转换为多个与OR结合的范围条件时,该访问算法适用,但索引合并联合算法不适用。
例如:
SELECT * FROM tbl_name
WHERE key_col1 < 10 OR key_col2 < 20;
SELECT * FROM tbl_name
WHERE (key_col1 > 10 OR key_col2 = 20) AND nonkey_col = 30;
排序合并(sort-union)算法和联合(union)算法的区别:
排序合并算法必须首先为所有行获取行id,并在返回任何行之前对它们进行排序。
1.3.4 影响索引合并优化(Influencing Index Merge Optimization)
索引合并的使用受 optimizer_switch变量的 index_merge、index_merge_intersection、index_merge_union和 index_merge_sort_union标志的值的影响。
请参考第8.9.2节,“可切换优化(Switchable Optimizations)”。
默认情况下,所有这些标志都是 on。
要只启用某些算法,将index_merge设置为off,并只启用应该允许的其他算法。
除了使用 optimizer_switch系统变量来控制优化器在会话范围内使用索引合并算法之外,MySQL还支持优化器提示,以在每个语句的基础上影响优化器。请参考第8.9.3节,“优化器提示(Optimizer Hints)”
1.4 Hash Join优化
默认情况下,MySQL(8.0.18及更高版本)尽可能使用 hash join连接。
可以控制是否使用 BNL和 NO_BNL优化器提示之一来使用 hash join,或者通过设置block_nested_loop=on 或 block_nested_loop=off作为 optimizer_switch服务器系统变量的设置的一部分。
提示
MySQL 8.0.18支持在 optimizer_switch中设置一个 hash_join标志,以及优化器提示HASH_JOIN和NO_HASH_JOIN。
在MySQL 8.0.19及以后版本中,这些都再也没有任何效果了。
从MySQL8.0.18开始,MySQL对任何查询使用 hash连接,其中每个连接都有一个等连接条件,并且没有可以应用于任何连接条件的索引,例如:
SELECT * FROM t1 JOIN t2 ON t1.c1=t2.c1;
当有一个或多个可用于单表断言的索引时,也可以使用哈希连接。
hash join通常比在这种情况下使用的速度更快,而不是在以前版本的MySQL中使用的块嵌套循环算法(参考块嵌套-循环连接算法 Block Nested-Loop Join Algorithm)。从MySQL8.0.20开始,删除了对块嵌套循环的支持,并且在以前使用块嵌套循环的地方,服务器都使用hash join。
在刚才显示的示例和本节中的其余示例中,我们假设这三个表t1、t2和t3都是使用以下语句创建的:
CREATE TABLE t1 (c1 INT, c2 INT);
CREATE TABLE t2 (c1 INT, c2 INT);
CREATE TABLE t3 (c1 INT, c2 INT);
您可以看到,一个 hash join正在被使用使用 EXPLAIN,如下:
mysql> EXPLAIN
-> SELECT * FROM t1
-> JOIN t2 ON t1.c1=t2.c1\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: t1
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 1
filtered: 100.00
Extra: NULL
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: t2
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 1
filtered: 100.00
Extra: Using where; Using join buffer (hash join)
(在MySQL8.0.20之前,有必要包含 FORMAT=TREE 选项,以查看是否将 hash join用于给定的连接。)
EXPLAIN ANALYZE 还将显示有关所使用的 hash join的信息。
hash join也用于涉及多个连接的查询,只要每对表至少有一个连接条件是一个等值连接,就像这里所示的查询:
SELECT * FROM t1
JOIN t2 ON (t1.c1 = t2.c1 AND t1.c2 < t2.c2)
JOIN t3 ON (t2.c1 = t3.c1);
在像刚才显示的使用内部连接的情况下,任何非等值连接的附加条件都会在连接执行后作为过滤器应用。(对于 outer joins,如 left joins、semijoins 和 ,它们被打印为连接的一部分。)这可以在这里的 EXPLAIN的输出中看到:
mysql> EXPLAIN FORMAT=TREE
-> SELECT *
-> FROM t1
-> JOIN t2
-> ON (t1.c1 = t2.c1 AND t1.c2 < t2.c2)
-> JOIN t3
-> ON (t2.c1 = t3.c1)\G
*************************** 1. row ***************************
EXPLAIN: -> Inner hash join (t3.c1 = t1.c1) (cost=1.05 rows=1)
-> Table scan on t3 (cost=0.35 rows=1)
-> Hash
-> Filter: (t1.c2 < t2.c2) (cost=0.70 rows=1)
-> Inner hash join (t2.c1 = t1.c1) (cost=0.70 rows=1)
-> Table scan on t2 (cost=0.35 rows=1)
-> Hash
-> Table scan on t1 (cost=0.35 rows=1)
从刚才显示的输出中也可以看出,多个 hash joins可以(和已被)用于具有多个等值连接条件的连接。
在MySQL8.0.20之前,如果任何一对连接的表没有至少一个等值连接条件,则不能使用 hash join,并且采用了较慢的块嵌套循环算法。
在MySQL 8.0.20及更高版本中,在此类情况下使用了散hash join,如下所示:
mysql> EXPLAIN FORMAT=TREE
-> SELECT * FROM t1
-> JOIN t2 ON (t1.c1 = t2.c1)
-> JOIN t3 ON (t2.c1 < t3.c1)\G
*************************** 1. row ***************************
EXPLAIN: -> Filter: (t1.c1 < t3.c1) (cost=1.05 rows=1)
-> Inner hash join (no condition) (cost=1.05 rows=1)
-> Table scan on t3 (cost=0.35 rows=1)
-> Hash
-> Inner hash join (t2.c1 = t1.c1) (cost=0.70 rows=1)
-> Table scan on t2 (cost=0.35 rows=1)
-> Hash
-> Table scan on t1 (cost=0.35 rows=1)
(本节后面将提供其他示例。)
hash join也应用于笛卡尔积,即,当没有指定连接条件时,如下所示:
mysql> EXPLAIN FORMAT=TREE
-> SELECT *
-> FROM t1
-> JOIN t2
-> WHERE t1.c2 > 50\G
*************************** 1. row ***************************
EXPLAIN: -> Inner hash join (cost=0.70 rows=1)
-> Table scan on t2 (cost=0.35 rows=1)
-> Hash
-> Filter: (t1.c2 > 50) (cost=0.35 rows=1)
-> Table scan on t1 (cost=0.35 rows=1)
在MySQL 8.0.20及更高版本中,连接不再需要包含至少一个等值连接条件。
这意味着可以使用hash join进行优化的查询类型包括以下列表中的查询类型(包括示例):
- 内部非等值连接
mysql> EXPLAIN FORMAT=TREE SELECT * FROM t1 JOIN t2 ON t1.c1 < t2.c1\G *************************** 1. row *************************** EXPLAIN: -> Filter: (t1.c1 < t2.c1) (cost=4.70 rows=12) -> Inner hash join (no condition) (cost=4.70 rows=12) -> Table scan on t2 (cost=0.08 rows=6) -> Hash -> Table scan on t1 (cost=0.85 rows=6)
- 半连接
mysql> EXPLAIN FORMAT=TREE SELECT * FROM t1 -> WHERE t1.c1 IN (SELECT t2.c2 FROM t2)\G *************************** 1. row *************************** EXPLAIN: -> Nested loop inner join -> Filter: (t1.c1 is not null) (cost=0.85 rows=6) -> Table scan on t1 (cost=0.85 rows=6) -> Single-row index lookup on <subquery2> using <auto_distinct_key> (c2=t1.c1) -> Materialize with deduplication -> Filter: (t2.c2 is not null) (cost=0.85 rows=6) -> Table scan on t2 (cost=0.85 rows=6)
- 反连接
mysql> EXPLAIN FORMAT=TREE SELECT * FROM t2 -> WHERE NOT EXISTS (SELECT * FROM t1 WHERE t1.col1 = t2.col1)\G *************************** 1. row *************************** EXPLAIN: -> Nested loop antijoin -> Table scan on t2 (cost=0.85 rows=6) -> Single-row index lookup on <subquery2> using <auto_distinct_key> (c1=t2.c1) -> Materialize with deduplication -> Filter: (t1.c1 is not null) (cost=0.85 rows=6) -> Table scan on t1 (cost=0.85 rows=6)
- 左外连接
mysql> EXPLAIN FORMAT=TREE SELECT * FROM t1 LEFT JOIN t2 ON t1.c1 = t2.c1\G *************************** 1. row *************************** EXPLAIN: -> Left hash join (t2.c1 = t1.c1) (cost=3.99 rows=36) -> Table scan on t1 (cost=0.85 rows=6) -> Hash -> Table scan on t2 (cost=0.14 rows=6)
- 右外部连接(观察MySQL将所有右外部连接重写为左外部连接):
mysql> EXPLAIN FORMAT=TREE SELECT * FROM t1 RIGHT JOIN t2 ON t1.c1 = t2.c1\G *************************** 1. row *************************** EXPLAIN: -> Left hash join (t1.c1 = t2.c1) (cost=3.99 rows=36) -> Table scan on t2 (cost=0.85 rows=6) -> Hash -> Table scan on t1 (cost=0.14 rows=6)
默认情况下,MySQL 8.0.18和以后的版本会尽可能使用hash join。
可以使用 BNL和 NO_BNL优化器提示之一来控制是否使用hash join。
(MySQL 8.0.18支持的hash_join = on或 hash_join = off作为optimizer_switch服务器系统变量的设置的一部分,以及优化器提示HASH_JOIN或NO_HASH_JOIN。
在MySQL 8.0.19及以后的版本中,这些都不再有任何影响。)
可以使用join_buffer_size系统变量来控制hash join产生的内存使用情况;hash join使用的内存不能超过此量。
当 hash join所需的内存超过可用的内存量时,MySQL将通过使用磁盘上的文件来处理此问题。
如果发生这种情况,您应该知道,如果 hash join不能装入内存,并且它创建的文件超过为open_files_limit置的文件,则连接可能不会成功。
为避免此类问题,请进行以下任一更改:
- 增加join_buffer_size,使 hash join不会溢出到磁盘。
- 增加open_files_limit
从MySQL8.0.18开始,hash join的连接缓冲区被增量分配;
因此,您可以设置 join_buffer_size大小,而无需小查询分配大量的RAM,但外部连接分配完整的缓存。
在MySQL 8.0.20及更高版本中,hash join也用于外部连接(包括反连接antijoins和半连接semijoins),因此这不再是一个问题。
1.5 引擎条件下推优化(Engine Condition Pushdown Optimization)
这种优化提高了非索引列和常数之间的直接比较的效率。在这种情况下,条件被“推”到存储引擎上进行评估。此优化只能由NDB存储引擎使用。
NDB集群,这种优化可以消除需要发送不匹配的行在网络集群的数据节点和MySQL服务器发布查询,并可以加快查询,使用的5到10倍的情况下条件下推可以但不使用。
假设一个NDB集群表的定义如下:
CREATE TABLE t1 (
a INT,
b INT,
KEY(a)
) ENGINE=NDB;
引擎条件下推可以用于查询,如这里所示的查询,其中包括非索引列和常量之间的比较:
SELECT a, b FROM t1 WHERE b = 10;
引擎条件下推的使用,可以在 EXPLAIN的输出中看到:
mysql> EXPLAIN SELECT a, b FROM t1 WHERE b = 10\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: t1
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 10
Extra: Using where with pushed condition
但是,引擎条件下推不能在以下查询使用:
SELECT a,b FROM t1 WHERE a = 10;
引擎条件下推不适用于这里,因为a列上存在索引。(索引访问方法会更有效,因此会优先选择,而不是条件下推。)
当使用>或<操作符将索引列与常量进行比较时,也可以使用引擎条件下推:
mysql> EXPLAIN SELECT a, b FROM t1 WHERE a < 2\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: t1
type: range
possible_keys: a
key: a
key_len: 5
ref: NULL
rows: 2
Extra: Using where with pushed condition
其他支持的引擎状态下推的比较包括:
-
column [NOT] LIKE pattern
-
pattern 必须是包含要匹配的模式的字符串字面文字;有关语法,请参考第12.8.1节“字符串比较函数和运算符”。
-
-
column IS [NOT] NULL
-
column IN ( value_list )
-
value_list中的每个项都必须是一个常量的文字值。
-
-
column BETWEEN constant1 AND constant2
- constant1和constant2必须是常量值。
在前面列表中的所有情况下,都可以将条件转换为列和常数之间的一个或多个直接比较的形式。
默认情况下,将启用引擎条件下推功能。要在服务器启动时禁用它,请将 optimizer_switch系统变量的 engine_condition_pushdown标志设置为off。例如,在my的.cnf文件中,使用以下几行:
[mysqld]
optimizer_switch=engine_condition_pushdown=off
在MySQL运行时,使用以下命令禁用条件下推:
SET optimizer_switch='engine_condition_pushdown=off'
限制。 引擎条件下推功能受到以下限制:
- 只有NDB存储引擎才支持引擎条件下推功能
- 在NDB 8.0.18之前,列可以与常数或仅计算为常数值的表达式进行比较。
在NDB 8.0.18及更高版本中,列可以相互比较,只要它们具有完全相同的类型,包括相同的符号性、长度、字符集、精度和比例。 - 在比较中使用的列不能属于任何BLOB或TEXT类型。此排除性也扩展到JSON、BIT和ENUM列。
- 要与列进行比较的字符串值必须使用与该列相同的排序规则。
- 不直接支持join连接;涉及多个表的条件,如果可能,就会被单独push推送。
使用EXPLAIN输出来确定哪些条件实际上被下推了。请参考第8.8.3节,“扩展解释输出格式”。
以前,引擎条件下推仅限于引用条件被推到的同一表中的列值的条件。从NDB 8.0.16开始,查询计划中早期的表中的列值也可以从推送条件中引用。这减少了在join连接处理过程中SQL节点必须处理的行数。过滤也可以在LDM线程中并行执行,而不是在单个mysqld进程中执行。这有可能显著提高查询的性能。
从NDB 8.0.20开始,如果在同一join连接中使用的任何表上,或在它所依赖的连接巢上的任何表上没有不可推送的条件,则可以推送使用扫描的outer join外部连接。对于半连接也是如此,提供优化策略应用的是 firstMatch(参考第8.2.2.1节,“使用半连接转换优化和存在子查询断言”)。
在以下两种情况下,join连接算法不能与引用以前表中的列相结合:
- 当前面引用的任何一个表都在连接缓冲区中时。在这种情况下,从扫描过滤表中检索到的每一行都与缓冲区中的每一行进行匹配。这意味着在生成扫描过滤器时,没有可以从其中获取特定的列值。
- 当列源自推送联接中的子操作时。这是因为在生成扫描筛选器时,尚未检索到从连接中的祖先操作中引用的行。
从NDB 8.0.27开始,连接中来自祖先表的列可以往下推,前提是它们满足前面列出的要求。
这里显示了这样种查询的一个例子,使用之前创建的表t1:
mysql> EXPLAIN
-> SELECT * FROM t1 AS x
-> LEFT JOIN t1 AS y
-> ON x.a=0 AND y.b>=3\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: x
partitions: p0,p1
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 4
filtered: 100.00
Extra: NULL
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: y
partitions: p0,p1
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 4
filtered: 100.00
Extra: Using where; Using pushed condition (`test`.`y`.`b` >= 3); Using join buffer (hash join)
2 rows in set, 2 warnings (0.00 sec)
1.6 索引条件下推优化
索引条件下推(ICP, Index Condition Pushdown)是针对MySQL使用索引从表中检索行的情况而进行的一种优化。如果没有ICP,存储引擎将遍历索引来找到基表中的行,并将它们返回给MySQL服务器,该服务器将计算这些行的位置条件。启用ICP后,如果仅使用索引中的列来评估WHERE条件,则MySQL服务器会将WHERE条件的这部分推到存储引擎。然后,存储引擎通过使用索引条目来计算推送的索引条件,并且只有当满足该条件时,才是从表中读取的行。ICP可以减少存储引擎必须访问基表的次数,以及MySQL服务器必须访问存储引擎的次数。
索引条件下推优化的适用性取决于以下条件:
- 当需要访问完整的表行时,ICP用于range、ref、eq_ref和ref_or_null访问方法。
- ICP可用于InnoDB和MyISAM表,包括分区的InnoDB和MyISAM表。
- 对于InnoDB表,ICP仅用于辅助索引。ICP的目标是减少全行读取的次数,从而减少I/O操作。
对于InnoDB集群索引,完整的记录已经被读入到InnoDB缓冲区中。在这种情况下,使用ICP并不会减少I/O。 - 在虚拟生成的列上创建的辅助索引不支持ICP。InnoDB支持在虚拟生成的列上的辅助索引。
- 不能向下推引用子查询的条件。
- 不能向下引用存储函数的条件。存储引擎无法调用已存储的函数。
- 被触发的条件不能被推下。(有关已触发条件的信息,请参阅第8.2.2.3节,“使用现有策略优化子查询”。)
要了解这种优化是如何工作的,首先考虑当不使用索引条件下推时索引扫描如何进行:
- 获取下一行,首先通过读取索引元组,然后通过使用索引元组来查找并读取完整的表行。
- 测试适用于此表的所在位置条件的部分。根据测试结果接受或拒绝该行。
使用索引条件下推,扫描如下进行:
- 获取下一行的索引元组(但不是完整的表行)。
- 测试适用于此表的条件,仅使用索引列进行检查。如果不满足该条件,请继续执行下一行的索引元组。
- 如果满足该条件,请使用索引元组来查找并读取完整的表行。
- 测试适用于此表的所在位置条件的其余部分。根据测试结果接受或拒绝该行。
EXPLAIN输出显示,在使用索引条件下推时,在额外的列中使用索引条件。它不显示使用索引,因为当必须读取完整的表行时,这并不适用。
假设一个表包含有关人员及其地址的信息,并且该表有一个定义为索引的索引(邮政编码、姓氏、姓)。如果我们知道一个人的邮政编码值,但不确定他的姓氏,我们可以这样搜索:
SELECT * FROM people
WHERE zipcode='95054'
AND lastname LIKE '%etrunia%'
AND address LIKE '%Main Street%';
MySQL可以使用该索引扫描 zipcode='95054' 的人。
第二部分(lastname LIKE '%etrunia%')不能用于限制必须扫描的行数,因此在没有索引条件下推的情况下,此查询必须检索所有 zipcode='95054'的人的完整表行。
使用索引条件下推,MySQL在读取完整表行之前检查 lastname LIKE '%etrunia%'部分。这避免了读取与 zipcode条件匹配而不与 lastname条件匹配的索引元组对应的完整行。
默认情况下,将启用索引条件下推状态。
过设置 index_condition_pushdown标志,可以通过optimizer_switch系统变量来控制它:
SET optimizer_switch = 'index_condition_pushdown=off';
SET optimizer_switch = 'index_condition_pushdown=on';
请参考第8.9.2节,“可切换优化”。
1.7 嵌套循环Join连接算法
MySQL使用嵌套循环算法或其中的变体来执行表之间的join连接。
- Nested-Loop Join Algorithm
- Block Nested-Loop Join Algorithm
1.7.1 Nested-Loop Join Algorithm
一个简单的嵌套循环连接(NLJ)算法每次从一个循环中的第一个表中读取一个行,将每一行传递给一个嵌套循环,该循环处理连接中的下一个表。这个过程会重复很多次,只要还有表需要join连接。
假设三个表t1、t2、t3之间的join连接将使用以下连接类型来执行:
Table Join Type
t1 range
t2 ref
t3 ALL
如果使用了一个简单的NLJ算法,连接将被这样处理:
for each row in t1 matching range {
for each row in t2 matching reference key {
for each row in t3 {
if row satisfies join conditions, send to client
}
}
}
因为NLJ算法每次将一行从外部循环传递到内部循环,所以它通常会多次读取在内部循环中处理的表。
1.7.2 Block Nested-Loop Join Algorithm
块嵌套循环(BNL)连接算法使用外部循环中读取的行的缓冲,来减少内部循环中必须读取表的次数。例如,如果将10行读入缓冲区并将缓冲区传递到下一个内循环,则可以将内环中读取的每行与缓冲区中的所有10行进行比较。这将使必须读取内部表的次数减少了一个数量级。
在MySQL 8.0.18之前,该算法应用于无法使用索引的等连接;
在MySQL 8.0.18及其中,在这种情况下采用散列连接优化。
从MySQL8.0.20开始,MySQL不再使用块嵌套循环,并且对以前使用块嵌套循环的所有情况都使用哈希连接。参考第8.2.1.4节“散希连接优化”。
MySQL连接缓冲具有以下特征:
- 当连接类型为ALL或 index(换句话说,当不能使用可能的键,并且完成了完全扫描数据行或索引行)或范围时,可以使用连接缓冲。使用缓冲也适用于外部连接,如第8.2.1.12节“块嵌套循环和批处理键访问连接”中所述。
- 连接缓冲区不会为第一个常量表分配,即使它的类型为ALL或 index。
- 只有对连接感兴趣的列存储在其连接缓冲区中,而不是存储在整个行中。
- join_bufder_size系统变量确定用于处理查询的每个连接缓冲区的大小。
- 为每个可以缓冲的连接分配一个缓冲区,因此可以使用多个连接缓冲区来处理给定的查询。
- 在执行连接之前分配连接缓冲区,在查询完成后释放。
- 对于前面描述的NLJ算法的连接示例(没有缓冲),使用连接缓冲执行如下连接:
for each row in t1 matching range { for each row in t2 matching reference key { store used columns from t1, t2 in join buffer if buffer is full { for each row in t3 { for each t1, t2 combination in join buffer { if row satisfies join conditions, send to client } } empty join buffer } } } if buffer is not empty { for each row in t3 { for each t1, t2 combination in join buffer { if row satisfies join conditions, send to client } } }
如果S是连接缓冲区中每个存储的t1、t2组合的大小,C是缓冲区中的组合数,则表t3的扫描次数为:
(S * C)/join_buffer_size + 1
随着join_buffer_size的值的增加,t3扫描的次数会减少,直到join_buffer_size足够大,可以保存所有以前的行组合。在这一点上,使它变大并无法提高速度。
1.8 嵌套Join优化
join连接的语法允许嵌套join连接。
下面的讨论提到了第13.2.10.2节“join连接子句”中描述的连接语法。
与SQL标准相比,table_factor的语法进行了扩展。
后者table_factor只接受table_reference,不接受括号内的列表。如果我们认为table_reference项列表中的每个逗号等价于一个inner join,那么这是一个保守的扩展。例如:
SELECT * FROM t1
LEFT JOIN (t2, t3, t4)
ON (t2.a=t1.a AND t3.b=t1.b AND t4.c=t1.c)
相当于:
SELECT * FROM t1
LEFT JOIN (t2 CROSS JOIN t3 CROSS JOIN t4)
ON (t2.a=t1.a AND t3.b=t1.b AND t4.c=t1.c)
在MySQL中,CROSS JOIN在语法上等同于INNER JOIN;它们可以相互替换。
在标准的SQL中,它们并不是等价的。INNER JOIN与ON子句一起使用;否则将使用CROSS JOIN。
通常,在只包含inner join操作的连接表达式中,可以忽略圆括号。思考下列join表达式:
t1 LEFT JOIN (t2 LEFT JOIN t3 ON t2.b=t3.b OR t2.b IS NULL)
on t1.a=t2.a
删除左侧的括号和分组操作后,join表达式转换为此表达式:
(t1 LEFT JOIN t2 ON t1.a=t2.a) LEFT JOIN t3
ON t2.b=t3.b OR t2.b IS NULL
然而,这两个表达式并不是等价的。要看到这一点,假设表t1、t2和t3具有以下状态:
- 表 t1包含行(1),(2)
- 表 t2包含行(1, 101)
- 表 t3包含行(101)
在这种情况下,第一个表达式返回一个包含这些行(1,1,101,101),(2,NULL,NULL,NULL)的结果集,而第二个表达式则返回这些行 (1,1,101,101),(2,NULL,NULL,101)。
mysql> SELECT *
FROM t1
LEFT JOIN
(t2 LEFT JOIN t3 ON t2.b=t3.b OR t2.b IS NULL)
ON t1.a=t2.a;
+------+------+------+------+
| a | a | b | b |
+------+------+------+------+
| 1 | 1 | 101 | 101 |
| 2 | NULL | NULL | NULL |
+------+------+------+------+
mysql> SELECT *
FROM (t1 LEFT JOIN t2 ON t1.a=t2.a)
LEFT JOIN t3
ON t2.b=t3.b OR t2.b IS NULL;
+------+------+------+------+
| a | a | b | b |
+------+------+------+------+
| 1 | 1 | 101 | 101 |
| 2 | NULL | NULL | 101 |
+------+------+------+------+
在以下示例中,outer join操作与 inner join操作一起使用:
t1 LEFT JOIN (t2, t3) ON t1.a=t2.a
该表达式不能转换为下面的表达式:
t1 LEFT JOIN t2 ON t1.a=t2.a, t3
对于给定的表状态,这两个表达式返回不同的行集:
mysql> SELECT *
FROM t1 LEFT JOIN (t2, t3) ON t1.a=t2.a;
+------+------+------+------+
| a | a | b | b |
+------+------+------+------+
| 1 | 1 | 101 | 101 |
| 2 | NULL | NULL | NULL |
+------+------+------+------+
mysql> SELECT * FROM t1 LEFT JOIN t2 ON t1.a=t2.a, t3;
+------+------+------+------+
| a | a | b | b |
+------+------+------+------+
| 1 | 1 | 101 | 101 |
| 2 | NULL | NULL | 101 |
+------+------+------+------+
因此,如果我们在带有outer join运算符的join表达式中省略圆括号,我们可能会更改原始表达式的结果集。
更准确地说,我们不能忽略left outer join操作的右操作数和 right join操作的左操作数中的圆括号。
换句话说,我们不能忽略 outer join操作的内部表表达式的圆括号。其他操作的圆括号(外部表的操作数)可以被忽略。以下表达式:
(t1,t2) LEFT JOIN t3 ON P(t2.b,t3.b)
等同于任何表t1、t2、t3和属性t2.b和t3.b上的任何条件P的下面这个表达式:
t1, t2 LEFT JOIN t3 ON P(t2.b,t3.b)
当连接表达式(joined_table)中连接操作的执行顺序不是从左到右时,我们就讨论嵌套连接。
请思考以下查询:
SELECT * FROM t1 LEFT JOIN (t2 LEFT JOIN t3 ON t2.b=t3.b) ON t1.a=t2.a
WHERE t1.a > 1
SELECT * FROM t1 LEFT JOIN (t2, t3) ON t1.a=t2.a
WHERE (t2.b=t3.b OR t2.b IS NULL) AND t1.a > 1
这些查询被认为包含以下嵌套连接:
t2 LEFT JOIN t3 ON t2.b=t3.b
t2, t3
在第一个查询中,嵌套连接由left join操作形成。
在第二个查询中,它由一个inner join操作形成。
在第一个查询中,可以省略圆括号:连接表达式的语法结构规定了连接操作的相同执行顺序。
对于第二个查询,不能省略圆括号,尽管这里的连接表达式可以在没有它们的情况下被明确地解释。在我们的扩展语法,第二个查询里括号(t2,t3)是必需的,尽管理论上查询可以解析没有他们:我们仍然会有明确的语法结构查询,因为LEFT JOIN和ON扮演了表达式的左右分隔符的角色(t2、t3)。
前面的例子说明了以下几点:
- 对于只涉及inner join(而不涉及外部连接)的连接表达式,可以删除括号并从左到右计算连接。实际上,表可以按任何顺序进行计算。
- 通常,对于 outer join或与 inner join混合的 outer join则不是如此。删除圆括号可能会改变结果。
具有嵌套的外部连接的查询以与具有内部连接的查询相同的管道方式执行。更准确地说,我们利用了嵌套循环连接算法的一种变体。重新调用嵌套循环算法执行一个查询(查看8.2.1.7章节,嵌套循环Join算法)。假设对3个表T1、T2、T3的连接查询的形式为:
SELECT * FROM T1 INNER JOIN T2 ON P1(T1,T2)
INNER JOIN T3 ON P2(T2,T3)
WHERE P(T1,T2,T3)
这里,P1(T1,T2)和P2(T3,T3)是一些连接条件(关于表达式),而P(T1,T2,T3)是表T1、T2、T3列上的条件。
嵌套循环join算法将以下列方式执行此查询:
FOR each row t1 in T1 {
FOR each row t2 in T2 such that P1(t1,t2) {
FOR each row t3 in T3 such that P2(t2,t3) {
IF P(t1,t2,t3) {
t:=t1||t2||t3; OUTPUT t;
}
}
}
}
符号 t1 || t2 || t3 表示通过连接行t1、t2和t3的列而构造的行。在下面的一些示例中,出现表名时的NULL表示对该表的每个列都使用NULL的行。例如,t1 || t2 || NULL 表示通过连接行 t1和行 t2的列而构造的行,以及连接行 t3的每个列的NULL。这样的一行据说是由 NULL补充的。
现在,请思考一个具有嵌套的外部连接的查询:
SELECT * FROM T1
LEFT JOIN (T2 LEFT JOIN T3 ON P2(T2,T3))
ON P1(T1,T2)
WHERE P(T1,T2,T3)
对于此查询,请修改嵌套循环模式,以获得:
FOR each row t1 in T1 {
BOOL f1:=FALSE;
FOR each row t2 in T2 such that P1(t1,t2) {
BOOL f2:=FALSE;
FOR each row t3 in T3 such that P2(t2,t3) {
IF P(t1,t2,t3) {
t:=t1||t2||t3; OUTPUT t;
}
f2=TRUE;
f1=TRUE;
}
IF (!f2) {
IF P(t1,t2,NULL) {
t:=t1||t2||NULL; OUTPUT t;
}
f1=TRUE;
}
}
IF (!f1) {
IF P(t1,NULL,NULL) {
t:=t1||NULL||NULL; OUTPUT t;
}
}
}
通常,对于外部连接操作中第一个内表的任何嵌套循环,都将引入一个标志,该标志在循环之前关闭,并在循环之后进行检查。当当前行从外部表中可以找到表示内部操作数的表中的匹配项时,该标志已打开。如果在循环循环结束时标志仍然关闭,则没有找到外部表当前行的匹配。在这种情况下,该行由内部表的列的NULL值进行补充。结果行将传递给输出的最终检查,或传递给下一个嵌套循环,但仅当该行满足所有嵌入的外部连接的连接条件时。
在本例中,嵌入了由以下表达式表示的外部连接表:
(T2 LEFT JOIN T3 ON P2(T2,T3))
对于带有内部连接的查询,优化器可以选择不同顺序的嵌套循环,例如:
FOR each row t3 in T3 {
FOR each row t2 in T2 such that P2(t2,t3) {
FOR each row t1 in T1 such that P1(t1,t2) {
IF P(t1,t2,t3) {
t:=t1||t2||t3; OUTPUT t;
}
}
}
}
对于具有外部连接的查询,优化器只能选择这样的顺序,其中外部表的循环先于内部表的循环。
因此,对于具有外部连接的查询,只能有一个嵌套顺序。对于下面的查询,优化器将计算两个不同的嵌套。在两个嵌套中,T1必须在外环中处理,因为它在外部连接中使用。T2和T3在内连接中使用,因此该连接必须在内环中进行处理。但是,由于连接是一个内部连接,T2和T3可以按任意顺序处理。
SELECT * T1 LEFT JOIN (T2,T3) ON P1(T1,T2) AND P2(T1,T3)
WHERE P(T1,T2,T3)
一个嵌套计算出 T2,然后是T3:
FOR each row t1 in T1 {
BOOL f1:=FALSE;
FOR each row t2 in T2 such that P1(t1,t2) {
FOR each row t3 in T3 such that P2(t1,t3) {
IF P(t1,t2,t3) {
t:=t1||t2||t3; OUTPUT t;
}
f1:=TRUE
}
}
IF (!f1) {
IF P(t1,NULL,NULL) {
t:=t1||NULL||NULL; OUTPUT t;
}
}
}
另一个嵌套计算出 T3,然后是T2:
FOR each row t1 in T1 {
BOOL f1:=FALSE;
FOR each row t3 in T3 such that P2(t1,t3) {
FOR each row t2 in T2 such that P1(t1,t2) {
IF P(t1,t2,t3) {
t:=t1||t2||t3; OUTPUT t;
}
f1:=TRUE
}
}
IF (!f1) {
IF P(t1,NULL,NULL) {
t:=t1||NULL||NULL; OUTPUT t;
}
}
}
在讨论内部连接的嵌套循环算法时,我们省略了一些对查询执行性能的影响可能很大的细节。我们没有提到所谓的“pushed-down”条件。假设WHERE的WHERE条件P(T1、T2、T3)可以用连接公式表示:
P(T1,T2,T2) = C1(T1) AND C2(T2) AND C3(T3)
在这种情况下,MySQL实际上使用了以下嵌套循环算法来执行具有内部连接的查询:
FOR each row t1 in T1 such that C1(t1) {
FOR each row t2 in T2 such that P1(t1,t2) AND C2(t2) {
FOR each row t3 in T3 such that P2(t2,t3) AND C3(t3) {
IF P(t1,t2,t3) {
t:=t1||t2||t3; OUTPUT t;
}
}
}
}
您可以看到,每个连接符C1(T1)、C2(T2)、C3(T3)被推出最内部的循环到最外部的循环,在那里可以计算它。如果C1(T1)是一个非常严格的条件,那么这个条件下推可能会大大减少从表T1传递到内环的行数。因此,查询的执行时间可能会大大提高
对于具有外部连接的查询,只有在发现外部表中的当前行在内部表中有匹配项之后,才能检查WHERE条件。因此,将条件推出内部嵌套循环的优化不能直接应用于具有外部连接的查询。在这里,我们必须引入条件下推断言,由遇到匹配时打开的标志保护。
回想一下使用外部连接的此示例:
P(T1,T2,T3)=C1(T1) AND C(T2) AND C3(T3)
对于这个例子,使用有保护的下推条件的嵌套循环算法如下所示:
FOR each row t1 in T1 such that C1(t1) {
BOOL f1:=FALSE;
FOR each row t2 in T2
such that P1(t1,t2) AND (f1?C2(t2):TRUE) {
BOOL f2:=FALSE;
FOR each row t3 in T3
such that P2(t2,t3) AND (f1&&f2?C3(t3):TRUE) {
IF (f1&&f2?TRUE:(C2(t2) AND C3(t3))) {
t:=t1||t2||t3; OUTPUT t;
}
f2=TRUE;
f1=TRUE;
}
IF (!f2) {
IF (f1?TRUE:C2(t2) && P(t1,t2,NULL)) {
t:=t1||t2||NULL; OUTPUT t;
}
f1=TRUE;
}
}
IF (!f1 && P(t1,NULL,NULL)) {
t:=t1||NULL||NULL; OUTPUT t;
}
}
一般来说,可以从P1(T1, T2)和P(T2,T3)等连接条件中提取下推断言。在这种情况下,下推断言还由一个标志保护,该标志防止检查由相应的outer join操作生成的 NULL补充行的断言。
如果它是由来自WHERE条件中的断言诱导的,则禁止通过键从同一嵌套连接中的一个内表到另一个内表的访问。
1.9 Outer Join外连接优化
Outer join包括 LEFT JOIN 和 RIGHT JOIN
MySQL实现了 A LEFT JOIN B join_specification 像以下这样:
- 表B被设置为依赖于表A和表A所依赖的所有表。
- 表A被设置为依赖于在LEFT JOIN条件中使用的所有表(B除外)
- LEFT JOIN条件用于决定如何从表B中检索行。(换句话说,不使用WHERE子句中的任何条件。)
- 所有标准连接优化都被执行,除了总是在其依赖的所有表之后读取表。如果存在循环依赖关系,则会发生错误。
- 所有标准WHERE 优化被执行。
- 如果A中有一行与WHERE子句相匹配,但B中没有匹配ON条件的行,则会生成一个额外的B行,所有列都设置为NULL。
- 如果你使用左连接LEFT JOIN找到行不存在于某些表,你有以下测试:
在WHERE部分,col_name_IS_NULL,其中 col_name是一个被声明为NOT NULL的列,MySQL停止搜索更多的行(对于特定的键组合)后找到一行匹配 LEFT JOIN条件。
RIGHT JOIN实现类似于 表角色反转的LEFT JOIN。
右连接被转换为等效的左连接,如第8.2.1.10节“Outer Join简化”中所述。
对于 LEFT JOIN,如果生成的NULL行的WHERE条件总是false,则 LEFT JOIN将更改为内部连接。例如,如果t2.column1为NULL,则在以下查询中的WHERE子句将为false:
SELECT * FROM t1 LEFT JOIN t2 ON (column1) WHERE t2.column2=5;
因此,将查询转换为内部连接是安全的:
SELECT * FROM t1, t2 WHERE t2.column2=5 AND t1.column1=t2.column1;
在MySQL 8.0.14及以后的版本中,在准备过程中删除了由常量文字表达式产生的不重要的WHERE条件,而不是在优化的后期阶段,此时连接已经被简化了。早期删除不重要的条件允许优化器将外部连接转换为内部连接;这可能会导致对在WHERE子句中包含不重要的条件的外部连接的查询的改进计划,例如:
SELECT * FROM t1 LEFT JOIN t2 ON condition_1 WHERE condition_2 OR 0 = 1
优化器现在在准备过程中看到0=1总是false,使 OR 0=1冗余,并删除它,留下这个:
SELECT * FROM t1 LEFT JOIN t2 ON condition_1 where condition_2
现在优化器可以将查询重写为内部连接,就像这样
SELECT * FROM t1 JOIN t2 WHERE condition_1 AND condition_2
现在优化器可以在表 t1之前使用表 t2,如果这样做会产生更好的查询计划。
要提供有关表连接顺序的提示,请使用优化器提示;请参考第8.9.3节“优化器提示”。
或者,也可以使用STRAIGHT_JOIN;请参考第13.2.10节,“ SELECT语句”。
但是,STRAIGHT_JOIN可能会阻止使用索引,因为它禁用了半连接转换;请参考第8.2.2.1节,“使用半连接转换优化和存在的子查询断言子查询断言”。
1.10 Outer Join外连接简单化
1.11 多范围读取优化
当表很大且没有存储在存储引擎的缓存中时,使用辅助索引上的范围扫描读取行可能导致对基表的许多随机磁盘访问。通过磁盘扫描多范围读取(MRR)优化,MySQL尝试通过首先只扫描索引并收集相关行的键来减少范围扫描的随机磁盘访问次数。然后对键进行排序,最后使用主键的顺序从基表中检索行。磁盘扫描MRR的动机是为了减少随机磁盘访问的数量,而不是为了实现对基表数据的更连续的扫描。
多范围读取优化提供了这些好处:
- MRR允许基于索引元组按顺序而不是随机顺序访问数据行。服务器获得一组满足查询条件的索引元组,根据数据行ID的顺序对它们进行排序,并使用排序后的元组按顺序检索数据行。这使得数据访问更高效和更便宜。
- MRR允许批处理需要通过索引元组访问数据行的操作的键访问请求,例如范围索引扫描和为连接属性使用索引的等量连接。MRR遍历一系列索引范围,以获得合格的索引元组。当这些结果累积后,它们被用于访问相应的数据行。在开始读取数据行之前,不需要获取所有的索引元组。
在虚拟生成的列上创建的辅助索引不支持MRR优化。InnoDB支持在虚拟生成的列上的辅助索引。
以下场景说明了MRR优化何时可能是有利的:
方案A:MRR可用于InnoDB和MyISAM表,用于索引范围扫描和等连接操作。
- 索引元组的一部分被累积在一个缓冲区中。
- 缓冲区中的元组按其数据行ID进行排序。
- 根据排序后的索引元组序列访问数据行。
场景B:MRR可用于多范围索引扫描的NDB表,或在执行属性的等连接时。
- 一部分范围,可能是单键范围,被累积在提交查询的中心节点上的缓冲区中。
- 这些范围将被发送到访问数据行的执行节点。
- 被访问的行被打包到包中,并被发送回中心节点。
- 接收到的带有数据行的数据包被放置在一个缓冲区中。
- 从缓冲区中读取数据行。
当使用MRR时,EXPLAIN 输出中的额外列显示了使用MRR。
如果不需要访问完整的表行来产生查询结果,则InnoDB和MyISAM不使用MRR。如果结果可以完全基于索引元组中的信息(通过覆盖索引)来产生,则会出现这种情况;MRR没有提供任何好处。
两个 optimizer_switch系统变量标志为MRR优化的使用提供了一个接口。mrr标志控制是否启用了MRR。如果启用了mrr(on),mrr_cost_based标志将控制优化器是否尝试在使用和不使用MRR之间做出基于成本的选择,还是尽可能使用MRR(off)。默认情况下,mrr打开(on),mrr_cost_based打开(on)。请参见第8.9.2节,“可切换优化”。
对于MRR,存储引擎使用 read_rnd_buffer_size系统变量的值作为它可以为其缓冲区分配多少内存的指导方针。引擎最多使用 read_rnd_buffer_size个字节,并确定在一次传递中要处理的范围数。
【未完待续...】
1.12 块嵌套循环和批处理密钥访问连接
1.13 条件过滤
1.14 常量折叠优化
1.15 IS NULL优化
1.16 ORDER BY优化
1.17 GROUP优化
1.18 DISTINCT优化
1.19 LIMIT查询优化
1.20 函数调用优化
1.21 窗口功能优化
1.22 行构造函数表达式优化
1.23 避免全表扫描
全部内容:
Where语句优化、范围优化、索引合并优化、Hash Join连接优化、引擎条件下推优化、索引条件下推优化、嵌套循环Join连接算法、嵌套Join优化、Outer Join外连接优化、Outer Join外连接简单化、多范围读优化、块嵌套循环和批处理密钥访问连接、条件过滤、常量折叠优化、IS NULL优化、ORDER BY优化、GROUP优化、DISTINCT优化、LIMIT查询优化、函数调用优化、行构造函数表达式优化、避免全表扫描。
优化INFORMATION_SCHEMA查询
优化性能架构查询
优化数据更改语句(INSERT插入、UPDATE更新、DELETE删除)
优化数据库权限
其他优化技巧