一、什么是回表查询?
通俗的讲就是,如果索引的列在 select
所需获得的列中(因为在 mysql 中索引是根据索引列的值进行排序的,所以索引节点中存在该列中的部分值)或者根据一次索引查询就能获得记录就不需要回表,如果 select 所需获得列中有大量的非索引列,索引就需要到表中找到相应的列的信息,这就叫回表。
InnoDB聚集索引的叶子节点存储行记录,因此, InnoDB必须要有,且只有一个聚集索引:
(1)如果表定义了主键,则PK就是聚集索引;
(2)如果表没有定义主键,则第一个非空唯一索引(not NULL unique)列是聚集索引;
(3)否则,InnoDB会创建一个隐藏的row-id作为聚集索引;
先创建一张表,sql 语句如下:
create table xttblog(
id int primary key,
k int not null,
name varchar(16),
index (k)
)engine = InnoDB;
然后,我们再执行下面的 SQL 语句,插入几条测试数据。
INSERT INTO xttblog(id, k, name) VALUES(1, 2, 'xttblog'),
(2, 1, '业余草'),
(3, 3, '业余草公众号');
假设,现在我们要查询出 id 为 2 的数据。那么执行 select * from xttblog where ID = 2; 这条 SQL 语句就不需要回表。原因是根据主键的查询方式,则只需要搜索 ID 这棵 B+ 树。主键是唯一的,根据这个唯一的索引,MySQL 就能确定搜索的记录。
但当我们使用 k 这个索引来查询 k = 2 的记录时就要用到回表。select * from xttblog where k = 2; 原因是通过 k 这个普通索引查询方式,则需要先搜索 k 索引树,然后得到主键 ID 的值为 1,再到 ID 索引树搜索一次。这个过程虽然用了索引,但实际上底层进行了两次索引查询,这个过程就称为回表。
也就是说,基于非主键索引的查询需要多扫描一棵索引树。因此,我们在应用中应该尽量使用主键查询。
我这里表里的数据量比较少,如果数据量大的话,你能很明显的看出两次查询所用的时间,很明显使用主键查询效率更高。
更多如下图:
(1)先通过普通索引定位到主键值id=5;
(2)在通过聚集索引定位到行记录;
这就是所谓的回表查询,先定位主键值,再定位行记录,它的性能较扫一遍索引树更低
小总结
使用聚集索引(主键或第一个唯一索引)就不会回表,普通索引就会回表。
二、什么是索引覆盖?
只需要在一棵索引树上就能获取SQL所需的所有列数据,无需回表,速度更快。
explain
的输出结果Extra
字段为Using index
时,能够触发索引覆盖。
三、如何实现索引覆盖?
1、常见的方法是:将被查询的字段,建立到联合索引里去。
例子
create table user (
id int primary key,
name varchar(20),
sex varchar(5),
index(name)
)engine=innodb;
第一个sql:
select id,name from user where name='shenjian';
能够命中name索引,索引叶子节点存储了主键id,通过name的索引树即可获取id和name,无需回表,符合索引覆盖,效率较高。
Extra:Using index。
第二个sql:
select id,name,sex from user where name='shenjian';
能够命中name索引,索引叶子节点存储了主键id,没有储存sex,sex字段必须回表查询才能获取到,不符合索引覆盖,需要再次通过id值扫描聚集索引获取sex字段,效率会降低。
Extra:Using index condition。
如果把(name)单列索引升级为联合索引(name, sex)就不同了。
create table user1 (
id int primary key,
name varchar(20),
sex varchar(5),
index(name, sex)
)engine=innodb;
可以看到:
select id,name … where name=‘shenjian’;
select id,name,sex … where name=‘shenjian’;
单列索升级为联合索引(name, sex)后
,索引叶子节点存储了主键id,name,sex,都能够命中索引覆盖,无需回表。
画外音,Extra:Using index。
四、哪些场景可以利用索引覆盖来优化SQL?
场景1:全表count查询优化
原表为:
user(PK id, name, sex);
直接:
select count(name) from user;
不能利用索引覆盖。
添加索引:
alter table user add key(name);
就能够利用索引覆盖提效。
场景2:列查询回表优化
这个例子不再赘述,将单列索引(name)升级为联合索引(name, sex),即可避免回表。
场景3:分页查询
将单列索引(name)升级为联合索引(name, sex),也可以避免回表。
五、什么是索引下推
索引下推
(Index condition pushdown)简称ICP,是一种优化数据库查询的技术,它利用了数据库索引的特性,在一定条件下,在索引层面就过滤掉不需要的数据,从而减少查询时需要访问的数
据块,提高查询效率。
在普通的查询中,数据库需要先从表中读取所有的数据记录,然后再根据查询条件过滤不需要的记录,最后返回查询结果。而在索引下推中,数据库会在索引树的节点上进行条件过滤,只将满足条件的数据块返回,而不是读取整个数据记录。这样可以避免从磁盘读取不必要的数据,降低IO开销,提升查询速度。
索引下推的主要优点是减少了回表操作,即减少了访问磁盘的次数和需要传输的数据量,从而提高了查询效率和响应速度。具体来说,如果查询条件涉及到的字段都可以通过索引直接获取,而不需要回表操作,那么查询速度将大大提高。
需要注意的是,索引下推并不是适用于所有类型的查询,它涉及到查询中所使用的索引类型和查询条件的限制。通常,只有涉及到等值查询或范围查询的情况下,才能使用索引下推技术实现优化。同时,索引下推也会产生额外的开销,需要消耗更多的CPU资源,因此需要在实际应用中进行评估和优化。
MySQL的大概框架为:
索引下推的下推其实就是指将部分上层(服务层)负责的事情,交给了下层(引擎层)去处理。
假设有这样一个用户表:
创建一个联合索引(age, birthday),并查询出年龄>20,且生日为03-01的用户:
select * from user where age>20 and birthday=“03-01”
为在没有索引下推的情况下,执行步骤如下:
- 存储引擎根据索引查找出age>20的用户id,分别是:4,5,7
- 存储引擎到表格中取出id in (4,5,7)的3条记录,返回给服务层
- 服务层过滤掉不符合birthday="03-01"条件的记录,最后返回查询结果为id=4的1行记录。
如果开启了索引下推优化,执行步骤如下:
- 存储引擎根据索引查找出age>20的用户id,并使用索引中的birthday字段过滤掉不符合birthday="03-01"条件的记录,最后得到id=4;
- 存储引擎到表格中取出id=4的1条记录,返回给服务层;
- 服务层过滤掉不符合birthday="03-01"条件的记录,最后返回查询结果为id=4的1行记录。
- 启用索引下推后,把where条件由MySQL服务层放到了存储引擎层去执行,带来的好处就是存储引擎根据id到表格中读取数据的次数变少了。在上面这个例子中,没有索引下推时需要多回表查询2次。并且回表查询很可能是离散IO,在某些情况下,对数据库性能会有较大提升。
假设有这么个需求,查询表中“名字第一个字是张,性别男,年龄为10岁的所有记录”。那么,查询语句是这么写的
mysq> select * from tuser where name like '张 %' and age=10 and ismale=1;
根据前面说的“最左前缀原则”,该语句在搜索索引树的时候,只能匹配到名字第一个字是‘张’的记录(即记录ID3),接下来是怎么处理的呢?当然就是从ID3开始,逐个回表,到主键索引上找出相应的记录,再比对age和ismale这两个字段的值是否符合。
但是!MySQL 5.6引入了索引下推优化,可以在索引遍历过程中,对索引中包含的字段先做判断,过滤掉不符合条件的记录,减少回表字数。
下面图1、图2分别展示这两种情况。
图 1 中,在 (name,age) 索引里面我特意去掉了 age 的值,这个过程 InnoDB 并不会去看 age 的值,只是按顺序把“name 第一个字是’张’”的记录一条条取出来回表。因此,需要回表 4 次。
图 2 跟图 1 的区别是,InnoDB 在 (name,age) 索引内部就判断了 age 是否等于 10,对于不等于 10 的记录,直接判断并跳过。在我们的这个例子中,只需要对 ID4、ID5 这两条记录回表取数据判断,就只需要回表 2 次。
总结
如果没有索引下推优化(或称ICP优化),当进行索引查询时,首先根据索引来查找记录,然后再根据where条件来过滤记录;在支持ICP优化后,MySQL会在取出索引的同时,判断是否可以进行where条件过滤再进行索引查询,也就是说提前执行where的部分过滤操作,在某些场景下,可以大大减少回表次数,从而提升整体性能。