Mysql 覆盖索引及其使用注意事项

一,什么叫覆盖索引

网上对覆盖索引的定义有如下三种:

  • 解释一: 就是select的数据列只用从索引中就能够取得,不必从数据表中读取,换句话说查询列要被所使用的索引覆盖。
  • 解释二: 索引是高效找到行的一个方法,当能通过检索索引就可以读取想要的数据,那就不需要再到数据表中读取行了。如果一个索引包含了(或覆盖了)满足查询语句中字段与条件的数据就叫做覆盖索引。
  • 解释三:是非聚集组合索引的一种形式,它包括在查询里的Select、Join和Where子句用到的所有列(即建立索引的字段正好是覆盖查询语句[select子句]与查询条件[Where子句]中所涉及的字段,也即,索引包含了查询正在查找的所有数据)。

个人认为,覆盖索引,通俗些说,就是能在某次查询中能同时覆盖到所有的查询结果和查询条件的索引。这里澄清下,覆盖索引首先是一个索引;覆盖索引覆盖的是什么呢?覆盖的是所有查要读取的所有的列,同时也覆盖所有的查询条件。

另外有一个争论的问题:一个索引是否是覆盖索引,是针对某次查询而言的?还是一个索引定义之后,它是否是覆盖索引也就确定了?针对这个问题,在几个群里讨论了之后,没有得出一个统一的结论,感觉每一种解释都不和先前网上的一些定义冲突,都能说的过去。本篇更偏向于第一种观点:一个索引是否是覆盖索引,它定义好之后是无法确定是否是覆盖索引的,只有具体到某次查询,才知道这个索引是否同时覆盖了查询结果的列和查询条件,从而做出是否是覆盖索引的判定,所以一个索引是否是覆盖索引是针对某次查询而言的。

在Explain的时候,输出的Extra信息中如果有“Using Index”,就表示这条查询使用了覆盖索引。

使用了覆盖索引的查询也称为索引覆盖查询。

二,覆盖索引的优点:

  覆盖索引是非常有用的工具,能够极大的提高性能。如果SQL查询只需要扫描索引而无需回表,会带来多少好处:

  • 索引条目通常远小于数据行大小,所以如果只需读取索引,那mysql就会极大的减少数据访问量。这对缓存的负载非常重要,因为这种情况下响应时间大部分花费在数据拷贝上。覆盖索引对于I/O密集型的应用也很有帮助,因为索引比数据更小,更容易全部放入内存中。
  • 由于InnoDB的聚簇索引,覆盖索引对InnoDB特别有用。InnoDB的二级索引在叶子节点中保存了行的主键值,所以如果二级主键能够覆盖查询,则可以避免对主键索引的二次查询。减少IO,提高效率。
  • 因为索引是按照列值顺序存储的,所以对于I/O密集型的范围查询会比随机从磁盘读取每一行数据的I/O要少的多。对于某些存储引擎,例如MyISAM,甚至可以通过optimize命令使得索引完全顺序排列,这让简单的范围查询能使用完全顺序的索引访问。

三,覆盖索引的适用范围

不是所有类型的索引都可以成为覆盖索引。覆盖索引必须要存储索引的列值(因为SQL查询结果和查询条件的都涉及到具体的值,而覆盖索引需要覆盖查询结果和查询条件),而哈希索引、空间索引和全文索引等都不存储索引列的值,所以MySQL只能使用B-Tree或B+Tree索引做覆盖索引。另外,不同的存储引擎实现覆盖索引的方式也不同,而且不是所有的引擎都支持覆盖索引。

四, 覆盖索引优化示例

有这么一张表:

CREATE TABLE `user_group` ( 
  `id` int(11) NOT NULL auto_increment, 
  `uid` int(11) NOT NULL, 
  `group_id` int(11) NOT NULL, 
  PRIMARY KEY  (`id`), 
  KEY `uid` (`uid`), 
  KEY `group_id` (`group_id`)
) ENGINE=InnoDB AUTO_INCREMENT=750366 DEFAULT CHARSET=utf8

看AUTO_INCREMENT就知道数据并不多,75万条。然后是一条简单的查询:

SELECT SQL_NO_CACHE uid FROM user_group WHERE group_id = 245;

 该条语句查询需要0.15s左右。但是真实的业务需求比这个复杂,查询需要2.2s.,最终定位到问题症结是在这条SQL。

Explain的结果是:

+----+-------------+------------+------+---------------+----------+---------+-------+------+-------+ 
| id | select_type | table      | type | possible_keys | key      | key_len | ref   | rows | Extra | 
+----+-------------+------------+------+---------------+----------+---------+-------+------+-------+ 
|  1 | SIMPLE      | user_group | ref  | group_id      | group_id | 4       | const | 5544 |       | 
+----+-------------+------------+------+---------------+----------+---------+-------+------+-------+

看上去已经用上索引了,好像已经没有办法优化了。实际上不然,当我们再创建一个如下联合索引时(注意,创建该索引后要把原来的group_id索引删除,因为按照定义的先后顺序,where条件会优先使用group_id索引而不是联系索引查询):

ALTER TABLE user_group ADD INDEX group_id_uid (group_id, uid);

然后,不可思议的事情发生了……这句SQL查询的性能发生了巨大的提升,居然已经可以跑到0.00s左右了。经过优化的SQL再结合真实的业务需求,也从之前2.2s下降到0.05s。

再Explain一次:

+----+-------------+------------+------+-----------------------+--------------+---------+-------+------+-------------+ 
| id | select_type | table      | type | possible_keys         | key          | key_len | ref   | rows | Extra       | 
+----+-------------+------------+------+-----------------------+--------------+---------+-------+------+-------------+ 
|  1 | SIMPLE      | user_group | ref  | group_id,group_id_uid | group_id_uid | 4       | const | 5378 | Using index | 
+----+-------------+------------+------+-----------------------+--------------+---------+-------+------+-------------+

可以看到使用了覆盖索引,MySQL只需要通过索引就可以返回查询所需要的数据,而不必在查到索引之后再去查询数据。

五,使用覆盖索引注意事项

1,当只查询表的id字段时,是索引覆盖查询,如下为索引覆盖查询

SELECT SQL_NO_CACHE id FROM user_group;

2,当where和select查询的列分别创建单独的索引时,不是索引覆盖查询

如id为主键,为uid和group_id分别创建索引时,则如下不是索引覆盖查询:

SELECT SQL_NO_CACHE uid FROM user_group WHERE group_id = 123;

因为该查询是通过group_id索引获取到主键值,通过主键在数据表中获取数据(回表查询),再查询其中uid的值

3,当无where时,select查询的列为主键和另一个索引时,是索引覆盖查询

如id为主键,uid为索引,则如下查询是索引覆盖查询(其中id和uid顺序可以改变):

SELECT SQL_NO_CACHE id, uid FROM user_group;

因为通过uid索引,可以直接获取到uid和id的值,而无需回表查询。

当如上查询有where时,则where条件必须是另一个非主键索引,才是索引覆盖查询

4,select查询的列为两个及两个以上单独加了索引的字段时(或包含非索引字段),不是索引覆盖查询

如id为主键,分别为uid和group_id创建索引,如下查询不是索引覆盖查询

SELECT SQL_NO_CACHE uid,group_id FROM user_group;

因为此时不会使用到索引,而是会做全表查询。

而explain SELECT SQL_NO_CACHE uid,group_id FROM user_group where uid = 2 and group_id = 123;会同时使用两个索引,但也不是索引覆盖查询。

5,如果select查询的列只要都同时属于同一个组合索引中的全部或部分列时,都是索引覆盖查询

5.1 如id为主键,存在组合索引group_id_uid(group_id, uid)时,如下查询都是索引覆盖查询:

SELECT SQL_NO_CACHE group_id FROM user_group;
SELECT SQL_NO_CACHE uid FROM user_group;
SELECT SQL_NO_CACHE uid,group_id FROM user_group;

  原因:MySQL查询优化器会在执行查询前判断是否有一个索引能进行覆盖,如果存在的话,则会使用该索引进行查询。

5.2 当上述带上where条件查询时,如果where后面要是包含除了该联合索引以外的字段(或不符合索引的前缀匹配原则)时,都不是索引覆盖查询(联合索引和主键同时作为查询条件也不行)。

5.3 如果where后面的查询条件都为联合索引中的字段且是符合前缀匹配原则时,是索引覆盖查询,如下是索引覆盖查询:

SELECT SQL_NO_CACHE id, group_id FROM user_group where uid = 2 and group_id = 123

6, where后面的查询条件包含非索引字段 或两个及两个以上的所属不同索引字段 时,都不是索引覆盖查询。

6.1 如group_id为联合索引的前缀,name为一个单独的索引,则如下不是索引覆盖查询

SELECT SQL_NO_CACHE group_id, name FROM user_group where group_id = 123 and name = "test" ;

6.2 分别为uid和group_id创建独立索引, 即使只查询其中一个索引字段的值或id的值,也不是索引覆盖查询。

SELECT SQL_NO_CACHE uid FROM user_group where uid = 2 and group_id = 123;

7,where条件中包含like的操作可能也是索引覆盖查询

如果id为主键,name为索引

7.1 执行如下,关键词前缀匹配查询,只范围查询具有对应前缀的数据值

explain SELECT name FROM `user_group` where name like "test%";

显示结果为 Using where; Using index:

+----+-------------+------------+------------+-------+---------------+------+---------+------+------+----------+--------------------------+
| id | select_type | table      | partitions | type  | possible_keys | key  | key_len | ref  | rows | filtered | Extra                    |
+----+-------------+------------+------------+-------+---------------+------+---------+------+------+----------+--------------------------+
|  1 | SIMPLE      | user_group | NULL       | range | name          | name | 768     | NULL |    1 |   100.00 | Using where; Using index |
+----+-------------+------------+------------+-------+---------------+------+---------+------+------+----------+--------------------------+

7.2 执行如下,关键词非前缀匹配查询,查询所有的name索引值

explain SELECT name FROM `user_group` where name like "%test";
# 或
explain SELECT name FROM `user_group` where name like "%test%";

显示结果为 Using where; Using index:

+----+-------------+------------+------------+-------+---------------+------+---------+------+------+----------+--------------------------+
| id | select_type | table      | partitions | type  | possible_keys | key  | key_len | ref  | rows | filtered | Extra                    |
+----+-------------+------------+------------+-------+---------------+------+---------+------+------+----------+--------------------------+
|  1 | SIMPLE      | user_group | NULL       | index | NULL          | name | 768     | NULL |    3 |    33.33 | Using where; Using index |
+----+-------------+------------+------------+-------+---------------+------+---------+------+------+----------+--------------------------+

则以上都是索引覆盖查询,但是无需回表查询。只是如果查询的列中包含其它非索引的列,可能需要回表查询,则不是索引覆盖查询。

注:以上都是Mysql 5.7.14版本:Server version: 5.7.14 MySQL Community Server (GPL)

参考:

https://www.cnblogs.com/happyflyingpig/p/7662881.html

https://www.cnblogs.com/zl0372/articles/mysql_32.html

  • 1
    点赞
  • 14
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:技术黑板 设计师:CSDN官方博客 返回首页
评论

打赏作者

未名who

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值