文章目录

  • 11|怎么给字段加索引?
  • 再谈前缀索引
  • 前缀索引对覆盖索引的影响
  • 其他方式
  • 倒序存储和Hash字段的异同点


11|怎么给字段加索引?

再谈前缀索引

现在,几乎是所有的系统都支持邮箱登录了,如何在邮箱这样的字段上建立合理的索引,是我们今天要讨论的问题

假设,现在在维护一个支持邮箱登录的系统,用户表是如此定义的:

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='123456@xxx.com';

如果使用的是index1,那么执行顺序如下:

  1. 从index1索引树上找到满足索引值为’123456@xxxx.com’的这条记录,取得ID2的值
  2. 到主键索引上查找ID为2的行,判断email的值是否正确,将这行记录加入结果集
  3. 取得index1索引树上刚刚查到的位置的下一条记录,发现已经不满足email='123456@xxx.com’的条件了,循环结束

这个过程中,只需要回表一次即可,所以系统认为只扫描了一行

那么,如果是index2,执行顺序如下:

  1. 从index2索引树上找到索引值是’123456’,找到的第一个主键是ID1
  2. 到主键树上找到主键值是ID1的行,判断出email的值,不是则丢弃这行记录
  3. 取index2上刚查到的位置的下一条记录,发现还是’123456’,则继续回表,到主键索引树上取整行,然后判断email,这次如果判断通过,则将这行记录加入结果集中
  4. 重复上一步,直到普通索引树上的值不是’123456’为止,循环结束

在这个过程中,回表的次数却决于普通索引树匹配的长度,也就是,如果尽可能的长,那么就更容易匹配到正确结果,相应的代价就是更多的磁盘占用

所以,使用前缀索引,定义一个好的长度,就可以做到既省空间,又减少查询成本,那么如何确定应该使用多长的前缀呢?

实际上我们只需要在创建索引的时候,注意区分度即可,区分度越高,重复值越少

首先,你可以使用以下的语句,算出这个列上有多少个不同的值:

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 = '123456@xxx.com';

与先前的例子:

mysql>select id, name, email from SUser where email = '123456@xxx.com';

相比之下,这个语句只返回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个字节,比原先小了许多

倒序存储和Hash字段的异同点

相同点:

  • 不支持范围查询,倒序存储的字段上创建的索引已经没法查出一个区间的所有市民了,同样的,hash字段的方式也只支持等值查询

区别:

  • 从占用的额外空间来看,倒序存储的方式在主键索引上,不会消耗额外的存储空间,而hash字段方法需要创建一个新的字段。当然,倒序存储方式使用4个字节的前缀长度应该是不够的,如果再长一些,这个消耗就和hash差不多抵消了
  • 在cpu消耗方面,倒序方式每次读写的时候,都需要额外调用reverse函数,而hash需要额外调用crc32()函数,如果只从两个函数的复杂度来看,reverse函数消耗的资源更少一些
  • 从查询效率看,使用hash查询的性能更加稳定,因为crc32计算出来的值虽然有冲突的概率,但是概率很小,可以认为每次查询平均扫描行数接近1,而倒序存储毕竟还是前缀索引的方式,也就是说,还是可能会增加扫描行数

特别的,crc32冲突的话,是用的拉链法,似乎都倾向于使用拉链法解决冲突