本文为《MySQL归纳学习》专栏的第十篇文章,同时也是关于《MySQL索引》知识点的第二篇文章。
相关文章:
MySQL索引大揭秘:优化查询的秘籍(上)
在上一篇文章中,我们介绍了MySQL索引的基础知识,如今,迎来引领数据库查询速度的下篇!从覆盖索引、最左前缀原则,到索引下推,本文将用丰富案例演示,手把手教你优化查询性能,让你的应用轻松应对海量数据。而不容错过的是,还详细讲解了普通索引和唯一索引的区别,让你对索引的理解更上一层楼!
普通索引与唯一索引
我们基于如下表结构进行讲解:
CREATE TABLE t (
id int(11) NOT NULL,
k int(11) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB;
insert into t(id, k) values(1,1),(2,2);
查询过程
普通索引跟唯一索引执行上的区别: 普通索引的等值查询,会继续遍历到第一个不相等的值才会结束,而唯一索引等值查询,命中则结束(性能差距微乎其微)。
InnoDB 按数据页读取数据,当需要读一条记录的时候,并不是将这个记录本身从磁盘读出来,而是以页为单位,将其整体读入内存。
那么普通索引的等值查询又分为哪些情况呢?为什么与唯一索引查询相比性能差距很小?
- 一种要查询的记录不在页尾;
- 一种是恰好在页尾(因索引的有序性,需要读取下一个数据页,此种情况复杂一些)
更新过程
在《Change Buffer内幕:从Merge到持久化的操作解析》一文中详细讲解了 change buffer ,我们再来看看它与普通索引、唯一索引之间的关联关系。
什么条件下可以使用 change buffer 呢?
对于唯一索引来说,所有的更新操作都要先判断这个操作是否违反唯一性约束。
要判断表中是否存在这个数据,而这必须要将数据页读入内存才能判断,如果都已经读入到内存了,那直接更新内存会更快,就没必要使用 change buffer 了。
那么普通索引和唯一索引在处理更新操作有什么区别呢?
如果要在表中插入一个新记录 (4,400) 的话,InnoDB 的处理流程是怎样的。
第一种情况是,这个记录要更新的目标页在内存中。这时,InnoDB 的处理流程如下:
- 对于唯一索引来说,找到 3 和 5 之间的位置,判断到没有冲突,插入这个值,语句执行结束;
- 对于普通索引来说,找到 3 和 5 之间的位置,插入这个值,语句执行结束。
这样看来,普通索引和唯一索引对更新语句性能影响的差别,只是一个判断,只会耗费微小的 CPU 时间。
第二种情况是,这个记录要更新的目标页不在内存中。这时,InnoDB 的处理流程如下:
- 对于唯一索引来说,需要将数据页读入内存,判断到没有冲突,插入这个值,语句执行结束;
- 对于普通索引来说,则是将更新记录在 change buffer,语句执行就结束了。
将数据从磁盘读入内存涉及随机 IO 的访问,是数据库里面成本最高的操作之一。change buffer 因为减少了随机磁盘访问,所以对更新性能的提升是会很明显的。
凡事并无绝对,普通索引和唯一索引的选择还是要结合具体的业务场景。比如说写多读少使用 changebuffer 可以加快执行速度(减少数据页磁盘 io);但是,如果业务模型是写后立马会做查询,则会触发 changebuff 立即 merge 到磁盘, 这样 的场景磁盘 io 次数不会减少,反而会增加 changebuffer 的维护代价。
**在写多读少的场景下,**如果将普通索引改为唯一索引,导致磁盘 IO 次数增多,进而导致某个业务的库内存命中率降低,整个系统处于阻塞状态,更新语句全部堵住。
覆盖索引
假设有如下表结构:
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`city` varchar(16) NOT NULL,
`name` varchar(16) NOT NULL,
`age` int(11) NOT NULL,
`addr` varchar(128) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `city` (`city`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--存储过程
CREATE DEFINER=`root`@`localhost` PROCEDURE `init_t`()
BEGIN
DECLARE i INT DEFAULT 0;
DECLARE j INT DEFAULT 0;
DECLARE k INT DEFAULT 0;
DECLARE city_list VARCHAR(200) DEFAULT '杭州,上海,武汉,北京';
DECLARE name_len INT DEFAULT 0;
DECLARE city VARCHAR(16);
DECLARE namef VARCHAR(16);
DECLARE age INT;
DECLARE addr VARCHAR(128);
SET name_len = LENGTH('abcdefghijklmnopqrstuvwxyz');
-- 第一层循环:按照city遍历
loop_city: LOOP
SET i = i + 1;
SET city = SUBSTRING_INDEX(SUBSTRING_INDEX(city_list, ',', i), ',', -1); -- 获取城市
-- 第二层循环:生成数据
while(k