MySQL 优化流程:使用索引典型场景 AND 索引失效场景
MySQL 优化流程索引问题:MySQL 中使用索引典型场景,存在但不能使用索引的场景。
友情提示:在某些情况,你测试的结果可能与我演示有所不同,我省略了查询结果的部分参数。
如果你是从 MySQL5.6 或者 5.7 版本过渡到 MySQL8.0。学习之前,建议线看官方文档这一 1.3 What Is New MySQL8.0 。在做对比的时候,文档中带有 Note 标识是你应该注意的地方。比如下面这张截图:

MySQL 优化流程 ——MySQL 索引问题
索引问题,是一个老生常谈的问题。如果是数据库优化场景,职场面试中经常被提到。
索引是在 MySQL 存储引擎中实现,而不是在服务器层实现。
每种存储引擎索引不一定完全相同,并不是所有存储引擎支持索引类型都一致。
tips:图片资源可能被防盗链(寄)了,可以右键属性复制地址在地址栏查看哈。
以下列举几种常见索引介绍(索引存储分类):
- B-Tree 索引:最常见的使索引类型,大部分存储引擎都支持 B 树索引。
- HASH 索引:MEMORY、HEAP、NDB 支持,使用场景较为简单。
- R-Tree 索引(空间索引):空间索引是 MyISAM 存储引擎一个特殊索引类型,主要用于地理空间数据类型,一般使用较少。
- Full-text(全文索引):全文索引是 MyISAM 存储引擎一个特殊索引类型,主要用于全文索引。在 MySQL5.6 版本开始对 InnoDB 提供全文索引支持。
注意:索引类型子句不能用于 FULLTEXT(全文索引)或 (在 MySQL 8.0.12 之前) 空间索引规范。全文索引的实现依赖于存储引擎。空间索引实现为 R-tree 索引。
1 索引介绍
几种常见的 MySQL 存储引擎支持索引类型:
| 序号 | 存储引擎 | 支持索引 |
|---|---|---|
| 1 | InnoDB | BTREE |
| 2 | MyISAM | BTREE |
| 3 | MEMORY/HEAP | HASH, BTREE |
| 4 | NDB | HASH, BTREE |
以上四种存储引擎支持索引特点对比:Primary key(主键索引),Unique(唯一索引),key(普通索引),FULLTEXT(全文索引),SPATIAL(空间索引)。
InnoDB 存储引擎:
| 索引类别 | 索引类型 | Stores NULL 值 | Permits Multiple NULL 值 | IS NULL 扫描类型 | IS NOT NULL 扫描类型 |
|---|---|---|---|---|---|
| Primary key | BTREE | No | No | N/A | N/A |
| Unique | BTREE | Yes | Yes | Index | Index |
| Key | BTREE | Yes | Yes | Index | Index |
| FULLTEXT | N/A | Yes | Yes | Table | Table |
| SPATIAL | N/A | No | No | N/A | N/A |
MyISAM 存储引擎:
| 索引类别 | 索引类型 | Stores NULL 值 | Permits Multiple NULL 值 | IS NULL 扫描类型 | IS NOT NULL 扫描类型 |
|---|---|---|---|---|---|
| Primary key | BTREE | No | No | N/A | N/A |
| Unique | BTREE | Yes | Yes | Index | Index |
| Key | BTREE | Yes | Yes | Index | Index |
| FULLTEXT | N/A | Yes | Yes | Table | Table |
| SPATIAL | N/A | No | No | N/A | N/A |
MEMORY 存储引擎:
| 索引类别 | 索引类型 | Stores NULL 值 | Permits Multiple NULL 值 | IS NULL 扫描类型 | IS NOT NULL 扫描类型 |
|---|---|---|---|---|---|
| Primary key | BTREE | No | No | N/A | N/A |
| Unique | BTREE | Yes | Yes | Index | Index |
| Key | BTREE | Yes | Yes | Index | Index |
| Primary key | HASH | No | No | N/A | N/A |
| Unique | HASH | Yes | Yes | Index | Index |
| Key | HASH | Yes | Yes | Index | Index |
NDB 存储引擎:
| 索引类别 | 索引类型 | Stores NULL 值 | Permits Multiple NULL 值 | IS NULL 扫描类型 | IS NOT NULL 扫描类型 |
|---|---|---|---|---|---|
| Primary key | BTREE | No | No | Index | Index |
| Unique | BTREE | Yes | Yes | Index | Index |
| Key | BTREE | Yes | Yes | Index | Index |
| Primary key | HASH | No | No | Table | Table |
| Unique | HASH | Yes | Yes | Table | Table |
| Key | HASH | Yes | Yes | Table | Table |
关于更多用法介绍,你可以找到参考内容:
8.3.9 Comparison of B-Tree and Hash Indexes
12.10 Full-Text Search Functions
13.1 Index Types Per Storage Engine
13.1.15 CREATE INDEX Statement
– 摘自 MySQL8.0 官方文档:refman-8.0-en
2 MySQL 如何使用索引
InnoDB 存储引擎 Information Schema 一些视图脚本名称更新(MySQL8.0.3 或者更高版本):
| 旧名称 | 新名称 |
|---|---|
| INNODB_SYS_COLUMNS | INNODB_COLUMNS |
| INNODB_SYS_DATAFILES | INNODB_DATAFILES |
| INNODB_SYS_FIELDS | INNODB_FIELDS |
| INNODB_SYS_FOREIGN | INNODB_FOREIGN |
| INNODB_SYS_FOREIGN_COLS | INNODB_FOREIGN_COLS |
| INNODB_SYS_INDEXES | INNODB_INDEXES |
| INNODB_SYS_TABLES | INNODB_TABLES |
| INNODB_SYS_TABLESPACES | INNODB_TABLESPACES |
| INNODB_SYS_TABLESTATS | INNODB_TABLESTATS |
| INNODB_SYS_VIRTUAL | INNODB_VIRTUAL |
tips:如果你升级到 MySQL8.0.3 或者更高版本:会发现与 MySQL 绑定的 zlib 库版本从版本 1.2.3 提升到版本 1.2.11。
2.1 InnoDB 存储引擎索引
一般情况,针对 InnoDB 存储引擎进行描述索引使用,因为 MySQL5.5.5 开始默认存储引擎是 InnoDB。
InnoDB 存储引擎支持索引:
- B-tree indexs(B + 树索引);
- Full-text search indexes(全文索引):需要在 MySQL5.6 或者更高的版本中使用。
本不支持 HASH indexs(NO Support),用户没有控制权,但 InnoDB 内部利用哈希索引来实现自适应哈希索引特性。
B + 树索引是传统意义上的索引,目前关系型数据库系统中查找最为常用和最为有效地的引。B + 树索引构造类似于二叉树,根据键值(Key Value)快速查找数据。
注意:B + 树中的 B 不是代表二叉树(binary),而是平衡树(balance),因为 B + 树是从平衡二叉树演化而来,但 B + 树也不是一个二叉树。B + 树索引并不能找到一个给定键值的具体行,能找到的是被查找数据行所在页。然后数据库通过将页读到内存,再从内存中进行查找,最后得到要查找的数据。
上面简单介绍了下 InnoDB 存储引擎支持的索引,以及部分新特性,以及 B + 树索引。如果想深入理解 B + 树索引,可以从算法角度去分析,但不是此次内容的重点,可以私下查找文档去了解。接着讨论如何使用索引。
2.2 MySQL 中使用索引典型场景
- 匹配全值(Match the whole value)。对索引中所有列都指定具体指,即索引所有列都有等值匹配条件。
- 匹配范围查询(March range)。对索引值能够进行范围查找。
- 匹配最左前缀(Matches the leftmost prefix)。仅仅使用到索引中的最左边列进行查找。
- 仅仅对索引查询(Only for index queries)。当查询列都在索引字段中,查询效率更高。
- 匹配列前缀(Match a column prefix ),仅仅使用索引中的第一列,并且只包含索引第一列开头一部分。
- 索引匹配部分精确,其它部分范围匹配( Match a part)。
- 如果列名是索引,使用 column_name is null 就会使用索引。
以上是对 7 种使用到索引场景进行说明,下面将使用 explain 执行计划进行详细示例。
匹配全值(Match the whole value)。对索引中所有列都指定具体指,即索引所有列都有等值匹配条件。
1 | mysql> explain select * from sakila.rental where rental_date='2005-05-27 07:33:54' and customer_id=134 and inventory_id=360\G |
通过观察 explain 输出结果,发现 type=const。表示常量;字段 key 值为 uk_rental_date,表示优化器使用索引 uk_rental_date 进行扫描。
匹配范围查询(March range)。对索引值能够进行范围查找。例如,查找租赁表 rental 中客户编号 customer_id 在指定范围记录:
1 | mysql> explain select * from sakila.rental where customer_id>=366 and customer_id<=399\G |
通过 explain 分析,发现 type=range 以及 Extra: Using index condition。使用到范围性查找,以及索引下放操作。
匹配最左前缀(Matches the leftmost prefix)。仅仅使用到索引中的最左边列进行查找,比如在多个字段(col1、col2、col3)字段上的联合索引能够被包含 col1、(col1、col2)、(col1、col2、col3)的等值查询利用到,但是不能被(col2、col3)、col2 的等值查询利用到。以 sakila 数据库中支付(payment)表进行示例。
下面创建组合索引 idx_payment_date 便于测示:
1 | mysql> ALTER TABLE sakila.payment add index idx_payment_date(payment_date,amount,last_update); |
使用 explain 执行分析:
1 | mysql> explain select * from sakila.payment where payment_date='2005-06-15 21:08:46' and last_update='2005-06-15 21:08:46'\G |
通过观察执行结果,发现 type=ref 以及 Extra: Using index condition,根据最左匹配原则,你会发现 payment_date 处于索引 1 号位,此时扫描利用到组合索引 idx_payment_date。
如果使用 last_update 和 amount 进行测试分析:
1 | mysql> explain select * from sakila.payment where last_update='2005-06-15 21:08:46' and amount=9.99\G |
通过观察查询结果,发现 type=ALL 走全表扫描,索引没有使用到。
仅仅对索引查询(Only for index queries)。当查询列都在索引字段中,查询效率更高。
1 | mysql> explain select last_update from sakila.payment where payment_date='2005-06-15 21:08:46' and amount=9.99\G |
Extra: Using index,意味着现在直接访问索引就足够获取到所有需要的数据,无需索引回表,Using index 也是通常所说的覆盖索引扫描。只访问必须访问的数据,一般而言,减少不必要数据访问可以提高效率。
匹配列前缀(Match a column prefix ),仅仅使用索引中的第一列,并且只包含索引第一列开头一部分。例如,查询出标题是 AGENT 开头的电影信息。
创建列前缀索引:
1 | mysql> create index idx_title_desc_part on sakila.film_text(title(10),description(20)); |
执行 explain 进行分析,注意:在 B-tree 索引中使用时,不要以通配符 % 开头,不然索引会失效。
1 | mysql> explain select title from sakila.`film_text` where title like 'AGENT%'\G |
分析执行计划,看到 idx_title_desc_part 被利用到,type=range,使用范围性查询。Extra: Using where 表示优化器需要通过索引回表查询数据。
索引匹配部分精确,其它部分范围匹配(Match a part)。
1 | mysql> explain select inventory_id from sakila.rental where rental_date='2006-02-14 15:16:03' and customer_id>=300 and customer_id<=400\G |
上面通过 explain 分析,查询出出租日期(rental_date)、指定日期的客户编号(customer_id)指定范围的库存。根据 type=ref,以及 key=uk_rental_date,优化器建议走唯一索引。
如果列名是索引,使用 column_name is null 就会使用索引。
1 | mysql> explain select * from sakila.payment where rental_id is null\G |
通过 explain 执行分析,查询支付表(payment)租赁编号(rental_id)字段为空的记录使用到了索引。
MySQL5.6 以及更高版本支持 Index Condition Pushdown (ICP) 特性,索引条件下放操作,进一步优化了查询。某些情况操作下放到存储引擎。
- ICP 可以用于 InnoDB 和 MyISAM 表,包括分区的 InnoDB 和 MyISAM 表。
- 当需要访问全表时,ICP 用于 range、ref、eq_ref 和 ref 或 null 访问方法。
- 对于 InnoDB 表,ICP 仅用于二级索引(次索引、辅助索引)。ICP 的目标是减少全行读取的数量,从而减少 I/O 操作。对于 InnoDB 聚集索引,完整的记录已经被读取到 InnoDB 缓冲区。在这种情况下使用 ICP 不会减少 I/O。
- 如果在虚拟列上创建二级索引,则不支持 ICP。InnoDB 支持在虚拟列上建立二级索引。
- 引用到子查询条件不能使用操作下放。
- 引用存储函数的条件不支持操作下放,存储引擎无法调用存储函数。
- 使用触发器(触发的条件),不能使用操作下放。
如下示例,查询支付表,强制使用索引查询内容。
1 | mysql> explain select * from sakila.payment force index(fk_payment_rental) where rental_id > 1\G |
经过 explain 分析,看到 Extra 值为 Using index condition,表示 MySQL 使用了 ICP 进一步优化查询,在检索时,将条件 rental_id 过滤操作下推到到存储引擎层来完成,可以降低不必要的 IO 访问。
注意:前缀限制以字节为单位,而 CREATE TABLE、ALTER TABLE 和 CREATE INDEX 语句中的前缀长度,被解析为非二进制字符串类型 (CHAR、VARCHAR、TEXT) 的字符数,和二进制字符串类型 (binary、VARBINARY、BLOB) 的字节数。使用多字节字符集的非二进制字符串列指定前缀长度时,请考虑这一点。
2.3 存在但不能使用索引的场景
- 以 % 开头 LIKE 查询不能够利用 B-Tree 索引。
- 数据类型出现隐式转换时不会使用索引,如果列类型是字符串,使用 where 条件记得将字符常量用引号引起来。
- 复合索引场景下,如果查询条件不包含索引列最左部分,即不满足最左原则(LeftMost),不会利用到复合索引。
- 如果 MySQL 判断使用索引比扫描全表慢,则不会使用索引。
- 使用 OR 分割开的条件,如果 OR 条件前列有索引,OR 后列没有索引,那么涉及到的索引都不会被利用。
以上总结了 5 中存在索引,但不适合使用索引的场景。下面将给出 explain 分析示例。
B-Tree 索引可用于使用 =、>、>=、<、<= 或 BETWEEN 操作符表达式中的列做比较。如果 LIKE 的参数是一个不以通配符开头的常量字符串,则索引也可以用于 LIKE 比较。例如,下面的 SELECT 语句使用索引场景:
以 % 开头 LIKE 查询不能够利用 B-Tree 索引,执行计划中 Key 值为 NULL 表示没有使用索引。如下示例:
1 | -- 没有利用到索引场景 |
第一种场景,使用 explain 执行优化分析后:key=NULL,没有利用到索引。第二种场景,以 % 结束,执行 explain 优化分析,明显索引起作用了,type=range,属于范围性扫描。
因为 B-Tree 索引结构特性,以通配符(%)开头的查询自然无法利用到索引,一般建议使用全文索引(fulltext)来解决类似问题。或者考虑利用 InnoDB 聚簇表特点,采用一种轻量级别解决方式:一般情况,索引比表小,扫描索引比扫描表更快。
数据类型出现隐式转换时不会使用索引,如果列类型是字符串,使用 where 条件记得将字符常量用引号引起来。MySQL 默认将输入的常量值进行转换以后才进行检索。
如下示例:
1 | -- 场景一 |
在场景二中,字符串(char)类型将 1 引起来,通过 explain 分析使用到索引。场景一中没有加引号,索引没有利用,从而走全表扫描。
复合索引场景下,如果查询条件不包含索引列最左部分,即不满足最左原则(LeftMost),不会利用到符合索引:
1 | mysql> explain select * from sakila.payment where amount=9.99 and last_update='2006-02-15 22:12:30'\G |
如果 MySQL 判断使用索引比扫描全表慢,则不会使用索引。比如,返回记录很大,但使用索引扫描更费时间,优化器更倾向于使用全表扫描,这样代价更低,效率更高。(使用 Trace 可以追踪更多信息,前面也提到过)
使用 OR 分割开的条件,如果 OR 条件前列有索引,OR 后列没有索引,那么涉及到的索引都不会被利用。
1 | mysql> explain select * from sakila.payment where customer_id=9 or amount=9.99\G |
因为 OR 后列没有索引,那么后继查询需要走全表扫描。存在全表扫描情况下,也没必要多走一次索引扫描增加磁盘 I/O 访问。如果前面列无索引,后面列有索引,执行结果一样走全表扫描。(在接下来的优化 OR 查询部分,进行了对比)
3 查看索引使用情况
查看 Handler_read_key 值判断索引工作频率,基于键值读取一行的请求数。如果这个值(Handler_read_key)很高,说明您的表在查询时已经建立了适当的索引。读取一行请求数值很低,则表明增加索引改善并不明显,索引没有经常使用。
可以通过 show status like ‘Handler_read%’查询参数值,分析索引使用状况。
1 | mysql> show status like 'Handler_read%'; |
初始时(索引还未工作),上述查询出默认值为零,当你使用索引后,这些参数会有变化。
Handler_read_rnd:基于固定位置读取一行的请求数。如果执行大量需要对结果进行排序的查询,则该值会很高。你可能有大量需要 MySQL 扫描全表的查询,或者你没有合理地使用键连接。
Handler_read_rnd_next:读取数据文件中下一行的请求数。如果要进行大量的表扫描,这个值就会很高。一般来说,这意味着您的表没有正确索引,或者说是写入查询没有利用到索引。
4 MySQL 官方示例数据库
给出 sakila-db 数据库包含三个文件,便于大家获取与使用:
- sakila-schema.sql:数据库表结构;
- sakila-data.sql:数据库示例模拟数据;
- sakila.mwb:数据库物理模型,在 MySQL workbench 中可以打开查看。
只是用于用于简单测试学习,建议使用 world-db:
world-db 数据库,包含三张表:city、country、countrylanguage。
最后附上官方示例数据库,sakila-db 数据库一个非常完整的示例。包含:视图、函数、触发器以及存储过程,当然也存在使用外键。