这先要从InnoDB的索引实现说起,InnoDB有两大类索引:
聚集索引(clustered index)
普通索引(secondary index)
InnoDB聚集索引的叶子节点存储行记录,因此, InnoDB必须要有,且只有一个聚集索引:
如果表定义了PK,则PK就是聚集索引;
如果表没有定义PK,则第一个not NULL unique列是聚集索引;
否则,InnoDB会创建一个隐藏的row-id作为聚集索引;
所以PK查询非常快,直接定位行记录。
InnoDB普通索引的叶子节点存储主键值。
注意,不是存储行记录头指针,MyISAM的索引叶子节点存储记录指针。
举个栗子,不妨设有表:
t(id PK, name KEY, sex, flag); # id是聚集索引,name是普通索引。
# 表中有四条记录:
1, shenjian, m, A
3, zhangsan, m, A
5, lisi, m, A
9, wangwu, f, B
两个B+树索引分别如上图:
id为PK,聚集索引,叶子节点存储行记录;
name为KEY,普通索引,叶子节点存储PK值,即id;
从上图可知普通索引无法直接定位行记录,通常情况下,需要扫码两遍索引树。
例如:
select * from t where name='lisi';
是如何执行的呢?
如粉红色路径,需要扫码两遍索引树:
先通过普通索引定位到主键值id=5;
在通过聚集索引定位到行记录;
这就是所谓的回表查询,先定位主键值,再定位行记录,它的性能较扫一遍索引树更低。
回表查询概念引出了数据查询的优化概念:索引覆盖。
具体看下面内容
查询条件走索引,且查询字段包含于走索引的查询条件字段中;
只需要在一棵索引树上就能获取SQL所需的所有列数据,无需回表,速度更快;
在explainz执行计划中的表现为Extra字段为Using index时,能够触发索引覆盖。
表结构:
CREATE TABLE `users` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`email_verified_at` timestamp NULL DEFAULT NULL,
`password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `users_email_unique` (`email`)
) ENGINE=InnoDB AUTO_INCREMENT=202001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
表内数据行数为202001行
以下面sql语句为例:
# 不能索引覆盖,需要回表
select * from users where email = '[email protected]';
# 索引覆盖,不需回表
select email from users where email = '[email protected]';
执行时间比较:
执行计划比较:
explain select * from users where email = '[email protected]';
explain select email from users where email = '[email protected]';
常见的方法是:将被查询的字段,建立到联合索引里去。
索引覆盖时执行计划Extra项会显示:Using index。
当不能索引覆盖需要回表时,执行计划Extra选线显示:Using index condition;这个特性是5.6版本后新加的,我执行时发现回表时,执行计划Extra选线显示为null,我的mysql是5.7版本,不知道为什么没有显示Using index condition。
注意: 不能为了索引覆盖而建立太大的复合索引,索引本身也是消耗资源的。