MYSQL 优化

2020-09-11 11:39:44 浏览数 (1)

MYSQL 优化

数据库性能依赖于数据库层面的一些诸如表、查询及配置等因素。而软件功能的构成最终反映到硬件上面,即CPU使用及I/O操作。减少CPU消耗,增加I/O效率则是提高软件性能的根本驱动。着眼于数据库性能的优化,首先我们需要从较高层次软件层面规则作指导,使用wall-clock 时间测算性能。当专业知识进一步提升,了解了更多的内部机制,则可以从CPU时钟及I/O操作方面进行改进。

典型的数据库使用者基于数据库软硬件配置来获取最由的性能。深度应用者则着眼于从改进Mysql软件自身层面,或者开发另外的数据库引擎和硬件应用来拓展Mysql的生态系统。

数据库层面优化

影响数据库应用性能的最重要因素其设计的合理性:

  • 表的结构是否合适?列的数据类型是否适合,各个表的列设置是否合理?例如,对于更新操作比较多的应用多设置较多少列的表,而对于经常进行大数据量分析的应用则多设置少量多列的表。
  • 是否设置了合适的索引
  • 表是否设置了合适的引擎,例如,涉及事物应用的InnoDB 引擎和非事务性应用的 MyISAM 引擎对于应用的性能及伸缩性有很大的影响(InnoDB 是新表默认的存储引擎。实际应用中,先进的InnoDB 存储引擎表现优于简单的MyISAM 。特别是对于高负载数据库)。
  • 表是行格式是否合适(行格式依赖于存储引擎)。例如,使用压缩行格式的表,占用的空间更少,读写数据需要较少的I/O操作。压缩适用于所有类型操作的InnoDB 表,及只读MyISAM 表。
  • 应用锁策略。例如,共享锁的并发访问,排它锁的核心业务调用。存储引擎的选择至关重要,InnoDB能够自动处理大部分的锁问题,维持较好的并发性及减少超时操作数量。
  • 缓存设置。缓存需要能够支撑频繁的数据访问,但不能超过物理内存。主要需要设置的内存区域是InnoDB的缓冲池 buffer pool 和MyISAM key cache.

硬件层面优化

任何数据库应用随着业务的愈加繁忙,终究会触及硬件性能的瓶颈。DBA需要能够评估是否能够通过调整应用或者重新配置数据库服务器来解决问题,或者添加额外的硬件资源来提升硬件服务能力。引发系统瓶颈原因通常如下:

  • 磁盘检索。磁盘检索数据需要耗费一定的时间。现在技术条件下的磁盘,通常磁盘检索数据的平均时间低于10ms,因此,理论上说,一秒内我们可以执行100次检索。这种时间耗费无法通过添加新盘来改善,对于单表检索也很难优化。数据检索可以通过将数据分散存储到不同的磁盘来优化。
  • 磁盘读写。当磁盘寻址完毕,则开始数据的读写操作。目前磁盘传输速度能够达到至少10–20MB/s。相对于寻址,我们可以通过并行的从多个磁盘读取数据来优化读写。
  • CPU cycles。当数据加载到内存,我们需要进行额外的处理来获取需要的结果。大表内存加载是常见的资源限制。
  • 主内存带宽。CPU缓存带宽需要适应CPU核算能力。

平衡便携性及性能

SQL语句优化

数据库应用的核心逻辑最终需要通过SQL语句来执行。

SELECT 语句优化

数据库查询,SELECT 语句形式,用以执行数据库的所有查询操作。因此查询优化主要就是调整SELECT语句。例如对于动态web页面强调亚秒级响应及利用晚上的时间生成大数据量报表。

除了 SELECT语句,查询调整技术也适用于CREATE TABLE...AS SELECT, INSERT INTO...SELECT, 和DELETE 语句中的WHERE 条件。因为这些语句既包括读语句,也包括写更新语句,因此优化需要特别的注意。

查询优化主要需要考虑的问题如下:

  • 加速SELECT ... WHERE查询,首先需要考虑的是是否需要添加索引。通过将 WHERE语句中的查询条件添加为索引来加速查询评估,过滤和结果检索。为了减少磁盘消耗,我们需要综合应用的查询需求,只构建必要的索引来加速查询。

索引对于联合查询(使用 joins 和foreign keys)尤为重要。可以使用EXPLAIN 语句来检查SELECT语句使用了那些索引。

  • 隔离然后调整查询的各个部分。例如,对于应用函数的,不同的位置决定了是只对检索结果应用还是对所有需要检索的表数据应用。两者的效率截然不同。
  • 减少全表扫描,尤其是对大表操作。
  • 定期执行ANALYZE TABLE语句,保持表的统计数据最新,以便优化器能够构建最有效的执行计划。
  • 了解不同存储引擎的优化技术、索引技术及配置参数。InnoDBMyISAM 都有一系列优化保持高性能查询的指导。
  • 优化InnoDB表单查询事务。.
  • 避免将查询语句变得晦涩,尤其是对于优化器会自动执行的一些优化转换。
  • 如果基础的优化指引无法解决性能问题,则可以通过执行计划调整索引,查询条件,联合条件等(执行计划优先)。
  • 调整MySQL缓存使用的内存大小及属性。合理利用InnoDB buffer pool, MyISAM key cache, 及MySQL查询缓存,重复查询因为数据从内存获取,所以查询的速度特别快。
  • 即使对于使用缓存加速了查询,也可以进一步在缓存使用上进行优化,使得应用更具伸缩性(应用在同时处理更多的用户请求同时,不会在性能上有较大下降)。
  • 处理锁问题。不同会话多表访问加锁的查询影响。
WHERE条件优化

以下以SELECT语句为例介绍,但同时也适用于DELETEUPDATE语句。

不要执行不要的sql语句调整,尤其对于优化器能够自动处理的转换。最好是保持sql语句的可读性及可维护性。

  • 移除不必要的圆括号:
  • 常量折叠:
  • 移除常亮条件:
  • 索引使用的常量表达式只会评估一次。
  • MyISAM及MEMORYCOUNT(*) 操作是直接从表信息中获取的。.
  • 及早发现不合理的常量表达式,MySQL能够很快发现SELECT语句的可查性,并不返回行数据。
  • 未使用Group或者其它聚合函数(COUNT(), MIN()等)情况下,HAVING 会和 WHERE 进行合并。
    • 对于联合表,简单的WHERE 查询条件能够快速评估,并尽早略过不符合的行数据。
  • 常量表会优先查询:
    • 空表、或者只有一行数据的表。
    • 条件语句使用主键或者唯一索引的表。所有的索引和常量表达式进行比较,并且部位NULL如下:
  • 最优的join联合条件是尝试出来的。如果 ORDER BYGROUP BY 条件中的所有列都来自于同一个表,那么这个表就会优先使用。
  • 如果ORDER BY条件和 GROUP BY 条件不同,或者 ORDER BYGROUP BY 包含条件列不是来自于第一个联合表,那么就会创建临时表使用。
  • 如果使用了SQL_SMALL_RESULT关键字,MySQL 会使用内存临时表。
  • 优化器会自动选择最优索引,是否使用全表扫描基于是否所要使用的索引会引起超过30%的表扫描。但是现在已不是简单的一个扫描比例决定了。优化器机制更加复杂,能够根据包括表大小,行树及I/O块大小等因素来进行评估。
  • 覆盖索引情况。
  • HAVING条件过滤
代码语言:javascript复制
((a AND b) AND c OR (((a AND b) AND (c AND d))))-> (a AND b AND c) OR (a AND b AND c AND d)
  (a<b AND b=c) AND a=5 -> b>5 AND b=c AND a=5
  (B>=5 AND B=5) OR (B=6 AND 5=5) OR (B=7 AND 5=6)-> B=5 OR B=6
  SELECT * FROM t WHERE primary_key=1;
  SELECT * FROM t1,t2
    WHERE t1.primary_key=1 AND t2.primary_key=t1.id;

以下一些执行非常快的示例:

代码语言:javascript复制
SELECT COUNT(*) FROM tbl_name;
  SELECT MIN(key_part1),MAX(key_part1) FROM tbl_name;
  SELECT MAX(key_part2) FROM tbl_name
    WHERE key_part1=constant;
  SELECT ... FROM tbl_name
    ORDER BY key_part1,key_part2,... LIMIT 10;
  SELECT ... FROM tbl_name
    ORDER BY key_part1 DESC, key_part2 DESC, ... LIMIT 10;

以下MySQL 只使用索引的情况:假定索引列是数值型的。

代码语言:javascript复制
SELECT key_part1,key_part2 FROM tbl_name WHERE key_part1=val; 
  SELECT COUNT(*) FROM tbl_name
    WHERE key_part1=val1 AND key_part2=val2; 
  SELECT key_part2 FROM tbl_name GROUP BY key_part1;

以下查询使用索引查询,并按照指定的排序字段顺序排序。

代码语言:javascript复制
SELECT ... FROM tbl_name
    ORDER BY key_part1,key_part2,... ;
  SELECT ... FROM tbl_name
    ORDER BY key_part1 DESC, key_part2 DESC, ... ;
范围查询优化
单索引范围查询

对于单索引,范围索引值可以非常方便的转化为相应的 WHERE 条件,表示为范围查询条件。

单索引范围查询条件的定义如下:

  • 包括BTREEHASH 索引,索引和常量比较,包括=, <=>, IN(), IS NULL, 或者 IS NOT NULL
  • 对于BTREE 类型索引。索引和常量比较,包括 >, <, >=, <=, BETWEEN, !=, 及 <> 操作, 另外包括对比值为常量且不含通配符的 LIKE比较。
  • 对于所有索引, ORAND连接的条件

常量定义:

  • 查询字符串常量
  • 常数列或者系统表
  • 非关联的子查询结果
  • 任何由上述构成的表达式

如下,范围查询:

代码语言:javascript复制
SELECT * FROM t1
  WHERE key_col > 1
  AND key_col < 10;
 
SELECT * FROM t1
  WHERE key_col = 1
  OR key_col IN (15,18,20);
 
SELECT * FROM t1
  WHERE key_col LIKE 'ab%'
  OR key_col BETWEEN 'bar' AND 'foo';

在优化器的常量传播过程中,非常亮值也可能会被转化为常量。

MySQL会尝试从WHERE 条件提取范围查询条件。在提取过程中,无法组成范围查询的条件会被丢弃;形成重叠的会被组合,产生空范围的会被移除。

实际上,范围查询的条件限制较WHERE 宽泛,MySQL 会针对范围条件进行一次额外的数据过滤。

范围提取算法可以处理同步程度的AND/OR 组合,并且,输出不依赖条件在WHERE 中的顺序。

MySQL不支持空间索引多范围整合。为了解决这个问题,可以使用UNION 连接多个使用不同范围的相同SELECT语句。

多索引范围查询

多索引范围查询是但索引的扩展。限制数据结果在一个或多个索引条件范围内。

考虑如下多索引: key1(key_part1, key_part2, key_part3),

代码语言:javascript复制
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 可以定义如下区间:

代码语言:javascript复制
(1,-inf,-inf) <= (key_part1,key_part2,key_part3) < (1, inf, inf)

这个区间包含第4、5、6条记录,可以工作范围查询。

相反,条件key_part3 = 'abc' 无法定义唯一的区间,不能用于范围查询。

如下详细描述:

  • HASH 索引, 只能定义如下:
代码语言:javascript复制
key_part1 cmp const1
  AND key_part2 cmp const2
  AND ...
  AND key_partN cmp constN;

cmp 包括 =, <=>, 或者 IS NULL ,条件包含所有的索引部分(如包含三列的索引,则需要三个条件)例如,如下三部分HASH 索引范围条件:

代码语言:javascript复制
key_part1 = 1 AND key_part2 IS NULL AND key_part3 = 'foo'
  • BTREE索引, AND/OR连接的系列关于索引部分的条件=, <=>, IS NULL, >, <, >=, <=, !=, <>, BETWEEN, 或者 LIKE 'pattern' ( 'pattern' 不能以通配符开始)。

当时用=, <=>, or IS NULL 时,优化器会尝试使用额外的键部分来处理查询范围;如果条件是 >, <, >=, <=, !=,<>, BETWEEN, 或者 LIKE,则不需要额外处理。如下不使用额外键条件的情景: 首先优化器使用第一个条件的= 。同时使用第二个条件的>= ,并不再使用条件三来组合范围查询:

代码语言:javascript复制
key_part1 = 'foo' AND key_part2 >= 10 AND key_part3 > 10

单个区间:

代码语言:javascript复制
('foo',10,-inf) < (key_part1,key_part2,key_part3) < ('foo', inf, inf)

创建的范围条件可能包含比初始条件跟多的数据行。

  • OR 等价于UNION 组合;AND 等价于个范围条件的交集:
代码语言:javascript复制
(key_part1 = 1 AND key_part2 < 2) OR (key_part1 > 5)

范围为:

代码语言:javascript复制
(1,-inf) < (key_part1,key_part2) < (1,2)
(5,-inf) < (key_part1,key_part2)

示例中,第一行的条件,左半部分只使用了索引一个部分,右半部分使用了两个部分;第二行则只使用了索引一个部分。执行计划中的 key_len 列信息,预示索引前缀使用的最大长度。

一些情况下, key_len 表明使用的键并不是你期望的。例如,假如key_part1key_part2 可能为NULL,执行计划的 key_len 展示使用了两个键的长度:

代码语言:javascript复制
key_part1 >= 1 AND key_part2 < 2

但是,实际上,查询条件会被转化为:

代码语言:javascript复制
key_part1 >= 1 AND key_part2 IS NOT NULL
多值比较的等值范围优化

如下:假定col_name 索引列:

代码语言:javascript复制
col_name IN(val1, ..., valN)
col_name = val1 OR ... OR col_name = valN

只要 col_name 值等于范围中的任何一个值,条件就成立。优化器评估如下:

  • 如果col_name 是唯一索引,则范围评估结果只有一行数据。
  • 其它非唯一索引情况根据范围条件评估。

根据index dive,优化器对各个范围逐一检索。例如, col_name IN (10, 20, 30) 有三个等值范围,优化器对于每个范围执行两次检索,来生成行估计。

index dive能够提供精确的估计,但是,随着比较值的增多,预估需要花费优化器更多的时间。对于大数据量对比值估计可以使用索引统计数计以加快速度。

eq_range_index_dive_limit系统变量用以配置优化器切换使用不同预估方式的阈值。例如,最多可以使用index dive处理对比值数量N,则设置 eq_range_index_dive_limit = N 1,设置为0,则表明不使用索引统计信息。

使用ANALYZE TABLE更新表索引统计信息。

MySQL 8.0版本前,只有通过 eq_range_index_dive_limit 变量设置。MySQL 8.0,对于满足如下所有情景,可能跳过使用index dive。

  • 单表检索。
  • 单索引FORCE INDEX 索引提示。如果是使用强制索引,使用index dive则也不能获取任何其它的信息。
  • 非唯一索引,也不是全文索引。
  • 没有子查询。
  • 未使用DISTINCT, GROUP BY, 或者 ORDER BY

对于 EXPLAIN FOR CONNECTION,未使用index dive时,输入会有如下变化:

  • 事务输出,rowsfiltered 值都是null。
  • JSON 输出,没有 rows_examined_per_scanrows_produced_per_join 出现, skip_index_dive_due_to_forcetrue,耗费计算是不准确的。

不使用FOR CONNECTION语句,在未使用index dive情况下,执行计划 EXPLAIN 输出不变。

未使用index dive情况下执行查询,相应的INFORMATION_SCHEMA.OPTIMIZER_TRACE 中的相关行会包括一条skipped_due_to_force_index 的index_dives_for_range_access

范围查询优化

优化器可以执行如下的范围扫描:

代码语言:javascript复制
SELECT ... FROM t1 WHERE ( col_1, col_2 ) IN (( 'a', 'b' ), ( 'c', 'd' ));

如前所述,为了使用范围扫描,需要做如下转换:

代码语言:javascript复制
SELECT ... FROM t1 WHERE ( col_1 = 'a' AND col_2 = 'b' )
OR ( col_1 = 'c' AND col_2 = 'd' );

使用范围扫描需要满足如下条件:

  • IN() 而不是 NOT IN().
  • IN()左边只有列。
  • IN()右边只有运行时常量(字面量或者常量列)。
  • IN() 右边多余一个的 row constructor.
有限内存的范围查询优化

range_optimizer_max_mem_size系统变量:

  • 0意为无限制。
  • 大于0的值,优化器会跟踪范围查询的内存消耗,当查过设置值时,会改用其它诸如权标扫描等方法替代。这种情景不是优化所需要的,发生这种情景,我们可以看到如下警告信息(N代表 range_optimizer_max_mem_size):
  • 对于更新和删除操作,当退回到全表扫描并且启用sql_safe_updates ,就会引发错误,而不只是警告信息。因为实际上,没有使用键来决定更新记录。
代码语言:javascript复制
Warning    3170    Memory capacity of N bytes for
                   'range_optimizer_max_mem_size' exceeded. Range
                   optimization was not done for this query.

对于此优化问题,可以通过调整 range_optimizer_max_mem_size来处理。为了估计范围查询需要的内存,需要执行如下指引:

  • 如下查询,有一个候选键,组合OR条件,则会使用内存近230 bytes:
  • 相似的使用组合AND条件,则会使用内存近125 bytes:
  • 对于使用IN()的查询:
代码语言:javascript复制
SELECT COUNT(*) FROM t
WHERE a=1 OR a=2 OR a=3 OR .. . a=N;
SELECT COUNT(*) FROM t
WHERE a=1 AND b=1 AND c=1 ... N;
SELECT COUNT(*) FROM t
WHERE a IN (1,2, ..., M) AND b IN (1,2, ..., N);

这种情境下,会产生M × N.条件情景。

索引合并优化

索引合并访问,使用多个范围扫描,然后合并查询结果为结果输出。这一方法只发生在单表查询。合并算法会产生并集,交集及交集的并集等。

如下,使用索引合并的实例:

代码语言:javascript复制
SELECT * FROM tbl_name WHERE key1 = 10 OR key2 = 20;
 
SELECT * FROM tbl_name
  WHERE (key1 = 10 OR key2 = 20) AND non_key = 30;
 
SELECT * FROM t1, t2
  WHERE (t1.key1 IN (1,2) OR t1.key2 LIKE 'value%')
  AND t2.key1 = t1.some_col;
 
SELECT * FROM t1, t2
  WHERE t1.key1 = 1
  AND (t2.key1 = t1.some_col OR t2.key2 = t1.some_col2);

索引合并限制:

  • WHERE 条件使用了复杂的AND/OR条件组合,MySQL 也没有选择最佳的计划,可以执行如下的等价变换。
  • 不适用于全文索引。
代码语言:javascript复制
(x AND y) OR z => (x OR z) AND (y OR z)
(x OR y) AND z => (x AND z) OR (y AND z)

索引合并在执行计划输出中展示位type列 index merge。在这种情景下,key列包含所有的使用索引, key_len展示包含最多使用索引部分的索引长度

索引合并算法:展示在Extra列中

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

索引合并算法包括 index_merge, index_merge_intersection, index_merge_union, 及index_merge_sort_union 变量标志。默认情况下,所有的标志都是设置为on的,为了使用某一算法,则设置index_merge =off,同时设置相应的算法标志位on。

  • Index Merge Intersection Access Algorithm
  • Index Merge Union Access Algorithm
  • Index Merge Sort-Union Access Algorithm
索引合并交集算法

AND组合

  • 包含索引所有部分的查询:
  • Innodb的主键的范围查询
代码语言:javascript复制
key_part1 = const1 AND key_part2 = const2 ... AND key_partN = constN

示例:

代码语言:javascript复制
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;

索引合并交集算法会同时执行所有的索引。

如果查询的列包含在索引列中,则不会访问表数据(执行计划Extra列包含 Using index提示):

代码语言:javascript复制
SELECT COUNT(*) FROM t1 WHERE key1 = 1 AND key2 = 1;
索引合并并集方法

OR组合查询条件:

  • 包含索引所有部分的查询:
  • 包含索引所有部分的查询:
  • 交集算法
代码语言:javascript复制
key_part1 = const1 AND key_part2 = const2 ... AND key_partN = constN
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;
索引合并排序并集算法

OR组合的多个范围查询,单索引合并并集算法不适用的情况:

代码语言:javascript复制
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;

The difference between the sort-union algorithm and the union algorithm is that the sort-union algorithm must first fetch row IDs for all rows and sort them before returning any rows.

区别并计算法:排序并集算法在返回行前必须首先获取IDs,排序。

Engine Condition Pushdown 优化

这一优化提升了非索引列和常量条件查询。这种情况下,查询将会在引擎层面执行。此优化只适用NDB引擎。

对于NDB 集群,这一优化可以避免返回不必要的数据行到服务器,加速查询。

示例如下,NDB集群表定义:

代码语言:javascript复制
CREATE TABLE t1 (
    a INT,
    b INT,
    KEY(a)
) ENGINE=NDB;

可以使用Condition pushdown优化的情景,非索引列的常量对比:

代码语言:javascript复制
SELECT a, b FROM t1 WHERE b = 10;

执行计划结果显示Extra信息:

代码语言:javascript复制
mysql> EXPLAIN SELECT a,b FROM t1 WHERE b = 10G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 10
        Extra: Using where with pushed condition

如下两个查询不支持condition pushdown:

代码语言:javascript复制
SELECT a,b FROM t1 WHERE a = 10;
SELECT a,b FROM t1 WHERE b   1 = 10;

对于第一个查询,因为使用了索引列a做查询条件,索引查询更有效率。

第二个查询使用了非索引列条件,但是是非直接使用。如果要使用Condition pushdown,则需修改为 b = 9

Condition pushdown 使用情景包括:索引列使用 >< 运算符条件:

代码语言:javascript复制
mysql> EXPLAIN SELECT a, b FROM t1 WHERE a < 2G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
         type: range
possible_keys: a
          key: a
      key_len: 5
          ref: NULL
         rows: 2
        Extra: Using where with pushed condition

其它:

  • column [NOT] LIKE pattern。
  • column IS [NOT] NULL
  • column IN (value_list)

value_list 必须是常量或者字面量

  • column BETWEEN constant1 AND constant2

constant1constant2 必须是常量或者字面量。

Engine condition pushdown 默认启用,关闭设置: optimizer_switch

代码语言:javascript复制
[mysqld]
optimizer_switch=engine_condition_pushdown=off

运行时设置:

代码语言:javascript复制
SET optimizer_switch='engine_condition_pushdown=off';

限制:

  • 只支持NDB 引擎。
  • 列条件运算对比只能是常量或者常量表达式。
  • 条件列类型不能是BLOBTEXT
  • 条件列和字符常量需要使用相同的排序规则
  • Joins 是分别判断处理的。使用执行计划来观察使用。
Index Condition Pushdown 优化

Index Condition Pushdown (ICP) 即,使用索引条件查询时的Condition Pushdown 优化。如果不使用ICP,存储引擎会遍历索引定位数据表中数据,然后返回给MySQL 服务器。ICP机制,则判断,当部条件列覆盖部分索引,服务就就会将这部分条件下放到存储引擎进行处理。然后,存储引擎会对比索引与条件,只有当匹配时,才会从表中获取数据。ICP机制可以减少存储引擎访问数据表的次数及Mysql 服务器访问存储引擎的次数。

ICP应用场景:

  • ICP 适用于权标扫描时的 range, ref, eq_refref_or_null访问方法。
  • ICP适用于InnoDBMyISAM表,包括分区表。
  • 对于 InnoDB表,ICP 只适用于二级索引。ICP的目标值减少行读取,I/O操作。对于InnoDB 聚簇索引(主索引),记录已经完全被读入InnoDB 缓存,因此使用ICP 不会减少I/O操作。
  • ICP 不支持建立在虚拟列上的二级索引。InnoDB 支持虚拟列上建立二级索引。Cenerated Column:由其它列产生的列。virtual generated column:生成的列只保存在数据字典中,不会讲列的数据持久化磁盘中。Stored Generated Column:数据里的数据存储在磁盘上。(v5.7)
代码语言:javascript复制
CREATE TABLE bought ( 

    id int(11) not null, 

    book_id int(11) not null, 

    price double DEFAULT 0,

    num double DEFAULT 0,

    amount DOUBLE GENERATED ALWAYS AS (price * num) VIRTUAL

);
  • 子查询作为条件不适用ICP。
  • 使用函数作为条件的不适用。
  • 触发器作为条件不适用。

为了理解优化过程,在不使用ICP机制时,索引扫描执行过程如下:

  1. 获取行数据:读取索引数组,然后定位读取表中整个数据行。
  2. 匹配WHERE 中的条件和读取的行数据,接受后丢弃数据。

使用ICP机制,查询执行过程:

  1. 获取下一行的索引数组。
  2. 匹配索引列是否满足WHERE 中涉及的索引条件。
  3. 如果条件满足,使用此索引数组定位读取整个数据行。
  4. 匹配WHERE 中剩余的条件,接受或丢弃读取的数据行。

使用ICP时,执行计划 Extra列会显示Using index condition 提示,因为可能需要读取所有的表数据行,索引不显示Using index

示例:宝航地址及姓名信息的表,索引INDEX (zipcode, lastname, firstname)。通过zipcode 及模糊last name查询:

代码语言:javascript复制
SELECT * FROM people
  WHERE zipcode='95054'
  AND lastname LIKE '%etrunia%'
  AND address LIKE '%Main Street%';

MySQL可以使用索引条件zipcode='95054'扫描表数据行,条件第二部分 (lastname LIKE '%etrunia%')不能用来限制需要扫描的行,在不适用ICP情景下,需要扫描索引表中所有符合条件zipcode='95054'的数据行。

使用ICP时,基于索引列条件获取索引列数据,然后匹配astname LIKE '%etrunia%' 条件,避免了一开始就读取所有的数据行。

ICP默认启用,配置:

代码语言:javascript复制
SET optimizer_switch = 'index_condition_pushdown=off';//关闭 
SET optimizer_switch = 'index_condition_pushdown=on'; //启用
Nested-Loop Join Algorithms

MySQL join算法:

  • Nested-Loop Join Algorithm
  • Block Nested-Loop Join Algorithm
Nested-Loop Join Algorithm

nested-loop join (NLJ) 算法:首先从第一张表读取数据行,然后逐行循环处理下一张表数据,有多少张表联合,嵌套多少层。

如下:三表联合

代码语言:javascript复制
Table   Join Type
t1      range
t2      ref
t3      ALL

NLJ算法执行如下:

代码语言:javascript复制
for each row in t1 matching range {
  for each row in t2 matching reference key {
    for each row in t3 {
      if row satisfies join conditions, send to client
    }
  }
}

算法外部循环一次传一行数据到内部循环,因此每次都需要访问数据表读取数据(内外部)。

Block Nested-Loop Join Algorithm

Nested-Loop (BNL)算法:使用缓存存储外部循环行数据,以减少需要访问表的次数。例如,外部一次读入10条记录到缓存,然后传递给内部循环,这样内部循环读取的的数据就可以一次和10条记录做匹配,极大的减少了内存循环读取表次数。

MySQL join 缓存有一下特点:

  • Join buffering 应用场景: ALLindex (没有可用的索引,需要全表扫描(索引或者全部数据))或者 range联合类型。缓存对于外链接同样适用。
  • 第一个非常量表部分join buffer,即使查询类型为 ALL 或者 index.
  • 只有联合使用的列会存于join buffer,而不是整个列。
  • join_buffer_size 系统变量限制每个查询可使用的缓存大小。
  • 每个join会被分配使用一个buffer,一个查询可能包含多个join,因此可能使用多个buffer。
  • join buffer在执行join操作之前分配,在查询结束后释放。

BNLJ:算法窒执行如下

代码语言:javascript复制
for each row in t1 matching range {
  for each row in t2 matching reference key {
    store used columns from t1, t2 in join buffer
    if buffer is full {
      for each row in t3 {
        for each t1, t2 combination in join buffer {
          if row satisfies join conditions, send to client
        }
      }
      empty join buffer
    }
  }
}
 
if buffer is not empty {
  for each row in t3 {
    for each t1, t2 combination in join buffer {
      if row satisfies join conditions, send to client
    }
  }
}

如下,S 代表join buffer 中的t1, t2 各自的组合数, C 代表buffer中所有的组合数,那么表t3 需要扫描的次数:

代码语言:javascript复制
(S * C)/join_buffer_size   1

表扫描次数会随着join_buffer_size 变大而减少,当 join_buffer_size 足够大能够存储之前所有的行组合,再增大缓存将不会再有查询速度的提升。

IS NULL 优化

MySQL对col_name IS NULL 的优化和对col_name = constant_value的优化方式一样。如下:MySQL 可以使用索引和范围查询来处理IS NULL查询。

代码语言:javascript复制
SELECT * FROM tbl_name WHERE key_col IS NULL;
SELECT * FROM tbl_name WHERE key_col <=> NULL;
SELECT * FROM tbl_name
  WHERE key_col=const1 OR key_col=const2 OR key_col IS NULL;

col_name IS NULL 要求条件声明为NOT NULL,如果可能存在NULL 值,那么就不会执行优化。

MySQL也可以优化类似col_name = expr OR col_name IS NULL这样的组合。这种形式通常见于子查询。通过执行计划可以观察到,当执行此类优化时,会有ref_or_null 提示信息。

优化可以对索引的任何部分执行IS NULL条件优化。

如下:可以执行优化的一些查询情景,表t2中列a 和 b上含有索引:

代码语言:javascript复制
SELECT * FROM t1 WHERE t1.a=expr OR 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
  WHERE t1.a=t2.a AND (t2.b=t1.b OR t2.b IS NULL);
SELECT * FROM t1, t2
  WHERE (t1.a=t2.a AND t2.a IS NULL AND ...)
  OR (t1.a=t2.a AND t2.a IS NULL AND ...);

ref_or_null :首先根据相关key查询记录,然后执行查询索引为NULL的数据。

优化只能执行一层IS NULL 优化。如下,MySQL只能使用 (t1.a=t2.a AND t2.a IS NULL) 执行查询,而无法适使用列b

代码语言:javascript复制
SELECT * FROM t1, t2
  WHERE (t1.a=t2.a AND t2.a IS NULL)
  OR (t1.b=t2.b AND t2.b IS NULL);
ORDER BY 优化
  • 使用索引排序
  • 使用文件排序
Use of Indexes to Satisfy ORDER BY

一些情境下,MySQL会主动使用索引来满足ORDER BY ,避免使用文件排序filesort

即使 ORDER BY 所使用的列和索引不完全匹配,只要未使用的索引部分和额外的条件列是常量,也会使用索引排序。如果索引未包含查询中所有的列,MySQL只有在使用索引代价更小的时候使用索引。

假定有索引存在于 (key_part1, key_part2)上。如下查询,是否使用索引排序,取决于读取非索引列不同方式的效率。

  • In this query, the index on (key_part1, key_part2) enables the optimizer to avoid sorting:
代码语言:javascript复制
SELECT * FROM t1
  ORDER BY key_part1, key_part2;

查询中SELECT * 查询的列数量可能包括key_part1key_part2之外的列。这种情况下,扫描整个索引查找表所有的数据来查询需要查询的非索引列,可能比单单全表扫描然后排序结果成本更高。这样优化器就不会再使用索引。如果SELECT * 只包含索引列,那么查询就只会使用索引,结果也将不用再排序

如下,使用索引查询排序:

代码语言:javascript复制
SELECT pk, key_part1, key_part2 FROM t1
  ORDER BY key_part1, key_part2;
  • 如下查询,key_part1 是常量,所以查询结果会以key_part2 顺序。(key_part1, key_part2) 上存在索引,当条件列具有一定的选择性,使得索引范围查询比全包扫描更有效率,就可以使用索引顺序,避免排序:
  • 以下包含DESC 的查询,优化判断如上:
  • 如下查询,ORDER BY 中并没有使用 key_part1,但是选择的列中有key_part1 常数列,所以仍然会使用索引:
代码语言:javascript复制
SELECT * FROM t1
  WHERE key_part1 = constant
  ORDER BY key_part2;
SELECT * FROM t1
  ORDER BY key_part1 DESC, key_part2 DESC;
 
SELECT * FROM t1
  WHERE key_part1 = constant
  ORDER BY key_part2 DESC;
SELECT * FROM t1
  WHERE key_part1 = constant1 AND key_part2 > constant2
  ORDER BY key_part2;

一些情境下,即使查询使用了索引来获取满足条件的数据,也不能使用索引排序。如下:

查询使用了另外的索引列(和查询的列不同)作为排序列:

代码语言:javascript复制
SELECT * FROM t1 ORDER BY key1, key2;

查询使用了不相邻的索引部分:

代码语言:javascript复制
SELECT * FROM t1 WHERE key2=constant ORDER BY key1_part1, key1_part3;

查询混用了ASCDESC:

代码语言:javascript复制
SELECT * FROM t1 ORDER BY key_part1 DESC, key_part2 ASC;

查询使用的索引和排序使用的索引不同:

代码语言:javascript复制
SELECT * FROM t1 WHERE key2=constant ORDER BY key1;
  • ORDER BY 后使用的表达式:
  • 多表联合,ORDER BY 中的列不是来自第一个联合的非常量类型表(执行计划中排在第一位的,联合类型为非 const 的表)。
  • ORDER BYGROUP BY 使用不同的表达式。
  • 部分索引不能替代文件排序。
  • 索引未排序,如对MEMORY引擎的HASH 索引。
代码语言:javascript复制
SELECT * FROM t1 ORDER BY ABS(key);
SELECT * FROM t1 ORDER BY -key;
SELECT a FROM t1 ORDER BY a;

基于列表达式的别名,不可使用索引排序。如下,a为ABS(a)的别名,列a上的索引不可用于排序:

代码语言:javascript复制
SELECT ABS(a) AS a FROM t1 ORDER BY a;

如下,ORDER BY 中的列不再select 中,但是a是t1中的列,可以使用索引排序:

代码语言:javascript复制
SELECT ABS(a) AS b FROM t1 ORDER BY a;

默认情况下,MySQL会对 GROUP BY col1, col2, ... 进行排序,就好象使用了ORDER BY col1, col2, ... 。即使严格声明了ORDER BY,MySQL也会优化掉,不会对查询速度有任何影响。

对于包含GROUP BY 的查询,如果要避免排序,则需要使用ORDER BY NULL。如下:

代码语言:javascript复制
INSERT INTO foo
SELECT a, COUNT(*) FROM bar GROUP BY a ORDER BY NULL;

优化器仍然会选择使用排序来完成分组。ORDER BY NULL 会避免对结果进行排序,而不是处理GROUP BY的排序。

Note

GROUP BY 使用默认排序。如果需要确定的排序,则使用ORDER BY条件。

ORDER BY文件排序

当索引无法满足排序,MySQL会执行 filesort 文件排序,读取表记录然后排序。Filesort 文件按排序构成了查询中的一次额外的排序阶段。

为了获取执行filesort 操作的内存,优化器会预先分配 sort_buffer_size 设定大小的内存。单个会话会根据实际需求修改此变量的值,或者动态的分配内存。

filesort 文件排序操作,当内存不足时,会使用磁盘空间。一些类型的查询会使用完全的基于内存的文件排序,如下:

代码语言:javascript复制
SELECT ... FROM single_table ... ORDER BY non_index_column [DESC] LIMIT [M,]N;

查询使用LIMIT,限制查询条目。

代码语言:javascript复制
SELECT col1, ... FROM t1 ... ORDER BY name LIMIT 10;
SELECT col1, ... FROM t1 ... ORDER BY RAND() LIMIT 15;
ORDER BY 优化

对于未使用ORDER BY 的慢查询,可以试着减少max_length_for_sort_data 系统变量来触发文件排序,system variable to a value that is appropriate to trigger afilesort. (max_length_for_sort_data 设置较大的值适合于高吞低CPU活跃的查询)

要提高排序速度,首先检查是否可以使用索引排序,如果不能,则执行如下步骤:

  • 增加 sort_buffer_size 变量值,以足够容纳下整个结果集在内存中排序(避免磁盘读写)。但至少为15个临时磁盘文件存储大小。

考虑需要排序的列大小:max_sort_lengthsort_buffer_size 需要随着变化。

  • 增加read_rnd_buffer_size 变量,以一次可以读取更多的行。
  • tmpdir 变量指向有足够空间的文件系统。比变量可以指定多个路径,轮询使用。分隔符Unix使用(:),Windows使用 (;)。不同路径需要放置在不同的磁盘上,而不要使用同一个磁盘上的不同分区。
ORDER BY Execution Plan Information Available

使用执行计划检验是否可以使用索引排序。查看Extra 列是否包含Using filesort

执行filesort 时,会有如下输出: filesort_summary

代码语言:javascript复制
"filesort_summary": {
  "rows": 100,
  "examined_rows": 100,
  "number_of_tmp_files": 0,
  "sort_buffer_size": 25192,
  "sort_mode": "<sort_key, packed_additional_fields>"
}
  • <sort_key, rowid>: 排序sort key,读取表 rowid。
  • <sort_key, additional_fields>: 排序缓存中包含排序的key和相关的查询列。
  • <sort_key, packed_additional_fields>:和如上的指示,相关的列会以紧凑的方式存储。

执行计划无法不说明文件排序是否在内存中执行。可以通过优化器输出,查看 filesort_priority_queue_optimization 变量。

DISTINCT 优化

DISTINCT 结合ORDER BY 大多数情况下需要使用临时表。因为DISTINCT 可能使用GROUP BY

多数情况下, DISTINCT 可以等价于特殊的GROUP BY。例如,如下两个查询:

代码语言:javascript复制
SELECT DISTINCT c1, c2, c3 FROM t1
WHERE c1 > const;
 
SELECT c1, c2, c3 FROM t1
WHERE c1 > const GROUP BY c1, c2, c3;

此种情况下,适用GROUP BY 的优化也适用于DISTINCT。

DISTINCT结合LIMIT row_count 时,MySQL会在查找到在足够的记录数时就会立马停止查询。

当使用的列包含在所有表中,当在一个表中找到记录时,就会终止读取其它的表。如下

代码语言:javascript复制
SELECT DISTINCT t1.a FROM t1, t2 where t1.a=t2.a;
LIMIT Query 优化

LIMIT用户查询特定数量的结果集:

  • 可以使用索引时,会索引优先。
  • row_count 结合 ORDER BY情况下,MySQL会在查找需要的记录时立刻停止排序。使用索引排序非常快。使用文件排序时,满足条件的记录会被全部读取,在找打满足limit 条件的记录前,所有记录都会被排序。余下的将不会再被排序。
  • LIMIT row_count 结合 DISTINCT 时,找到即停止。
  • 一些情况下,GROUP BY 可以通过索引计算获得结果。
  • 在MySQL将客户端需要的行数据发送完后,就会停止查询,除非使用了SQL_CALC_FOUND_ROWS,这种情况下,可以通过使用SELECT FOUND_ROWS()查看返回了多少行。
  • LIMIT 0 会立马返回空集。可以用于检验查询合法性;获取结果列元数据;mysql 客户端可以通过--column-type-info 选项展示结果列类型。
  • 如果服务器需要使用临时表,则服务器使用LIMIT row_count 条件来估算需要的内存空间。
  • 使用Limit 而不使用ORDER BY时,优化器可以使用基于内存的文件排序。
避免全表扫描

执行计划 type列显示all,则表明为全表扫描,会出现这种描述的情景包括如下:

  • 表很小,全表扫描比使用索引更快,更有效。
  • 对于on或者where中的索引条件列没有可用的界定限制条件。
  • 索引条件列使用了常量比较值,而常量值会覆盖到表中的大部分数据,效率不如权标扫描高。
  • 使用的条件列cardinality 低,使用系列查询会比全表扫描效率低。

小表全表扫描效率比较高,大表要避免全表扫描:

  • 使用ANALYZE TABLE tbl_name 更新优化表的键分布。
  • 使用FORCE INDEX ,强制使用某些索引:
  • mysqld启动使用 --max-seeks-for-key=1000 选项,或者设置max_seeks_for_key=1000 命令,来告知mysql服务器最大索引扫描量。.
代码语言:javascript复制
SELECT * FROM t1, t2 FORCE INDEX (index_for_column)
  WHERE t1.col_name=t2.col_name;
 

0 人点赞