MySQL 8.0 参考手册——8.2优化 SQL 语句

数据库应用程序的核心逻辑是通过 SQL 语句来执行的,无论是通过解释器直接发出还是通过 API 在后台提交。本节中的调整指南有助于提高各种 MySQL 应用程序的速度。指南涵盖读写数据的 SQL 操作、一般 SQL 操作的幕后开销,以及数据库监控等特定场景中使用的操作。

一、 优化 SELECT 语句

8.2.1.1 优化 WHERE 子句

本节讨论在处理 WHERE 子句时可以进行的优化。示例使用的是 SELECT 语句,但同样的优化也适用于 DELETE 和 UPDATE 语句中的 WHERE 子句。

你可能会受到诱惑,为了加快算术运算的速度,而重写查询,牺牲可读性。但由于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.16 开始,数字类型列与常量值的比较会进行检查,删除无效或过时的值:

# CREATE TABLE t (c TINYINT UNSIGNED NOT NULL);
  SELECT * FROM t WHERE c ≪ 256;
-SELECT * FROM t WHERE 1;

对于 MyISAM 表和 MEMORY 表,将直接从表信息中获取单个表不带 WHERE条件 的 COUNT(*)。如果只对一个表使用任何 NOT NULL 表达式,也会这样做。

早期检测无效常量表达式。MySQL 能快速检测到某些 SELECT 语句是不可能的,因此不返回任何记录。

如果不使用 GROUP BY 或聚合函数(COUNT()、MIN() 等),HAVING 将与 WHERE 合并。

对于连接中的每个表,都会构造一个更简单的 WHERE,以便快速评估表的 WHERE 值,并尽快跳过记录。

首先读取所有常量表,然后才读取查询中的其他表。常量表是指以下任何一种表:

1,空表或只有一行的表。

2,在 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, ... ;

 

8.2.1.2 范围优化

范围访问方法使用单个索引来检索包含在一个或多个索引值区间内的表行子集。它可用於单部分或多字段索引。下文将介绍优化程序使用范围访问的条件。

  • 单字段索引的范围访问方法
  • 多字段索引的范围访问方法
  • 多值比较的相等范围优化
  • 跳过扫描范围访问方法
  • 行构造函数表达式的范围优化
  • 限制范围优化的内存使用
单字段索引的范围访问方法

对於单字段索引,索引值区间可以方便地用 WHERE 子句中的相应条件来表示,表示为范围条件而不是 "区间"。

单字段索引的范围条件定义如下:

  • 对于 BTREE 和 HASH 索引,在使用 =、<=>、IN()、IS NULL 或 IS NOT NULL 操作符时,键部分与常量值的比较是范围条件。
  • 此外,对于 BTREE 索引,在使用 >、<、>=、<=、BETWEEN、!= 或 <> 操作符或 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,折叠条件总是为真或为假:

  • (key1 LIKE 'abcde%' OR TRUE) is always true

  • (key1 < 'uux' AND key1 > 'z') is always 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 不支持合并多个范围。要绕过这一限制,可以使用 UNION 和相同的 SELECT 语句,只是将每个空间谓词放在不同的 SELECT 中。

 多字段索引的范围访问方法

多字段索引的范围条件是单字段索引范围条件的扩展。多字段索引的范围条件限制索引行位于一个或多个关键元组区间内。关键元组区间是在一组关键元组上定义的,使用的是索引的排序。

例如,考虑一个定义为 key1(key_part1、key_part2、key_part3)的多字段索引,以及以下按 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 比较操作符之一,条件涵盖所有索引部分。(例如,下面是一个由三部分组成的 HASH 索引的范围条件:

key_part1 = 1 AND key_part2 IS NULL AND key_part3 = 'foo'

对于 BTREE 索引,区间可用于与 AND 结合使用的条件,其中每个条件使用 =、<=>、IS NULL、>、<、>=、<=、!=、<>、BETWEEN 或 LIKE "模式"(其中 "模式 "不以通配符开头)比较关键部分和常量值。只要能确定一个包含符合条件的所有行的单键元组(如果使用 <> 或 != 则为两个区间),就可以使用区间。

只要比较操作符是 =、<=> 或 IS NULL,优化器就会尝试使用其他关键部分来确定区间。如果运算符为 >、<、>=、<=、!=、<>、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

但事实上,条件是这样转换的:

key_part1 >= 1 AND key_part2 IS NOT NULL
多值比较的相等范围优化

考虑这些表达式,其中 col_name 是索引列:

col_name IN(val1, ..., valN)
col_name = val1 OR ... OR col_name = valN

如果 col_name 等于多个值中的任意一个,则每个表达式为真。这些比较是相等范围比较(其中 "范围 "是单个值)。优化器会按以下方式估算为相等范围比较读取合格记录的成本:

如果 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。

在 MySQL 8.0 之前,除了使用 eq_range_index_dive_limit 系统变量外,无法跳过使用索引潜入来估计索引有用性。在 MySQL 8.0 中,满足所有这些条件的查询都可以跳过索引潜入:

  • 查询针对的是单个表,而不是多个表的连接。
  • 存在单索引 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 输出不会发生变化。

在执行了跳过索引潜入的查询后,信息模式 OPTIMIZER_TRACE 表中的相应行包含一个跳过索引潜入的索引_dives_for_range_access 值。

跳过扫描范围访问方法

请考虑以下情况:

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 可以选择索引扫描来获取所有记录(索引包括要选择的所有列),然后应用 WHERE 子句中的 f2 > 40 条件来生成最终结果集。

范围扫描比全索引扫描更有效,但在本例中不能使用,因为在第一个索引列 f1 上没有条件。不过,从 MySQL 8.0.13 开始,优化器可以执行多次范围扫描,对 f1 的每个值都执行一次,使用的方法称为跳过扫描,与松散索引扫描类似

  1. 跳过第一个索引部分 f1(索引前缀)的不同值。
  2. 针对剩余索引部分中 f2 > 40 条件的每个不同前缀值执行子范围扫描。

对于前面显示的数据集,算法是这样运行的:

  1. 获取第一个关键字部分的第一个不同值 (f1 = 1)。
  2. 根据第一个和第二个关键字部分构建范围(f1 = 1 AND f2 > 40)。
  3. 执行范围扫描。
  4. 获取第一个关键字部分的下一个不同值 (f1 = 2)。
  5. 根据第一个和第二个关键字部分构建范围(f1 = 2 AND f2 > 40)。
  6. 执行范围扫描。

使用此策略可减少访问记录的数量,因为 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() 操作符。
  • 查询必须是连接查询,即 OR 条件的 AND:(cond1(key_part1) OR cond2(key_part1)) AND (cond1(key_part2) OR ...) AND ...
  • C 列上必须有一个范围条件。
  • 允许在 D 列上设置条件。D 列上的条件必须与 C 列上的范围条件相结合。

跳转扫描的使用在 EXPLAIN 输出中显示如下:

  • 在 Extra 列中使用索引进行跳过扫描表示使用了松散索引跳过扫描访问方法。
  • 如果索引可用于跳过扫描,则索引应在 possible_keys 列中可见。

在优化器跟踪输出中,"skip scan"(跳过扫描)元素以这种形式表示使用了跳过扫描:

"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 标志值。要禁用它,请将 skip_scan 设置为 off。

除了使用 optimizer_switch 系统变量控制优化器在整个会话范围内使用 Skip Scan 之外,MySQL 还支持优化器提示,以便在每条语句的基础上影响优化器。

行构造表达式的范围优化

优化器可以对这种形式的查询应用范围扫描访问方法:

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() 谓词的右侧,有不止一个行构造函数。
限制内存使用以优化范围

要控制范围优化器的可用内存,请使用 range_optimizer_max_mem_size 系统变量:

  • 值为 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 系统变量,则会出现错误而不是警告,因为实际上没有使用键来确定要修改哪些行。

如果个别查询超出了可用的范围优化内存,优化器会退回到较差的优化计划,那么增加 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。

8.2.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 输出中,索引合并方法在类型列中显示为 index_merge。在这种情况下,key 列包含使用的索引列表,key_len 包含这些索引的最长键部分列表。

索引合并访问方法有多种算法,这些算法显示在 EXPLAIN 输出的 Extra 字段中:

  • Using intersect(...)
  • Using union(...)
  • Using sort_union(...)

下文将详细介绍这些算法。优化器会根据各种可用选项的成本估算,在不同的索引合并算法和其他访问方法中做出选择。

  • 索引合并交叉访问算法
  • 索引合并联合访问算法
  • 索引合并排序-联合访问算法
  • 影响索引合并优化的因素
索引合并交叉访问算法

这种访问算法适用于将 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;

索引合并交集算法会对所有使用的索引执行同步扫描,并生成从合并索引扫描中获得的行序列交集。

如果所使用的索引覆盖了查询中使用的所有列,则不会检索到完整的表行(在这种情况下,EXPLAIN 输出包含 Extra 字段中的 Using index)。下面是这样一个查询示例:

SELECT COUNT(*) FROM t1 WHERE key1 = 1 AND key2 = 1;

如果使用的索引没有覆盖查询中使用的所有列,则只有在满足所有使用键的范围条件时,才会检索到完整的行。

如果合并条件之一是 InnoDB 表主键的条件,则不会用于检索记录,而是用于过滤使用其他条件检索的记录。

索引合并联合访问算法

该算法的标准与索引合并交叉算法的标准类似。该算法适用于将表中的 WHERE 子句转换为不同键上的多个范围条件,并与 OR 结合使用,且每个条件都是以下条件之一时:

这种形式的 N 部分表达式,其中索引正好有 N 部分(即所有索引部分都包括在内):

key_part1 = const1 OR key_part2 = const2 ... OR 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;
索引合并排序联合访问算法

这种访问算法适用于 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 算法的区别在于,sort-union 算法必须首先获取所有行的 ID 并对其进行排序,然后才能返回任何行。

影响索引合并优化

索引合并的使用受优化器开关系统变量 index_merge、index_merge_intersection、index_merge_union 和 index_merge_sort_union 标志值的限制。默认情况下,所有这些标志都处于开启状态。要只启用某些算法,可将 index_merge 设置为关闭,并只启用其他应允许的算法。

除了使用 optimizer_switch 系统变量控制优化器在整个会话范围内使用索引合并算法外,MySQL 还支持优化器提示,以便在每条语句的基础上影响优化器

 8.2.1.4 哈希连接优化

默认情况下,MySQL(8.0.18 及更高版本)会尽可能使用散列连接。可以使用 BNL 和 NO_BNL 优化器提示,或通过设置 block_nested_loop=on 或 block_nested_loop=off 作为优化器开关服务器系统变量设置的一部分,来控制是否采用散列连接。

#MySQL 8.0.18 支持在 optimizer_switch 中设置 hash_join 标志,以及优化器提示 HASH_JOIN 和 NO_HASH_JOIN。在 MySQL 8.0.19 及更高版本中,这些提示都不再有效。

从 MySQL 8.0.18 开始,MySQL 对每个联接都有等联接条件的任何查询都采用哈希联接,其中没有可应用于任何联接条件的索引,如查询:

SELECT *
    FROM t1
    JOIN t2
        ON t1.c1=t2.c1;

当有一个或多个索引可用於单表谓词时,也可以使用哈希连接。

哈希连接通常比以前版本的 MySQL 中使用的块嵌套循环算法(见块嵌套循环连接算法)更快,因此在这种情况下使用哈希连接。从 MySQL 8.0.20 开始,对块嵌套循环的支持被移除,服务器会在以前使用块嵌套循环的地方使用哈希连接。

在刚才的示例和本节的其他示例中,我们假设使用以下语句创建了三个表 t1、t2 和 t3:

CREATE TABLE t1 (c1 INT, c2 INT);
CREATE TABLE t2 (c1 INT, c2 INT);
CREATE TABLE t3 (c1 INT, c2 INT);

使用 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)

只要每对表中至少有一个连接条件是等连接,哈希连接也可用于涉及多重连接的查询,如下面所示的查询:

SELECT * FROM t1
    JOIN t2 ON (t1.c1 = t2.c1 AND t1.c2 < t2.c2)
    JOIN t3 ON (t2.c1 = t3.c1);

在如图所示的使用内连接的情况下,任何非等连接的附加条件都会在连接执行后作为筛选器应用。(对于外连接,如左连接、半连接和反连接,它们会作为连接的一部分打印出来)。在 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)

从刚刚显示的输出结果中也可以看出,多重哈希连接可以(而且已经)用于具有多个等连接条件的连接。

在 MySQL 8.0.20 之前,如果任何一对连接的表没有至少一个等连接条件,就不能使用散列连接,只能使用较慢的块嵌套循环算法。在 MySQL 8.0.20 及更高版本中,散列连接可用于这种情况,如下所示:

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)

哈希连接也适用于笛卡尔乘积,即未指定连接条件时,如图所示:

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 及更高版本中,为了使用散列连接,连接不再需要至少包含一个等连接条件。这意味着可以使用散列连接优化的查询类型包括下面列表中的查询(附示例):

内部非相等连接

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: -> Hash semijoin (t2.c2 = t1.c1)  (cost=0.70 rows=1)
    -> Table scan on t1  (cost=0.35 rows=1)
    -> Hash
        -> Table scan on t2  (cost=0.35 rows=1)

反连接

mysql> EXPLAIN FORMAT=TREE SELECT * FROM t2 
    ->     WHERE NOT EXISTS (SELECT * FROM t1 WHERE t1.c1 = t2.c1)\G
*************************** 1. row ***************************
EXPLAIN: -> Hash antijoin (t1.c1 = t2.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)

1 row in set, 1 warning (0.00 sec)

mysql> SHOW WARNINGS\G
*************************** 1. row ***************************
  Level: Note
   Code: 1276
Message: Field or reference 't3.t2.c1' of SELECT #2 was resolved in SELECT #1

左外连接

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=0.70 rows=1)
    -> Table scan on t1  (cost=0.35 rows=1)
    -> Hash
        -> Table scan on t2  (cost=0.35 rows=1)

右外连接(注意,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=0.70 rows=1)
    -> Table scan on t2  (cost=0.35 rows=1)
    -> Hash
        -> Table scan on t1  (cost=0.35 rows=1)

默认情况下,MySQL 8.0.18 及更高版本会尽可能使用哈希连接。可以使用 BNL 和 NO_BNL 优化器提示之一来控制是否使用哈希连接。

(MySQL 8.0.18 支持 hash_join=on 或 hash_join=off,作为 optimizer_switch 服务器系统变量以及优化器提示 HASH_JOIN 或 NO_HASH_JOIN 设置的一部分。在 MySQL 8.0.19 及更高版本中,这些提示不再有任何作用)。

可以使用 join_buffer_size 系统变量来控制哈希连接的内存使用量;哈希连接使用的内存不能超过此值。当哈希连接所需的内存超过可用容量时,MySQL 会使用磁盘上的文件来处理。如果出现这种情况,你应该注意,如果哈希连接无法容纳内存,并且创建的文件超过 open_files_limit 的设置,那么连接可能不会成功。为避免此类问题,请进行以下更改:

  • 增大 join_buffer_size,以免散列连接溢出到磁盘。
  • 增加 open_files_limit.

从 MySQL 8.0.18 开始,哈希连接的连接缓冲区以增量方式分配;因此,您可以将 join_buffer_size 设置得更高,而不会让小查询分配大量 RAM,但外连接会分配整个缓冲区。在 MySQL 8.0.20 及更高版本中,哈希连接也用于外部连接(包括反连接和半连接),因此这不再是一个问题。

8.2.1.5 引擎条件下推优化

这一优化提高了非索引列与常数之间直接比较的效率。在这种情况下,条件会被 "下推 "到存储引擎进行评估。该优化仅适用于 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
  • column IS [NOT] NULL
  • column IN (value_list)
  • column BETWEEN constant1 AND constant2

在前面列出的所有情况中,都可以将条件转换为列和常量之间的一个或多个直接比较的形式。

 

引擎条件下推默认为启用。要在服务器启动时禁用,请将 optimizer_switch 系统变量的 engine_condition_pushdown 标志设置为关闭。例如,在 my.cnf 文件中使用以下几行:

[mysqld]
optimizer_switch=engine_condition_pushdown=off

在运行时,像这样禁用条件下推:

SET optimizer_switch='engine_condition_pushdown=off';

仅 NDB 存储引擎支持引擎条件推送。

在 NDB 8.0.18 之前,列只能与常量或只评估常量值的表达式进行比较。在 NDB 8.0.18 及更高版本中,只要列的类型完全相同,包括相同的符号性、长度、字符集、精度和比例(如果适用),列之间就可以相互比较。

用于比较的列不能是任何 BLOB 或 TEXT 类型。这种排除也适用于 JSON、BIT 和 ENUM 列。

要与列进行比较的字符串值必须使用与列相同的校对方式。

不直接支持连接;涉及多个表的条件尽可能单独推送。请使用扩展的 EXPLAIN 输出来确定哪些条件被实际推送

以前,引擎条件下推仅限于引用条件被下推到的同一表中的列值。从 NDB 8.0.16 开始,查询计划中较早的表中的列值也可以从推送条件中引用。这就减少了 SQL 节点在连接处理过程中必须处理的行数。过滤也可以在 LDM 线程中并行执行,而不是在单个 mysqld 进程中执行。这有可能显著提高查询性能。

 

从 NDB 8.0.20 开始,如果在同一连接巢中使用的任何表上,或在其所依赖的连接巢上的任何表上没有不可推送的条件,那么使用扫描的外部连接就可以被推送。如果使用的优化策略是 firstMatch,那么半连接也是如此。

在以下两种情况下,不能将连接算法与引用以前表中的列结合起来:

当引用的任何先前表都在连接缓冲区中时。在这种情况下,从扫描过滤表中检索的每一行都要与缓冲区中的每一行进行匹配。这意味着在生成扫描筛选器时,无法从单一特定行中获取列值。

当列来自推入连接中的子操作时。这是因为在生成扫描筛选器时,尚未检索从连接中的祖先操作引用的行。

 

从 NDB 8.0.27 开始,只要符合前面列出的要求,就可以向下推送连接中来自祖表的列。下面是使用之前创建的表 t1 进行此类查询的示例:

 

 


8.2.1.6 索引条件下推优化
8.2.1.7 嵌套循环连接算法
8.2.1.8 嵌套连接优化
8.2.1.9 外连接优化
8.2.1.10 外连接简化
8.2.1.11 多范围读取优化
8.2.1.12 块嵌套循环和分批密钥访问连接
8.2.1.13 条件过滤
8.2.1.14 恒定折叠优化
8.2.1.15 IS NULL 优化
8.2.1.16 ORDER BY 优化
8.2.1.17 GROUP BY 优化
8.2.1.18 DISTINCT 优化
8.2.1.19 LIMIT 查询优化
8.2.1.20 函数调用优化
8.2.1.21 窗口函数优化
8.2.1.22 行构造表达式优化
8.2.1.23 避免全表扫描

二、 优化子查询、派生表、视图引用和公用表 表达 式

三、 优化INFORMATION_SCHEMA查询

四、 优化性能架构查询

五、 优化数据变更语句

六、 优化数据库权限

七、 其他优化技巧

column [NOT] LIKE pattern
column IS [NOT] NULL
column IN (value_list)
column BETWEEN constant1 AND constant2
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章