关于MYSQL索引,前面有一篇文章《MySQL实战45讲》——学习笔记04-05 “深入浅出索引、最左前缀原则、索引下推优化“,本篇文章属于索引知识的补充点并且偏实际应用,主要讨论如何对字符串的表字段加索引;
假设,一个支持邮箱登录的系统的用户表是这么定义的:
- mysql> create table SUser(
- ID bigint unsigned primary key,
- email varchar(64),
- ...
- )engine=innodb;
由于要使用邮箱登录,所以业务代码中一定会出现类似于这样的语句:
mysql> select f1, f2 from SUser where email='xxx';
如果email这个字段上没有索引,那么这个语句就只能做全表扫描;因此,准备考虑对email这个字符串字段加索引;
如果字符串较长,索引字段占用磁盘空间更大,相同的数据页能放下的索引值就越少,可能导致B+树高度较高,这样查询IO次数较多,耗时长,搜索的效率也就会越低;因此,互联网公司一般都有MYSQL规范,要求索引最大长度小于XX字节;当你尝试建索引时,DDL被Daas系统检测不通过提示索引过长时,使用前缀索引就是一种解决方案;
实际上,当未出现性能瓶颈,不需要过度优化,尽量全字段索引,因此本文主要是补充前缀索引知识,实际工作中仅参考即可,无需太过纠结;
MySQL是支持前缀索引的,也就是说,你可以定义字符串的一部分作为索引;默认地,如果你创建索引的语句不指定前缀长度,那么索引就会包含整个字符串;比如,这两个在email字段上创建索引的语句:
- mysql> alter table SUser add index index1(email);
-
- mysql> alter table SUser add index index2(email(6));
第一个语句创建的index1索引里面,包含了每个记录的整个字符串;而第二个语句创建的index2索引里面,对于每个记录都是只取前6个字节;下面是这两个索引在数据结构和存储上的示意图:


从图中你可以出,由于email(6)这个索引结构中每个邮箱字段都只取前6个字节(即:zhangs),所以占用的空间会更小,这就是使用前缀索引的优势;但是,这可能会增加额外的回表次数;接下来看下面这个语句在这两个索引定义下分别是怎么执行的;
select id,name,email from SUser where email='zhangssxyz@xxx.com';
(1)从index1索引树找到满足索引值是’zhangssxyz@xxx.com’的这条记录,取得ID2的值;
(2)到主键上查到主键值是ID2的行,判断email的值是正确的,将这行记录加入结果集;
(3)取index1索引树上刚刚查到的位置的下一条记录,发现已经不满足email = 'zhangssxyz@xxx.com’的条件了,循环结束;
这个过程中,只需要回主键索引取一次数据,所以系统认为只扫描了一行;
(1)从index2索引树找到满足索引值是’zhangs’的记录,找到的第一个是ID1;
(2)到主键上查到主键值是ID1的行,判断出email的值不是’zhangssxyz@xxx.com’,这行记录丢弃;
(3)取index2上刚刚查到的位置的下一条记录,发现仍然是’zhangs’,取出ID2,再到ID索引上取整行然后判断,这次值对了,将这行记录加入结果集;
(4)重复上一步,直到在idxe2上取到的值不是’zhangs’时,循环结束;
在这个过程中,要回主键索引取4次数据,也就是扫描了4行;
通过这个对比,你很容易就可以发现:使用前缀索引后,可能会导致查询语句读数据的次数变多;
但是,对于这个查询语句来说,如果定义的index2不是email(6)而是email(7),也就是说取email字段的前7个字节来构建索引的话,即满足前缀’zhangss’的记录只有一个,也能够直接查到ID2,只扫描一行就结束了;也就是说,使用前缀索引需要注意定义好前缀长度,就可以做到既节省空间,又不用额外增加太多的查询成本;
实际上,我们在建立索引时关注的是区分度,区分度越高越好;因为区分度越高,意味着重复的键值越少;因此,我们可以通过统计索引上有多少个不同的值来判断要使用多长的前缀;
首先,查出这个列上有多少个不同的值,group by 或者 distinct都可以,如:
mysql> select count(distinct email) as L from SUser;
然后,依次选取不同长度的前缀来看这个值,比如我们要看一下 4~7 个字节的前缀索引,可以用下面的语句:
- mysql> select
- count(distinct left(email,4))as L4,
- count(distinct left(email,5))as L5,
- count(distinct left(email,6))as L6,
- count(distinct left(email,7))as L7,
- from SUser;
当然,使用前缀索引很可能会损失区分度,所以你需要预先设定一个可以接受的损失比例,比如5%;然后,在返回的L4~L7中,找出不小于L*95%的值,假设这里L6、L7都满足,你就可以选择前缀长度更小的6;
根据上面的前缀索引的存储结构图,索引树的叶子节点并没有索引字段的完整信息,因此尽管查询内容全部在索引字段上,但由于是字段的前缀索引,因此也需要回到 ID 索引再去查询完整的列信息;也就是说,使用前缀索引就用不上覆盖索引对查询性能的优化了,这也是你在选择是否使用前缀索引时需要考虑的一个因素;
使用前缀索引不仅可能会增加扫描行数,并且无法用到覆盖索引,这都会影响到性能;
除了上述的存在弊端的前缀索引,还有没有方法既可以占用更小的空间,也能达到相同的查询效率?还是有一些别的骚操作,但是也同样存在缺点,建议慎用,这里仅作介绍;
由于身份证前缀是根据省市区号,因此区分度不高;但如果你存储身份证号的时候把它倒过来存,相当于根据用户的出生年月及编号,这样区分度就高了;
每次查询的时候,你可以这么写:
mysql> select field_list from t where id_card = reverse('input_id_card_string');
可以在表上再创建一个整数字段,来保存身份证的校验码,同时在这个字段上创建索引,从而起到把长字符串索引列变短的目的;如下:
mysql> alter table t add id_card_crc int unsigned, add index(id_card_crc);
然后每次插入新记录的时候,都同时用 crc32() 这个函数得到校验码填到这个新字段;由于校验码可能存在冲突,也就是说两个不同的身份证号通过 crc32() 函数得到的结果可能是相同的,所以你的查询语句 where 部分要判断 id_card 的值是否精确相同;这样,索引的长度变成了4个字节,比原来小了很多;
下面分析下使用倒序索引和使用hash字段这两种方法的异同点:
相同点:都不支持范围查询;倒序存储的字段上创建的索引是按照倒序字符串的方式排序的,已经没有办法利用索引方式查出身份证号码在 [ID_X, ID_Y] 的所有市民了;同样地,hash字段的方式也只能支持等值查询;
区别:
(1)从占用的额外空间来看,倒序存储方式在主键索引上,不会消耗额外的存储空间,而hash字段方法需要增加一个字段;
(2)在CPU消耗方面,倒序方式每次写和读的时候,都需要额外调用一次reverse函数,而hash字段的方式需要额外调用一次crc32()函数;如果只从这两个函数的计算复杂度来看的话,reverse函数额外消耗的CPU资源会更小些;
(3)从查询效率上看,使用hash字段方式的查询性能相对更稳定一些;因为crc32算出来的值虽然有冲突的概率,但是概率非常小,可以认为每次查询的平均扫描行数接近1;而倒序存储方式毕竟还是用的前缀索引的方式,也就是说还是会增加扫描行数;
字符串字段加索引的优化思路:
(1)前缀索引:在建立索引的时候指定索引长度,且该长度的字段区分度高;
优点:相比全字段索引,每页存储的索引更多,查询索引IO次数少,效率高;即既节省了内存空间,又提高了查询效率;
缺点:
a.指定索引长度的区分度低的话,扫描主键索引次数就会多,效率低;
b.不会使用覆盖索引,即使索引长度定义为全字段,也会去主键索引查询;
适用场景:前缀索引区分度高;
(2)倒序索引:字段保存的时候反序存储;
优点:同前缀索引;
缺点:
a.只适用于等值查询,不适用于范围、模糊查询;
b.每次保存、查询时需调用reverse()函数,产生额外的CPU资源消耗;
c.若后缀索引区分度低,扫描行数会增多;
适用场景:索引字段后缀区分度高,前缀区分度低;
(3)添加hash字段作为索引:在表中添加一个hash字段并加索引,用于存储索引字段的哈希值如使用crc32()哈希函数,每次查询时先计算出字段的hash值,再利用hash字段查询;可能存在hash冲突,所以where需要加索引字段字段的等值条件;
优点:哈希函数冲突概率低的话,平均扫描行数接近1;
缺点:只适用于等值查询,不适用于范围、模糊查询;
适用场景:只适用于等值查询,不适用范围查询或模糊查询;
问题:如果你在维护一个学校的学生信息数据库,学生登录名的统一格式是”学号@gmail.com",而学号的规则是:十五位的数字,其中前三位是学校所在城市编号、第四到第六位是学校编号、第七位到第十位是入学年份、最后五位是顺序编号;系统登录的时候都需要学生输入登录名和密码,验证正确后才能继续使用系统;就只考虑登录验证这个行为的话,你会怎么设计这个登录名的索引呢?
答案:
(1)优化成本和收益上来看,不需要优化,因为一个学校的总人数这种数据量,50年才100万学生,这个表肯定是小表,为了业务简单,直接存原来的字符串;
(2)尝试用一下本章的优化思想,前6位和邮箱尾缀都是固定的,由于这个学号的规则,无论是正向还是反向的前缀索引,重复度都比较高;学号中间的入学年份+编号辨识度最高,并且可以用用数字类型来存这9位数字,这样只需要占4个字节,如 201100001;其实这个就是一种hash方式,只是它用了最简单的转换规则:字符串转数字的规则;并且这个字段天然的可以保证唯一性,索引辨识度高;