最近学习极客时间上面的专栏,准备把每篇文章写成博客,加上自己的理解,写的循序没有按照专栏的来,会持续跟新!
今天讨论的是如何在邮箱这样的字段上建立合理的索引:
用户表这样定义:
mysql> create table SUser(
ID bigint unsigned primary key,
email varchar(64),
...
)engine=innodb;
如果使用邮箱登录,业务代码中一定会出现类似的语句:
mysql> select f1, f2 from SUser where email='xxx';
如果email这个字段上没有索引,那么这个语句就只能做全表扫描。
同时,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';
如果使用index1 :
1、从index1索引树找到满足索引值是‘zhangssxyz@xxx.com’的这条记录,取得ID2的值
2、到主键上查找主键值得是ID2的行,判断email的值是正确的,将这行记录加入结果集
3、取index1索引树上刚刚查到的位置的下一条记录,发现已经不满足条件了,循环结果结束。
这个过程中,只需要回主键索引取一次数据,所以系统认为只扫描了一行。
如果使用的时index2 :
1、从index2索引树找到满足索引值的是‘zhangs’的记录,找到第一个是ID1;
2、到主键上查到的主键值是ID1的行,判断出的email的值不是’zhangssxyz@xxx.com’,这行记录丢弃;
3、取index2上刚刚查到的位置的下一条记录,发现仍旧是‘zhangs’,取出ID2,再到ID索引上取整行然后判断,这次值就对了,将这行数据加到结果集中。
4、重复上一步,直到idex2上取到的值不是‘zahngs’时,循环结束。
在这个过程中,要回主键索引取4次数据,也就是扫描了4行。
通过这个对比,可以发现,使用前缀索引后,可能会导致查询语句读数据的次数变多。
但是,对于这个查询语句来书,如果你定义的index2不是email(6)而是email(7),也就是说取email字段的前7个字节构建索引的话,即满足‘zhangss’的记录只有一个,也能够直接查到ID2,只扫描一行就结束了。
也就是说使用前缀索,定义好长度,就可以做到节省空间,又不用额外增加太多的查询成本。
问题是:
当给字符串创建索引时,有什么方法能够确定我应该使用多长的前缀呢?
实际上,我们建立索引是关注的时区分度,区分度越高越好,因为区分度越高,意味着重复的值就越少,因此,我们可以听过统计索引上有多少个不同的值来判断要使用多长的前缀。
首先,你可以使用下面的语句,算出这个列上有多少个不同的值:
select count(distinct email) as L from SUser;
然后,依次选取长度的后缀来看这个值,比如看4~7个字节的前缀索引,可以用这个语句:
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。
前缀索引对覆盖索引的影响
前缀索引可能会增加扫描行数,这回印象性能,其实前缀索引的影响不止于此,
第一个sql
select id,email from SUser where email='zhangssxyz@xxx.com';
前面一个sql:
select id,name,email from SUser where email='zhangssxyz@xxx.com';
相比这个sql只要返回id和email字段;
所以,如果使用index1的话,可以利用覆盖索引,从index1查到结果后直接就返回了,不需要回到ID索引再去查一次。如果使用index2的话,就不得不回到ID索引再去判断email字段的值。
即使你将index2的定义修改为email(18)的前缀索引,这时候虽然index2已经包含了所有的信息,但是InnoDB还是要回到id索引在查一次,因为系统不知道是否截取了全部的信息。
也就是说,使用前缀索引就用不上覆盖索引对查询性能的优化了,这也是你在选择是否使用前缀索引时需要考虑的因素。
其他方式:
对于类似邮箱这样的字段来说,使用前缀索引的效果可能不错,但是,遇到前缀的区分苏不够好的情况时,我们该怎么办?
比如我国的身份证号码,前面6位是地址码,所以一个县的人的前6位是一样的。这个索引区分度就非常低了。
按照前面的方法,可能需要创建长度为12的前缀索引才能满足区分度的要求。
但是索引越长,占用的空间就越大,相同的数据页放的数据就越少,搜索的效率就越低
那么什么方法既可以占用更小的空间,又能达到相同的搜索效率。
第一种方式:
倒序存储:如果你存储的身份证号码的时候把它倒过来存,每次查询的时候,你可以写:
select field_list from t where id_card = reverse('input_id_card_string');
由于身份证号的最后6位没有地址码的重复逻辑,所以后6位可以提供足够的区分度。
当然,实践中不要忘了使用count(distinct)方法做个验证。
第二种方式:
hash字段:那可以在表上再创建一个整数字段,来保存身份证的校验码,同时在这个字段上创建索引。
alter table t add id_card_crc int unsigned, add index(id_card_crc);
然后每次插入新记录的时候,都使用crc32()函数得到校验码,由于校验码可能存在冲突,也就是说不同的身份证也可能得到的结果是相同的,所以你查询语句where部分要加上id_card的值是否精确相等。
select field_list from t where id_card_crc=crc32('input_id_card_string') and id_card='input_id_card_string'
这样,索引长度为4个字节,比原来小很多。
接下来看看这两种方式的异同点:
相同点:
都不支持范围查找了;
不同点:
1、从占用额外空间来说,倒序存储方式在主键索引上,不会消耗额外的存储空间,而hash字段方法需要增加一个字段,当然,倒序存储使用4个字节的前缀索引应该是不够的,如果再长一点,这个消耗跟额外这个hash字段也差不多抵消了;
2、在cpu消耗方面;倒序方式每次写和读的时候,都额外调用一次reverse函数,而hash字段的方式需要额外一次调用crc32()函数,如果只从这两个函数的计算复杂度来说,reverse函数 额外消耗的cpu资源会更小一些
3、从查询效率来说,使用hash字段的方式来查询性能相对更稳定一些,因为crc32算出来的值虽然有冲突的概率,但是概率非常小,可以认为每次查询的平均扫描行数接近1.而倒序存储方式毕竟还是使用前缀索引的方式,也就是说还是会增加扫描行数。
小结:
1、直接创建完整索引,这样可能会比较占用空间
2、创建前缀索引,节省空间,但会增加扫描行行数,并且蹦年使用覆盖索引
3、倒序存储,再创建前缀索引,用于绕过字符串本身前缀的区分度不够的问题
4、创建hash字段索引,查询性能稳定,有额外的存储计算的消耗,跟第三种方式一样,不支持范围查询。
来源:极客时间《MySQL实战45讲》
扩展:
关于前缀索引长度的选择方法,可以学习《高性能MySQL》P153 的前缀索引和索引选择性
CRC的概念:
CRC全称为Cyclic Redundancy Check,又叫循环冗余校验。CRC32是CRC算法的一种,常用于校验网络上传输的文件。
CRC32的基本特征:
#1.CRC32函数返回值的范围是0-4294967296(2的32次方减1)
#2.相比MD5,CRC32函数很容易碰撞