MySQL之如何对字符串字段设计索引?

1,019 阅读4分钟

这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战

在实际应用场景中,存在给字符串设置索引的情况。如果是给长字符串设置索引,例如邮箱、身份证等信息。那么设置的索引占用内存空间是会很大的。

字符串索引设计方案

前缀索引

MySQL支持前缀索引,即可以定义字符串的一部分作为索引。通过设置前缀索引,可以有效减少索引占用的空间。

但设置前缀索引也是有成本的,如果设置的前缀区分度不高,意味着扫描行数可能也会增多。以下面的一个例子来说明。

假设有个用户邮箱表。

idemail
1zhangsan01@qq.com
2zhangsan011@qq.com
3zhangsan02@qq.com
....

假设有两种索引情况,一种是全字符串索引,即不设前缀。还有一种是前缀索引。

alter table UserEmail add index index1(email);
alter table UserEmail add index index1(email(7));

那么对于索引index1,叶子节点的值就是 email字段;对于索引index2,叶子节点的值就是email字段前7个字节,即 zhangsan。(当然叶子节点还有主键的)

那么,对于查询语句

select * from UserEmail where email='zhangsan02@qq.com';

如果使用索引index1,查询步骤可分解为:

  • 在索引树index1下找到zhangsan02@qq.com的记录,取得主键Id=3;
  • 回表到主键上找到对应的行,判断email是否正确,将这条记录加入结果集;
  • 继续查询index1索引树下刚才位置的下一条记录,直到email值不等于查询的参数,条件结束。

这个过程中,只需要回表一次就能取得数据,只扫描了一行。

如果使用索引index2,查询步骤就变为:

  • 在索引树index2中找到等于zhangsan的记录(即email参数的前7个字节),找到Id=1的记录;
  • 回表,发现email值不对,继续下一个匹配;
  • 找到Id=2的记录,回表,还是不对,继续下一个匹配;
  • 找到Id=3的记录,回表,值对了,继续下一个匹配;
  • 重复上一步,直到index2索引节点的值不等于zhangsan即停止匹配,返回结果集。

从这里看出,使用email的前7个字节作为索引,所扫描的行数需要3行。

得出结论,使用前缀索引,可能会导致查询语句读数据的次数变多。

所以使用前缀索引,定义好前缀长度是很有必要的,既可以节省内存,也可以实现减少扫描的行数。

可以通过使用以下SQL语句进行索引的区分度比较,索引区分度越高,说明重复的键值就越少。

> select count(distinct email) as Len from UserEmail;
> select 
    count(distinct left(email,4)) as Len4,
    count(distinct left(email,5)) as Len5,
    count(distinct left(email,6)) as Len6
from UserEmail;

使用前缀索引很可能会损失区分度,所以要预设一个可以接受的损失比例,比如5%。然后在返回的Len4-Len6中,找出不小于Len * 95%的值。

由于使用前缀索引都需要回表进行校验查询值是否正确,所以在使用前缀索引时,会用不上覆盖索引对查询性能的优化了。因为覆盖索引的优化就是避免回表。

倒叙索引 和 hash字段

除了使用前缀索引,还有倒叙索引和hash字段可以解决字符串索引占用内存空间大的问题。

倒叙索引,是为了解决前缀索引区分度不大的问题,而后面几个字符恰好区分大较高。使用方式是跟前缀索引类似的,实践中也是需要通过count(distinct) 语句去做验证的。只不过在使用时,需要倒过来查询(插入也是的!)

select * from user where idCard = reverse('5453132123');

Hash字段,通过在表中再创建一个整数字段,来保存字符串的校验码(hash值),同时在这个字段上创建索引。hash值也是有可能冲突的,所以查询的时候需要把查询条件等值匹配;

select * from user where idCard = 21312312 and id_crc=crc('21312312');

这两种方式的特点都是不支持范围查询,只能等值查询。

区别的话,可以从占用的额外空间、CPU消耗、查询效率上做比较。

总结

  • 直接创建完整索引,可能比较占用空间;
  • 创建前缀索引,节省空间,但会增加查询扫描次数,并且不能使用覆盖索引;
  • 倒叙存储,再创建前缀索引,用于绕过字符串本身前缀区分度不够的问题;
  • 创建Hash字段索引,查询性能稳定,有额外的存储和计算消耗,不支持范围查询。