MySQL 组合索引、唯一组合索引的原理
组合索引
前言
之前在网上看到过很多关于 mysql 联合索引最左前缀匹配的文章,自以为就了解了其原理,最近面试时和面试官交流,发现遗漏了些东西,这里自己整理一下这方面的内容。
什么时候创建组合索引?
当我们的 where 查询存在多个条件查询的时候,我们需要对查询的列创建组合索引。
为什么不对没一列创建索引
- 减少开销
- 覆盖索引
- 效率高
减少开销:假如对 col1、col2、col3 创建组合索引,相当于创建了(col1)、(col1,col2)、(col1,col2,col3)3 个索引。注意,根据最左原则,不会创建(col2, col3)索引!
覆盖索引:假如查询 SELECT col1, col2, col3 FROM 表名,由于查询的字段存在索引页中,那么可以从索引中直接获取,而不需要回表查询。
效率高:对 col1、col2、col3 三列分别创建索引,MySQL 只会选择辨识度高的一列作为索引。假设有 100w 的数据,一个索引筛选出 10% 的数据,那么可以筛选出 10w 的数据;对于组合索引而言,可以筛选出 100w * 10% * 10% * 10% = 1000 条数据。
最左匹配原则
假设我们创建(col1,col2,col3)这样的一个组合索引,那么相当于对 col1 列进行排序,也就是我们创建组合索引,以最左边的为准,只要查询条件中带有最左边的列,那么查询就会使用到索引。
创建测试表
CREATE TABLE student (
id int(11) NOT NULL,
name varchar(16) NOT NULL,
age int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY idx_id_name_age (`id`,name(10),`age`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 建表时,name 长度为 16,这里用 10。这是因为一般情况下名字的长度不会超过 10,这样会加速索引查询速度,还会减少索引文件的大小,提高 INSERT 的更新速度。
-- 如果分别在 id, name, age 上建立单列索引,让该表有 3 个单列索引,查询时和上述的组合索引效率也会大不一样,远远低于我们的组合索引。因为此时虽然有了三个索引,但 MySQL 只能用到其中的那个它认为似乎是最有效率的单列索引。
填充 100w 条测试数据
-- 因为 mysql 一遇到分号,它就要自动执行。因此在输入多条语句的时候要使用 DELIMITER 重新定义结束符号,告诉 mysql 解释器,该段命令是否已经结束了,mysql 是否可以执行了。MYSQL 的默认结束符为";"。
DELIMITER $$
DROP PROCEDURE IF EXISTS pro10$$
CREATE PROCEDURE pro100()
BEGIN
DECLARE i INT;
DECLARE char_str varchar(100) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
DECLARE return_str varchar(255) DEFAULT '';
DECLARE age INT;
SET i = 1;
WHILE i < 1000000 do
SET return_str = substring(char_str, FLOOR(1 + RAND()*62), 8);
SET i = i+1;
SET age = FLOOR(RAND() * 100);
INSERT INTO student (id, name, age) values (i, return_str, age);
END WHILE;
END$$
-- 恢复 MYSQL 的默认结束符为";"
DELIMITER ;
-- 执行写入 100 万条数据
CALL pro100();
场景测试
EXPLAIN SELECT * FROM student WHERE id = 2;
可以看到该查询使用到了索引(type=const&possible_keys=PRIMARY, idx_id_name_age&key=PRIMARY)
EXPLAIN SELECT * FROM student WHERE id = 2 AND name = 'defghijk';
可以看到该查询使用到了索引 (type=const&possible_keys=PRIMARY, idx_id_name_age&key=PRIMARY)
EXPLAIN SELECT * FROM student WHERE id = 2 AND name = 'defghijk' and age = 8;
可以看到该查询使用到了索引(type=const&possible_keys=PRIMARY, idx_id_name_age&key=PRIMARY)
EXPLAIN SELECT * FROM student WHERE id = 2 AND age = 8;
可以看到该查询使用到了索引(type=const&possible_keys=PRIMARY, idx_id_name_age&key=PRIMARY)
EXPLAIN SELECT * FROM student WHERE name = 'defghijk' AND age = 8;
可以看到该查询没有使用到索引 (type=all&possible_keys=null&key=null),类型为 all,查询行数为 4989449,几乎进行了全表扫描,由于组合索引只针对最左边的列进行了排序,对于 name、age 列只能进行全部扫描
EXPLAIN SELECT * FROM student WHERE name = 'defghijk' AND id = 2;
EXPLAIN SELECT * FROM student WHERE age = 8 AND id = 2;
EXPLAIN SELECT * FROM student WHERE name = 'defghijk' and age = 8 AND id = 2;
可以看到如上查询也使用到了索引(type=const&possible_keys=PRIMARY, idx_id_name_age&key=PRIMARY),id 放前面和放后面查询到的结果是一样的,MySQL 会找出执行效率最高的一种查询方式,就是先根据 id 进行查询
总结
如上测试,可以看到只要查询条件的列中包含组合索引最左边的那一列,不管该列在查询条件中的位置,都会使用索引进行查询。
联合唯一索引
例如: tbl_test 表中有 aa,bb 两个字段,如果不希望有 2 条一模一样的记录(即:aa 字段的值可以重复; bb 字段的值也可以重复,但是一条记录(aa, bb)组合值不允许重复),需要给 tbl_test 表添加多个字段的联合唯一索引:
alter table tbl_test add unique index (aa, bb);
这样如果向表中添加相同记录的时候,会返回一下错误信息。
但是配合Insert into…ON DUPLICATE KEY UPDATE…
来使用就不会报错,存在相同的记录,直接忽略。
INSERT INTO unit (id, unitsubclass, name, state)
VALUES('1111','CPU','CPU','0' ) ON DUPLICATE KEY UPDATE unitsubclass=VALUES(unitsubclass),name =VALUES(name),state =VALUES(state);
还有一种情况就是,我们需要为以前的表 创建这个索引,有可能以前的数据中存在重复的记录 那怎么办呢?
alter ignore table tbl_test add unique index (aa,bb);
它会删除重复的记录(会保留一条),然后建立唯一索引,高效而且人性化。
参考资料:
[MySQL组合索引与最左匹配原则详解] https://www.jb51.net/article/157920.htm
[普通索引和组合索引] https://www.jianshu.com/p/40edfbb50046