请支持正版:MySQL实战45讲
现在,几乎是所有的系统都支持邮箱登录了,如何在邮箱这样的字段上建立合理的索引,是我们今天要讨论的问题
假设,现在在维护一个支持邮箱登录的系统,用户表是如此定义的:
mysql>create table SUser(
ID bigint unsigned primaey key,
email varchar(64),
...
)engine=innodb;
由于要使用邮箱登录,所以业务中一定会出现如下的代码:
mysql>select f1, f2 from SUser where email = 'xxxx';
在先前讲索引的文章中,如果字段email字段上没有索引,那么这个语句就只能全表扫描
当然,因为MySQL支持前缀索引,所以,可以定义字符串的一部分作为索引,默认地,如果你创建索引的语句不知道前缀长度,那么索引就会包括整个字符串,如:
mysql>alter table SUser add index index1(email);
# 或
mysql> alter table SUser add index index2(email(6));
第一个语句创建的index1索引里,包括了每个记录的整个字符串,而语句二只包含前6个字符
那么,现在的问题是,这两种不同的定义在数据结构和存储上有什么区别呢?
答案显而易见:优点是减少磁盘占用,缺点是可能会增加额外的扫描次数
那么我们看一个查询语句:
select id, name, email from SUser where email='[email protected]';
如果使用的是index1,那么执行顺序如下:
这个过程中,只需要回表一次即可,所以系统认为只扫描了一行
那么,如果是index2,执行顺序如下:
在这个过程中,回表的次数却决于普通索引树匹配的长度,也就是,如果尽可能的长,那么就更容易匹配到正确结果,相应的代价就是更多的磁盘占用
所以,使用前缀索引,定义一个好的长度,就可以做到既省空间,又减少查询成本,那么如何确定应该使用多长的前缀呢?
实际上我们只需要在创建索引的时候,注意区分度即可,区分度越高,重复值越少
首先,你可以使用以下的语句,算出这个列上有多少个不同的值:
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
前面我们提到了使用前缀索引可能会增加扫描行数,这会影响到性能,其实,前缀索引的影响不止如此,我们再看一下另一个场景
mysql>select id, email from SUser where email = '[email protected]';
与先前的例子:
mysql>select id, name, email from SUser where email = '[email protected]';
相比之下,这个语句只返回id和email字段
所以,如果只使用index1,可以利用覆盖索引,从index1查到结果后就直接返回了,不需要回到ID索引再去查询依次,而如果是index2,就不得不回表,再判断一下email的值
即使你将index2的定义修改为email(8)的前缀索引,这个时候,虽然index2已经包含了所有的信息,但是InnoDB还是要回到ID索引上再查询一下,因为系统并不确定前缀的索引的定义是否截断了完整信息
也就是锁,使用前缀索引就用不上覆盖索引对查询性能的优化了,这也是选择是否使用前缀索引时需要考虑的因素了
对于邮箱这种字段,使用前缀索引的效果可能还不错,但是如果遇到区分度不高的情况,该怎么办?
比如,我们的身份证号,一共18位,其中前6位是地址码,所以同一个人的县的身份证号前6位一般会是相同的
假设现在在维护一个市民信息系统,这个时候如果对身份证号做长度为6的前缀索引的话,那么区分度就非常低
按照先前的方法,需要创建12以上的索引对比才能直到区分度大概划分
但是,索引越长,占用的磁盘空间越大,相同的数据页能放下的索引值就越少,搜索的效率也会越低
那么,如果我们能够确定业务需求里面只有按照身份证进行等值查询的需求,还有没有别的处理方法呢?
第一种方法:使用倒序存储,如:
mysql>select field_list from t where id_card = reverse('input_id_card_string');
由于身份证号后6位没有地址码这样的重复逻辑,所以最后6位很可能就提供了足够的区分度,当然了,实践的话,还是要count验证一下
第二种:使用hash字段,可以在表上创建一个整数字段,来保存身份证的校验码,同时在这个字段上创建索引
mysql>alter table t add id_card_crc int unsigned, add index(id_card_crc);
然后每次插入新记录的时候,都同时用crc32()这个函数得到hash后的结果,也就是校验码,然后把校验码填到这个新字段,由于校验码可能存在冲突,也就是说,两个不同的身份证号通过crc32()函数可能得到的结果是一样的,所以,你的查询语句的where部分要判断id_crad的只是否精确相同
mysql>select field_list from t where id_card_crc = crc32('input_id_card_string') and id_card = 'xxx'
这样,索引的长度就变成了4个字节,比原先小了许多
相同点:
区别:
特别的,crc32冲突的话,是用的拉链法,似乎都倾向于使用拉链法解决冲突?