环境: MySQL 5.7.24, for linux-glibc2.12 (x86_64)
索引是为了 提高数据查询 的效率,MySQL 中,索引是在存储引擎层1实现的。
假设,表T有一个主键ID,表中有字段k,并且在k上有索引。新增5条R1~R5记录,如下
-- CREATE
mysql> create table T(
id int primary key,
k int not null,
name varchar(16),
index k(k)) engine=InnoDB;
-- INSERT
mysql> insert T(`id`, `k`, `name`) values
(100,1,'nm1'),
(200,2,'nm2'),
(300,3,'nm3'),
(500,5,'nm5'),
(600,6,'nm6');
表T的主键ID和k索引的两棵树示例如下:
如果执行以下SQL语句,需要执行几次树的搜索操作,会扫描多少行?
mysql> select * from T where k between 3 and 5;
这条SQL查询语句的执行流程简化如下:
步骤 | 执行动作 |
---|---|
1 | 在k索引树 上查找k=3 的记录,取得ID=300 |
2 | 回到ID索引树 查到ID=300 对应的记录R3 |
3 | 在k索引树 取下一个值k=5 的记录,取得ID=500 |
4 | 再回到ID索引树 查到ID=500 对应的记录R4 |
5 | 在k索引树 取下一个值k=6 的记录,不满足where 条件,循环结束 |
以上的查询步骤中,读了k索引树
的3条记录(步骤1、3、5),回表了2次(步骤2、4)。
回到主键索引树搜索的过程,成为回表。
覆盖索引 可以减少树的搜索次数,显著提升查询性能。所以使用覆盖索引是一个常用的性能优化手段。
如果执行以下SQL语句select ID
,因为只需查ID
的值,而ID
的值已经在k索引树
上,因此可以直接提供查询结构,不需要回表。也就是说,索引k
已经覆盖了查询需求,称之为覆盖索引。
mysql> select ID from T where k between 3 and 5;
-- select * from T where k between 3 and 5;
索引k
上其实读了3个记录,R3~R5(对应的索引k
上的记录项),但是对于 MySQL的Server 层1来说,它就是找存储引擎拿到了2条记录,因此MySQL认为扫描行数是2。B+树 这种索引结构,可以利用索引的最左前缀,来定位记录。
CREATE TABLE `tuser` (
`id` int(11) NOT NULL,
`id_card` varchar(32) DEFAULT NULL,
`name` varchar(32) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`ismale` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `id_card` (`id_card`),
KEY `name_age` (`name`,`age`)
) ENGINE=InnoDB
在实际业务中,如果为每一种查询都设计一个索引,索引就太多了;但如果查询在业务中出行的概率不高,但有这种业务,单独为一个不频繁的请求创建一个索引,有些浪费。这种情况可以利用索引的最左前缀规则。
为了直观说明这个概念,用name_age
(name
,age
)这个联合索引来分析。
索引项是按照索引定义里出现的字段的顺序排序的。
当需求是查询所有姓氏是"张"的人,SQL语句条件是where name like '张%'
,这时,能够用上name_age
(name
,age
)这个索引,查找到第一个符合条件的记录是ID3
,然后向后遍历,直到不满足条件为止。
在建立联合索引的时候,如何安排索引内的字段顺序?评估的标准是,索引的复用能力。
如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。
如果既有联合查询,又有基于name
,age
各自的查询呢?例如,查询条件里只有age
的需求,是无法使用name_age
(name
,age
)这个索引的。这时,需要同时维护两个索引因为name
字段是比age
字段大的,所以建议的2个索引是name_age
(name
,age
)联合索引、age
(age
)的单字段索引。
这时候需要考虑的原则就是空间了。
不符合最左前缀规则的,会怎么样呢?
仍以上述表中的name_age
(name
,age
)这个联合索引为例,如果现在有个需求,需要检索出表中"姓氏为张,且年龄是10岁的所有男孩"。那么SQL语句可以这样写:
mysql> select * from tuser where name like '张%' and age=10 and ismale=1;
这个SQL语句,根据最左前缀规则,所以在搜索name_age索引树
的时候,只能用"张",找到第一个满足条件的记录ID3
。当然,这也不错,总比全表扫描要好。
接下来,要判断其他条件是否满足。
MySQL 5.6之前,只能从
ID3
开始一个个回表,到主键索引上找出数据行,再对比字段值。
MySQL 5.6引入索引下推(index condition pushdown)优化,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
name_age
(name
,age
)索引里age
的值,只是按顺序把name
的第一个字是"张"的记录一条条取出来回表。因此,需要回表4次。name_age
(name
,age
)索引内部就判断了age
的值是否等于10,对于不等于10的记录,直接判断并跳过。只需要对等于10的ID4
、ID5
取出来回表。因此,需要回表2次。覆盖索引、前缀索引、索引下推的概念。在满足需求的情况下,尽量少的访问资源是数据库设计的重要原则之一。
在使用数据库时,尤其在设计表结构时,也要以减少资源消耗作为目标。
-- 为什么表数据删掉一半,表文件大小不变?
alter table T engine=InnoDB;
《高性能MySQL》
《MySQL实战45讲》 作者:丁奇
之前学习了大神丁奇的《MySQL实战45讲》,目前在看《高性能高MySQL》,也想自己整理一下MySQL知识点,发现力不从心,也发现大神之所以是大神,那是因为真的牛。
推荐大家还是去学习丁奇的《MySQL实战45讲》,条理清晰,循序渐进,深入浅出,通俗易懂。而且每一讲后面都有高质量的留言评论, 从中能获益良多。感谢!
MySQL基础架构 ↩︎ ↩︎