全值匹配
SELECT * FROM `demo` WHERE a = 1;
在这种类型的查询语句中,如何命中索引非常简单直白,只要查询条件对应的列包含了索引列,就可以使用索引进行查询,所以上面三个查询语句都可以使用索引。而如果查询条件不包含索引列,或者没有命中,就不会使用索引而是通过全表扫描。
注:唯一索引与普通索引的性能对比,由于唯一索引全表只包含一行符合条件的记录,所以定位到索引值后可以立即返回查询结果,而普通索引需要将满足条件的查询结果都返回,这里可能包含多条记录,不过既然是索引,那就都已经做好了排序,因此符合条件的多条记录是扎堆在一起的,只需要通过简单的位移操作即可返回所有满足条件的结果集,除非满足条件的记录特别多,否则,两者的性能差异微乎其微,但是唯一索引在插入或者更新数据时需要额外的开销,所以一般情况下将索引设置为普通索引即可,然后把去重逻辑放到业务代码层实现
联合索引的优势和最左前缀原则
创建联合索引例如(b,c)的好处在于,首先可以减少索引b+tree的数量,减少维护成本,其次如果在已知 b 列值的情况下,再去过滤 c 列值就可以应用二分查找进行非常高效的查询了,并且如果查询字段只包含主键和索引字段的情况下,也可以避免回表操作
最后,对于联合索引的使用,查询条件不区分索引列的先后顺序,比如如下这条 SQL 语句,也会命中联合索引,这是因为在 InnoDB 存储引擎之上的 MySQL Server 层,有一个优化器对 SQL 语句进行优化,从而生成最优的执行计划。
字符串模糊查询
除了联合索引之外,最左前缀原则在数据库的字符串查询场景中也有体现,比如在模糊查询中,只有形如 WHERE d LIKE xxx% 这样的查询条件才能应用设置在 d 列上的索引。
其实细想一下背后的原因,字符串列值的排序和联合索引很像:从第一个字符开始比较,如果第一个字符相同,再看第二个,依次类推。。。把这里的字符对应联合索引中的列值,可不就是一个模子刻出来的嘛!
也就是说,对于应用了索引的字符串列而言,前 N 个字符都是排序好的,这也是为什么通过 'xxx%' 可以使用索引,而 %xxx 不行的原因,就好比 WHERE b = ? 可以使用索引,而 WHERE c = ? 不行。当然,如果你的业务逻辑需要匹配的是 %xxx 后缀,比如域名、邮箱字段,可以将字符串逆序存放到数据库,这样,就可以使用 xxx% 使用索引进行匹配了。
字符串前缀索引
此外,我们可以为字符串列设置指定长度的前缀索引,以便缩短索引长度,提高查询效率,降低空间成本:
CREATE TABLE `demo` (
`a` int(10) unsigned NOT NULL,
`b` int(10) unsigned NOT NULL,
`c` int(10) unsigned NOT NULL,
`d` varchar(32) NOT NULL,
PRIMARY KEY (`a`),
KEY `b_c` (`b`, `c`),
KEY `d` (d(6))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这里我们通过 d(6) 为 d 列的前 6 个字符创建索引,这样一来,初始化数据后,在 d 索引树的叶子节点中,索引值就是 d 列值的前 6 个字符,而不是完整的 d 列值。
至于这个前缀长度如何设置,可以先通过如下语句查询该字段有多少条相同记录:
SELECT COUNT(DISTINCT `d`) AS len FROM `demo`;
如果太多字段值相同不建议使用前缀索引,接下来可以进一步通过如下 SQL 语句获取该字段指定长度前缀有多少条相同记录:
SELECT COUNT(DISTINCT LEFT(`d`,4)) AS len4 FROM `demo`;
SELECT COUNT(DISTINCT LEFT(`d`,5)) AS len5 FROM `demo`;
SELECT COUNT(DISTINCT LEFT(`d`,6)) AS len6 FROM `demo`;
SELECT COUNT(DISTINCT LEFT(`d`,7)) AS len7 FROM `demo`;
SELECT COUNT(DISTINCT LEFT(`d`,8)) AS len8 FROM `demo`;
通过 LIMIT 优化未命中索引的查询
如果 where 条件中的字段可以命中索引,使用 LIMIT 限定可以非常高效地从已经排好序的 B+ 树索引数据返回满足 where 条件之后的限定条数数据,即便是 where 条件中的字段没有命中索引,通过 LIMIT 也可以提升性能。
关于 LIMIT 分页时偏移量过大引起的性能问题优化
不过,即便是 where 语句命中了索引,如果数据量很大的话,LIMIT 在分页查询中有一个非常常见的性能问题,就是随着分页偏移量越来越大,分页查询的性能也越来越低
因为偏移量是相对于 a = 1001 这个匹配到的第一个索引值来的,如果偏移量很大,就又相当于进行全表扫描了,所以这也是为什么你会看到很多大型网站只会提供前面几十页的真实分页,后面的分页查询都是不可用的。
对于这种分页查询,一个优化方式是改变 where 语句中的偏移量起始值,比如对于上面这个 查询,可以这样优化
这样一来,我们就又可以通过 B+ 索引树快速定位到 501001 这个索引值位置,然后在其基础上往后偏移 10 条记录,进行高效的查询了!
排序查询
然后我们来看看排序,排序操作通常是在数据库查询返回的结果集上进行的,如果结果集不大,可以直接在内存中通过快排、归并等排序算法完成,如果数据集很大,内存不能完全加载,还需要借助磁盘空间完成排序操作(这种排序被称作文件排序,显然,涉及到磁盘 IO 的操作性能肯定是不行的),最后把排序结果返回给调用的客户端。
当然,如果 where 条件语句命中了索引,由于 B+ 树索引数据本身已经做好了排序,则可以高效返回排序结果,这里的排序比较规则和索引排序一样,依然遵循设置的字符集和字符集比较规则
对于使用联合索引的字段而言,如果要设定多个排序字段,需要遵循索引列的顺序设置排序字段顺序,这样,才可以使用索引的排序,不过,对于结果集很小的查询,排序本身的开销可以忽略。
不能使用索引排序的几种排序查询
排序字段未出现在 where 语句中
当然如果排序字段没有出现在 where 条件语句中,或者压根没有 where 条件,是无法使用索引已有的排序的:
SELECT * FROM demo ORDER BY a DESC;
(a如果不是主键,将不会用到索引)
联合索引字段混合使用升序和降序
如果联合索引混合使用了升序和降序,则不能直接使用索引排序:
SELECT * FROM demo WHERE a BETWEEN 100000 AND 100010 ORDER BY a, b ASC;
排序包含函数调用
最后,对于使用函数的表达式,也通通不能使用索引排序:
SELECT * FROM demo WHERE d LIKE 'xxx%' ORDER BY upper(`d`) DESC;
这一点,不仅限于排序,如果是 where 语句中调用了函数,则也不能使用索引:
SELECT * FROM demo WHERE lower(`d`) = 'c51ce410c124a10e0db5e4b97fc2af39';