第8章优化

目录

8.1优化概述
8.2优化SQL语句
8.2.1优化SELECT语句
8.2.2优化子查询,派生表,查看引用和公用表表达式
8.2.3优化INFORMATION_SCHEMA查询
8.2.4优化性能模式查询
8.2.5优化数据变更声明
8.2.6优化数据库权限
8.2.7其他优化技巧
8.3优化和索引
8.3.1 MySQL如何使用索引
8.3.2主键优化
8.3.3空间索引优化
8.3.4外键优化
8.3.5列索引
8.3.6多列索引
8.3.7验证索引使用情况
8.3.8 InnoDB和MyISAM索引统计信息收集
8.3.9 B树和哈希索引的比较
8.3.10索引扩展的使用
8.3.11生成列索引的优化程序使用
8.3.12隐形指数
8.3.13降序索引
8.3.14 TIMESTAMP列的索引查找
8.4优化数据库结构
8.4.1优化数据大小
8.4.2优化MySQL数据类型
8.4.3优化许多表格
8.4.4 MySQL中的内部临时表使用
8.5优化InnoDB表
8.5.1优化InnoDB表的存储布局
8.5.2优化InnoDB事务管理
8.5.3优化InnoDB只读事务
8.5.4优化InnoDB重做日志
8.5.5 InnoDB表的批量数据加载
8.5.6优化InnoDB查询
8.5.7优化InnoDB DDL操作
8.5.8优化InnoDB磁盘I / O.
8.5.9优化InnoDB配置变量
8.5.10为具有多个表的系统优化InnoDB
8.6优化MyISAM表
8.6.1优化MyISAM查询
8.6.2 MyISAM表的批量数据加载
8.6.3优化REPAIR TABLE语句
8.7优化MEMORY表
8.8了解查询执行计划
8.8.1使用EXPLAIN优化查询
8.8.2 EXPLAIN输出格式
8.8.3扩展EXPLAIN输出格式
8.8.4获取命名连接的执行计划信息
8.8.5估计查询性能
8.9控制查询优化器
8.9.1控制查询计划评估
8.9.2可切换的优化
8.9.3优化器提示
8.9.4索引提示
8.9.5优化器成本模型
8.9.6优化器统计
8.10缓冲和缓存
8.10.1 InnoDB缓冲池优化
8.10.2 MyISAM密钥缓存
8.10.3准备好的声明和存储程序的缓存
8.11优化锁定操作
8.11.1内部锁定方法
8.11.2表锁定问题
8.11.3并发插入
8.11.4元数据锁定
8.11.5外部锁定
8.12优化MySQL服务器
8.12.1优化磁盘I / O.
8.12.2使用符号链接
8.12.3优化内存使用
8.12.4优化网络使用
8.12.5资源组
8.13衡量绩效(基准)
8.13.1测量表达式和函数的速度
8.13.2使用您自己的基准
8.13.3使用performance_schema测量性能
8.14检查线程信息
8.14.1线程命令值
8.14.2一般线程状态
8.14.3复制主线程状态
8.14.4复制从站I / O线程状态
8.14.5复制从属SQL线程状态
8.14.6复制从站连接线程状态
8.14.7 NDB群集线程状态
8.14.8事件调度程序线程状态

本章介绍如何优化MySQL性能并提供示例。 优化涉及在多个级别配置,调整和测量性能。 根据您的工作角色(开发人员,DBA或两者的组合),您可以在单个SQL语句,整个应用程序,单个数据库服务器或多个联网数据库服务器的级别进行优化。 有时您可以主动并提前计划性能,而有时您可能会在出现问题后解决配置或代码问题。 优化CPU和内存使用还可以提高可伸缩性,允许数据库处理更多负载而不会降低速度。

8.1优化概述

数据库性能取决于数据库级别的多个因素,例如表,查询和配置设置。 这些软件构造导致硬件级别的CPU和I / O操作,您必须尽可能地最小化并尽可能高效。 在处理数据库性能时,首先要学习软件方面的高级规则和指南,并使用挂钟时间来衡量性能。 当您成为专家时,您将了解内部发生的更多信息,并开始测量诸如CPU周期和I / O操作之类的事情。

典型用户的目标是从现有的软件和硬件配置中获得最佳的数据库性能。 高级用户寻找改进MySQL软件本身的机会,或者开发自己的存储引擎和硬件设备来扩展MySQL生态系统。

在数据库级别进行优化

使数据库应用程序快速运行的最重要因素是其基本设计:

  • 表格结构合理吗? 特别是,列是否具有正确的数据类型,并且每个表是否具有适合工作类型的列? 例如,执行频繁更新的应用程序通常具有许多具有少量列的表,而分析大量数据的应用程序通常具有很少列的表。

  • 是否有适当的 索引 来提高查询效率?

  • 您是否为每个表使用适当的存储引擎,并利用您使用的每个存储引擎的优势和功能? 特别是,事务性存储引擎(例如 InnoDB 事务性存储引擎)的选择 MyISAM 对于性能和可伸缩性来说非常重要。

    注意

    InnoDB 是新表的默认存储引擎。 实际上,高级 InnoDB 性能特征意味着 InnoDB 表通常优于更简单的 MyISAM 表,尤其是对于繁忙的数据库。

  • 每个表是否使用适当的行格式? 此选择还取决于用于表的存储引擎。 特别是,压缩表使用较少的磁盘空间,因此需要较少的磁盘I / O来读取和写入数据。 压缩适用于具有 InnoDB 表的 所有类型的工作负载 以及只读 MyISAM 表。

  • 应用程序是否使用适当的 锁定策略 例如,通过允许可能的共享访问,以便数据库操作可以并发运行,并在适当时请求独占访问,以便关键操作成为最高优先级。 同样,存储引擎的选择也很重要。 InnoDB 存储引擎处理大部分锁定的问题,而不需要您的参与,允许在数据库更好的并发,减少试验和调整的金额,让您的代码。

  • 是否 正确 使用了用于缓存的 所有 内存区域 也就是说,足够大以容纳频繁访问的数据,但不能太大以至于它们会超载物理内存并导致分页。 要配置的主要内存区域是 InnoDB 缓冲池和 MyISAM 密钥缓存。

在硬件级别进行优化

随着数据库变得越来越繁忙,任何数据库应用程序最终都会达到硬件限制。 DBA必须评估是否可以调整应用程序或重新配置服务器以避免这些 瓶颈 ,或者是否需要更多硬件资源。 系统瓶颈通常来自这些来源:

  • 磁盘寻求。 磁盘需要一段时间才能找到一块数据。 对于现代磁盘,平均时间通常低于10毫秒,因此我们理论上可以做到大约100次寻找。 这个时间用新磁盘慢慢改善,并且很难针对单个表进行优化。 优化寻道时间的方法是将数据分配到多个磁盘上。

  • 磁盘读写。 当磁盘位于正确位置时,我们需要读取或写入数据。 使用现代磁盘,一个磁盘可提供至少10-20MB / s的吞吐量。 这比搜索更容易优化,因为您可以从多个磁盘并行读取。

  • CPU周期。 当数据在主存储器中时,我们必须处理它以获得我们的结果。 与内存量相比具有大表是最常见的限制因素。 但是对于小桌子,速度通常不是问题。

  • 内存带宽。 当CPU需要的数据量超过CPU缓存容量时,主内存带宽成为瓶颈。 对于大多数系统来说,这是一个不常见的瓶颈,但需要注意的是。

平衡可移植性和性能

要在可移植的MySQL程序中使用面向性能的SQL扩展,您可以在 /*! */ 注释分隔符 的语句中包含特定于MySQL的关键字 其他SQL服务器忽略注释的关键字。 有关编写注释的信息,请参见 第9.6节“注释语法”

8.2优化SQL语句

数据库应用程序的核心逻辑是通过SQL语句执行的,无论是直接通过解释器发出还是通过API在后台提交。 本节中的调整准则有助于加速各种MySQL应用程序。 这些准则涵盖了读取和写入数据的SQL操作,一般SQL操作的幕后开销,以及特定方案(如数据库监视)中使用的操作。

8.2.1优化SELECT语句

查询以 SELECT 语句 的形式 执行数据库中的所有查找操作。 调整这些语句是首要任务,无论是为动态网页实现亚秒响应时间,还是为了产生巨大的隔夜报告而缩短工作时间。

此外 SELECT 语句,进行查询调谐技术也适用于结构,如 CREATE TABLE...AS SELECT INSERT INTO...SELECT WHERE 在条款 DELETE 的语句。 这些语句具有额外的性能考虑因素,因为它们将写操作与面向读取的查询操作相结合。

NDB Cluster支持连接下推优化,从而将合格连接完整地发送到NDB Cluster数据节点,在NDB Cluster数据节点中可以将它们分布在它们之间并且并行执行。 有关此优化的更多信息,请参阅 NDB下推连接的条件

优化查询的主要考虑因素是:

  • SELECT ... WHERE 加快查询速度,首先要检查的是是否可以添加 索引 WHERE 子句中 使用的列上设置索引 ,以加快评估,过滤和结果的最终检索。 为避免浪费磁盘空间,请构建一小组索引,以加速应用程序中使用的许多相关查询。

    索引对于引用不同表的查询尤其重要,使用 联接 外键等功能 您可以使用该 EXPLAIN 语句来确定用于a的索引 SELECT 请参见 第8.3.1节“MySQL如何使用索引” 第8.8.1节“使用EXPLAIN优化查询”

  • 隔离并调整查询的任何部分,例如函数调用,这会占用过多时间。 根据查询的结构,可以为结果集中的每一行调用一次函数,甚至可以为表中的每一行调用一次函数,从而大大减轻任何低效率。

  • 最大限度地减少 查询中 全表扫描 ,尤其是对于大表。

  • 通过 ANALYZE TABLE 定期 使用 语句 使表统计信息保持最新 ,因此优化程序具有构建高效执行计划所需的信息。

  • 了解特定于每个表的存储引擎的调优技术,索引技术和配置参数。 双方 InnoDB MyISAM 有两套准则的实现和维持查询高性能。 有关详细信息,请参见 第8.5.6节“优化InnoDB查询” 第8.6.1节“优化MyISAM查询”

  • 您可以 InnoDB 使用 第8.5.3节“优化InnoDB只读事务”中 的技术 优化 表的 单查询事务

  • 避免以难以理解的方式转换查询,尤其是在优化程序自动执行某些相同转换的情况下。

  • 如果其中一个基本准则无法轻松解决性能问题,请通过阅读 EXPLAIN 计划并调整索引, WHERE 子句,连接子句等来 调查特定查询的内部详细信息 (当您达到一定的专业水平时,阅读 EXPLAIN 计划可能是您每次查询的第一步。)

  • 调整MySQL用于缓存的内存区域的大小和属性。 通过有效使用 InnoDB 缓冲池 MyISAM 密钥缓存和MySQL查询缓存,重复查询运行得更快,因为在第二次及以后的时间内从内存中检索结果。

  • 即使对于使用高速缓存存储区域快速运行的查询,您仍可以进一步优化,以便它们需要更少的高速缓存,从而使您的应用程序更具可伸缩性。 可伸缩性意味着您的应用程序可以处理更多的并发用户,更大的请求等,而不会出现性能大幅下降的情况。

  • 处理锁定问题,其中查询的速度可能会受到同时访问表的其他会话的影响。

8.2.1.1 WHERE子句优化

本节讨论可以为处理 WHERE 子句 进行的优化 这些示例使用 SELECT 语句,但相同的优化适用 WHERE DELETE UPDATE 语句中的 子句

注意

由于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或b = 6
    

    在MySQL 8.0.14及更高版本中,这是在准备期间而不是在优化阶段期间进行的,这有助于简化连接。 有关 更多信息和示例, 请参见 第8.2.1.8节“外部连接优化”

  • 索引使用的常量表达式仅计算一次。

  • 从MySQL 8.0.16开始,对具有常量值的数值类型列进行比较,检查并折叠或删除无效或不合法值:

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

    有关 更多信息 请参见 第8.2.1.13节“常量折叠优化”

  • COUNT(*) 在没有a的单个表上 WHERE 直接从表信息 MyISAM MEMORY 表中 检索 NOT NULL 当仅与一个表一起使用时, 也可以对任何 表达式执行 此操作

  • 早期检测无效常量表达式。 MySQL快速检测到某些 SELECT 语句是不可能的,并且不返回任何行。

  • HAVING WHERE 如果您不使用 GROUP BY 或聚合函数( COUNT() MIN() 等等), 则合并

  • 对于连接中的每个表, WHERE 构造 一个更简单 WHERE 的表来 快速 评估表,并尽快跳过行。

  • 在查询中的任何其他表之前首先读取所有常量表。 常量表是以下任何一种:

    • 一张空表或一行表。

    • 与a index WHERE 子句一起 使用的表 ,其中所有索引部分都与常量表达式进行比较并定义为 PRIMARY KEY UNIQUE NOT NULL

    以下所有表都用作常量表:

    SELECT * FROM t WHERE primary_key= 1;
    SELECT * FROM t1,t2
      在哪里t1。primary_key= 1和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_part1key_part2... LIMIT 10;

SELECT ... FROM tbl_name
  ORDER BY key_part1DESC,key_part2DESC,... LIMIT 10;

MySQL仅使用索引树解析以下查询,假设索引列是数字:

SELECT key_part1key_part2FROM tbl_nameWHERE key_part1= val;

SELECT COUNT(*)FROM tbl_name
  WHERE key_part1= val1AND key_part2= val2;

SELECT key_part2FROM tbl_nameGROUP BY key_part1;

以下查询使用索引来按排序顺序检索行,而不使用单独的排序传递:

SELECT ... FROM tbl_name
  ORDER BY key_part1key_part2...;

SELECT ... FROM tbl_name
  ORDER BY key_part1DESC,key_part2DESC,...;

8.2.1.2范围优化

range 访问方法使用单个索引来检索包含一个或若干个索引值的时间间隔内表行的子集。 它可用于单部分或多部分索引。 以下部分描述了优化程序使用范围访问的条件。

单部分索引的范围访问方法

对于单部分索引,索引值间隔可以方便地由 WHERE 子句中 的相应条件 表示,表示为 范围条件 而不是 间隔”。

单部分索引的范围条件的定义如下:

  • 对于这两种 BTREE HASH 索引,使用时具有恒定值的关键部分的比较是一个范围条件 = <=> IN() IS NULL ,或 IS NOT NULL 运营商。

  • 另外,对于 BTREE 索引,使用时具有恒定值的关键部分的比较是一个范围条件 > < >= <= BETWEEN != ,或 <> 运营商,或者 LIKE 比较,如果该参数 LIKE 是一个常数字符串不与通配符开始。

  • 对于所有索引类型,多个范围条件与范围条件组合 OR AND 形成范围条件。

前面描述中的 常量值 表示以下之一:

  • 来自查询字符串的常量

  • 来自同一连接 的a const system table的

  • 不相关子查询的结果

  • 任何表达式完全由前面类型的子表达式组成

以下是 WHERE 子句中 具有范围条件的查询的一些示例

SELECT * FROM t1
  在哪里key_col> 1
  AND key_col<10;

SELECT * FROM t1
  WHERE key_col= 1key_col(15,18,20);

SELECT * FROM t1
  在哪里key_col'ab%'key_col'BETWEEN'bar'和'foo';

在优化器常量传播阶段,一些非常量值可以转换为常量。

MySQL尝试从 WHERE 每个可能索引 子句中 提取范围条件 在提取过程期间,丢弃不能用于构建范围条件的条件,组合产生重叠范围的条件,并且去除产生空范围的条件。

请考虑以下语句,其中 key1 是索引列 nonkey 且未编入索引:

SELECT * FROM t1 WHERE
  (key1 <'abc'AND(key1 LIKE'abcde%'或key1 LIKE'%b'))或者
  (key1 <'bar'和nonkey = 4)或
  (key1 <'uux'和key1>'z');

密钥的提取过程 key1 如下:

  1. 从原始 WHERE 条款 开始

    (key1 <'abc'AND(key1 LIKE'abcde%'或key1 LIKE'%b'))或者
    (key1 <'bar'和nonkey = 4)或
    (key1 <'uux'和key1>'z')
    
  2. 删除 nonkey = 4 key1 LIKE '%b' 因为它们不能用于范围扫描。 删除它们的正确方法是用它们替换它们 TRUE ,这样我们在进行范围扫描时不会错过任何匹配的行。 TRUE 产量 替换它们

    (key1 <'abc'AND(key1 LIKE'abcde%'或TRUE))或
    (key1 <'bar'和TRUE)或
    (key1 <'uux'和key1>'z')
    
  3. 折叠条件始终为真或假:

    • (key1 LIKE 'abcde%' OR TRUE) 总是如此

    • (key1 < 'uux' AND key1 > 'z') 总是假的

    用常数替换这些条件会产生:

    (key1 <'abc'和TRUE)或(key1 <'bar'和TRUE)或(FALSE)
    

    删除不必要的 TRUE FALSE 常量会产生:

    (key1 <'abc')或(key1 <'bar')
    
  4. 将重叠间隔组合成一个会产生用于范围扫描的最终条件:

    (key1 <'bar')
    

通常(并且如前面的示例所示),用于范围扫描的条件比该 WHERE 子句的 限制性更小 MySQL执行额外的检查以过滤掉满足范围条件但不满足完整 WHERE 子句的行。

范围条件提取算法可以处理 任意深度的 嵌套 AND / OR 构造,并且其输出不依赖于条件在 WHERE 子句中 出现的顺序

MySQL不支持合并 range 空间索引 访问方法的 多个范围 要解决此限制, 除了将每个空间谓词放在不同的语句中之外 ,您可以使用 UNION 具有相同 SELECT 语句的语句 SELECT

多部分索引的范围访问方法

多部分索引的范围条件是单部分索引的范围条件的扩展。 多部分索引上的范围条件将索引行限制在一个或多个关键元组间隔内。 使用索引中的排序在一组关键元组上定义关键元组间隔。

例如,考虑定义为的多部分索引 ,以及按键顺序列出的以下一组关键元组: key1(key_part1, key_part2, key_part3)

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 )<(1,+ INF,+ INF)
key_part2key_part3

间隔覆盖前面数据集中的第4,第5和第6个元组,并且可以由范围访问方法使用。

相反,条件 key_part3 = 'abc' 不定义单个间隔,并且不能由范围访问方法使用。

以下描述更详细地说明了范围条件如何适用于多部分索引。

  • 对于 HASH 索引,可以使用包含相同值的每个间隔。 这意味着只能为以下形式的条件生成间隔:

        key_part1 cmp const1key_part2 cmp const2
    和......
    AND ;
    key_partN cmp constN

    这里 const1 const2 ...是常数, cmp 是一个 = <=> 或者 IS NULL 比较运营商,以及条件覆盖所有指数部分。 (也就是说,有一些 N 条件,一个用于 N -part索引的 每个部分 。)例如,以下是三部分 HASH 索引 的范围条件

    key_part1= 1 AND key_part2IS NULL AND key_part3='foo'
    

    有关被认为是常量的定义,请参阅 单部分索引的范围访问方法

  • 对于一个 BTREE 索引,以一定间隔可能是条件组合可用 AND ,其中每个条件使用的恒定值的关键部分进行比较 = <=> IS NULL > < >= <= != <> BETWEEN ,或 (其中 LIKE 'pattern' 'pattern' 不以通配符开头)。 可以使用间隔,只要可以确定包含与条件匹配的所有行的单个密钥元组(或者如果使用 <> 或者 != 使用 两个间隔 )。

    只要比较运算符是 ,或 = 优化程序就会尝试使用其他关键部分来确定间隔 如果操作是 ,或者 ,优化器使用它,但认为没有更多的关键部分。 对于以下表达式,优化程序将使用 第一个比较。 它也使用 <=> IS NULL > < >= <= != <> BETWEEN LIKE = >= 从第二次比较,但没有考虑其他关键部分,并没有使用间隔构造的第三个比较:

    key_part1='foo'AND key_part2> = 10 AND key_part3> 10
    

    单个间隔是:

    ( '富',10,-INF)<( ,key_part1 )<( '富',+ INF,+ INF)
    key_part2key_part3

    创建的间隔可能包含比初始条件更多的行。 例如,前面的间隔包括 ('foo', 11, 0) 不满足原始条件的值。

  • 如果包含区间内包含的行集的条件与其组合 OR ,则它们形成一个条件,该条件覆盖其间隔的并集中包含的一组行。 如果条件与 AND 它们 组合 ,则它们形成一个条件,该条件覆盖其间隔的交集中包含的一组行。 例如,对于这个由两部分组成的索引:

    key_part1= 1 AND key_part2<2)OR(key_part1> 5)
    

    间隔是:

    (1,-inf)<(key_part1key_part2)<(1,2)
    (5,-inf)<(key_part1key_part2

    在此示例中,第一行的间隔使用左边界的一个关键部分和右边界的两个关键部分。 第二行的间隔仅使用一个关键部分。 输出中 key_len EXPLAIN 指示使用的密钥前缀的最大长度。

    在某些情况下, key_len 可能表示使用了关键部件,但这可能不是您所期望的。 假设 key_part1 并且 key_part2 可以 NULL 然后该 key_len 列显示以下条件的两个关键部分长度:

    key_part1> = 1 AND key_part2<2
    

    但是,事实上,条件转换为:

    key_part1> = 1并且key_part2不是NULL
    

有关如何执行优化以组合或消除单部分索引上的范围条件的间隔的说明,请参阅单部分索引的 范围访问方法 对多部分索引的范围条件执行类似步骤。

多值比较的等价范围优化

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

col_nameIN(val1,...,valNcol_name= val1OR ... 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 ,如果跳过索引潜水,则输出更改如下:

  • 对于传统输出, rows filtered 值是 NULL

  • 对于JSON输出, rows_examined_per_scan 并且 rows_produced_per_join 不显示, skip_index_dive_due_to_force true ,并且成本计算不准确。

如果没有 FOR CONNECTION EXPLAIN 则跳过索引潜水时输出不会更改。

在执行跳过索引潜水的查询后, INFORMATION_SCHEMA.OPTIMIZER_TRACE 表中 的相应行 包含 index_dives_for_range_access skipped_due_to_force_index

跳过扫描范围访问方法

请考虑以下情形:

CREATE TABLE t1(f1 INT NOT NULL,f2 INT NOT NULL,PRIMARY KEY(f1,f2));
插入t1值
  (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;
分析表t1;

EXPLAIN SELECT f1,f2 FROM t1 WHERE f2> 40;

要执行此查询,MySQL可以选择索引扫描来获取所有行(索引包括要选择的所有列),然后应用 子句中 f2 > 40 条件 WHERE 以生成最终结果集。

范围扫描比完整索引扫描更有效,但在这种情况下不能使用,因为 f1 第一个索引列 没有条件 但是,从MySQL 8.0.13开始,优化器可以 f1 使用类似于松散索引扫描的称为Skip Scan的方法 对每个值执行多个范围扫描 (请参见 第8.2.1.16节“GROUP BY优化” ):

  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会跳过不符合每个构造范围的行。 此Skip Scan访问方法适用于以下条件:

  • 表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上的范围条件一起使用。

EXPLAIN 输出中 指示使用Skip Scan 如下:

  • Using index for skip scan Extra 列中表示使用松散索引Skip Scan访问方法。

  • 如果索引可用于Skip Scan,则索引应在 possible_keys 列中 可见

"skip scan" 此格式 元素的 优化程序跟踪输出中指示使用Skip Scan

“skip_scan_range”:{
  “type”:“skip_scan”,
  “指数”: index_used_for_skip_scan
  “key_parts_used_for_access”:[ key_parts_used_for_access],
  “范围”:[ range]
}

您可能还会看到一个 "best_skip_scan_summary" 元素。 如果选择“跳过扫描”作为最佳范围访问变体, "chosen_range_access_summary" 则会写入a。 如果选择“跳过扫描”作为总体最佳访问方法, "best_access_path" 则存在元素。

使用Skip Scan取决于 系统变量 skip_scan 标志的 optimizer_switch 值。 请参见 第8.9.2节“可切换的优化” 默认情况下,此标志为 on 要禁用它,请设置 skip_scan off

除了使用 optimizer_switch 系统变量控制优化程序在会话范围内使用Skip Scan之外,MySQL还支持优化程序提示,以便在每个语句的基础上影​​响优化程序。 请参见 第8.9.3节“优化程序提示”

行构造函数表达式的范围优化

优化器能够将范围扫描访问方法应用于此表单的查询:

SELECT ... FROM t1 WHERE(col_1,col_2)IN(('a','b'),('c','d'));

以前,对于要使用的范围扫描,有必要将查询编写为:

SELECT ... FROM t1 WHERE(col_1 ='a'和col_2 ='b')
或(col_1 ='c'和col_2 ='d');

要使优化器使用范围扫描,查询必须满足以下条件:

  • 只使用 IN() 谓词,而不是 NOT IN()

  • IN() 谓词 的左侧 ,行构造函数仅包含列引用。

  • IN() 谓词 的右侧 ,行构造函数仅包含运行时常量,这些常量是在执行期间绑定到常量的文字或本地列引用。

  • IN() 谓词 的右侧 ,有多个行构造函数。

有关优化程序和行构造函数的更多信息,请参见 第8.2.1.21节“行构造函数表达式优化”

限制内存使用范围优化

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

  • 值为0表示 无限制。

  • 值大于0时,优化程序会在考虑范围访问方法时跟踪消耗的内存。 如果要超过指定的限制,则放弃范围访问方法,并考虑其他方法,包括全表扫描。 这可能不太理想。 如果发生这种情况,会发生以下警告( N 当前 range_optimizer_max_mem_size 在哪里 ):

    警告3170内存容量的N字节数
                       超出'range_optimizer_max_mem_size'。范围
                       此查询未执行优化。
    
  • 对于 UPDATE DELETE 语句,如果优化器回退到全表扫描并且 sql_safe_updates 启用 系统变量,则会发生错误而不是警告,因为实际上没有使用任何键来确定要修改的行。 有关更多信息,请参阅 使用安全更新模式(--safe-updates)

对于超出可用范围优化内存并且优化程序回退到不太理想的计划的单个查询,增加该 range_optimizer_max_mem_size 值可以提高性能。

要估计处理范围表达式所需的内存量,请使用以下准则:

  • 对于诸如以下的简单查询,其中有一个候选键用于范围访问方法,每个谓词组合 OR 使用大约230个字节:

    SELECT COUNT(*)FROM t
    其中a = 1或a = 2或a = 3 OR .. a = N;
    
  • 类似地,对于诸如以下的查询,每个谓词组合 AND 使用大约125个字节:

    SELECT COUNT(*)FROM t
    在哪里a = 1 AND b = 1 AND c = 1 ... N;
    
  • 对于带 IN() 谓词 的查询

    SELECT COUNT(*)FROM t
    IN(1,2,...,M)和B IN(1,2,...,N);
    

    IN() 列表 中的每个文字值都 计为一个谓词组合 OR 如果有两个 IN() 列表,则组合的谓词 OR 数是每个列表中文字值的数量的乘积。 因此,与 OR 前一种情况 相结合的谓词数 M × N

8.2.1.3索引合并优化

指数合并 访问方法检索与多行 range 扫描和他们的结果合并到一个。 此访问方法仅合并来自单个表的索引扫描,而不是跨多个表扫描。 合并可以生成其基础扫描的联合,交叉或交叉联合。

可以使用Index Merge的示例查询:

SELECT * FROM tbl_nameWHERE 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。IN key1(1,2)OR key2t1。LIKE' value%')
  和t2。key1= t1。some_col;

SELECT * FROM t1,t2
  在哪里t1。key1= 1
  AND(t2。key1= t1。some_col或者t2。key2= t1。some_col2);
注意

索引合并优化算法具有以下已知限制:

  • 如果您的查询具有 WHERE 带深度 AND / OR 嵌套 的复杂 子句 且MySQL未选择最佳计划,请尝试使用以下身份转换来分发术语:

    xAND y)OR z=>(xOR z)AND(yOR zxy)AND z=>(xAND z)OR(yAND z
  • 索引合并不适用于全文索引。

EXPLAIN 输出中,索引合并方法显示 index_merge type 列中。 在这种情况下,该 key 列包含使用的索引列表,并 key_len 包含这些索引的最长关键部分的列表。

Index Merge访问方法有几种算法,它们显示在 输出 Extra 字段中 EXPLAIN

  • Using intersect(...)

  • Using union(...)

  • Using sort_union(...)

以下部分更详细地描述了这些算法。 优化器根据各种可用选项的成本估算在不同的可能索引合并算法和其他访问方法之间进行选择。

索引合并交叉口访问算法

WHERE 子句在不同的键上组合 转换为多个范围条件 时,此访问算法适用 AND ,并且每个条件都是以下条件之一:

  • N 这个表单的 一个 -part表达式,其中索引具有完全 N 部分(即,所有索引部分都被覆盖):

    key_part1= const1AND 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 输出包含 Using index Extra 字段中)。 以下是此类查询的示例:

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

如果使用的索引未涵盖查询中使用的所有列,则仅在满足所有使用的键的范围条件时才检索完整行。

如果其中一个合并条件是 InnoDB 的主键上的条件 ,则它不用于行检索,而是用于过滤掉使用其他条件检索的行。

索引合并联盟访问算法

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

  • N 这个表单的 一个 -part表达式,其中索引具有完全 N 部分(即,所有索引部分都被覆盖):

    key_part1= const1AND 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;
索引合并排序联合访问算法

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 该旗 optimizer_switch 系统变量。 请参见 第8.9.2节“可切换的优化” 默认情况下,所有这些标志都是 on 要仅启用某些算法,请设置 index_merge off ,并仅启用应允许的其他 算法

除了使用 optimizer_switch 系统变量来控制优化程序在会话范围内使用索引合并算法之外,MySQL还支持优化程序提示以基于每个语句影响优化程序。 请参见 第8.9.3节“优化程序提示”

8.2.1.4发动机状态下推优化

这种优化提高了非索引列和常量之间直接比较的效率。 在这种情况下,将条件 下推 到存储引擎以进行评估。 此优化只能由 NDB 存储引擎使用。

对于NDB Cluster,此优化可以消除在群集的数据节点和发出查询的MySQL服务器之间通过网络发送不匹配行的需要,并且可以将查询的使用速度提高5到10倍。条件下推可能但不使用。

假设NDB Cluster表定义如下:

CREATE TABLE t1(
    一个INT,
    b INT,
    KEY(一)
)ENGINE = NDB;

条件下推可以用于查询,例如此处显示的查询,其中包括非索引列和常量之间的比较:

SELECT a,b FROM t1 WHERE b = 10;

条件下推的使用可以在输出中看到 EXPLAIN

MySQL的> EXPLAIN SELECT a,b FROM t1 WHERE b = 10\G
*************************** 1。排******************** *******
           id:1
  select_type:SIMPLE
        表:t1
         类型:全部
possible_keys:NULL
          key:NULL
      key_len:NULL
          ref:NULL
         行:10
        额外:使用推动条件的地方

但是,条件下推 不能 与这两个查询中的任何一个一起使用:

SELECT a,b FROM t1 WHERE a = 10;
SELECT a,b FROM t1 WHERE b + 1 = 10;

条件下推不适用于第一个查询,因为列上存在索引 a (索引访问方法将更有效,因此将优先选择条件下推。)条件下推不能用于第二个查询,因为涉及非索引列的比较 b 是间接的。 (但是,如果您 b + 1 = 10 b = 9 WHERE 条款中 减少条件,则可以应用条件下推 。)

当索引列与使用 > < 运算符 的常量进行比较时,也可以使用条件下推

MySQL的> EXPLAIN SELECT a, b FROM t1 WHERE a < 2\G
*************************** 1。排******************** *******
           id:1
  select_type:SIMPLE
        表:t1
         类型:范围
possible_keys:a
          关键:a
      key_len:5
          ref:NULL
         行:2
        额外:使用推动条件的地方

其他支持的条件下推比较包括以下内容:

  • column [NOT] LIKE pattern

    pattern 必须是包含要匹配的模式的字符串文字; 有关语法,请参见 第12.5.1节“字符串比较函数”

  • column IS [NOT] NULL

  • column IN (value_list)

    每个项目 value_list 必须是一个常量,字面值。

  • column BETWEEN constant1 AND constant2

    constant1 并且 constant2 每个都必须是一个恒定的字面值。

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

默认情况下启用发动机状态下推。 要在服务器启动时禁用它,请设置 optimizer_switch 系统变量。 例如,在 my.cnf 文件中,使用以下行:

的[mysqld]
optimizer_switch = engine_condition_pushdown =关

在运行时,禁用条件下推,如下所示:

SET optimizer_switch ='engine_condition_pushdown = off';

限制。  发动机状态下推受以下限制:

  • 条件下推仅由 NDB 存储引擎 支持

  • 列可以仅与常量进行比较; 但是,这包括评估为常量值的表达式。

  • 比较中使用的列不能是任何 类型 BLOB TEXT 类型。

  • 要与列进行比较的字符串值必须使用与列相同的排序规则。

  • 不直接支持联接; 涉及多个表的条件在可能的情况下单独推送。 使用扩展 EXPLAIN 输出来确定实际按下哪些条件。 请参见 第8.8.3节“扩展EXPLAIN输出格式”

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

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

  1. 当任何前面提到的表都在连接缓冲区中时。 在这种情况下,从扫描过滤表中检索的每一行都与缓冲区中的每一行进行匹配。 这意味着在生成扫描过滤器时,没有单个特定行可从中获取列值。

  2. 当列来自推送连接中的子操作时。 这是因为在生成扫描过滤器时尚未检索从连接中的祖先操作引用的行。

8.2.1.5指数条件下推优化

索引条件下推(ICP)是对MySQL使用索引从表中检索行的情况的优化。 如果没有ICP,存储引擎会遍历索引以查找基表中的行,并将它们返回给MySQL服务器,该服务器会评估 WHERE 条件。 启用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节“使用EXISTS策略优化子查询” 。)

要了解此优化的工作原理,请首先考虑在不使用索引条件下推时索引扫描的进度:

  1. 获取下一行,首先读取索引元组,然后使用索引元组找到并读取整个表行。

  2. 测试 WHERE 适用于此表 条件 部分 根据测试结果接受或拒绝该行。

使用索引条件下推,扫描会像这样进行:

  1. 获取下一行的索引元组(但不是完整的表行)。

  2. 测试 WHERE 适用于此表 条件 部分, 并且只能使用索引列进行检查。 如果不满足条件,则继续下一行的索引元组。

  3. 如果满足条件,请使用索引元组来查找并读取整个表行。

  4. 测试 WHERE 适用于此表 条件 的剩余部分 根据测试结果接受或拒绝该行。

EXPLAIN 使用“索引条件下推”时, 输出显示 Using index condition Extra 列中。 它没有显示, Using index 因为当必须读取完整的表行时,这不适用。

假设一个表包含有关人员及其地址的信息,并且该表的索引定义为 INDEX (zipcode, lastname, firstname) 如果我们知道一个人的 zipcode 价值但不确定姓氏,我们可以这样搜索:

选择*来自人
  WHERE zipcode ='95054'
  AND lastname LIKE'%etrunia%'
  和地址LIKE'%Main Street%';

MySQL可以使用索引来扫描人 zipcode='95054' 第二部分( lastname LIKE '%etrunia%' )不能用于限制必须扫描的行数,因此如果没有Index Condition Pushdown,此查询必须为所有拥有的人检索完整的表行 zipcode='95054'

使用索引条件下推,MySQL lastname LIKE '%etrunia%' 在读取整个表行之前 检查该 部分。 这样可以避免读取与索引元组相对应的完整行,这些行匹配 zipcode 条件而不是 lastname 条件。

默认情况下启用索引条件下推。 可以 optimizer_switch 通过设置 index_condition_pushdown 标志 来控制 系统变量

SET optimizer_switch ='index_condition_pushdown = off';
SET optimizer_switch ='index_condition_pushdown = on';

请参见 第8.9.2节“可切换的优化”

8.2.1.6嵌套循环连接算法

MySQL使用嵌套循环算法或其变体在表之间执行连接。

嵌套循环连接算法

一个简单的嵌套循环连接(NLJ)算法一次一个循环地从第一个表中读取行,将每一行传递给一个嵌套循环,该循环处理连接中的下一个表。 这个过程重复多次,因为还有待连接的表。

假设三个表之间的连接 t1 t2 以及 t3 是使用以下类型的连接来执行:

表连接类型
t1范围
t2 ref
t3 ALL

如果使用简单的NLJ算法,则连接处理如下:

对于t1匹配范围中的每一行{
  对于t2中匹配引用键的每一行{
    对于t3中的每一行{
      如果行满足连接条件,则发送给客户端
    }
  }
}

因为NLJ算法一次一行地从外循环到内循环传递行,所以它通常多次读取在内循环中处理的表。

块嵌套循环连接算法

块嵌套循环(BNL)连接算法使用外部循环中读取的行的缓冲来减少必须读取内部循环中的表的次数。 例如,如果将10行读入缓冲区并将缓冲区传递给下一个内部循环,则可以将内循环中读取的每一行与缓冲区中的所有10行进行比较。 这将内表必须读取的次数减少一个数量级。

MySQL join缓存具有以下特征:

  • 当连接类型为 ALL index 换句话说,当不能使用任何可能的键,并且分别完成数据或索引行的完全扫描时),或者, 可以使用连接缓冲 range 缓冲的使用也适用于外连接,如 第8.2.1.11节“块嵌套循环和批量密钥访问连接”中所述

  • 永远不会为第一个非常量表分配连接缓冲区,即使它是类型 ALL index

  • 只有连接感兴趣的列存储在其连接缓冲区中,而不是整行。

  • join_buffer_size 系统变量来确定每个的大小联接缓冲液用于处理查询。

  • 为可以缓冲的每个连接分配一个缓冲区,因此可以使用多个连接缓冲区处理给定查询。

  • 在执行连接之前分配连接缓冲区,并在查询完成后释放。

对于之前针对NLJ算法(不使用缓冲)描述的示例连接,使用join buffering进行如下连接:

对于t1匹配范围中的每一行{
  对于t2中匹配引用键的每一行{
    存储在连接缓冲区中使用t1,t2中的列
    如果缓冲区已满{
      对于t3中的每一行{
        对于连接缓冲区中的每个t1,t2组合{
          如果行满足连接条件,则发送给客户端
        }
      }
      空连接缓冲区
    }
  }
}

如果缓冲区不为空{
  对于t3中的每一行{
    对于连接缓冲区中的每个t1,t2组合{
      如果行满足连接条件,则发送给客户端
    }
  }
}

如果 S 是每一个存储的大小 t1 t2 在加入缓冲液组合和 C 是组合在缓冲器中的数,次表的数量 t3 被扫描的是:

S* C)/ join_buffer_size + 1

t3 扫描降低为价值 join_buffer_size 时增加,最高可达点 join_buffer_size 是大到足以容纳所有上一行组合。 在那时,通过使其变大不会获得速度。

8.2.1.7嵌套连接优化

表达联接的语法允许嵌套联接。 以下讨论涉及 第13.2.10.2节“JOIN语法”中 描述的连接语法

table_factor 与SQL标准相比, 语法 扩展了。 后者只接受 table_reference ,而不是在一对括号内的列表。 如果我们将 table_reference 项目 列表中的每个逗号 视为等同于内部 联接,则这是保守扩展 例如:

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 否则使用。

通常,在仅包含内部联接操作的联接表达式中可以忽略括号。 考虑这个连接表达式:

t1 LEFT JOIN(t2 LEFT JOIN t3 ON t2.b = t3.b或t2.b IS NULL)
   ON t1.a = t2.a

删除括号并将操作分组到左侧后,该连接表达式将转换为此表达式:

(t1 LEFT JOIN t2 ON t1.a = t2.a)LEFT JOIN t3
    ON t2.b = t3.b或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 |
+ ------ + ------ + ------ + ------ +

在以下示例中,外部联接操作与内部联接操作一起使用:

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 |
+ ------ + ------ + ------ + ------ +

因此,如果我们在带有外连接运算符的连接表达式中省略括号,我们可能会更改原始表达式的结果集。

更准确地说,我们不能忽略左外连接操作的右操作数和右连接操作的左操作数中的括号。 换句话说,我们不能忽略外连接操作的内部表表达式的括号。 可以忽略其他操作数(外部表的操作数)的括号。

以下表达式:

(t1,t2)LEFT JOIN t3 ON P(t2.b,t3.b)

相当于这个表达式的任何表 t1,t2,t3 和任何条件 P 在属性 t2.b t3.b

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
  在哪里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

在第一个查询中,嵌套连接由左连接操作形成。 在第二个查询中,它由内部联接操作形成。

在第一个查询中,可以省略括号:连接表达式的语法结构将指示连接操作的相同执行顺序。 对于第二个查询,不能省略括号,尽管这里的连接表达式可以在没有它们的情况下明确解释。 在我们的扩展语法中, (t2, t3) 第二个查询 的括号 是必需的,尽管理论上可以在没有它们的情况下解析查询:我们仍然会为查询提供明确的语法结构,因为 LEFT JOIN ON 扮演了表达式的左右分隔符的角色 (t2,t3)

前面的例子证明了这些要点:

  • 对于仅涉及内部联接(而不是外部联接)的联接表达式,可以删除括号并从左到右计算联接。 实际上,可以按任何顺序评估表。

  • 通常,对于外连接或与内连接混合的外连接,情况也是如此。 删除括号可能会改变结果。

具有嵌套外连接的查询以与具有内连接的查询相同的管道方式执行。 更确切地说,利用了嵌套循环连接算法的变体。 回想一下嵌套循环连接执行查询的算法(参见 第8.2.1.6节“嵌套循环连接算法” )。 假设3个表的连接查询 T1,T2,T3 具有以下形式:

SELECT * FROM T1 INNER JOIN T2 ON P1(T1,T2)
                 INNER JOIN T3 ON P2(T2,T3)
  P(T1,T2,T3)

这里, P1(T1,T2) P2(T3,T3) 是一些连接条件(上表达),而 P(T1,T2,T3) 超过表中的列的条件 T1,T2,T3

嵌套循环连接算法将以下列方式执行此查询:

对于T1中的每一行t1 {
  对于T2中的每一行t2,使得P1(t1,t2){
    对于T3中的每一行t3,使得P2(t2,t3){
      IF P(t1,t2,t3){
         T:= T 1 || || T2 T3; 输出t;
      }
    }
  }
}

符号 t1||t2||t3 表示通过连接的行的列构成的行 t1 t2 t3 在以下某些示例中, NULL 表名称的显示表示一行 NULL 用于该表的每一列。 例如, t1||t2||NULL 指示通过连接的行的列构成的行 t1 t2 ,以及 NULL 对于每一列 t3 据说这样一行是完成的 NULL

现在考虑使用嵌套外连接的查询:

SELECT * FROM T1 LEFT JOIN
              (T2 LEFT JOIN T3 ON P2(T2,T3))
              ON P1(T1,T2)
  P(T1,T2,T3)

对于此查询,修改嵌套循环模式以获取:

对于T1中的每一行t1 {
  BOOL f1:= FALSE;
  对于T2中的每一行t2,使得P1(t1,t2){
    BOOL f2:= FALSE;
    对于T3中的每一行t3,使得P2(t2,t3){
      IF P(t1,t2,t3){
        T:= T 1 || || T2 T3; 输出t;
      }
      F2 = TRUE;
      F1 = TRUE;
    }
    IF(!f2){
      IF P(t1,t2,NULL){
        T:= T 1 || || T2 NULL; 输出t;
      }
      F1 = TRUE;
    }
  }
  IF(!f1){
    IF P(t1,NULL,NULL){
      T:= T 1 || || NULL NULL; 输出t;
    }
  }
}

通常,对于外连接操作中第一个内部表的任何嵌套循环,引入一个在循环之前关闭并在循环之后检查的标志。 当对于来自外部表的当前行,找到与表示内部操作数的表的匹配时,该标志被打开。 如果在循环周期结束时标志仍然关闭,则找不到外部表的当前行的匹配项。 在这种情况下,行由 NULL 内部表的列的值 补充 结果行将传递给输出的最终检查或下一个嵌套循环,但前提是该行满足所有嵌入外连接的连接条件。

在该示例中,嵌入了由以下表达式表示的外连接表:

(T2 LEFT JOIN T3 ON P2(T2,T3))

对于具有内部联接的查询,优化程序可以选择不同的嵌套循环顺序,例如:

对于T3中的每一行t3 {
  对于T2中的每一行t2,使得P2(t2,t3){
    对于T1中的每一行t1,使得P1(t1,t2){
      IF P(t1,t2,t3){
         T:= T 1 || || T2 T3; 输出t;
      }
    }
  }
}

对于具有外连接的查询,优化器只能选择这样的顺序,其中外部表的循环位于内部表的循环之前。 因此,对于具有外连接的查询,只能有一个嵌套顺序。 对于以下查询,优化程序将评估两种不同的嵌套。 在两个嵌套中, T1 必须在外部循环中处理,因为它在外部联接中使用。 T2 T3 在内部联接中使用,因此必须在内部循环中处理联接。 但是,因为连接是内连接, T2 并且 T3 可以按任何顺序处理。

SELECT * T1 LEFT JOIN(T2,T3)ON P1(T1,T2)和P2(T1,T3)
  P(T1,T2,T3)

一个嵌套评估 T2 ,然后 T3

对于T1中的每一行t1 {
  BOOL f1:= FALSE;
  对于T2中的每一行t2,使得P1(t1,t2){
    对于T3中的每一行t3,使得P2(t1,t3){
      IF P(t1,t2,t3){
        T:= T 1 || || T2 T3; 输出t;
      }
      F1:= TRUE
    }
  }
  IF(!f1){
    IF P(t1,NULL,NULL){
      T:= T 1 || || NULL NULL; 输出t;
    }
  }
}

另一个嵌套评估 T3 ,然后 T2

对于T1中的每一行t1 {
  BOOL f1:= FALSE;
  对于T3中的每一行t3,使得P2(t1,t3){
    对于T2中的每一行t2,使得P1(t1,t2){
      IF P(t1,t2,t3){
        T:= T 1 || || T2 T3; 输出t;
      }
      F1:= TRUE
    }
  }
  IF(!f1){
    IF P(t1,NULL,NULL){
      T:= T 1 || || NULL NULL; 输出t;
    }
  }
}

在讨论内部联接的嵌套循环算法时,我们省略了一些细节,这些细节对查询执行性能的影响可能很大。 我们没有提到所谓的 推倒 条件。 假设我们的 WHERE 条件 P(T1,T2,T3) 可以用一个连接公式表示:

P(T1,T2,T2)= C1(T1)和C2(T2)和C3(T3)。

在这种情况下,MySQL实际上使用以下嵌套循环算法来执行带有内连接的查询:

对于T1中的每一行t1,使得C1(t1){
  对于T2中的每一行t2,使得P1(t1,t2)和C2(t2){
    对于T3中的每一行t3,使得P2(t2,t3)和C3(t3){
      IF P(t1,t2,t3){
         T:= T 1 || || T2 T3; 输出t;
      }
    }
  }
}

你看,每个合取的 C1(T1) C2(T2) C3(T3) 是最内环的推到最外环的地方进行评估。 如果 C1(T1) 是一个非常严格的条件,这种情况下推可能会大大减少 T1 传递给内部循环的 表中的行数 结果,查询的执行时间可能会大大改善。

对于具有外部联接的查询, WHERE 仅在发现外部表中的当前行与内部表中的匹配项之后才检查条件。 因此,从内嵌套循环中推出条件的优化不能直接应用于具有外连接的查询。 这里我们必须引入条件下推谓词,这些谓词由遇到匹配时打开的标志保护。

回想一下外连接的这个例子:

P(T1,T2,T3)= C1(T1)和C(T2)和C3(T3)

对于该示例,使用受保护的下推条件的嵌套循环算法如下所示:

对于T1中的每一行t1,使得C1(t1){
  BOOL f1:= FALSE;
  对于T2中的每一行t2
      这样P1(t1,t2)AND(f1?C2(t2):TRUE){
    BOOL f2:= FALSE;
    对于T3中的每一行t3
        这样P2(t2,t3)AND(f1 && f2?C3(t3):TRUE){
      IF(f1 && f2?TRUE:(C2(t2)AND C3(t3))){
        T:= T 1 || || T2 T3; 输出t;
      }
      F2 = TRUE;
      F1 = TRUE;
    }
    IF(!f2){
      IF(f1?TRUE:C2(t2)&& P(t1,t2,NULL)){
        T:= T 1 || || T2 NULL; 输出t;
      }
      F1 = TRUE;
    }
  }
  IF(!f1 && P(t1,NULL,NULL)){
      T:= T 1 || || NULL NULL; 输出t;
  }
}

通常,可以从诸如 P1(T1,T2) 和的 连接条件中提取下推谓词 P(T2,T3) 在这种情况下,下推谓词也受到一个标志的保护,该标志阻止检查 NULL 由相应的外连接操作生成 - 实现行 的谓词

如果由 WHERE 条件中 的谓词引起,则禁止在同一嵌套连接中通过键从一个内部表访问另一个内部表

8.2.1.8外连接优化

外连接包括 LEFT JOIN RIGHT JOIN

MySQL实现 如下: A LEFT JOIN B join_specification

  • B 设置为依赖于表 A 和所有 依赖的表 A

  • A 设置为依赖于 条件 B 中使用的 所有表(除外 LEFT JOIN

  • LEFT JOIN 条件用于决定如何从表中检索行 B (换句话说, WHERE 不使用 子句中的 任何条件 。)

  • 执行所有标准连接优化,但始终在所依赖的所有表之后读取表。 如果存在循环依赖关系,则会发生错误。

  • WHERE 执行 所有标准 优化。

  • 如果 A 匹配该 WHERE 子句 的行中有一行 ,但没有 B 匹配 ON 条件的 B 行,则会生成 一个额外的 行,并将所有列设置为 NULL

  • 如果您用于 LEFT JOIN 查找某些表中不存在的行,并且您有以下测试: col_name IS NULL WHERE 部件中,哪 col_name 一个是声明为的列 NOT NULL ,MySQL在找到后停止搜索更多行(对于特定的键组合)一行符合 LEFT JOIN 条件。

RIGHT JOIN 实现类似于 LEFT JOIN 表格角色颠倒的实现。 右连接转换为等效的左连接,如 第8.2.1.9节“外连接简化”中所述

对于a LEFT JOIN ,如果 WHERE 生成的 NULL 条件始终为false ,则将 LEFT JOIN 其更改为内部联接。 例如, WHERE 如果条款是在下面的查询错误的 t2.column1 NULL

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_1WHERE condition_2OR 0 = 1

优化器现在在准备期间看到0 = 1始终为false,使得 OR 0 = 1 冗余,并将其删除,留下:

SELECT * FROM t1 LEFT JOIN t2 ON condition_1在哪里condition_2

现在,优化器可以将查询重写为内部联接,如下所示:

SELECT * FROM t1 JOIN t2 WHERE condition_1ANDcondition_2

现在优化器可以在表 t2 之前 使用表 t1 如果这样做会导致更好的查询计划。 要提供有关表连接顺序的提示,请使用优化程序提示; 请参见 第8.9.3节“优化程序提示” 或者,使用 STRAIGHT_JOIN ; 请参见 第13.2.10节“SELECT语法” 但是, STRAIGHT_JOIN 可能会阻止使用索引,因为它会禁用半连接转换; 请参见 第8.2.2.1节“使用半连接转换优化子查询,派生表,视图引用和公用表表达式”

8.2.1.9外连接简化

FROM 在许多情况下,简化了查询子句中的 表表达式

在解析器阶段,具有右外连接操作的查询将转换为仅包含左连接操作的等效查询。 在一般情况下,执行转换以使此右连接:

(T1,...)RIGHT JOIN(T2,...)ON P(T1,...,T2,...)

成为等效的左连接:

(T2,...)LEFT JOIN(T1,...)ON P(T1,...,T2,...)

表单的所有内连接表达式 T1 INNER JOIN T2 ON P(T1,T2) 都被列表替换 T1,T2 P(T1,T2) 作为 WHERE 条件的连接(或嵌入连接的连接条件,如果有的话)连接。

当优化器评估外连接操作的计划时,它仅考虑计划,对于每个此类操作,在内部表之前访问外部表。 优化器选项是有限的,因为只有这样的计划才能使用嵌套循环算法执行外连接。

考虑一下这个表单的查询,其中 R(T2) 大大缩小了表中匹配行的数量 T2

SELECT * T1 LEFT JOIN T2 ON P1(T1,T2)
  P(T1,T2)和R(T2)

如果查询是按写入的方式执行的,那么优化器别无选择,只能在限制 T1 较多的 之前 访问限制较少的 T2 ,这可能会产生效率非常低的执行计划。

相反,如果 WHERE 条件为空拒绝 ,MySQL会将查询转换为不带外连接操作的查询 (也就是说,它将外连接转换为内连接。)如果一个条件计算为 FALSE UNKNOWN 为任何 NULL 为该操作生成的已完成行,则该 条件对于外连接操作被称为空拒绝

因此,对于此外连接:

T1 LEFT JOIN T2 ON T1.A = T2.A

诸如此类的条件被null拒绝,因为对于任何已完成的 NULL 行( T2 列设置为 NULL ), 它们不能为真

T2.B不是空的
T2.B> 3
T2.C <= T1.C
T2.B <2或T2.C> 1

诸如此类的条件不会被拒绝,因为它们对于已完成的 NULL 可能是真的

T2.B是空的
T1.B <3或T2.B不为空
T1.B <3或T2.B> 3

检查外部联接操作的条件是否为空拒绝的一般规则很简单:

  • 它是形式 A IS NOT NULL ,其中 A 是任何内部表的属性

  • 它是一个谓词,包含对内部表的引用,该内部表计算 UNKNOWN 其参数之一的时间 NULL

  • 它是一个包含null-rejected条件作为合取的连接

  • 它是零拒绝条件的分离

对于查询中的一个外部联接操作,条件可以为null,而对于另一个条件,则不为null-rejected。 在此查询中, WHERE 对于第二个外部联接操作 条件为空 - 拒绝,但对于第一个外部联接操作,条件不为空 - 拒绝:

SELECT * FROM T1 LEFT JOIN T2 ON T2.A = T1.A
                 左接点T3接通T3.B = T1.B
  在哪里T3.C> 0

如果 WHERE 查询中的外连接操作 条件为空 - 拒绝,则外连接操作将由内连接操作替换。

例如,在前面的查询中,第二个外部联接是空的拒绝,可以由内部联接替换:

SELECT * FROM T1 LEFT JOIN T2 ON T2.A = T1.A
                 INNER JOIN T3 ON T3.B = T1.B
  在哪里T3.C> 0

对于原始查询,优化程序仅评估与单个表访问顺序兼容的计划 T1,T2,T3 对于重写的查询,它还考虑访问顺序 T3,T1,T2

一个外连接操作的转换可以触发另一个外连接操作的转换。 因此,查询:

SELECT * FROM T1 LEFT JOIN T2 ON T2.A = T1.A
                 LE3 JOIN T3 ON T3.B = T2.B
  在哪里T3.C> 0

首先转换为查询:

SELECT * FROM T1 LEFT JOIN T2 ON T2.A = T1.A
                 INNER JOIN T3 ON T3.B = T2.B
  在哪里T3.C> 0

这相当于查询:

SELECT * FROM(T1 LEFT JOIN T2 ON T2.A = T1.A),T3
  T3.C> 0且T3.B = T2.B

剩余的外连接操作也可以由内连接替换,因为条件 T3.B=T2.B 为空拒绝。 这导致查询没有外部联接:

SELECT * FROM(T1 INNER JOIN T2 ON T2.A = T1.A),T3
  T3.C> 0且T3.B = T2.B

有时,优化器会成功替换嵌入式外连接操作,但无法转换嵌入外连接。 以下查询:

SELECT * FROM T1 LEFT JOIN
              (T2 LEFT JOIN T3 ON T3.B = T2.B)
              在T2.A = T1.A
  在哪里T3.C> 0

转换为:

SELECT * FROM T1 LEFT JOIN
              (T2 INNER JOIN T3 ON T3.B = T2.B)
              在T2.A = T1.A
  在哪里T3.C> 0

这只能重写为仍然包含嵌入外连接操作的表单:

SELECT * FROM T1 LEFT JOIN
              (T2,T3)
              ON(T2.A = T1.A和T3.B = T2.B)
  在哪里T3.C> 0

在查询中转换嵌入式外连接操作的任何尝试都必须考虑嵌入外连接和 WHERE 条件的 连接 条件。 在此查询中, WHERE 嵌入式外连接 条件不会被拒绝,但嵌入外连接的连接条件 T2.A=T1.A AND T3.C=T1.C 为空拒绝:

SELECT * FROM T1 LEFT JOIN
              (T2 LEFT JOIN T3 ON T3.B = T2.B)
              在T2.A = T1.A和T3.C = T1.C
  在哪里T3.D> 0或T1.D> 0

因此,查询可以转换为:

SELECT * FROM T1 LEFT JOIN
              (T2,T3)
              在T2.A = T1.A和T3.C = T1.C和T3.B = T2.B
  在哪里T3.D> 0或T1.D> 0

8.2.1.10多范围读取优化

当表很大并且没有存储在存储引擎的缓存中时,使用辅助索引上的范围扫描读取行可能导致对基表的许多随机磁盘访问。 通过磁盘扫描多范围读取(MRR)优化,MySQL尝试通过首先扫描索引并收集相关行的密钥来减少范围扫描的随机磁盘访问次数。 然后对键进行排序,最后使用主键的顺序从基表中检索行。 磁盘扫描MRR的动机是减少随机磁盘访问的数量,而是实现对基表数据的更顺序扫描。

多范围读取优化提供以下好处:

  • MRR使数据行能够按顺序而不是按随机顺序访问,具体取决于索引元组。 服务器获取一组满足查询条件的索引元组,根据数据行ID顺序对它们进行排序,并使用排序的元组按顺序检索数据行。 这使数据访问更有效,更便宜。

  • MRR允许批量处理需要通过索引元组访问数据行的操作的密钥访问请求,例如范围索引扫描和使用连接属性索引的等连接。 MRR迭代一系列索引范围以获得合格的索引元组。 随着这些结果的累积,它们用于访问相应的数据行。 在开始读取数据行之前,不必获取所有索引元组。

在虚拟生成列上创建的二级索引不支持MRR优化。 InnoDB 支持虚拟生成列上的二级索引。

以下方案说明了MRR优化何时有利:

场景A:MRR可 用于索引范围扫描和等连接操作的表 InnoDB MyISAM 表。

  1. 索引元组的一部分累积在缓冲区中。

  2. 缓冲区中的元组按其数据行ID排序。

  3. 根据排序的索引元组序列访问数据行。

场景B:MRR可 NDB 用于多范围索引扫描的表或通过属性执行等连接时。

  1. 范围的一部分(可能是单键范围)累积在提交查询的中心节点上的缓冲区中。

  2. 范围被发送到访问数据行的执行节点。

  3. 访问的行被打包到包中并发送回中央节点。

  4. 收到的包含数据行的包放在缓冲区中。

  5. 从缓冲区读取数据行。

使用MRR时 Extra EXPLAIN 输出中 显示 Using MRR

InnoDB MyISAM 如果不需要访问完整的表行来生成查询结果 则不要使用MRR。 如果结果可以完全基于索引元组中的信息(通过 覆盖索引 )生成,则情况就是这样; MRR没有任何好处。

两个 optimizer_switch 系统变量标志提供了使用MRR优化的接口。 mrr 标志控制是否启用MRR。 如果 mrr 是enabled( on ),则该 mrr_cost_based 标志控制优化器是否尝试在使用和不使用MRR( on 之间进行基于成本的选择, 或者尽可能使用MRR( off )。 默认情况下, mrr on mrr_cost_based on 请参见 第8.9.2节“可切换的优化”

对于MRR,存储引擎使用 read_rnd_buffer_size 系统变量 的值 作为其缓冲区可分配的内存量的指导。 引擎最多使用 read_rnd_buffer_size 字节数,并确定一次传递中要处理的范围数。

8.2.1.11块嵌套循环和批量密钥访问连接

在MySQL中,可以使用批量密钥访问(BKA)连接算法,该算法使用对连接表的索引访问和连接缓冲区。 BKA算法支持内连接,外连接和半连接操作,包括嵌套外连接。 BKA的优点包括提高连接性能,因为更高效的表扫描。 此外,先前仅用于内连接的块嵌套循环(BNL)连接算法已扩展,可用于外连接和半连接操作,包括嵌套外连接。

以下部分讨论了连接缓冲区管理,它是原始BNL算法扩展,扩展BNL算法和BKA算法的基础。 有关半连接策略的信息,请参见 第8.2.2.1节“使用半连接转换优化子查询,派生表,视图引用和公用表表达式”

加入块嵌套循环和批量密钥访问算法的缓冲区管理

MySQL可以使用连接缓冲区不仅可以执行内部联接而无需对内部表进行索引访问,还可以执行子查询展平后出现的外部联接和半联接。 此外,当存在对内部表的索引访问时,可以有效地使用连接缓冲区。

当存储感兴趣的行列的值时,联接缓冲区管理代码稍微更有效地利用连接缓冲区空间:如果行的值为,则不在缓冲区中为行列分配额外的字节 NULL ,并且为任何值分配最小字节数该 VARCHAR 类型。

该代码支持两种类型的缓冲区,常规和增量。 假设使用连接缓冲区 B1 来连接表 t1 t2 并且 t3 使用连接缓冲区 将此操作的结果与表 连接 B2

  • 常规连接缓冲区包含每个连接操作数的列。 如果 B2 是一个普通的加入缓冲液,每行 r 投入 B2 由行的列 r1 B1 和匹配行的有趣列 r2 从表 t3

  • 增量连接缓冲区仅包含第二个连接操作数生成的表行中的列。 也就是说,它是第一个操作数缓冲区中一行的增量。 如果 B2 是增量加入缓冲液,它包含行的有趣列 r2 有链接到一起排 r1 B1

增量连接缓冲区始终相对于早期连接操作的连接缓冲区是增量的,因此第一个连接操作的缓冲区始终是常规缓冲区。 在刚刚给出的实例中,缓冲 B1 用来连接表 t1 t2 必须是常规缓冲区。

用于连接操作的增量缓冲区的每一行仅包含要连接的表中一行的有趣列。 这些列通过引用第一个连接操作数生成的表中匹配行的有趣列进行扩充。 增量缓冲区中的多行可以引用同一行, r 的列存储在先前的连接缓冲区中,只要所有这些行都匹配行 r

增量缓冲区可以减少从先前连接操作使用的缓冲区中复制列的频率。 这提供了缓冲空间的节省,因为在一般情况下,由第一连接操作数产生的行可以由第二连接操作数产生的若干行匹配。 不必从第一个操作数生成一行的多个副本。 由于复制时间的减少,增量缓冲区还可以节省处理时间。

系统变量 block_nested_loop batched_key_access 标志 optimizer_switch 控制优化器如何使用块嵌套循环和批量密钥访问连接算法。 默认情况下, block_nested_loop on batched_key_access off 请参见 第8.9.2节“可切换的优化” 也可以应用优化程序提示; 请参阅 块嵌套循环和批量密钥访问算法的优化程序提示

有关半连接策略的信息,请参见 第8.2.2.1节“使用半连接转换优化子查询,派生表,视图引用和公用表表达式”

用于外连接和半连接的块嵌套循环算法

MySQL BNL算法的原始实现被扩展为支持外连接和半连接操作。

当使用连接缓冲区执行这些操作时,为缓冲区中的每一行提供匹配标志。

如果使用连接缓冲区执行外连接操作,则检查第二个操作数生成的表的每一行是否与连接缓冲区中的每一行匹配。 找到匹配项后,将形成一个新的扩展行(原始行加上第二个操作数中的列),并通过其余的连接操作发送以进一步扩展。 此外,启用缓冲区中匹配行的匹配标志。 在检查了要连接的表的所有行之后,将扫描连接缓冲区。 缓冲区中未启用其匹配标志的每一行都由 NULL 补充 扩展 NULL 第二个操作数中每列的值),并由剩余的连接操作发送以进一步扩展。

系统变量 block_nested_loop 标志 optimizer_switch 控制优化器如何使用块嵌套循环算法。 默认情况下 block_nested_loop on 请参见 第8.9.2节“可切换的优化” 也可以应用优化程序提示; 请参阅 块嵌套循环和批量密钥访问算法的优化程序提示

EXPLAIN 输出端,为表使用BNL的当被所指 Extra 值包含 Using join buffer (Block Nested Loop) 与所述 type 值是 ALL index ,或 range

有关半连接策略的信息,请参见 第8.2.2.1节“使用半连接转换优化子查询,派生表,视图引用和公用表表达式”

批量密钥访问连接

MySQL实现了一种连接表的方法,称为批量密钥访问(BKA)连接算法。 当存在对第二个连接操作数生成的表的索引访问时,可以应用BKA。 与BNL连接算法一样,BKA连接算法使用连接缓冲区来累积由连接操作的第一个操作数生成的行的有趣列。 然后,BKA算法构建密钥以访问要为缓冲区中的所有行加入的表,并将这些密钥批量提交到数据库引擎以进行索引查找。 键通过多量程读取(MRR)接口提交给引擎(参见 第8.2.1.10节“多量程读取优化”) )。 在提交密钥之后,MRR引擎功能以最佳方式在索引中执行查找,获取由这些密钥找到的连接表的行,并开始向匹配行提供BKA连接算法。 每个匹配行与对连接缓冲区中的行的引用相耦合。

使用BKA时,值 join_buffer_size 定义了每个请求到存储引擎的批量密钥的大小。 缓冲区越大,对连接操作的右侧表的顺序访问就越多,这可以显着提高性能。

要使用BKA, 必须将系统变量 batched_key_access 标志 optimizer_switch 设置为 on BKA使用MRR,因此 mrr 标志也必须是 on 目前,MRR的成本估算过于悲观。 因此,也有必要对 mrr_cost_based off 用于要使用的BKA。 以下设置启用BKA:

MySQL的> SET optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';

MRR功能有两种执行方式:

  • 第一种方案用于传统的基于磁盘的存储引擎,例如 InnoDB MyISAM 对于这些引擎,通常将来自连接缓冲区的所有行的键一次提交给MRR接口。 特定于引擎的MRR函数对提交的密钥执行索引查找,从中获取行ID(或主键),然后根据BKA算法的请求逐个获取所有这些选定行ID的行。 返回的每一行都带有一个关联引用,该引用允许访问连接缓冲区中的匹配行。 MRR函数以最佳方式获取行:它们以行ID(主键)顺序获取。 这提高了性能,因为读取是按磁盘顺序而不是随机顺序。

  • 第二种方案用于远程存储引擎,例如 NDB 来自连接缓冲区的一部分行的密钥包及其关联由MySQL服务器(SQL节点)发送到MySQL Cluster数据节点。 作为回报,SQL节点接收匹配行的包(或多个包)以及相应的关联。 BKA连接算法获取这些行并构建新的连接行。 然后,一组新的密钥被发送到数据节点,并且返回的包中的行用于构建新的连接行。 该过程将继续,直到将来自联接缓冲区的最后一个密钥发送到数据节点,并且SQL节点已接收并加入与这些密钥匹配的所有行。

对于第一种情况,保留一部分连接缓冲区以存储由索引查找选择的行ID(主键),并作为参数传递给MRR功能。

没有特殊的缓冲区来存储为连接缓冲区中的行构建的密钥。 相反,为缓冲区中的下一行构建键的函数作为参数传递给MRR函数。

EXPLAIN 输出中,当 Extra 值包含 Using join buffer (Batched Key Access) type 值为 ref 时,表示将BKA用于表 eq_ref

块嵌套循环和批量密钥访问算法的优化器提示

除了使用 optimizer_switch 系统变量来控制优化程序在会话范围内使用BNL和BKA算法之外,MySQL还支持优化程序提示,以便在每个语句的基础上影​​响优化程序。 请参见 第8.9.3节“优化程序提示”

要使用BNL或BKA提示为外部联接的任何内部表启用联接缓冲,必须为外部联接的所有内部表启用联接缓冲。

8.2.1.12条件过滤

在连接处理中,前缀行是从连接中的一个表传递到下一个表的那些行。 通常,优化器尝试在连接顺序的早期放置具有低前缀计数的表,以保持行组合的数量不会快速增加。 在某种程度上,优化程序可以使用有关从一个表中选择并传递给下一个表的行的条件的信息,它可以更准确地计算行估计并选择最佳执行计划。

如果没有条件筛选,则表的前缀行计数基于 WHERE 子句根据优化程序选择的访问方法选择 的估计行数 条件筛选使优化器能够使用 WHERE 访问方法未考虑 子句 中的其他相关条件 ,从而改进其前缀行计数估计。 例如,即使可能存在可用于从连接中的当前表中选择行的基于索引的访问方法,但在该表中可能还存在其他条件。 WHERE 可以过滤(进一步限制)传递给下一个表的合格行的估计的子句。

只有在以下情况下,条件才会影响过滤估算:

  • 它指的是当前表。

  • 它取决于连接序列中早期表的常量值或值。

  • 访问方法尚未考虑它。

EXPLAIN 输出中, rows 列指示所选访问方法的行估计,该 filtered 列反映条件筛选的效果。 filtered 值以百分比表示。 最大值为100,这意味着不会对行进行过滤。 值从100开始减少表示过滤量增加。

前缀行计数(估计从连接中的当前表传递到下一个的行数)是 rows filtered 的乘积 也就是说,前缀行计数是估计的行计数,通过估计的过滤效果减少。 例如,如果 rows 是1000并且 filtered 是20%,则条件过滤将估计的行计数1000减少到前缀行计数1000×20%= 1000×.2 = 200。

请考虑以下查询:

选择 *
  来自员工JOIN部门ON employee.dept_no = department.dept_no
  WHERE employee.first_name ='John'
  AND employee.hire_date BETWEEN'2018-01-01'EN'2018-06-01';

假设数据集具有以下特征:

  • employee 表有1024行。

  • department 表有12行。

  • 两个表都有一个索引 dept_no

  • employee 表有一个索引 first_name

  • 8行符合以下条件 employee.first_name

    employee.first_name ='约翰'
    
  • 150行符合以下条件 employee.hire_date

    employee.hire_date BETWEEN'2018-01-01'和'2018-06-01'
    
  • 1行满足两个条件:

    employee.first_name ='约翰'
    AND employee.hire_date BETWEEN'2018-01-01'和'2018-06-01'
    

没有条件过滤, EXPLAIN 产生如下输出:

+ ---- + ------------ + -------- + ------------------ + --- ------ + --------- + ------ + ---------- +
| id | 表| 类型| possible_keys | 关键| ref | 行| 过滤|
+ ---- + ------------ + -------- + ------------------ + --- ------ + --------- + ------ + ---------- +
| 1 | 员工| ref | name,h_date,dept | 名字| const | 8 | 100.00 |
| 1 | 部门| eq_ref | 主要| 主要| dept_no | 1 | 100.00 |
+ ---- + ------------ + -------- + ------------------ + --- ------ + --------- + ------ + ---------- +

因为 employee name 索引 上的访问方法会 获取与名称匹配的8行 'John' 没有进行过滤( filtered 是100%),因此所有行都是下一个表的前缀行:前缀行计数是 rows × filtered = 8×100%= 8。

通过条件过滤,优化器还考虑 WHERE 了访问方法未考虑 子句中的 条件 在这种情况下,优化器使用试探法来估计所述的16.31%的滤波效果 BETWEEN 的条件下 employee.hire_date 结果, EXPLAIN 产生如下输出:

+ ---- + ------------ + -------- + ------------------ + --- ------ + --------- + ------ + ---------- +
| id | 表| 类型| possible_keys | 关键| ref | 行| 过滤|
+ ---- + ------------ + -------- + ------------------ + --- ------ + --------- + ------ + ---------- +
| 1 | 员工| ref | name,h_date,dept | 名字| const | 8 | 16.31 |
| 1 | 部门| eq_ref | 主要| 主要| dept_no | 1 | 100.00 |
+ ---- + ------------ + -------- + ------------------ + --- ------ + --------- + ------ + ---------- +

现在前缀行计数为 rows × filtered = 8×16.31%= 1.3,这更接近地反映了实际数据集。

通常,优化器不会为最后一个连接表计算条件过滤效果(前缀行数减少),因为没有下一个表要传递行。 发生异常 EXPLAIN :要提供更多信息,将为所有已连接的表(包括最后一个表)计算过滤效果。

要控制优化程序是否考虑其他过滤条件,请使用 系统变量 condition_fanout_filter 标志 optimizer_switch (请参见 第8.9.2节“可切换的优化” )。 默认情况下启用此标志,但可以禁用此标志以禁止条件筛选(例如,如果发现特定查询在没有它的情况下产生更好的性能)。

如果优化器过高估计条件过滤的影响,则性能可能比不使用条件过滤时更差。 在这种情况下,这些技术可能有所帮助:

  • 如果未对列建立索引,请对其进行索引,以便优化程序具有有关列值分布的一些信息,并可以改进其行估计值。

  • 同样,如果没有可用的列直方图信息,则生成直方图(请参见 第8.9.6节“优化程序统计信息” )。

  • 更改连接顺序。 完成此操作的方法包括连接顺序优化器提示(请参见 第8.9.3节“优化程序提示” ), STRAIGHT_JOIN 紧跟在 SELECT STRAIGHT_JOIN 连接运算符之后。

  • 禁用会话的条件筛选:

    SET optimizer_switch ='condition_fanout_filter = off';
    

    或者,对于给定查询,使用优化程序提示:

    SELECT / * + SET_VAR(optimizer_switch ='condition_fanout_filter = off')* / ...
    

8.2.1.13恒定折叠优化

常量值和列值之间的比较,其中常量值超出范围或相对于列类型的错误类型现在在查询优化期间而不是逐行处理而不是在执行期间处理一次。 可以以这种方式处理的比较是 > >= < <= <> / != = <=>

考虑以下语句创建的表:

CREATE TABLE t(c TINYINT UNSIGNED NOT NULL);

所述 WHERE 查询条件 SELECT * FROM t WHERE c < 256 包含积分常数256,其超出范围为 TINYINT UNSIGNED 柱。 以前,这是通过将两个操作数视为较大的类型来处理的,但现在,由于任何允许的值 c 小于常量, WHERE 表达式可以折叠为 WHERE 1 ,以便将查询重写为 SELECT * FROM t WHERE 1

这使得优化器可以 WHERE 完全 删除 表达式。 如果列可以 c 为空(即,仅定义为 TINYINT UNSIGNED ),则将重写查询,如下所示:

SELECT * FROM t WHERE ti not NULL

与支持的MySQL列类型相比,对常量执行折叠,如下所示:

  • 整数列类型。  将整数类型与以下类型的常量进行比较,如下所述:

    • 整数值。  如果常量超出列类型的范围,则比较将折叠为 1 IS NOT NULL ,如已显示。

      如果常量是范围边界,则比较折叠为 = 例如(使用与已定义相同的表):

      MySQL的> EXPLAIN SELECT * FROM t WHERE c >= 255;
      *************************** 1。排******************** *******
                 id:1
        select_type:SIMPLE
              表:t
         分区:NULL
               类型:全部
      possible_keys:NULL
                key:NULL
            key_len:NULL
                ref:NULL
               行:5
           过滤:20.00
              额外:使用在哪里
      1排,1警告(0.00秒)
      
      MySQL的> SHOW WARNINGS;
      *************************** 1。排******************** *******
        等级:注意
         代码:1003
      消息:/ *从`test``t`中选择#1 * / select`test``t``ti` AS`ti`,其中(`test``t``ti` = 255)
      1排(0.00秒)
      
    • 浮点或定点值。  如果常数是十进制类型之一(例如 DECIMAL REAL DOUBLE ,或 FLOAT ),并有一个非零小数部分,它可以是不相等的; 相应地折叠。 对于其他比较,根据符号向上或向下舍入为整数值,然后执行范围检查和处理,如已经描述的整数 - 整数比较。

      如果 REAL 值太小而无法表示, DECIMAL 则根据符号舍入为.01或-.01,然后作为a处理 DECIMAL

    • 字符串类型。  尝试将字符串值解释为整数类型,然后在整数值之间处理比较。 如果失败,请尝试将值作为a处理 REAL

  • DECIMAL或REAL列。  将十进制类型与以下类型的常量进行比较,如下所述:

    • 整数值。  对列值的整数部分执行范围检查。 如果没有折叠结果,将常量转换 DECIMAL 为与列值相同的小数位数,然后将其检查为a DECIMAL (参见下一个)。

    • DECIMAL或REAL值。  检查溢出(即,常量的整数部分中的数字是否多于列的十进制类型所允许的数字)。 如果是这样,折叠。

      如果常量具有比列的类型更重要的小数位数,则截断常量。 如果比较运算符是 = <> 折叠。 如果操作员是 >= <= ,则由于截断而调整操作员。 例如,如果列的类型是 DECIMAL(3,1) ,则 SELECT * FROM t WHERE f >= 10.13 变为 SELECT * FROM t WHERE f > 10.1

      如果常量的十进制数字少于列的类型,则将其转换为具有相同位数的常量。 对于 REAL 值的 下溢 (即,表示它的分数位数太少),将常量转换为十进制0。

    • 字符串值。  如果该值可以解释为整数类型,则按此处理。 否则,尝试将其处理为 REAL

  • FLOAT或DOUBLE列。  与常量相比较的值处理如下: FLOAT(m,n) DOUBLE(m,n)

    如果值溢出列的范围,则折叠。

    如果该值大于 n 小数,则截断,在折叠期间进行补偿。 对于 = <> 比较,倍至 TRUE FALSE IS [NOT] NULL 如前文所述; 对于其他运营商,调整运营商。

    如果该值超过 m 整数位数,则折叠。

限制。  在以下情况下不能使用此优化:

  1. 使用 BETWEEN 进行比较 IN

  2. 随着 BIT 使用日期或时间类型的列或列。

  3. 在准备语句的准备阶段,尽管可以在优化阶段应用实际执行准备好的语句。 这是因为在语句准备期间,常数的值尚不清楚。

8.2.1.14 IS NULL优化

MySQL能够执行相同的优化 ,它可以使用 例如,MySQL能使用索引和范围来搜索 col_name IS NULL col_name = constant_value NULL IS NULL

例子:

SELECT * FROM tbl_nameWHERE为key_colNULL;

SELECT * FROM tbl_nameWHERE key_col<=> NULL;

SELECT * FROM tbl_name
  WHERE key_col= const1OR key_col= const2OR为key_colNULL;

如果 WHERE 子句包含 声明为的列 条件,则 表达式将被优化掉。 当列可能 无论如何 产生时 (例如,如果它来自a右侧的表),则 不会发生此优化 col_name IS NULL NOT NULL NULL LEFT JOIN

MySQL还可以优化组合 ,这是一种在已解析子查询中很常见的形式。 显示 何时使用此优化。 col_name = expr OR col_name IS NULL EXPLAIN ref_or_null

此优化可以处理 IS NULL 任何关键部分。

假设列 a b 上有索引,优化的查询的一些示例 t2

SELECT * FROM t1 WHERE t1.a = exprOR t1.a IS NULL;

SELECT * FROM t1,t2 WHERE t1.a = t2.a OR t2.a IS NULL;

SELECT * FROM t1,t2
  WHERE(t1.a = t2.a OR t2.a IS NULL)AND t2.b = t1.b;

SELECT * FROM t1,t2
  在哪里t1.a = t2.a AND(t2.b = t1.b或t2.b IS NULL);

SELECT * FROM t1,t2
  WHERE(t1.a = t2.a AND t2.a IS NULL AND ...)
  或(t1.a = t2.a AND t2.a IS NULL AND ...);

ref_or_null 首先对引用键执行读操作,然后单独搜索具有 NULL 键值的

优化只能处理一个 IS NULL 级别。 在以下查询中,MySQL仅对表达式使用键查找, (t1.a=t2.a AND t2.a IS NULL) 并且无法使用关键部分 b

SELECT * FROM t1,t2
  WHERE(t1.a = t2.a AND t2.a IS NULL)
  或(t1.b = t2.b AND t2.b IS NULL);

8.2.1.15按优化顺序

本节描述MySQL何时可以使用索引来满足 ORDER BY 子句, 无法使用索引时使用 filesort 操作,以及优化器提供的执行计划信息 ORDER BY

一个 ORDER BY 有和没有 LIMIT 可能以不同的顺序返回行,在讨论 第8.2.1.18,“LIMIT查询优化”

使用索引来满足ORDER BY

在某些情况下,MySQL可能会使用索引来满足一个 ORDER BY 子句,并避免执行 filesort 操作时 涉及的额外排序

ORDER BY 只要索引的所有未使用部分和所有额外 ORDER BY 列都是 WHERE 子句 中的常量, 即使 索引与索引不完全匹配, 也可以使用 索引 如果索引不包含查询访问的所有列,则仅在索引访问比其他访问方法更便宜时才使用索引。

假设存在索引 ,则以下查询可以使用索引来解析该 部分。 优化程序是否实际执行此操作取决于读取索引是否比表扫描更有效,如果还必须读取索引中不存在的列。 (key_part1, key_part2) ORDER BY

  • 在此查询中,索引on 使优化器能够避免排序: (key_part1, key_part2)

    SELECT * FROM t1
      ORDER BY key_part1key_part2;
    

    但是,查询使用 SELECT * ,可以选择比 key_part1 更多的列 key_part2 在这种情况下,扫描整个索引并查找表行以查找不在索引中的列可能比扫描表并对结果进行排序更昂贵。 如果是这样,优化器可能不会使用索引。 如果 SELECT * 仅选择索引列,则将使用索引并避免排序。

    如果 t1 InnoDB 表,则表主键隐式地是索引的一部分,并且索引可用于解析 ORDER BY 此查询:

    SELECT pkkey_part1key_part2从T1
      ORDER BY key_part1key_part2;
    
  • 在此查询中, key_part1 是常量,因此通过索引访问的所有行都是 key_part2 有序的,并且 如果 子句具有足够的选择性以使索引范围扫描比表扫描更便宜, 索引将 避免排序 (key_part1, key_part2) WHERE

    SELECT * FROM t1
      WHERE key_part1= constant
      ORDER BY key_part2;
    
  • 在接下来的两个查询中,是否使用索引类似于 DESC 之前 显示 的相同查询

    SELECT * FROM t1
      ORDER BY key_part1DESC,key_part2DESC;
    
    SELECT * FROM t1
      WHERE key_part1= DESC constant
      订购key_part2;
    
  • a中的两列 ORDER BY 可以在相同方向(两个 ASC 或两个 DESC )或相反方向(一个 ASC ,一个 DESC 排序 索引使用的条件是索引必须具有相同的同质性,但不必具有相同的实际方向。

    如果查询混合 ASC DESC ,优化器可以使用的列的索引;如果该指数还采用了相应的混合升序和降序列:

    SELECT * FROM t1
      ORC BY key_part1DESC,key_part2ASC;
    

    优化器可以在( key_part1 key_part2 上使用索引 如果 key_part1 是降序并且 key_part2 正在升序。 它还可以在这些列上使用索引(使用向后扫描),如果它 key_part1 是递增的并且 key_part2 正在降序。 请参见 第8.3.13节“降序索引”

  • 在接下来的两个查询中, key_part1 将其与常量进行比较。 如果该 WHERE 子句具有足够的选择性以使索引范围扫描比表扫描更便宜, 则将使用该索引

    SELECT * FROM t1
      在哪里key_part1> ASC constant
      订购key_part1;
    
    SELECT * FROM t1
      在哪里key_part1<按DESC constant
      订购key_part1;
    
  • 在下一个查询中, ORDER BY 没有命名 key_part1 ,但所有选定的行都有一个常 key_part1 量值,因此仍然可以使用索引:

    SELECT * FROM t1
      WHERE key_part1= constant1AND key_part2> constant2
      ORDER BY key_part2;
    

在某些情况下,MySQL 无法 使用索引来解析 ORDER BY ,尽管它仍然可以使用索引来查找与该 WHERE 子句 匹配的行 例子:

  • 该查询用于 ORDER BY 不同的索引:

    SELECT * FROM t1 ORDER BY key1, key2;
    
  • 该查询用于 ORDER BY 索引的非连续部分:

    SELECT * FROM t1 WHERE key2= constantORDER BY key1_part1, key1_part3;
    
  • 用于获取行的索引与以下中使用的索引不同 ORDER BY

    SELECT * FROM t1 WHERE key2= constantORDER BY key1;
    
  • 该查询使用 ORDER BY 包含索引列名以外的术语的表达式:

    SELECT * FROM t1 ORDER BY ABS(key);
    SELECT * FROM t1 ORDER BY  - key;
    
  • 该查询连接了许多表,并且其中的列 ORDER BY 不是来自用于检索行的第一个非常量表。 (这是 EXPLAIN 输出中没有 const 连接类型 的第一个表 。)

  • 查询有不同的 ORDER BY GROUP BY 表情。

  • 只有该 ORDER BY 子句中 指定的列的前缀有一个索引 在这种情况下,索引不能用于完全解析排序顺序。 例如,如果仅 CHAR(20) 索引列 的前10个字节 ,则索引无法区分超过第10个字节的值,并且 filesort 需要a。

  • 索引不按顺序存储行。 例如,对于 表中 HASH 索引 ,这是正确的 MEMORY

用于排序的索引的可用性可能受到列别名的使用的影响。 假设列 t1.a 已编入索引。 在此语句中,选择列表中列的名称为 a 它指的是 t1.a ,引用 a 也是 ORDER BY 如此,因此 t1.a 可以使用 索引

选择一个FROM t1 ORDER BY a;

在此语句中,选择列表中列的名称也是 a ,但它是别名。 它指的是 ABS(a) ,在引用 a ORDER BY ,所以 t1.a 不能使用 索引

SELECT ABS(a)作为一个FROM t1 ORDER by a;

在以下语句中, ORDER BY 引用的名称不是选择列表中列的名称。 但是在 t1 命名中 有一个列 a ,所以 可以使用 ORDER BY 引用 t1.a 和索引 t1.a (当然,生成的排序顺序可能与顺序完全不同 ABS(a) 。)

选择ABS(a)AS b FROM t1 ORDER by a;

以前(MySQL 5.7及更低版本), GROUP BY 在某些条件下隐式排序。 在MySQL 8.0中,不再出现这种情况,因此 ORDER BY NULL 不再需要在最后 指定 抑制隐式排序(如前所述)。 但是,查询结果可能与以前的MySQL版本不同。 要生成给定的排序顺序,请提供一个 ORDER BY 子句。

使用filesort来满足ORDER BY

如果索引不能用于满足 ORDER BY 子句,则MySQL执行 filesort 读取表行并对其进行排序 操作。 A filesort 构成查询执行中的额外排序阶段。

为了获得 filesort 操作的 内存 ,从MySQL 8.0.12开始,优化器根据需要逐步分配内存缓冲区,直到 sort_buffer_size 系统变量 指示的大小 ,而不是 sort_buffer_size 像MySQL 8.0之前那样预先 分配固定数量的 字节。 0.12。 这使用户可以设置 sort_buffer_size 更大的值来加速更大的排序,而不用担心小排序的过多内存使用。 (对于Windows上的多个并发排序,可能不会出现此优势,因为它具有较弱的多线程 malloc 。)

一个 filesort 操作使用临时磁盘文件作为必要的,如果结果集是太大,无法在内存中。 某些类型的查询特别适合于完全内存中的 filesort 操作。 例如,优化器可以 filesort 用来有效地在内存中处理 ORDER BY 以下形式的查询(和子查询) ,而无需临时文件

SELECT ... FROM single_table... ORDER BY non_index_column[DESC] LIMIT [ M,] N;

此类查询在Web应用程序中很常见,只显示较大结果集中的几行。 例子:

SELECT col1,... FROM t1 ... ORDER BY name LIMIT 10;
SELECT col1,... FROM t1 ... ORDER BY RAND()LIMIT 15;
影响ORDER BY优化

对于 未使用的 ORDER BY 查询 filesort ,请尝试将 max_length_for_sort_data 系统变量 降低 到适合触发a的值 filesort (将此变量的值设置得过高的症状是高磁盘活动和低CPU活动的组合。)

要提高 ORDER BY 速度,请检查是否可以让MySQL使用索引而不是额外的排序阶段。 如果无法做到这一点,请尝试以下策略:

  • 增加 sort_buffer_size 变量值。 理想情况下,该值应足够大,以使整个结果集适合排序缓冲区(以避免写入磁盘和合并传递)。

    请考虑存储在排序缓冲区中的列值的大小受 max_sort_length 系统变量值的影响。 例如,如果元组存储长字符串列的值并且您增加了值 max_sort_length ,则排序缓冲区元组的大小也会增加,并且可能需要您增加 sort_buffer_size

    要监视合并传递的数量(合并临时文件),请检查 Sort_merge_passes 状态变量。

  • 增加 read_rnd_buffer_size 变量值,以便一次读取更多行。

  • tmpdir 系统变量 更改 为指向具有大量可用空间的专用文件系统。 变量值可以列出以循环方式使用的几个路径; 您可以使用此功能将负载分散到多个目录中。 : 在Unix上 用冒号字符( 分隔路径, ; 在Windows上用 分号字符( 分隔 路径 路径应命名位于不同 物理 磁盘 上的文件系统中的目录 ,而不是同一磁盘上的不同分区。

按订单执行计划信息

使用 EXPLAIN (参见 第8.8.1节“使用EXPLAIN优化查询” ),可以检查MySQL是否可以使用索引来解析 ORDER BY 子句:

  • 如果 输出 Extra EXPLAIN 不包含 Using filesort ,则使用索引并且 filesort 不执行a。

  • 如果 输出 Extra EXPLAIN 包含 Using filesort ,则不使用索引并 filesort 执行a。

此外,如果 filesort 执行a,则优化程序跟踪输出包括 filesort_summary 块。 例如:

“filesort_summary”:{
  “行”:100,
  “examine_rows”:100,
  “number_of_tmp_files”:0,
  “peak_memory_used”:25192,
  “sort_mode”:“<sort_key,packed_additional_fields>”
}

peak_memory_used 表示排序期间任何时候使用的最大内存。 这是一个值,但不一定与 sort_buffer_size 系统变量 的值一样大 在MySQL 8.0.12之前,输出显示 sort_buffer_size ,表示值 sort_buffer_size (在MySQL 8.0.12之前,优化器总是 sort_buffer_size 为排序缓冲区 分配 字节。从8.0.12开始,优化器逐步分配排序缓冲区内存,从少量开始并根据需要添加更多,最多为 sort_buffer_size 字节。)

sort_mode 值提供有关排序缓冲区中元组内容的信息:

  • <sort_key, rowid> :这表示排序缓冲区元组是包含原始表行的排序键值和行ID的对。 元组按排序键值排序,行ID用于从表中读取行。

  • <sort_key, additional_fields> :这表示排序缓冲区元组包含排序键值和查询引用的列。 元组按排序键值排序,列值直接从元组中读取。

  • <sort_key, packed_additional_fields> :与前一个版本类似,但是附加列紧密地打包在一起而不是使用固定长度编码。

EXPLAIN 不区分优化器是否执行 filesort 内存。 filesort 在优化器跟踪输出中可以看到 内存的使用 寻找 filesort_priority_queue_optimization 有关优化程序跟踪的信息,请参阅 MySQL内部:跟踪优化程序

8.2.1.16 GROUP BY优化

最满意的一般方式 GROUP BY 子句 是扫描整个表并创建一个新的临时表,其中每个组的所有行都是连续的,然后使用此临时表来发现组并应用聚合函数(如果有的话)。 在某些情况下,MySQL能够做得更好,并通过使用索引访问来避免创建临时表。

使用索引的最重要的前提条件 GROUP BY 是所有 GROUP BY 列都引用来自同一索引的属性,并且索引按顺序存储其键(例如,对于 BTREE 索引而不是 HASH 索引)。 是否可以通过索引访问替换临时表的使用还取决于查询中使用索引的哪些部分,为这些部分指定的条件以及所选的聚合函数。

有两种方法可以 GROUP BY 通过索引访问 来执行 查询,如以下各节所述。 第一种方法将分组操作与所有范围谓词(如果有)一起应用。 第二种方法首先执行范围扫描,然后对生成的元组进行分组。

松散的索引扫描也可以 GROUP BY 在某些条件下使用。 请参见 跳过扫描范围访问方法

松散的索引扫描

最有效的处理方法 GROUP BY 是使用索引直接检索分组列。 使用此访问方法,MySQL使用某些索引类型的属性(例如,键 BTREE )。 此属性允许在索引中使用查找组,而无需考虑索引中满足所有 WHERE 条件的 所有键 此访问方法仅考虑索引中的一小部分键,因此称为 松散索引扫描 当没有 子句包含范围谓词时(参见 连接类型 的讨论 WHERE 子句,则松散索引扫描会读取与组数一样多的密钥,这可能比所有密钥的数量小得多。 如果 WHERE range 第8.8.1节“使用EXPLAIN优化查询” ,松散索引扫描查找满足范围条件的每个组的第一个键,并再次读取尽可能少的键。 在以下条件下可以这样做:

  • 查询在一个表上。

  • GROUP BY 唯一名称构成的指数,没有其他列的最左边的前缀列。 (如果 GROUP BY 查询具有 DISTINCT 子句,则所有不同的属性引用形成索引的最左前缀的列。)例如,如果表 t1 具有索引 (c1,c2,c3) ,则在查询具有的情况下,松散索引扫描适用 GROUP BY c1, c2 如果查询具有 GROUP BY c2, c3 (列不是最左边的前缀)或 GROUP BY c1, c2, c4 c4 不在索引中), 则不适用

  • 选择列表中使用的唯一聚合函数(如果有)是 MIN() MAX() ,并且它们都引用同一列。 该列必须位于索引中,并且必须紧跟在列中的列之后 GROUP BY

  • 索引的任何其他部分都不是 GROUP BY 查询中引用的 那些部分 必须是常量(也就是说,它们必须以与常量相等的方式引用),但参数 MIN() MAX() 函数 除外

  • 对于索引中的列,必须为完整列值编制索引,而不仅仅是前缀。 例如,使用 c1 VARCHAR(20), INDEX (c1(10)) ,索引仅使用 c1 的前缀, 不能用于松散索引扫描。

如果“松散索引扫描”适用于查询,则 EXPLAIN 输出将显示 Using index for group-by Extra 列中。

假设 idx(c1,c2,c3) 表上 有索引 t1(c1,c2,c3,c4) 松散索引扫描访问方法可用于以下查询:

SELECT c1,c2 FROM t1 GROUP BY c1,c2;
SELECT DISTINCT c1,c2 FROM t1;
SELECT c1,MIN(c2)FROM t1 GROUP BY c1;
SELECT c1,c2 FROM t1 WHERE c1 < const GROUP BY c1,c2;
SELECT MAX(c3),MIN(c3),c1,c2 FROM t1 WHERE c2> constGROUP BY c1,c2;
SELECT c2 FROM t1 WHERE c1 < constGROUP BY c1,c2;
SELECT c1,c2 FROM t1 WHERE c3 = const GROUP BY c1,c2;

由于给出的原因,使用此快速选择方法无法执行以下查询:

  • 除了 MIN() 之外还有聚合函数 MAX()

    SELECT c1,SUM(c2)FROM t1 GROUP BY c1;
    
  • 中的列 GROUP BY 子句中 不构成索引的最左前缀:

    SELECT c1,c2 FROM t1 GROUP BY c2,c3;
    
  • 查询引用了一个密钥的一部分 GROUP BY 一部分,并且与常量不相等:

    SELECT c1,c3 FROM t1 GROUP BY c1,c2;
    

    如果要包含查询 ,可以使用松散索引扫描。 WHERE c3 = const

除了 已经支持 MIN() MAX() 引用 之外,松散索引扫描访问方法还可以应用于选择列表中的其他形式的聚合函数引用

假设 idx(c1,c2,c3) 表上 有索引 t1(c1,c2,c3,c4) 松散索引扫描访问方法可用于以下查询:

SELECT COUNT(DISTINCT c1),SUM(DISTINCT c1)FROM t1;

SELECT COUNT(DISTINCT c1,c2),COUNT(DISTINCT c2,c1)FROM t1;
紧密索引扫描

紧密索引扫描可以是完整索引扫描,也可以是范围索引扫描,具体取决于查询条件。

如果不满足松散索引扫描的条件,则仍可以避免为 GROUP BY 查询 创建临时表 如果 WHERE 子句中 有范围条件,则 此方法仅读取满足这些条件的键。 否则,它执行索引扫描。 因为此方法读取 WHERE 子句 定义的每个范围中的所有键 ,或者如果没有范围条件则扫描整个索引,因此称为 紧密索引扫描 使用紧密索引扫描时,仅在找到满足范围条件的所有键之后才执行分组操作。

为了使这个方法起作用,对于查询中的所有列,存在一个恒定的相等条件就足够了,该条件引用了密钥的部分之前或之间的部分 GROUP BY 密钥。 来自等式条件的常量填充 搜索关键字 中的任何 间隙 ,以便可以形成索引的完整前缀。 然后,这些索引前缀可用于索引查找。 如果 GROUP BY 结果需要排序,并且可以形成作为索引前缀的搜索键,MySQL也避免了额外的排序操作,因为在有序索引中使用前缀进行搜索已经按顺序检索了所有键。

假设 idx(c1,c2,c3) 表上 有索引 t1(c1,c2,c3,c4) 以下查询不适用于前面描述的松散索引扫描访问方法,但仍可使用紧密索引扫描访问方法。

  • 它有一个缺口 GROUP BY ,但它被条件覆盖 c2 = 'a'

    SELECT c1,c2,c3 FROM t1 WHERE c2 ='a'GROUP BY c1,c3;
    
  • GROUP BY 不与钥匙的第一部分开始,但它提供了该部分恒定的条件:

    SELECT c1,c2,c3 FROM t1 WHERE c1 ='a'GROUP BY c2,c3;
    

8.2.1.17 DISTINCT优化

DISTINCT ORDER BY 在许多情况下 结合 需要临时表。

因为 DISTINCT 可以使用 GROUP BY ,所以了解MySQL如何 使用 不属于所选列的列 ORDER BY HAVING 子句。 请参见 第12.20.3节“GROUP BY的MySQL处理”

在大多数情况下,一个 DISTINCT 条款可以被视为一个特例 GROUP BY 例如,以下两个查询是等效的:

SELECT DISTINCT c1,c2,c3 FROM t1
在哪里c1> const;

SELECT c1,c2,c3 FROM t1
在哪里c1> constGROUP BY c1,c2,c3;

由于这种等效性,适用于 GROUP BY 查询 的优化 也可以应用于带有 DISTINCT 子句的 查询 因此,有关 DISTINCT 查询 优化可能性的更多详细信息 ,请参见 第8.2.1.16节“GROUP BY优化”

与之 结合 使用时 ,MySQL会在找到 唯一行后 立即停止 LIMIT row_count DISTINCT row_count

如果不使用查询中指定的所有表中的列,MySQL会在找到第一个匹配项后立即停止扫描所有未使用的表。 在下面的例子中,假设 t1 之前使用过 t2 (你可以检查 EXPLAIN ),MySQL t2 t1 找到第一行时 停止从 (对于任何特定行 读取 t2

SELECT DISTINCT t1.a FROM t1,t2其中t1.a = t2.a;

8.2.1.18 LIMIT查询优化

如果只需要结果集中指定数量的行,请 LIMIT 在查询中 使用 子句,而不是获取整个结果集并丢弃额外数据。

MySQL有时会优化具有 子句且没有 子句 的查询 LIMIT row_count HAVING

  • 如果您只选择几行 LIMIT ,MySQL在某些情况下使用索引,通常它更喜欢进行全表扫描。

  • 如果你结合 使用 ,MySQL会在找到 排序结果 的第一 行后 立即停止排序 ,而不是整理整个结果。 如果使用索引完成排序,则速度非常快。 如果必须完成一个 文件排序,则选择与 没有该 子句 的查询匹配的所有行 ,并在 找到 第一个之前对其中的大部分或全部进行排序 在找到初始行之后,MySQL不会对结果集的任何剩余部分进行排序。 LIMIT row_count ORDER BY row_count LIMIT row_count

    此行为的一种表现形式是, ORDER BY 带有和不带 查询 LIMIT 可能会以不同的顺序返回行,如本节后面所述。

  • 如果你结合 使用 ,MySQL会在找到 唯一行后 立即停止 LIMIT row_count DISTINCT row_count

  • 在某些情况下, GROUP BY 可以通过按顺序读取索引(或对索引进行排序),然后计算摘要直到索引值更改来解析a。 在这种情况下, 不计算任何不必要的 值。 LIMIT row_count GROUP BY

  • 一旦MySQL向客户端发送了所需的行数,它就会中止查询,除非您使用 SQL_CALC_FOUND_ROWS 在这种情况下,可以使用检索行数 SELECT FOUND_ROWS() 请参见 第12.15节“信息功能”

  • LIMIT 0 快速返回一个空集。 这对于检查查询的有效性非常有用。 它还可用于获取使用MySQL API的应用程序中的结果列类型,该API使结果集元数据可用。 使用 mysql 客户端程序,您可以使用该 --column-type-info 选项显示结果列类型。

  • 如果服务器使用临时表来解析查询,则它使用该 子句计算需要多少空间。 LIMIT row_count

  • 如果未使用索引 ORDER BY 但是 LIMIT 也存在子句,则优化器可能能够避免使用合并文件并使用内存中 filesort 操作 对内存中的行进行排序

如果列中的多个行具有相同的值 ORDER BY ,则服务器可以按任何顺序自由返回这些行,并且可能根据整体执行计划的不同而不同。 换句话说,这些行的排序顺序相对于无序列是不确定的。

影响执行计划的一个因素是 LIMIT ,因此 ORDER BY 使用和不 使用 查询 LIMIT 可能会返回不同顺序的行。 考虑这个查询,它按 category 排序, 但对于 id rating 列是 不确定的

MySQL的> SELECT * FROM ratings ORDER BY category;
+ ---- + ---------- + -------- +
| id | 类别| 评级|
+ ---- + ---------- + -------- +
| 1 | 1 | 4.5 |
| 5 | 1 | 3.2 |
| 3 | 2 | 3.7 |
| 4 | 2 | 3.5 |
| 6 | 2 | 3.5 |
| 2 | 3 | 5.0 |
| 7 | 3 | 2.7 |
+ ---- + ---------- + -------- +

包括 LIMIT 可能影响每个 category 内的行的顺序 例如,这是一个有效的查询结果:

MySQL的> SELECT * FROM ratings ORDER BY category LIMIT 5;
+ ---- + ---------- + -------- +
| id | 类别| 评级|
+ ---- + ---------- + -------- +
| 1 | 1 | 4.5 |
| 5 | 1 | 3.2 |
| 4 | 2 | 3.5 |
| 3 | 2 | 3.7 |
| 6 | 2 | 3.5 |
+ ---- + ---------- + -------- +

在每种情况下,行都按 ORDER BY 排序 ,这是SQL标准所需的全部内容。

如果确保使用和不使用相同的行顺序很重要,请 LIMIT ORDER BY 子句中 包含其他列 以使订单具有确定性。 例如,如果 id 值是唯一的,则可以 通过如下排序 category id 顺序 显示 给定 值的

MySQL的> SELECT * FROM ratings ORDER BY category, id;
+ ---- + ---------- + -------- +
| id | 类别| 评级|
+ ---- + ---------- + -------- +
| 1 | 1 | 4.5 |
| 5 | 1 | 3.2 |
| 3 | 2 | 3.7 |
| 4 | 2 | 3.5 |
| 6 | 2 | 3.5 |
| 2 | 3 | 5.0 |
| 7 | 3 | 2.7 |
+ ---- + ---------- + -------- +

MySQL的> SELECT * FROM ratings ORDER BY category, id LIMIT 5;
+ ---- + ---------- + -------- +
| id | 类别| 评级|
+ ---- + ---------- + -------- +
| 1 | 1 | 4.5 |
| 5 | 1 | 3.2 |
| 3 | 2 | 3.7 |
| 4 | 2 | 3.5 |
| 6 | 2 | 3.5 |
+ ---- + ---------- + -------- +

8.2.1.19函数调用优化

MySQL函数在内部标记为确定性或非确定性。 如果给定参数的固定值,它可以为不同的调用返回不同的结果,那么函数是不确定的。 非确定性函数的例子: RAND() UUID()

如果函数被标记为非确定性的, WHERE 则会为每一行(从一个表中选择时)或行组合(从多表连接中选择时)评估子句中 对它的引用

MySQL还根据参数类型,参数是表列还是常量值来确定何时评估函数。 只要该列更改值,就必须计算将表列作为参数的确定性函数。

非确定性函数可能会影响查询性能。 例如,某些优化可能不可用,或者可能需要更多锁定。 以下讨论使用 RAND() 但也适用于其他非确定性函数。

假设一个表 t 有这个定义:

CREATE TABLE t(id INT NOT NULL PRIMARY KEY,col_a VARCHAR(100));

考虑这两个查询:

SELECT * FROM t WHERE id = POW(1,2);
SELECT * FROM t WHERE id = FLOOR(1 + RAND()* 49);

由于与主键的相等性比较,两个查询似乎都使用主键查找,但这仅适用于第一个:

  • 第一个查询总是产生最多一行,因为 POW() 常量参数是一个常量值,用于索引查找。

  • 第二个查询包含一个使用非确定性函数的表达式,该函数 RAND() 在查询中不是常量,但实际上对于表的每一行都有一个新值 t 因此,查询读取表的每一行,计算每行的谓词,并输出主键与随机值匹配的所有行。 这可能是零行,一行或多行,具体取决于 id 列值和 RAND() 序列中 的值

不确定性的影响不仅限于 SELECT 陈述。 UPDATE 语句使用非确定性函数来选择要修改的行:

UPDATE t SET col_a = some_exprWHERE id = FLOOR(1 + RAND()* 49);

据推测,目的是最多更新主键与表达式匹配的单个行。 但是,它可能会更新零行,一行或多行,具体取决于 id 列值和 RAND() 序列中 的值

刚才描述的行为对性能和复制有影响:

  • 由于非确定性函数不会生成常量值,因此优化程序无法使用可能适用的策略,例如索引查找。 结果可能是表扫描。

  • InnoDB 可能会升级到范围键锁,而不是对一个匹配的行进行单行锁定。

  • 不确定执行的更新对于复制是不安全的。

困难源于这样一个事实,即 RAND() 对表格的每一行评估一次函数。 要避免多项功能评估,请使用以下技术之一:

  • 将包含非确定性函数的表达式移动到单独的语句中,将值保存在变量中。 在原始语句中,将表达式替换为对变量的引用,优化程序可将其视为常量值:

    SET @keyval = FLOOR(1 + RAND()* 49);
    UPDATE t SET col_a = some_exprWHERE id = @keyval;
    
  • 将随机值分配给派生表中的变量。 这个技术使变量在用于 WHERE 子句 中的比较之前被赋值一次

    UPDATE / * + NO_MERGE(dt)* / t,(SELECT FLOOR(1 + RAND()* 49)AS r)AS dt
    SET col_a = some_exprWHERE id = dt.r;
    

如前所述, WHERE 子句中 的非确定性表达式 可能会阻止优化并导致表扫描。 但是, WHERE 如果其他表达式是确定性的 ,则可以部分地优化该 子句。 例如:

SELECT * FROM t WHERE partial_key = 5 AND some_column = RAND();

如果优化器可以 partial_key 用来减少所选择的行集, RAND() 则执行的次数会减少,这会减少非确定性对优化的影响。

8.2.1.20窗口函数优化

窗口函数会影响优化程序考虑的策略:

  • 如果子查询具有窗口函数,则禁用派生表合并子查询。 子查询始终具体化。

  • 半连接不适用于窗口函数优化,因为半连接适用于子查询, WHERE 并且 JOIN ... ON 不能包含窗口函数。

  • 优化器按顺序处理具有相同排序要求的多个窗口,因此可以在第一个窗口之后跳过排序。

  • 优化器不会尝试合并可在单个步骤中评估的窗口(例如,当多个 OVER 子句包含相同的窗口定义时)。 解决方法是在 WINDOW 子句中 定义窗口 并引用子句中的窗口名称 OVER

未用作窗口函数的聚合函数在最外面的可能查询中聚合。 例如,在这个查询中,MySQL看到 COUNT(t1.b) 外部查询中不存在的东西,因为它放在 WHERE 子句中:

SELECT * FROM t1 WHERE t1.a =(SELECT COUNT(t1.b)FROM t2);

因此,MySQL在子查询中聚合, t1.b 视为常量并返回行的计数 t2

WHERE HAVING 结果 替换 错误:

MySQL的> SELECT * FROM t1 HAVING t1.a = (SELECT COUNT(t1.b) FROM t2);
错误1140(42000):在没有GROUP BY的聚合查询中,表达式为#1
SELECT列表包含非聚合列'test.t1.a'; 这是
与sql_mode = only_full_group_by不兼容

发生错误是因为 COUNT(t1.b) 可以存在 HAVING ,因此会使外部查询聚合。

窗口函数(包括用作窗口函数的聚合函数)没有前面的复杂性。 它们总是在编写它们的子查询中聚合,而不是在外部查询中聚合。

窗口函数评估可能受 windowing_use_high_precision 系统变量 值的影响,该值 确定是否在不损失精度的情况下计算窗口操作。 默认情况下, windowing_use_high_precision 已启用。

对于某些移动帧聚合,可以应用反聚合函数来从聚合中移除值。 这可以提高性能,但可能会降低精度。 例如,将一个非常小的浮点值添加到一个非常大的值会导致非常小的值被 大值 隐藏 当稍后反转大值时,小值的效果会丢失。

由于反向聚合导致的精度损失仅是对浮点(近似值)数据类型的操作的因素。 对于其他类型,反向聚合是安全的; 这包括 DECIMAL ,允许小数部分,但是精确值类型。

为了更快地执行,MySQL在安全时始终使用反向聚合:

  • 对于浮点值,反向聚合并不总是安全的,可能会导致精度损失。 默认是避免反向聚合,反向聚合较慢但保留精度。 如果允许牺牲速度的安全性, windowing_use_high_precision 可以禁用以允许反向聚合。

  • 对于非浮点数据类型,逆聚合始终是安全的,无论 windowing_use_high_precision 如何都使用

  • windowing_use_high_precision MIN() 没有影响 MAX() ,在任何情况下都不使用反向聚合。

为的方差函数评价 STDDEV_POP() STDDEV_SAMP() VAR_POP() VAR_SAMP() ,和它们的同义词,可以发生在优化模式或默认模式的评价。 优化模式可能会在最后有效数字中产生略微不同的结果。 如果允许这样的差异, windowing_use_high_precision 可以禁用以允许优化模式。

因为 EXPLAIN ,窗口执行计划信息过于庞大,无法以传统输出格式显示。 要查看窗口信息,请使用 EXPLAIN FORMAT=JSON 并查找 windowing 元素。

8.2.1.21行构造函数表达式优化

行构造函数允许同时比较多个值。 例如,这两个语句在语义上是等价的:

SELECT * FROM t1 WHERE(column1,column2)=(1,1);
SELECT * FROM t1 WHERE column1 = 1 AND column2 = 1;

此外,优化器以相同的方式处理两个表达式。

如果行构造函数列未覆盖索引的前缀,则优化程序不太可能使用可用索引。 请考虑下表,其中包含一个主键 (c1, c2, c3)

CREATE TABLE t1(
  c1 INT,c2 INT,c3 INT,c4 CHAR(100),
  主键(c1,c2,c3)
);

在此查询中,该 WHERE 子句使用索引中的所有列。 但是,行构造函数本身不会覆盖索引前缀,结果是优化程序仅使用 c1 key_len=4 ,大小 c1 ):

MySQL的> EXPLAIN SELECT * FROM t1
       WHERE c1=1 AND (c2,c3) > (1,1)\G
*************************** 1。排******************** *******
           id:1
  select_type:SIMPLE
        表:t1
   分区:NULL
         类型:ref
possible_keys:PRIMARY
          关键:主要
      key_len:4
          ref:const
         行:3
     过滤:100.00
        额外:使用在哪里

在这种情况下,使用等效的非构造函数表达式重写行构造函数表达式可能会导致更完整的索引使用。 对于给定的查询,行构造函数和等效的非构造函数表达式是:

(c2,c3)>(1,1)
c2> 1 OR((c2 = 1)AND(c3> 1))

重写查询以使用非构造函数表达式会导致优化器使用index( key_len=12 中的所有三列

MySQL的> EXPLAIN SELECT * FROM t1
       WHERE c1 = 1 AND (c2 > 1 OR ((c2 = 1) AND (c3 > 1)))\G
*************************** 1。排******************** *******
           id:1
  select_type:SIMPLE
        表:t1
   分区:NULL
         类型:范围
possible_keys:PRIMARY
          关键:主要
      key_len:12
          ref:NULL
         行:3
     过滤:100.00
        额外:使用在哪里

因此,为了获得更好的结果,请避免将行构造函数与 AND / OR 表达式 混合 使用其中一个。

在某些情况下,优化器可以将范围访问方法应用于 IN() 具有行构造函数参数的表达式。 请参阅 行构造函数表达式的范围优化

8.2.1.22避免全表扫描

当MySQL使用 全表扫描 来解析查询 ,输出 EXPLAIN 显示 ALL type 列中 这通常在以下条件下发生:

  • 该表非常小,执行表扫描比使用键查找更麻烦。 这对于行少于10行且行长度较短的表来说很常见。

  • 索引列 ON or WHERE 子句 中没有可用的限制

  • 您正在将索引列与常量值进行比较,并且MySQL已经计算(基于索引树)常量覆盖了表的一部分,并且表扫描会更快。 请参见 第8.2.1.1节“WHERE子句优化”

  • 您正在使用具有低基数的键(许多行与键值匹配)通过另一列。 在这种情况下,MySQL假设通过使用密钥,它可能会执行许多密钥查找,并且表扫描会更快。

对于小型表,表扫描通常是合适的,并且性能影响可以忽略不计。 对于大型表,请尝试以下技术以避免优化程序错误地选择表扫描:

8.2.2优化子查询,派生表,查看引用和公用表表达式

MySQL查询优化器有不同的策略可用于评估子查询:

  • 对于 IN (或 =ANY )子查询,优化器具有以下选项:

    • 半连接

    • 实体化

    • EXISTS 战略

  • 对于 NOT IN (或 <>ALL )子查询,优化器具有以下选项:

    • 实体化

    • EXISTS 战略

对于派生表,优化器具有以下选项(也适用于视图引用和公用表表达式):

  • 将派生表合并到外部查询块中

  • 将派生表实现为内部临时表

以下讨论提供了有关上述优化策略的更多信息。

注意

使用子查询修改单个表 的限制 UPDATE DELETE 语句是优化程序不使用半连接或实现子查询优化。 作为一种解决方法,尝试将它们重写为 使用连接而不是子查询的 多表 UPDATE DELETE 语句。

8.2.2.1使用半连接转换优化子查询,派生表,视图引用和公用表表达式

半连接是一种准备时转换,它支持多种执行策略,例如表拉出,重复清除,首次匹配,松散扫描和物化。 优化程序使用半连接策略来改进子查询执行,如本节所述。

对于两个表之间的内部联接,连接从一个表返回一行,与另一个表中的匹配一样多次。 但是对于一些问题,唯一重要的信息是匹配,而不是匹配的数量。 假设有一些名为table的表 class roster 并且分别列出课程课程和班级名单(每个班级注册的学生)中的班级。 要列出实际注册学生的课程,您可以使用此连接:

SELECT class.class_num,class.class_name
来自INNER JOIN名单
WHERE class.class_num = roster.class_num;

但是,结果会为每个注册的学生列出每个班级一次。 对于被问到的问题,这是不必要的重复信息。

假设这 class_num class 表中 的主键, 可以通过使用重复抑制 SELECT DISTINCT ,但是先生成所有匹配的行只是为了稍后消除重复,效率很低。

使用子查询可以获得相同的无重复结果:

SELECT class_num,class_name
从班级
WHERE class_num IN(SELECT class_num FROM roster);

在这里,优化器可以识别该 IN 子句要求子查询只从 roster 表中 返回每个类号的一个实例 在这种情况下,查询可以使用 半连接 ; 也就是说,仅返回一个中的每一行的实例的操作 class 由按行匹配 roster

在MySQL 8.0.16及更高版本中, EXISTS 通过将 EXISTS 属于 WHERE 条件或连接条件的 IN 查询转换为 子查询 ,此策略也可以与 子查询 一起使用 ,如下所示:

在哪里... EXISTS(SELECT ... FROM ...)

- > WHERE ... 1 IN(SELECT 1 FROM ...)

在此之后,子查询操作可以作为半连接处理。

外部查询规范中允许外部联接和内部联接语法,表引用可以是基表,派生表,视图引用或公用表表达式。

在MySQL中,子查询必须满足这些条件才能作为半连接处理:

  • 它必须是 出现在 or 子句 顶层 IN (或 =ANY )子查询 ,可能作为 表达式中 的术语 例如: WHERE ON AND

    选择 ...
    来自ot1,......
    WHERE(oe1,...)IN(SELECT ie1,... FROM it1,... WHERE ...);
    

    这里, 表示在该查询的外侧和内侧的部分表,和 表示参照列中的外和内表中的表达式。 ot_i it_i oe_i ie_i

  • 它必须是 SELECT 没有 UNION 结构 的单一

  • 它不能包含 GROUP BY or HAVING 子句。

  • 它不能隐式分组(它必须不包含聚合函数)。

  • 它一定不能 ORDER BY LIMIT

  • 该语句不得 STRAIGHT_JOIN 在外部查询中 使用 连接类型。

  • STRAIGHT_JOIN 修改必须不存在。

  • 外表和内表的数量必须小于连接中允许的最大表数。

子查询可以是相关的或不相关的。 DISTINCT 是允许的, LIMIT 除非 ORDER BY 也使用。

如果子查询符合上述条件,MySQL会将其转换为半连接,并根据这些策略进行基于成本的选择:

  • 将子查询转换为连接,或使用表pullout并将查询作为子查询表和外部表之间的内部联接运行。 表pullout将表从子查询拉出到外部查询。

  • 重复的Weedout:运行半连接,就好像它是一个连接一样,并使用临时表删除重复的记录。

  • FirstMatch:当扫描内部表中的行组合并且存在给定值组的多个实例时,选择一个而不是全部返回它们。 这种“快捷方式”扫描并消除了不必要的行的产生。

  • LooseScan:使用索引扫描子查询表,该索引允许从每个子查询的值组中选择单个值。

  • 将子查询实现为用于执行连接的索引临时表,其中索引用于删除重复项。 在将临时表与外部表连接时,索引也可能稍后用于查找; 如果没有,则扫描表格。 有关实现的更多信息,请参见 第8.2.2.2节“使用实现优化子查询”

可以使用以下 optimizer_switch 系统变量标志 启用或禁用其中每个策略

  • semijoin 标志控制是否使用半连接。

  • 如果 semijoin 使能, firstmatch loosescan duplicateweedout ,和 materialization 旗帜能够在允许的半连接策略更精细的控制。

  • 如果 duplicateweedout 禁用半连接策略,则除非还禁用所有其他适用策略,否则不会使用该策略。

  • 如果 duplicateweedout 禁用,有时优化程序可能会生成远非最佳的查询计划。 这是由于在贪婪搜索期间进行启发式修剪,这可以通过设置来避免 optimizer_prune_level=0

默认情况下启用这些标志。 请参见 第8.9.2节“可切换的优化”

优化程序最大限度地减少了视图和派生表的处理差异。 这会影响使用 STRAIGHT_JOIN 修饰符的 IN 查询 和带有 可以转换为半连接 子查询 的视图 以下查询说明了这一点,因为处理中的更改会导致转换发生更改,从而导致执行策略不同:

创建视图与AS
选择 *
从t1
IN(选择b
           来自t2);

SELECT STRAIGHT_JOIN *
FROM t3 JOIN v ON t3.x = va;

优化器首先查看视图并将 IN 子查询转换为半连接,然后检查是否可以将视图合并到外部查询中。 由于 STRAIGHT_JOIN 外部查询中 修饰符会阻止半连接,因此优化程序会拒绝合并,从而导致使用实现表进行派生表评估。

EXPLAIN output表示使用半连接策略如下:

  • 对于扩展 EXPLAIN 输出,以下 SHOW WARNINGS 显示 的文本显示 重写的查询,该查询显示半连接结构。 (参见 第8.8.3节“扩展EXPLAIN输出格式” 。)从中可以了解哪些表从半连接中拉出。 如果子查询转换为半连接,您将看到子查询谓词已消失,其表和 WHERE 子句已合并到外部查询连接列表和 WHERE 子句中。

  • 对于重复Weedout临时表的使用是由指示 Start temporary End temporary Extra 列。 未被拉出并且在 EXPLAIN 输出行 范围内的 Start temporary 并将 End temporary 它们 rowid 放在临时表中。

  • FirstMatch(tbl_name) Extra 列中表示连接快捷方式。

  • LooseScan(m..n) Extra 列中表示使用LooseScan策略。 m 并且 n 是关键部件号。

  • 用于实现的临时表用于 select_type 值为的 MATERIALIZED 行和 table 值为的 <subqueryN>

8.2.2.2使用实现优化子查询

优化程序使用实现来实现更有效的子查询处理。 实现通过生成子查询结果作为临时表(通常在内存中)来加速查询执行。 MySQL第一次需要子查询结果时,它会将结果实现为临时表。 在任何后续需要结果的时候,MySQL再次引用临时表。 优化器可以使用哈希索引对表进行索引,以使查找快速且廉价。 索引包含唯一值以消除重复项并使表更小。

子查询实现在可能的情况下使用内存中的临时表,如果表变得太大,则会回退到磁盘上的存储。 请参见 第8.4.4节“MySQL中的内部临时表使用”

如果未使用实现,则优化程序有时会将非相关子查询重写为相关子查询。 例如,以下 IN 子查询是不相关的( where_condition 仅涉及列 t2 和不 包括列 t1 ):

SELECT * FROM t1
在哪里t1.a IN(SELECT t2.b FROM t2 WHERE where_condition);

优化器可能会将其重写为 EXISTS 相关子查询:

SELECT * FROM t1
WHERE EXISTS(从t2 WHERE where_condition和t1.a = t2.b中选择t2.b);

使用临时表的子查询实现避免了这种重写,并且可以仅执行一次子查询,而不是每次执行外部查询一次。

对于要在MySQL中使用的子查询实现, 必须启用 optimizer_switch 系统变量 materialization 标志。 (参见 第8.9.2节,“切换优化” )。随着 materialization 启用的标志,物化适用于子查询任何地方出现谓词(在选择列表中, WHERE ON GROUP BY HAVING ,或 ORDER BY ),对于属于任何这些用例谓词:

  • 当没有外部表达式 oe_i 或内部表达式 ie_i 可为空 时,谓词具有此形式 N 是1或更大。

    oe_1oe_2...,oe_N)[NOT] IN(SELECT ie_1i_2,...,ie_N...)
    
  • 当存在单个外部表达式 oe 和内部表达式 时,谓词具有此形式 ie 表达式可以为空。

    oe[NOT] IN(选择ie...)
    
  • 谓词是 IN 或者 NOT IN 的结果 具有与结果相同的含义 UNKNOWN NULL FALSE

以下示例说明了等效性 UNKNOWN FALSE 谓词式评估 的要求如何 影响是否可以使用子查询实现。 假设 where_condition 只涉及列 t2 而不是 t1 子查询是不相关的。

此查询取决于具体化:

SELECT * FROM t1
在哪里t1.a IN(SELECT t2.b FROM t2 WHERE where_condition);

在这里,没有关系是否 IN 断言返回 UNKNOWN FALSE 无论哪种方式, t1 查询结果中都不包含

不使用子查询实现的示例是以下查询,其中 t2.b 是可为空的列:

SELECT * FROM t1
WHERE(t1.a,t1.b)NOT IN(SELECT t2.a,t2.b FROM t2
                          在哪里where_condition);

以下限制适用于子查询实现的使用:

  • 内表达式和外表达式的类型必须匹配。 例如,如果两个表达式都是整数或两者都是十进制,则优化器可能能够使用实现,但如果一个表达式是整数而另一个表达式是十进制则不能。

  • 内在表达不能是一个 BLOB

使用 EXPLAIN with查询可以指示优化程序是否使用子查询实现:

  • 与不使用实现的查询执行相比, select_type 可能会更改 DEPENDENT SUBQUERY SUBQUERY 这表明,对于每个外行将执行一次的子查询,实现使子查询只执行一次。

  • 对于扩展 EXPLAIN 输出,以下显示的文本 SHOW WARNINGS 包括 materialize materialized-subquery

8.2.2.3使用EXISTS策略优化子查询

某些优化适用于使用 IN (或 =ANY )运算符测试子查询结果的比较。 本节讨论这些优化,特别是关于 NULL 价值所带来 的挑战 讨论的最后部分建议您如何帮助优化器。

考虑以下子查询比较:

outer_exprIN(inner_expr从...中选择subquery_where

MySQL的评估查询 从外到内。 也就是说,它首先获取外部表达式的值 outer_expr ,然后运行子查询并捕获它生成的行。

一个非常有用的优化是 通知 子查询,只有感兴趣的行是内部表达式 inner_expr 相等的行 outer_expr 这是通过将适当的相等性下推到子查询的 WHERE 子句中来实现的,以使其更具限制性。 转换后的比较如下所示:

EXISTS(选择1来自...在哪里subquery_whereouter_expr= inner_expr

转换后,MySQL可以使用下推的相等性来限制它必须检查以评估子查询的行数。

更一般地, N 值与返回 N -value行 的子查询 的比较 受到相同的转换。 如果 oe_i ie_i 表示相应的外部和内部表达式值,则此子查询比较:

oe_1,...,oe_N)IN
  (SELECT ie_1,...,ie_NFROM ... WHERE subquery_where

变为:

EXISTS(选择1来自...在哪里subquery_whereoe_1=ie_1
                          和......
                          AND oe_N= ie_N

为简单起见,以下讨论假设一对外部和内部表达值。

刚刚描述的转换有其局限性。 仅当我们忽略可能的 NULL 值时 它才有效 也就是说, 只要这两个条件都成立 下推 策略就会起作用:

  • outer_expr 而且 inner_expr 不可能 NULL

  • 你不必区分 NULL FALSE 子查询结果。 如果子查询是 子句中的 表达式 OR AND 表达式 的一部分 WHERE ,则MySQL假定您不关心。 优化器注意到 NULL 并且 FALSE 子查询结果无需区分的 另一个实例 是此构造:

    ......在哪里outer_exprsubquery

    在这种情况下,该 WHERE 条款拒绝该行是否 回报 IN (subquery) NULL FALSE

当这些条件中的任何一个或两个都不成立时,优化就更复杂了。

假设 outer_expr 已知它是非 NULL 值,但子查询不会产生一个行 outer_expr = inner_expr 然后 outer_expr IN (SELECT ...) 评估如下:

  • NULL 中,如果 SELECT 产生任何行,其中 inner_expr NULL

  • FALSE ,如果 SELECT 只产生非 NULL 值或什么都不产生

在这种情况下,查找行的方法 不再有效。 有必要找这样的行,但如果没有找到,还找行,其中 粗略地说,子查询可以转换为这样的东西: outer_expr = inner_expr inner_expr NULL

EXISTS(选择1来自...在哪里subquery_whereouter_expr= inner_exprOR为inner_expr空))

需要评估额外 IS NULL 条件是MySQL具有 ref_or_null 访问方法的原因:

MySQL的> EXPLAIN
       SELECT outer_expr IN (SELECT t2.maybe_null_key
                             FROM t2, t3 WHERE ...)
       FROM t1;
*************************** 1。排******************** *******
           id:1
  select_type:PRIMARY
        表:t1
...
*************************** 2.排******************** *******
           id:2
  select_type:DEPENDENT SUBQUERY
        表:t2
         type:ref_or_null
possible_keys:maybe_null_key
          key:maybe_null_key
      key_len:5
          ref:func
         行:2
        额外:使用在哪里; 使用索引
...

unique_subquery index_subquery 子查询,具体的访问方法也有 NULL 变体。

附加 OR ... IS NULL 条件使得查询执行稍微复杂一些(并且子查询中的一些优化变得不适用),但通常这是可以容忍的。

的情况更糟糕的时候 outer_expr 可能 NULL 根据SQL解释 NULL 未知值 ”, 应评估为: NULL IN (SELECT inner_expr ...)

  • NULL ,如果 SELECT 产生任何行

  • FALSE ,如果 SELECT 没有产生任何行

为了进行适当的评估,必须能够检查是否 SELECT 产生了任何行,因此 不能将其下推到子查询中。 这是一个问题,因为除非可以推迟相等,否则许多真实世界的子查询变得非常慢。 outer_expr = inner_expr

从本质上讲,必须有不同的方法来执行子查询,具体取决于 outer_expr

优化选择超速SQL合规性,所以它占的可能性 outer_expr 可能是 NULL

  • 如果 outer_expr NULL ,要评估以下表达式,有必要执行 SELECT 以确定它是否产生任何行:

    NULL IN(SELECT inner_exprFROM ... WHERE subquery_where

    有必要在 SELECT 这里 执行原件 ,没有任何前面提到的那种下推等式。

  • 另一方面,当 outer_expr 不是 NULL ,这个比较是绝对必要的:

    outer_exprIN(inner_expr从...中选择subquery_where

    转换为使用下推条件的表达式:

    EXISTS(选择1来自...在哪里subquery_whereouter_expr= inner_expr

    如果没有这种转换,子查询将会很慢。

为了解决是否将条件压入子查询的困境,条件包含在 触发器 函数中。 因此,表达式如下:

outer_exprIN(inner_expr从...中选择subquery_where

被转换成:

EXISTS(SELECT 1 FROM ... WHERE subquery_where
                          和trigcond(outer_expr= inner_expr))

更一般地说,如果子查询比较基于几对外部和内部表达式,则转换采用此比较:

oe_1,...,oe_N)IN(SELECT ie_1,...,ie_NFROM ... WHERE subquery_where

并将其转换为此表达式:

EXISTS(SELECT 1 FROM ... WHERE subquery_where
                          和trigcond(oe_1= ie_1
                          和......
                          AND trigcond(oe_N= ie_N

每个 都是一个特殊函数,其值为: trigcond(X)

  • X 链接 ”的 外表达 oe_i 不是 NULL

  • TRUE 链接 ”的 外表达 oe_i NULL

注意

触发器函数 不是 您创建的类型的触发器 CREATE TRIGGER

包含在 trigcond() 函数 内的等式 不是查询优化器的第一类谓词。 大多数优化都无法处理可能在查询执行时打开和关闭的谓词,因此他们认为任何 未知函数都会被忽略。 这些优化可以使用触发的等式: trigcond(X)

  • 参考优化: 可用于构建 表访问。 trigcond(X=Y [OR Y IS NULL]) ref eq_ref ref_or_null

  • 基于索引查找的子查询执行引擎: 可用于构造 访问。 trigcond(X=Y) unique_subquery index_subquery

  • 表条件生成器:如果子查询是多个表的连接,则会尽快检查触发条件。

当优化器使用触发条件创建某种基于索引查找的访问时(如前面列表的前两项),它必须具有关闭条件时的情况的回退策略。 此回退策略始终相同:执行全表扫描。 EXPLAIN 输出中,回退显示 Full scan on NULL key Extra 列中:

MySQL的> EXPLAIN SELECT t1.col1,
       t1.col1 IN (SELECT t2.key1 FROM t2 WHERE t2.col2=t1.col2) FROM t1\G
*************************** 1。排******************** *******
           id:1
  select_type:PRIMARY
        表:t1
        ...
*************************** 2.排******************** *******
           id:2
  select_type:DEPENDENT SUBQUERY
        表:t2
         type:index_subquery
possible_keys:key1
          key:key1
      key_len:5
          ref:func
         行:2
        额外:使用在哪里; 全键扫描NULL键

如果您运行 EXPLAIN 后跟 SHOW WARNINGS ,则可以看到触发条件:

*************************** 1。排******************** *******
  等级:注意
   代码:1003
消息:选择`test` .t1` .col1` AS`col1`,
         <in_optimizer>(`test`.`t1`.`col1`,
         t2中的<exists>(<index_lookup>(<cache>(`test` .t1` .col1`)
         在key1上检查NULL
         其中(`test`.`t2`.`col2` =`test`.`t1`.`col2`),其具有
         trigcond(<is_not_null_test>(`test` .t2` .key1`)))))AS
         `t1.col1 IN(从t2选择t2.key1,其中t2.col2 = t1.col2)`
         来自`test``t1`

触发条件的使用具有一些性能影响。 NULL IN (SELECT ...) 现在表达可能会导致全表扫描(这是慢)时,它以前没有。 这是为正确结果支付的价格(触发条件策略的目标是提高合规性,而不是速度)。

对于多表子查询,执行 NULL IN (SELECT ...) 速度特别慢,因为连接优化器不会针对外部表达式进行优化 NULL 它假设 NULL 左侧的 子查询评估 非常罕见,即使有统计数据表明不是这样。 另一方面,如果外部表达式可能 NULL 但实际上从未实际存在,则不存在性能损失。

为了帮助查询优化器更好地执行查询,请使用以下建议:

  • 声明一个列就像 NOT NULL 它确实一样。 通过简化列的条件测试,这也有助于优化器的其他方面。

  • 如果您不需要区分 NULL 来自 FALSE 子查询的结果,你可以很容易地避免慢的执行路径。 替换看起来像这样的比较:

    outer_exprIN(inner_expr从...中选择

    用这个表达式:

    outer_exprIS NOT NULL)AND(outer_exprIN(SELECT inner_exprFROM ...))
    

    然后 NULL IN (SELECT ...) 永远不会被评估,因为 AND 一旦表达式结果清楚, MySQL就会停止评估 部分。

    另一种可能的重写:

    EXISTS(inner_expr从...中选择)
            WHERE inner_expr= outer_expr

    当你需要分不清这将适用于 NULL FALSE 子查询结果,在这种情况下,你可能真的想 EXISTS

subquery_materialization_cost_based 所述的标志 optimizer_switch 系统变量使得能够在子查询物化和之间的选择控制 IN -到- EXISTS 子查询变换。 请参见 第8.9.2节“可切换的优化”

8.2.2.4使用合并或实现优化派生表,视图引用和公用表表达式

优化器可以使用两种策略(也适用于视图引用和公用表表达式)处理派生表引用:

  • 将派生表合并到外部查询块中

  • 将派生表实现为内部临时表

例1:

SELECT * FROM(SELECT * FROM t1)AS derived_t1;

通过合并派生表 derived_t1 ,该查询的执行类似于:

SELECT * FROM t1;

例2:

选择 *
  FROM t1 JOIN(SELECT t2.f1 FROM t2)AS derived_t2 ON t1.f2 = derived_t2.f1
  在哪里t1.f1> 0;

通过合并派生表 derived_t2 ,该查询的执行类似于:

SELECT t1。*,t2.f1
  FROM t1 JOIN t2 ON t1.f2 = t2.f1
  在哪里t1.f1> 0;

具有实现, derived_t1 derived_t2 在每个查询中被视为单独的表。

优化器以相同的方式处理派生表,视图引用和公用表表达式:它尽可能避免不必要的实现,这样可以将条件从外部查询推送到派生表,并生成更高效的执行计划。 (有关示例,请参见 第8.2.2.2节“使用实现优化子查询” 。)

如果合并将导致外部查询块引用超过61个基表,则优化程序将选择实现。

ORDER BY 如果这些条件都为真 ,则优化器将 派生表或视图引用中 子句 传播 到外部查询块:

  • 外部查询未分组或聚合。

  • 外部查询不指定 DISTINCT HAVING ORDER BY

  • 外部查询将此派生表或视图引用作为 FROM 子句中 的唯一源

否则,优化器会忽略该 ORDER BY 子句。

以下方法可用于影响优化程序是否尝试将派生表,视图引用和公用表表达式合并到外部查询块中:

  • MERGE NO_MERGE 优化器提示可以使用。 假设没有其他规则阻止合并,它们适用。 请参见 第8.9.3节“优化程序提示”

  • 同样,您可以使用 系统变量 derived_merge 标志 optimizer_switch 请参见 第8.9.2节“可切换的优化” 默认情况下,启用该标志以允许合并。 禁用该标志可防止合并并避免 ER_UPDATE_TABLE_USED 错误。

    derived_merge 标志也适用于不包含 ALGORITHM 子句的 视图 因此,如果 ER_UPDATE_TABLE_USED 使用等效于子查询的表达式的视图引用发生错误,则添加 ALGORITHM=TEMPTABLE 到视图定义会阻止合并并优先于该 derived_merge 值。

  • 可以通过在子查询中使用任何阻止合并的构造来禁用合并,尽管这些构造对实现的影响并不明确。 对于派生表,公用表表达式和视图引用,阻止合并的构造是相同的:

    • 聚集函数或窗函数( SUM() MIN() MAX() COUNT() ,等等)

    • DISTINCT

    • GROUP BY

    • HAVING

    • LIMIT

    • UNION 要么 UNION ALL

    • 选择列表中的子查询

    • 分配给用户变量

    • 仅引用文字值(在这种情况下,没有基础表)

如果优化器选择实现策略而不是合并派生表,它将按如下方式处理查询:

  • 优化器推迟派生表实现,直到在查询执行期间需要其内容。 这提高了性能,因为延迟实现可能导致根本不必执行此操作。 考虑将派生表的结果连接到另一个表的查询:如果优化器首先处理该另一个表并发现它不返回任何行,则不需要进一步执行连接,并且优化器可以完全跳过实现派生表的实现。

  • 在查询执行期间,优化器可以向派生表添加索引以加速从中检索行。

EXPLAIN 对于 SELECT 包含派生表 查询, 请考虑以下 语句

EXPLAIN SELECT * FROM(SELECT * FROM t1)AS derived_t1;

优化程序通过延迟派生表来避免实现派生表,直到 SELECT 执行 期间需要结果为止 在这种情况下,不执行查询(因为它出现在 EXPLAIN 语句中),因此永远不需要结果。

即使对于执行的查询,派生表实现的延迟也可能使优化器完全避免实现。 发生这种情况时,执行实现所需的时间会更快地执行查询。 请考虑以下查询,该查询将派生表的结果连接到另一个表:

选择 *
  FROM t1 JOIN(SELECT t2.f1 FROM t2)AS derived_t2
          ON t1.f2 = derived_t2.f1
  在哪里t1.f1> 0;

如果优化进程 t1 首先并且 WHERE 子句产生空结果,则连接必须为空并且不需要实现派生表。

对于派生表需要实现的情况,优化器可以向实现表添加索引以加速对其的访问。 如果这样的索引允许 ref 访问表,则可以大大减少查询执行期间读取的数据量。 请考虑以下查询:

选择 *
 FROM t1 JOIN(SELECT DISTINCT f1 FROM t2)AS derived_t2
         ON t1.f1 = derived_t2.f1;

优化器在列上构造索引 f1 derived_t2 如果这样做将允许使用 ref 最低成本执行计划 访问。 添加索引后,优化程序可以将具体化派生表视为具有索引的常规表,并且与生成的索引类似。 与没有索引的查询执行成本相比,索引创建的开销可以忽略不计。 如果 ref 访问会导致比其他访问方法更高的成本,则优化程序不会创建任何索引并且不会丢失任何内容。

对于优化程序跟踪输出,合并的派生表或视图引用不会显示为节点。 只有其基础表出现在顶部查询的计划中。

对于公用表表达式(CTE),派生表的具体化也是如此。 此外,以下考虑因素特别适用于CTE。

如果CTE由查询实现,则查询将实现一次,即使查询多次引用它。

递归CTE始终具体化。

如果CTE具体化,优化器会自动添加相关索引,如果它估计索引将加速顶级语句对CTE的访问。 这类似于派生表的自动索引,除了如果多次引用CTE,优化器可以创建多个索引,以最合适的方式加速每个引用的访问。

MERGE NO_MERGE 优化器提示可以应用到的CTE。 顶级语句中的每个CTE引用都可以有自己的提示,允许选择性地合并或实现CTE引用。 以下语句使用提示来指示 cte1 应合并并 cte2 应实现:

WITH
  cte1 AS(SELECT a,b FROM table1),
  cte2 AS(SELECT c,d FROM table2)
SELECT / * + MERGE(cte1)NO_MERGE(cte2)* / cte1.b,cte2.d
从cte1加入cte2
WHERE cte1.a = cte2.c;

ALGORITHM 对条款 CREATE VIEW 没有任何影响物化 WITH 前款 SELECT 视图定义语句。 请考虑以下声明:

CREATE ALGORITHM = {TEMPTABLE | MERGE}查看v1 AS ... ...选择...

ALGORITHM 值仅影响而 SELECT 不是 WITH 子句的实现。

在MySQL 8.0.16之前,如果 internal_tmp_disk_storage_engine=MYISAM 使用磁盘上临时表实现CTE的任何尝试都发生错误,因为对于CTE,用于磁盘内部临时表的存储引擎不可能 MyISAM 从MySQL 8.0.16开始,这不再是一个问题,因为 TempTable 现在总是 InnoDB 用于磁盘内部临时表。

如前所述,CTE如果具体化,即使多次引用,也会实现一次。 要指示一次性实现,优化程序跟踪输出包含 creating_tmp_table 一次或多次出现的事件 reusing_tmp_table

CTE与派生表类似, materialized_from_subquery 节点遵循引用。 这对于多次引用的CTE来说是正确的,因此不存在重复的 materialized_from_subquery 节点(这会给人一种子查询多次执行的印象,并产生不必要的详细输出)。 只有一个对CTE的引用有一个完整的 materialized_from_subquery 节点,其中包含子查询计划的描述。 其他引用具有减少的 materialized_from_subquery 节点。 同样的想法适用 EXPLAIN TRADITIONAL 格式 输出 :未显示其他引用的子查询。

8.2.3优化INFORMATION_SCHEMA查询

监视数据库的应用程序可能会频繁使用 INFORMATION_SCHEMA 表。 要最有效地为这些表编写查询,请使用以下一般准则:

  • 尝试仅查询 INFORMATION_SCHEMA 作为数据字典表的视图的表。

  • 尝试仅查询静态元数据。 选择列或使用动态元数据的检索条件以及静态元数据会增加处理动态元数据的开销。

注意

INFORMATION_SCHEMA 查询中 数据库和表名的比较行为 可能与您的预期不同。 有关详细信息,请参见 第10.8.7节“在INFORMATION_SCHEMA搜索中使用排序规则”

这些 INFORMATION_SCHEMA 表实现为数据字典表的视图,因此对它们的查询从数据字典中检索信息:

CHARACTER_SETS
CHECK_CONSTRAINTS
COLLATIONS
COLLATION_CHARACTER_SET_APPLICABILITY
活动
FILES
INNODB_COLUMNS
INNODB_DATAFILES
INNODB_FIELDS
INNODB_FOREIGN
INNODB_FOREIGN_COLS
INNODB_INDEXES
INNODB_TABLES
INNODB_TABLESPACES
INNODB_TABLESPACES_BRIEF
INNODB_TABLESTATS
KEY_COLUMN_USAGE
参数
PARTITIONS
REFERENTIAL_CONSTRAINTS
RESOURCE_GROUPS
例程
SCHEMATA
统计
TABLES
TABLE_CONSTRAINTS
TRIGGERS
VIEWS
VIEW_ROUTINE_USAGE
VIEW_TABLE_USAGE

某些类型的值(即使对于非视图 INFORMATION_SCHEMA 表)也可以通过数据字典中的查找来检索。 这包括诸如数据库和表名,表类型和存储引擎之类的值。

某些 INFORMATION_SCHEMA 表包含提供表统计信息的列:

STATISTICS.CARDINALITY
TABLES.AUTO_INCREMENT
TABLES.AVG_ROW_LENGTH
TABLES.CHECKSUM
TABLES.CHECK_TIME
TABLES.CREATE_TIME
TABLES.DATA_FREE
TABLES.DATA_LENGTH
TABLES.INDEX_LENGTH
TABLES.MAX_DATA_LENGTH
TABLES.TABLE_ROWS
TABLES.UPDATE_TIME

这些列表示动态表元数据; 也就是说,随着表格内容的变化而变化的信息。

默认情况下,MySQL的检索从这些列的缓存值 mysql.index_stats mysql.table_stats 这比直接从存储引擎检索统计数据更有效,当列查询字典表。 如果缓存的统计信息不可用或已过期,MySQL将从存储引擎中检索最新的统计信息,并将它们缓存在 mysql.index_stats mysql.table_stats 字典表中。 后续查询将检索缓存的统计信息,直到缓存的统计信息过期。

information_schema_stats_expiry 会话变量定义的时间段之前缓存统计到期。 默认值为86400秒(24小时),但时间段可以延长至一年。

要随时更新给定表的缓存值,请使用 ANALYZE TABLE

在这些情况下, 查询统计信息列不会在 mysql.index_stats mysql.table_stats 字典表中 存储或更新统计信息

information_schema_stats_expiry 是一个会话变量,每个客户端会话可以定义自己的到期值。 从存储引擎检索并由一个会话缓存的统计信息可供其他会话使用。

注意

如果 innodb_read_only 启用 系统变量,则 ANALYZE TABLE 可能会失败,因为它无法更新数据字典中使用的统计表 InnoDB 对于 ANALYZE TABLE 更新密钥分发的操作,即使操作更新表本身(例如,如果它是 MyISAM 表) ,也可能发生故障 要获取更新的分布统计信息,请设置 information_schema_stats_expiry=0

对于 INFORMATION_SCHEMA 作为数据字典表视图实现的表,基础数据字典表上的索引允许优化器构建有效的查询执行计划。 要查看优化程序所做的选择,请使用 EXPLAIN 要同时查看服务器用于执行 INFORMATION_SCHEMA 查询的查询,请 SHOW WARNINGS 立即 使用 以下内容 EXPLAIN

请考虑此语句,该语句标识 utf8mb4 字符集的 排序规则

MySQL的> SELECT COLLATION_NAME
       FROM INFORMATION_SCHEMA.COLLATION_CHARACTER_SET_APPLICABILITY
       WHERE CHARACTER_SET_NAME = 'utf8mb4';
+ ---------------------------- +
| COLLATION_NAME |
+ ---------------------------- +
| utf8mb4_general_ci |
| utf8mb4_bin |
| utf8mb4_unicode_ci |
| utf8mb4_icelandic_ci |
| utf8mb4_latvian_ci |
| utf8mb4_romanian_ci |
| utf8mb4_slovenian_ci |
...

服务器如何处理该语句? 要了解,请使用 EXPLAIN

MySQL的> EXPLAIN SELECT COLLATION_NAME
       FROM INFORMATION_SCHEMA.COLLATION_CHARACTER_SET_APPLICABILITY
       WHERE CHARACTER_SET_NAME = 'utf8mb4'\G
*************************** 1。排******************** *******
           id:1
  select_type:SIMPLE
        表:cs
   分区:NULL
         类型:const
possible_keys:PRIMARY,名字
          关键:名称
      key_len:194
          ref:const
         行:1
     过滤:100.00
        额外:使用索引
*************************** 2.排******************** *******
           id:1
  select_type:SIMPLE
        表:col
   分区:NULL
         类型:ref
possible_keys:character_set_id
          key:character_set_id
      key_len:8
          ref:const
         行:68
     过滤:100.00
        额外:NULL
2行,1个警告(0.01秒)

要查看用于满足该语句的查询,请使用 SHOW WARNINGS

MySQL的> SHOW WARNINGS\G
*************************** 1。排******************** *******
  等级:注意
   代码:1003
消息:/ *选择#1 * /选择`mysql` .col` .name` AS`COLLATION_NAME`
         来自`mysql``character_sets``cs`
         加入`mysql``collat​​ions``col`
         where((``mysql` .col``character_set_id` ='45')
         和('utf8mb4'='utf8mb4'))

如所指示 SHOW WARNINGS ,该服务器处理该查询上 COLLATION_CHARACTER_SET_APPLICABILITY 为对查询 character_sets collations 在数据字典表 mysql 系统数据库。

8.2.4优化性能模式查询

监视数据库的应用程序可能会频繁使用性能架构表。 要最有效地为这些表编写查询,请利用它们的索引。 例如,包含一个 WHERE 子句, 子句根据与索引列中特定值的比较来限制检索到的行。

大多数性能架构表都有索引。 不包含通常包含很少行或不太可能经常查询的表的表。 性能模式索引使优化器可以访问除全表扫描之外的执行计划。 这些索引还可以提高相关对象的性能,例如 sys 使用这些表的架构视图。

要查看给定的Performance Schema表是否具有索引及其含义,请使用 SHOW INDEX SHOW CREATE TABLE

MySQL的> SHOW INDEX FROM performance_schema.accounts\G
*************************** 1。排******************** *******
        表:帐户
   非独特:0
     Key_name:ACCOUNT
 Seq_in_index:1
  Column_name:USER
    排序规则:NULL
  基数:NULL
     Sub_part:NULL
       打包:NULL
         空:是的
   Index_type:HASH
      评论:
Index_comment:
      可见:是的
*************************** 2.排******************** *******
        表:帐户
   非独特:0
     Key_name:ACCOUNT
 Seq_in_index:2
  Column_name:HOST
    排序规则:NULL
  基数:NULL
     Sub_part:NULL
       打包:NULL
         空:是的
   Index_type:HASH
      评论:
Index_comment:
      可见:是的

MySQL的> SHOW CREATE TABLE performance_schema.rwlock_instances\G
*************************** 1。排******************** *******
       表:rwlock_instances
创建表:CREATE TABLE`rwlock_instances`(
  `NAME` varchar(128)NOT NULL,
  `OBJECT_INSTANCE_BEGIN` bigint(20)unsigned NOT NULL,
  `WRITE_LOCKED_BY_THREAD_ID` bigint(20)unsigned DEFAULT NULL,
  `READ_LOCKED_BY_COUNT` int(10)unsigned NOT NULL,
  PRIMARY KEY(`OBJECT_INSTANCE_BEGIN`),
  KEY`NAME`(`NAME`),
  KEY`WRITE_LOCKED_BY_THREAD_ID`(`WRITE_LOCKED_BY_THREAD_ID`)
)ENGINE = PERFORMANCE_SCHEMA DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci

要查看Performance Schema查询的执行计划以及它是否使用任何索引,请使用 EXPLAIN

MySQL的> EXPLAIN SELECT * FROM performance_schema.accounts
       WHERE (USER,HOST) = ('root','localhost')\G
*************************** 1。排******************** *******
           id:1
  select_type:SIMPLE
        表:帐户
   分区:NULL
         类型:const
possible_keys:ACCOUNT
          key:ACCOUNT
      key_len:278
          ref:const,const
         行:1
     过滤:100.00
        额外:NULL

EXPLAIN 输出表明优化器使用 accounts ACCOUNT 包括索引 USER HOST 列。

性能模式索引是虚拟的:它们是性能模式存储引擎的构造,不使用内存或磁盘存储。 性能模式将索引信息报告给优化器,以便它可以构建有效的执行计划。 性能模式反过来使用有关要查找内容的优化器信息(例如,特定键值),以便它可以执行有效的查找而无需构建实际的索引结构。 此实现提供了两个重要的好处:

  • 它完全避免了经常更新的表通常会产生的维护成本。

  • 它在查询执行的早期阶段减少了检索的数据量。 对于索引列的条件,性能模式仅有效地返回满足查询条件的表行。 如果没有索引,性能模式将返回表中的所有行,要求优化程序稍后针对每一行评估条件以生成最终结果。

性能模式索引是预定义的,不能删除,添加或更改。

性能模式索引与哈希索引类似。 例如:

  • 它们仅用于使用 = <=> 运算符的 相等比较

  • 它们是无序的。 如果查询结果必须具有特定的行排序特征,请包含 ORDER BY 子句。

有关哈希索引的其他信息,请参见 第8.3.9节“B树和哈希索引的比较”

8.2.5优化数据变更声明

这部分解释了如何加快数据更改语句: INSERT UPDATE ,和 DELETE 传统的OLTP应用程序和现代Web应用程序通常会执行许多小数据更改操作,其中并发性至关重要。 数据分析和报告应用程序通常会同时运行影响多行的数据更改操作,其中主要考虑因素是写入大量数据并使索引保持最新的I / O. 对于插入和更新大量数据(在行业中称为 提取 - 转换 - 加载 ),有时您使用模仿其效果的其他SQL语句或外部命令 INSERT UPDATE DELETE 陈述。

8.2.5.1优化INSERT语句

要优化插入速度,请将许多小型操作组合到一个大型操作中。 理想情况下,您进行单个连接,一次发送许多新行的数据,并将所有索引更新和一致性检查延迟到最后。

插入行所需的时间由以下因素决定,其中数字表示大致比例:

  • 连接:(3)

  • 向服务器发送查询:(2)

  • 解析查询:(2)

  • 插入行:(1×行的大小)

  • 插入索引:(1×索引数)

  • 结束:(1)

这没有考虑打开表的初始开销,每个并发运行的查询都会执行一次。

N 假设B树索引, 表的大小会减慢日志插入索引的速度

您可以使用以下方法来加速插入:

8.2.5.2优化UPDATE语句

更新语句优化为类似于 SELECT 具有写入的额外开销的查询。 写入速度取决于要更新的​​数据量和更新的索引数。 未更改的索引不会更新。

获得快速更新的另一种方法是延迟更新,然后在以后连续执行许多更新。 如果锁定表,一次执行多个更新比一次执行多个更新要快得多。

对于 MyISAM 使用动态行格式 表,将行更新为更长的总长度可能会拆分该行。 如果经常这样做, OPTIMIZE TABLE 偶尔 使用非常重要 请参见 第13.7.3.4节“OPTIMIZE TABLE语法”

8.2.5.3优化DELETE语句

删除 MyISAM 表中 各行所需的时间 与索引数完全成比例。 要更快地删除行,可以通过增加 key_buffer_size 系统变量 来增加密钥缓存的大小 请参见 第5.1.1节“配置服务器”

要删除 MyISAM 表中的 所有行 要快于 截断操作不是事务安全的; 在活动事务或活动表锁定过程中尝试一个错误时发生错误。 请参见 第13.1.37节“TRUNCATE TABLE语法” TRUNCATE TABLE tbl_name DELETE FROM tbl_name

8.2.6优化数据库权限

您的权限设置越复杂,所有SQL语句的开销就越大。 简化 GRANT 语句 建立的特权 使MySQL能够在客户端执行语句时减少权限检查开销。 例如,如果您不授予任何表级或列级权限,则服务器无需检查 tables_priv columns_priv 的内容 同样,如果您对任何帐户都没有资源限制,则服务器不必执行资源计数。 如果您的语句处理负载非常高,请考虑使用简化的授权结构来减少权限检查开销。

8.2.7其他优化技巧

本节列出了一些用于提高查询处理速度的其他技巧:

  • 如果您的应用程序发出多个数据库请求以执行相关更新,则将语句组合到存储例程中可以帮助提高性能。 同样,如果您的应用程序基于多个列值或大量数据计算单个结果,则将计算组合到UDF(用户定义的函数)可以帮助提高性能。 然后,由此产生的快速数据库操作可供其他查询,应用程序甚至用不同编程语言编写的代码重用。 有关更多信息 请参见 第24.2节“使用存储的例程” 第29.4节“向MySQL添加新函数”

  • 要解决 ARCHIVE 中发生的任何压缩问题 ,请使用 OPTIMIZE TABLE 请参见 第16.5节“ARCHIVE存储引擎”

  • 如果可能,将报告分类为 实时 统计 ,其中统计报告所需的数据仅从定期从实时数据生成的汇总表中创建。

  • 如果您的数据不符合行列表结构,则可以将数据打包并存储到 BLOB 列中。 在这种情况下,您必须在应用程序中提供打包和解压缩信息的代码,但这可能会节省I / O操作以读取和写入相关值集。

  • 使用Web服务器,将图像和其他二进制资产存储为文件,路径名存储在数据库中而不是文件本身。 大多数Web服务器在缓存文件方面比数据库内容更好,因此使用文件通常更快。 (尽管在这种情况下您必须自己处理备份和存储问题。)

  • 如果您需要非常高的速度,请查看低级MySQL接口。 例如,通过 直接 访问MySQL InnoDB MyISAM 存储引擎,与使用SQL接口相比,可以大幅提高速度。

  • 复制可以为某些操作提供性能优势。 您可以在复制服务器之间分发客户端检索以分割负载。 为避免在进行备份时减慢主服务器的速度,可以使用从服务器进行备份。 请参见 第17章, 复制

8.3优化和索引

提高操作性能的最佳方法 SELECT 是在查询中测试的一个或多个列上创建索引。 索引条目的作用类似于表行的指针,允许查询快速确定哪些行与 WHERE 子句 中的条件匹配 ,并检索这些行的其他列值。 可以索引所有MySQL数据类型。

尽管为查询中使用的每个可能列创建索引很有诱惑力,但不必要的索引会浪费空间并浪费时间让MySQL确定要使用哪些索引。 索引还会增加插入,更新和删除的成本,因为必须更新每个索引。 您必须找到适当的平衡,以使用最佳索引集实现快速查询。

8.3.1 MySQL如何使用索引

索引用于快速查找具有特定列值的行。 如果没有索引,MySQL必须从第一行开始,然后读取整个表以查找相关行。 表越大,成本越高。 如果表中有相关​​列的索引,MySQL可以快速确定要在数据文件中间寻找的位置,而无需查看所有数据。 这比按顺序读取每一行要快得多。

大多数MySQL索引( PRIMARY KEY UNIQUE INDEX FULLTEXT )存储在 B树 例外:空间数据类型的索引使用R树; MEMORY 表也​​支持 哈希索引 ; InnoDB 使用反向列表作为 FULLTEXT 索引。

通常,使用索引,如以下讨论中所述。 第8.3.9节“B树和哈希索引的比较” MEMORY 中描述了 哈希索引特有的特性(如 表中所

MySQL使用索引进行这些操作:

  • WHERE 快速 查找与 子句 匹配的行

  • 消除行的考虑。 如果在多个索引之间有选择,MySQL通常使用找到最小行数(最具 选择性的 索引)的索引。

  • 如果表具有多列索引,则优化程序可以使用索引的任何最左前缀来查找行。 例如,如果你有一个三列索引上 (col1, col2, col3) ,你有索引的搜索功能 (col1) (col1, col2) 以及 (col1, col2, col3) 有关更多信息,请参见 第8.3.6节“多列索引”

  • 在执行连接时从其他表中检索行。 如果声明它们的类型和大小相同,MySQL可以更有效地使用列上的索引。 在这种情况下, VARCHAR CHAR 被认为是相同的,如果它们被声明为相同的大小。 例如, VARCHAR(10) CHAR(10) 大小相同,但 VARCHAR(10) CHAR(15) 不是。

    对于非二进制字符串列之间的比较,两列应使用相同的字符集。 例如,将 utf8 列与 latin1 进行比较会 排除使用索引。

    不相似列的比较(例如,将字符串列与时间或数字列进行比较)可能会在没有转换的情况下无法直接比较值时阻止使用索引。 对于给定的值,如 1 在数值列,它可能比较等于在字符串列,例如任何数量的值 '1' ' 1' '00001' ,或 '01.e1' 这排除了对字符串列的任何索引的使用。

  • 查找 特定索引列的值 MIN() MAX() key_col 这是由预处理器优化的,该预处理器检查您是否正在使用 索引 之前出现的所有关键部分 在这种情况下,MySQL对每个 表达式 执行单个键查找, 并用常量替换它。 如果所有表达式都替换为常量,则查询立即返回。 例如: WHERE key_part_N = constant key_col MIN() MAX()

    SELECT MIN(key_part2),MAX(key_part2
      FROM tbl_nameWHERE key_part1= 10;
    
  • 如果对可用索引的最左前缀(例如, 进行排序或分组,则对表进行排序或分组 如果后面跟着所有关键部分 ,则按相反顺序读取密钥。 (或者,如果索引是降序索引, 则按 正向顺序读取密钥。)请参见 第8.2.1.15节“ORDER BY优化” 第8.2.1.16节“GROUP BY优化” 第8.3.13节“降序指数“ ORDER BY key_part1, key_part2 DESC

  • 在某些情况下,可以优化查询以在不咨询数据行的情况下检索值。 (为查询提供所有必要结果的 索引 称为 覆盖索引 。)如果查询仅使用表中包含某些索引的列,则可以从索引树中检索所选值以获得更快的速度:

    SELECT key_part3FROM tbl_name
      WHERE key_part1= 1
    

对于小型表或报表查询处理大多数或所有行的大型表的查询,索引不太重要。 当查询需要访问大多数行时,顺序读取比通过索引更快。 顺序读取可以最大限度地减少磁盘搜索,即使查询不需要所有行也是如此。 有关 详细信息 请参见 第8.2.1.22节“避免全表扫描”

8.3.2主键优化

表的主键表示您在最重要的查询中使用的列或列集。 它具有关联的索引,以实现快速查询性能。 查询性能受益于 NOT NULL 优化,因为它不能包含任何 NULL 值。 使用 InnoDB 存储引擎,表数据在物理上进行组织,以根据主键或列进行超快速查找和排序。

如果您的表很大且很重要,但没有明显的列或列集用作主键,则可以创建一个单独的列,其中包含自动增量值以用作主键。 使用外键连接表时,这些唯一ID可用作指向其他表中相应行的指针。

8.3.3空间索引优化

MySQL允许 SPATIAL NOT NULL 几何值列 创建 索引 (请参见 第11.5.10节“创建空间索引” )。 优化程序检查 SRID 索引列 属性,以确定要用于比较的空间参考系统(SRS),并使用适合SRS的计算。 (在MySQL 8.0之前,优化器 SPATIAL 使用笛卡尔计算 执行 索引值的 比较 ;如果列包含具有非笛卡尔SRID的值,则此类操作的结果是未定义的。)

要使比较正常工作, SPATIAL 索引中的 每个列都 必须受SRID限制。 也就是说,列定义必须包含显式 SRID 属性,并且所有列值必须具有相同的SRID。

优化程序 SPATIAL 考虑 SRID限制列的索引:

  • 限制为笛卡尔SRID的列的索引启用笛卡尔边界框计算。

  • 限制为地理SRID的列的索引启用地理边界框计算。

优化程序忽略 SPATIAL 没有 SRID 属性的 列上的索引 (因此不受SRID限制)。 MySQL仍然维护着这样的索引,如下所示:

  • 他们对表的修改(更新 INSERT UPDATE DELETE ,等等)。 更新发生就像索引是笛卡尔坐标一样,即使该列可能包含笛卡尔和地理值的混合。

  • 它们仅用于向后兼容(例如,在MySQL 5.7中执行转储和在MySQL 8.0中恢复的能力)。 由于 SPATIAL 非SRID限制的列上的索引对优化器没用,因此应修改每个此类列:

    • 验证列中的所有值是否具有相同的SRID。 要确定几何列中包含的SRID col_name ,请使用以下查询:

      SELECT DISTINCT ST_SRID(col_name)FROM tbl_name;
      

      如果查询返回多行,则该列包含多个SRID。 在这种情况下,请修改其内容,以使所有值具有相同的SRID。

    • 重新定义列以具有显式 SRID 属性。

    • 重新创建 SPATIAL 索引。

8.3.4外键优化

如果一个表有很多列,并且您查询了许多不同的列组合,那么将频率较低的数据拆分为每个都有几列的单独表可能会很有效,并通过复制数字ID将它们与主表关联起来。主表中的列。 这样,每个小表都可以有一个主键来快速查找其数据,您可以使用连接操作查询所需的列集。 根据数据的分布方式,查询可能会执行较少的I / O并占用较少的高速缓存,因为相关列在磁盘上打包在一起。 (为了最大限度地提高性能,查询尝试从磁盘中读取尽可能少的数据块;

8.3.5列索引

最常见的索引类型涉及单个列,在数据结构中存储该列的值的副本,允许快速查找具有相应列值的行。 B树数据结构可以让索引快速查找特定值,一组值,或值的范围,对应于运营商,如 = > BETWEEN IN ,等等,一在 WHERE 子句。

每个存储引擎定义每个表的最大索引数和最大索引长度。 请参见 第15章, InnoDB存储引擎 第16章, 备用存储引擎 所有存储引擎每个表至少支持16个索引,总索引长度至少为256个字节。 大多数存储引擎都有更高的限制

有关列索引的其他信息,请参见 第13.1.15节“CREATE INDEX语法”

索引前缀

使用 字符串列的索引规范中的语法,可以创建仅使用 的前 几个字符 的索引 以这种方式仅索引列值的前缀可以使索引文件更小。 索引 列时, 必须 为索引指定前缀长度。 例如: col_name(N) N BLOB TEXT

CREATE TABLE test(blob_col BLOB,INDEX(blob_col(10)));

对于 InnoDB 使用 REDUNDANT COMPACT 行格式的 表, 前缀最长可达767个字节 对于 InnoDB 使用 DYNAMIC COMPRESSED 行格式的 表, 前缀长度限制为3072字节 对于MyISAM表,前缀长度限制为1000个字节。

注意

前缀限制以字节为单位,而在前缀长度 CREATE TABLE ALTER TABLE CREATE INDEX 语句被解释为非二进制串类型的字符数( CHAR VARCHAR TEXT 二进制串类型()和字节数 BINARY VARBINARY BLOB )。 在为使用多字节字符集的非二进制字符串列指定前缀长度时,请考虑这一点。

如果搜索项超过索引前缀长度,则索引用于排除不匹配的行,并检查剩余的行以查找可能的匹配项。

有关索引前缀的其他信息,请参见 第13.1.15节“CREATE INDEX语法”

FULLTEXT索引

FULLTEXT 索引用于全文搜索。 只有 InnoDB MyISAM 存储引擎支持 FULLTEXT 索引和仅适用于 CHAR VARCHAR TEXT 列。 索引始终发生在整个列上,并且不支持列前缀索引。 有关详细信息,请参见 第12.9节“全文搜索功能”

优化适用于 FULLTEXT 针对单个 InnoDB 表的 某些类型的 查询 具有这些特征的查询特别有效:

  • FULLTEXT 仅返回文档ID或文档ID和搜索排名的查询。

  • FULLTEXT 查询按分数的降序对匹配的行进行排序,并应用 LIMIT 子句来获取前N个匹配的行。 要应用此优化,必须没有 WHERE 子句,只有一个 ORDER BY 子句按降序排列。

  • FULLTEXT 只检索 COUNT(*) 与搜索词匹配的行 值的查询,没有其他 WHERE 子句。 编码 WHERE 子句 ,没有任何 比较运算符。 WHERE MATCH(text) AGAINST ('other_text')> 0

对于包含全文表达式的查询,MySQL在查询执行的优化阶段评估这些表达式。 优化器不只是查看全文表达式并进行估计,它实际上是在开发执行计划的过程中对它们进行评估。

这种行为的含义就是这样 EXPLAIN 对于全文查询通常比在全局阶段没有进行表达式评估的非全文查询慢。

EXPLAIN 由于优化期间发生匹配, 可能会 Select tables optimized away Extra 列中 显示全文查询 ; 在这种情况下,在以后的执行期间不需要访问表。

空间索引

您可以在空间数据类型上创建索引。 MyISAM InnoDB 支持空间类型的R树索引。 其他存储引擎使用B树来索引空间类型(除了 ARCHIVE ,它不支持空间类型索引)。

MEMORY存储引擎中的索引

MEMORY 存储引擎使用 HASH 默认的索引,而且还支持 BTREE 索引。

8.3.6多列索引

MySQL可以创建复合索引(即多列索引)。 索引最多可包含16列。 对于某些数据类型,您可以索引列的前缀(请参见 第8.3.5节“列索引” )。

MySQL可以对测试索引中所有列的查询使用多列索引,或者只测试第一列,前两列,前三列等的查询。 如果在索引定义中以正确的顺序指定列,则单个复合索引可以加速同一表上的多种查询。

多列索引可以视为排序数组,其行包含通过连接索引列的值创建的值。

注意

作为复合索引的替代方法,您可以 根据其他列的信息 引入 散列 列。 如果此列很短,相当独特且已编制索引,则它可能比 许多列上 索引 更快 在MySQL中,使用这个额外的列很容易:

SELECT * FROM tbl_name
  WHERE hash_col= MD5(CONCAT(val1val2))
  AND col1= val1AND col2= val2;

假设一个表具有以下规范:

CREATE TABLE测试(
    id INT NOT NULL,
    last_name CHAR(30)NOT NULL,
    first_name CHAR(30)NOT NULL,
    PRIMARY KEY(id),
    INDEX名称(last_name,first_name)
);

name 指数是在一个索引 last_name first_name 列。 该索引可用于查询中的查找,这些查询指定已知范围内的 last_name first_name 组合的 值。 它也可以用于仅指定 last_name 值的 查询, 因为该列是索引的最左前缀(如本节后面所述)。 因此, name 索引用于以下查询中的查找:

SELECT * FROM test WHERE last_name ='Widenius';

SELECT * FROM test
  WHERE last_name ='Widenius'AND first_name ='Michael';

SELECT * FROM test
  WHERE last_name ='Widenius'
  AND(first_name ='Michael'OR / first_name ='Monty');

SELECT * FROM test
  WHERE last_name ='Widenius'
  AND first_name> ='M'AND first_name <'N';

但是, name 指数 不是 用于以下查询中的查找:

SELECT * FROM test WHERE first_name ='Michael';

SELECT * FROM test
  WHERE last_name ='Widenius'或first_name ='Michael';

假设您发出以下内容 SELECT 语句:

SELECT * FROM tbl_name
  WHERE col1 = val1AND col2 =val2 ;

如果 col1 和上 存在多列索引 col2 ,则可以直接获取相应的行。 如果 col1 和上 存在单独的单列索引 col2 ,则优化程序会尝试使用索引合并优化(请参见 第8.2.1.3节“索引合并优化” ),或尝试通过确定哪个索引排除更多行并使用它来查找限制性最强的索引获取行的索引。

如果表具有多列索引,则优化程序可以使用索引的任何最左前缀来查找行。 例如,如果你有一个三列索引上 (col1, col2, col3) ,你有索引的搜索功能 (col1) (col1, col2) 以及 (col1, col2, col3)

如果列不形成索引的最左前缀,则MySQL无法使用索引执行查找。 假设您有以下 SELECT 所示 语句:

SELECT * FROM tbl_nameWHERE col1 = val1;
SELECT * FROM tbl_nameWHERE col1 = val1AND col2 = val2;

SELECT * FROM tbl_nameWHERE col2 = val2;
SELECT * FROM tbl_nameWHERE col2 = val2AND col3 = val3;

如果存在索引 (col1, col2, col3) ,则只有前两个查询使用索引。 第三和第四个查询确实包括索引的列,但不使用索引来进行查找,因为 (col2) (col2, col3) 不是的最左边的前缀 (col1, col2, col3)

8.3.7验证索引使用情况

始终检查所有查询是否确实使用了您在表中创建的索引。 使用该 EXPLAIN 语句,如 第8.8.1节“使用EXPLAIN优化查询”中所述

8.3.8 InnoDB和MyISAM索引统计信息收集

存储引擎收集有关表的统计信息以供优化程序使用。 表统计信息基于值组,其中值组是具有相同键前缀值的一组行。 出于优化程序的目的,一个重要的统计数据是平均值组大小。

MySQL使用以下方式的平均值组大小:

  • 估计每次 ref 访问 必须读取的行数

  • 估计部分连接将产生多少行; 也就是说,此表单的操作将产生的行数:

    (...)JOIN tbl_nameON tbl_namekey=expr
    

随着索引的平均值组大小增加,索引对于这两个目的不太有用,因为每个查找的平均行数增加:为了使索引有利于优化目的,最好每个索引值都以小为目标表中的行数。 当给定的索引值产生大量行时,索引不太有用,MySQL不太可能使用它。

平均值组大小与表基数相关,表基数是值组的数量。 SHOW INDEX 语句显示基于的基数值 N/S ,其中 N 是表中的行数, S 是平均值组大小。 该比率产生表中的近似值组数。

为联接基础上, <=> 比较运营商, NULL 没有从任何其它值区别对待: NULL <=> NULL ,就像 任何其他 N <=> N N

但是,对于基于 = 运算符的连接, NULL 与非 NULL 不同 (或两者) 时不成立 这会影响 对表单比较的访问 :如果当前值为, MySQL不会访问该表 expr1 = expr2 expr1 expr2 NULL ref tbl_name.key = expr expr NULL ,因为比较不能为真。

对于 = 比较, NULL 表格中有 多少 无关紧要 出于优化目的,相关值是非平均大小 NULL 值组 但是,MySQL目前不支持收集或使用该平均大小。

对于 InnoDB MyISAM 表,您可以 分别 通过 innodb_stats_method myisam_stats_method 系统变量 控制表统计信息的收集 这些变量有三个可能的值,它们的不同之处如下:

  • 当变量设置为 nulls_equal all时 NULL 值都被视为相同(即,它们都形成单个值组)。

    如果 NULL 值组大小远远高于平均非 NULL 值组大小,则此方法会向上倾斜平均值组大小。 这使得优化器中的索引对于查找非 NULL 值的 连接的实际效果不如它 因此,该 nulls_equal 方法可能导致优化器 ref 在应该的时候 不使用索引进行 访问。

  • 当变量设置为时 nulls_unequal NULL 值不被视为相同。 相反,每个 NULL 值形成一个大小为1的单独值组。

    如果您有许多 NULL 值,则此方法会向下倾斜平均值组大小。 如果平均非 NULL 值组大小很大, 则将 NULL 每个 数值作为大小为1的组会导致优化器高估查找非 NULL 值的 联接的索引 值。 因此, 当其他方法可能更好时 ,该 nulls_unequal 方法可能导致优化器使用此索引进行 ref 查找。

  • 当变量设置为时 nulls_ignored NULL 将忽略值。

如果您倾向于使用许多使用 <=> 而不是 使用的连接 = ,则 NULL 值在比较中并不特殊,而且一个 NULL 等于另一个。 在这种情况下, nulls_equal 是适当的统计方法。

innodb_stats_method 系统变量具有全局值; myisam_stats_method 系统变量有全局和会话值。 设置全局值会影响相应存储引擎中表的统计信息收集。 设置会话值仅影响当前客户端连接的统计信息收集。 这意味着您可以通过设置会话值来强制使用给定方法重新生成表的统计信息,而不会影响其他客户端 myisam_stats_method

要重新生成 MyISAM 表统计信息,可以使用以下任一方法:

关于使用 innodb_stats_method 和的 一些警告 myisam_stats_method

  • 您可以强制显式收集表统计信息,如上所述。 但是,MySQL也可能会自动收集统计信息。 例如,如果在执行表的语句过程中,其中一些语句修改了表,MySQL可能会收集统计信息。 (例如,对于批量插入或删除,或某些 ALTER TABLE 语句, 可能会发生 这种情况。)如果发生这种情况,则使用任何值收集统计信息 innodb_stats_method myisam_stats_method 当时有。 因此,如果使用一种方法收集统计信息,但在稍后自动收集表的统计信息时将系统变量设置为另一种方法,则将使用另一种方法。

  • 无法确定使用哪种方法为给定表生成统计信息。

  • 这些变量仅适用于 InnoDB MyISAM 表。 其他存储引擎只有一种收集表统计信息的方法。 通常它更接近 nulls_equal 方法。

8.3.9 B树和哈希索引的比较

了解B树和哈希数据结构有助于预测不同查询在索引中使用这些数据结构的不同存储引擎上的执行情况,特别是对于 MEMORY 允许您选择B树或哈希索引 存储引擎。

B树指数特征

A B树索引可以在使用表达式中使用的对列的比较 = > >= < <= ,或 BETWEEN 运营商。 LIKE 如果参数to LIKE 是一个不以通配符开头的常量字符串,则 索引也可用于 比较 例如,以下 SELECT 语句使用索引:

SELECT * FROM tbl_nameWHERE key_colLIKE'Patrick%';
SELECT * FROM tbl_nameWHERE key_colLIKE'Pat%_ck%';

在第一个语句中,仅 考虑 具有行的行 在第二个语句中,仅 考虑 具有行的行 'Patrick' <= key_col < 'Patricl' 'Pat' <= key_col < 'Pau'

以下 SELECT 语句不使用索引:

SELECT * FROM tbl_nameWHERE key_colLIKE'%Patrick%';
SELECT * FROM tbl_nameWHERE key_colLIKE other_col;

在第一个语句中, LIKE 值以通配符开头。 在第二个语句中,该 LIKE 值不是常量。

如果您使用 超过三个字符,MySQL使用 Turbo Boyer-Moore算法 初始化字符串的模式,然后使用此模式更快地执行搜索。 ... LIKE '%string%' string

使用 col_name IS NULL 雇用索引 的搜索 是否 col_name 已编制索引。

不跨越 子句中 所有 AND 级别的 任何索引 WHERE 不用于优化查询。 换句话说,为了能够使用索引,必须在每个 AND 组中 使用索引的前缀

以下 WHERE 子句使用索引:

...... WHERE index_part1= 1 AND index_part2= 2 AND other_column= 3

    / * index= 1 OR index= 2 * /
...... WHERE index= 1或A = 10 AND index= 2

    / *优化如“ index_part1='hello'”* /
...... WHERE index_part1='你好'AND index_part3= 5

    / *可以使用索引index1但不能使用index2index3* /
...... WHERE index1= 1 AND index2= 2 OR index1= 3 AND index3= 3;