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语句外,查询的调优技术也适用于构造,如CREATE TABLE……AS SELECT,INSERT INTO...SELECT,以及在删除语句中的WHERE语句子句。
这些语句具有额外的性能考虑因素,因为它们将写操作与面向读的查询操作结合起来。
NDB集群支持连接下推优化,即将合格的连接完整地发送到NDB集群数据节点,在这些数据节点中可以在这些节点之间分布并并行执行。有关此优化的更多信息,请参考NDB下推连接的条件。
优化查询的主要考虑事项是:
这里讨论可以为处理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连接优化”。
- # CREATE TABLE t (c TINYINT UNSIGNED NOT NULL);
- SELECT * FROM t WHERE c ≪ 256;
- -≫ SELECT * FROM t WHERE 1;
有关更多信息,请参考第8.2.1.14节,“常量折叠优化”。
以下所有表都用作常量表:
- SELECT * FROM t WHERE primary_key=1;
- SELECT * FROM t1,t2
- WHERE t1.primary_key=1 AND t2.primary_key=t1.id;
一些非常快的查询的例子:
- 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, ... ;
范围访问方法使用单个索引,来检索包含在一个或多个索引值间隔内的表行的子集。
它可以用于单部分或多部分的索引。
以下部分描述了优化器使用范围访问的条件。
对于单部分索引,索引值区间可以方便地用WHERE子句中的相应条件表示,表示为范围条件,而不是“区间”。
单个部分索引的范围条件的定义如下:
上述描述中的“常数值”是指下列情况之一:
以下是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中。
多部分指标的范围条件是单部分指数的范围条件的扩展。
多部分索引上的范围条件将索引行限制在一个或几个关键元组间隔内。
关键元组区间在一组关键元组上定义,使用索引的排序。
例如,思考一个被定义为 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'没有定义单个时间间隔,并且不能被范围访问方法使用。
以下描述将更详细地说明范围条件如何在多部分索引中工作。
- 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)。
key_part1 = 'foo' AND key_part2 >= 10 AND key_part3 > 10 对应单区间为: ('foo',10,-inf) < (key_part1,key_part2,key_part3) < ('foo',+inf,+inf) 创建的区间可能比初始条件包含更多的行。(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)
在本例中,第一行上的区间使用一个关键部分作为左界,使用两个关键部分作为右界。key_part1 >= 1 AND key_part2 < 2 但是,实际上,条件是这样转换成这样的:key_part1 >= 1 AND key_part2 IS NOT NULL 有关如何执行优化以组合或消除单部分索引上的区间条件的区间描述,可参考:单个部分索引的范围访问方法(1.2.1)。思考以下表达式,其中col_name是一个索引列:
- col_name IN(val1, ..., valN)
- col_name = val1 OR ... OR col_name = valN
如果col_name等于几个值中的任意一个,则每个表达式都为true。
这些比较是相等的范围比较(其中“范围”是单个值)。
优化器估计读取相等范围比较的限定行的成本如下:
使用索引下钻,优化器在范围的边界处进行下钻,并使用范围内的行数作为估计值。
例如,表达式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中,对于满足所有这些条件的查询,可以跳过索引下钻:
对于 EXPLAIN FOR CONNECTION,如果跳过下钻索引,输出更改如下:
如果跳过索引下钻,没有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 .
思考以下场景:
- 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;
对于前面显示的数据集,该算法是如下操作的:
使用此策略减少访问的行数,因为MySQL跳过了不符合每个构造范围的行。
此跳过扫描访问方法适用于以下条件:
跳过扫描的使用如下所示:
Using index for skip scan 在 'Extra'中表示使用了松散索引跳过扫描访问方法。
- "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节,“优化器提示”。
优化器能够将范围扫描访问方法应用于此形式的查询:
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' );
要使优化器使用范围扫描,查询必须满足以下条件:
有关优化器和行构造函数的更多信息,请参阅第8.2.1.22节,“行构造函数表达式优化”
- Warning 3170 Memory capacity of N bytes for 'range_optimizer_max_mem_size' exceeded.
- Range optimization was not done for this query.
对于超过可用范围优化内存且优化器退回到较差最优计划的单个查询,增加range_optimizer_max_mem_size值可能会提高性能。
要估计处理范围表达式所需的内存量,请使用以下准则:
- SELECT COUNT(*) FROM t
- WHERE a=1 OR a=2 OR a=3 OR .. . a=N;
- SELECT COUNT(*) FROM t
- WHERE a=1 AND b=1 AND c=1 ... N;
- SELECT COUNT(*) FROM t
- WHERE a IN (1,2, ..., M) AND b IN (1,2, ..., N);
IN()列表中的每个文字值都算作与OR结合的结果。
如果有两个IN()列表,则与OR组合的结果的数量是每个列表中文字值的数量的乘积。
因此,在前一例中,与OR组合的结果数为M×N。
索引合并访问方法检索具有多个范围扫描的行,并将其结果合并为一个行。此访问方法仅合并来自单个表的索引扫描,而不是跨多个表的扫描。合并可以产生其底层扫描的联合、交集或联合交集。
可能使用索引合并的示例查询:
- 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字段中:
此访问算法适用于将WHERE子句转换为与AND组合的不同键上的多个范围条件,且每个条件都是以下条件之一:
例如:
- 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表的主键上的条件,则它不用于行检索,而是用于过滤出使用其他条件检索的行。
该算法的准则与索引合并交集算法的准则相似。
该算法适用于表的WHERE子句转换为多个范围条件,每个条件都是以下条件之一:
示例:
- 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;
当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,并在返回任何行之前对它们进行排序。
索引合并的使用受 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)”
默认情况下,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> 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置的文件,则连接可能不会成功。
为避免此类问题,请进行以下任一更改:
从MySQL8.0.18开始,hash join的连接缓冲区被增量分配;
因此,您可以设置 join_buffer_size大小,而无需小查询分配大量的RAM,但外部连接分配完整的缓存。
在MySQL 8.0.20及更高版本中,hash join也用于外部连接(包括反连接antijoins和半连接semijoins),因此这不再是一个问题。
这种优化提高了非索引列和常数之间的直接比较的效率。在这种情况下,条件被“推”到存储引擎上进行评估。此优化只能由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
其他支持的引擎状态下推的比较包括:
在前面列表中的所有情况下,都可以将条件转换为列和常数之间的一个或多个直接比较的形式。
默认情况下,将启用引擎条件下推功能。要在服务器启动时禁用它,请将 optimizer_switch系统变量的 engine_condition_pushdown标志设置为off。例如,在my的.cnf文件中,使用以下几行:
- [mysqld]
- optimizer_switch=engine_condition_pushdown=off
在MySQL运行时,使用以下命令禁用条件下推:
SET optimizer_switch='engine_condition_pushdown=off'
限制。 引擎条件下推功能受到以下限制:
以前,引擎条件下推仅限于引用条件被推到的同一表中的列值的条件。从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)
索引条件下推(ICP, Index Condition Pushdown)是针对MySQL使用索引从表中检索行的情况而进行的一种优化。如果没有ICP,存储引擎将遍历索引来找到基表中的行,并将它们返回给MySQL服务器,该服务器将计算这些行的位置条件。启用ICP后,如果仅使用索引中的列来评估WHERE条件,则MySQL服务器会将WHERE条件的这部分推到存储引擎。然后,存储引擎通过使用索引条目来计算推送的索引条件,并且只有当满足该条件时,才是从表中读取的行。ICP可以减少存储引擎必须访问基表的次数,以及MySQL服务器必须访问存储引擎的次数。
索引条件下推优化的适用性取决于以下条件:
要了解这种优化是如何工作的,首先考虑当不使用索引条件下推时索引扫描如何进行:
使用索引条件下推,扫描如下进行:
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节,“可切换优化”。
MySQL使用嵌套循环算法或其中的变体来执行表之间的join连接。
一个简单的嵌套循环连接(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算法每次将一行从外部循环传递到内部循环,所以它通常会多次读取在内部循环中处理的表。
块嵌套循环(BNL)连接算法使用外部循环中读取的行的缓冲,来减少内部循环中必须读取表的次数。例如,如果将10行读入缓冲区并将缓冲区传递到下一个内循环,则可以将内环中读取的每行与缓冲区中的所有10行进行比较。这将使必须读取内部表的次数减少了一个数量级。
在MySQL 8.0.18之前,该算法应用于无法使用索引的等连接;
在MySQL 8.0.18及其中,在这种情况下采用散列连接优化。
从MySQL8.0.20开始,MySQL不再使用块嵌套循环,并且对以前使用块嵌套循环的所有情况都使用哈希连接。参考第8.2.1.4节“散希连接优化”。
MySQL连接缓冲具有以下特征:
- 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足够大,可以保存所有以前的行组合。在这一点上,使它变大并无法提高速度。
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具有以下状态:
在这种情况下,第一个表达式返回一个包含这些行(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)。
前面的例子说明了以下几点:
具有嵌套的外部连接的查询以与具有内部连接的查询相同的管道方式执行。更准确地说,我们利用了嵌套循环连接算法的一种变体。重新调用嵌套循环算法执行一个查询(查看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条件中的断言诱导的,则禁止通过键从同一嵌套连接中的一个内表到另一个内表的访问。
Outer join包括 LEFT JOIN 和 RIGHT JOIN
MySQL实现了 A LEFT JOIN B join_specification 像以下这样:
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节,“使用半连接转换优化和存在的子查询断言子查询断言”。
当表很大且没有存储在存储引擎的缓存中时,使用辅助索引上的范围扫描读取行可能导致对基表的许多随机磁盘访问。通过磁盘扫描多范围读取(MRR)优化,MySQL尝试通过首先只扫描索引并收集相关行的键来减少范围扫描的随机磁盘访问次数。然后对键进行排序,最后使用主键的顺序从基表中检索行。磁盘扫描MRR的动机是为了减少随机磁盘访问的数量,而不是为了实现对基表数据的更连续的扫描。
多范围读取优化提供了这些好处:
在虚拟生成的列上创建的辅助索引不支持MRR优化。InnoDB支持在虚拟生成的列上的辅助索引。
以下场景说明了MRR优化何时可能是有利的:
方案A:MRR可用于InnoDB和MyISAM表,用于索引范围扫描和等连接操作。
场景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删除)
优化数据库权限
其他优化技巧