考虑如下场景,一个用户登陆系统需要用用邮箱做登录,如果 email 这个字段上没有索引,那么这个语句就只能做全表扫描。
MySQL 是支持前缀索引的,也就是说,你可以定义字符串的一部分作为索引。默认地,如果你创建索引的语句不指定前缀长度,那么索引就会包含整个字符串。
mysql> alter table SUser add index index1(email);
创建的 index1 索引里面,包含了每个记录的整个字符串
mysql> alter table SUser add index index2(email(6));
ndex2 索引里面,对于每个记录都是只取前 6 个字节
一、如果使用的是 index1(即 email 整个字符串的索引结构),执行顺序是这样的:
1)从 index1 索引树找到满足索引值是’zhangssxyz@xxx.com’的这条记录,取得 ID2 的值;
2)到主键上查到主键值是 ID2 的行,判断 email 的值是正确的,将这行记录加入结果集;
(如果使用 index1(即 email 整个字符串的索引结构)的话,可以利用覆盖索引,从 index1 查到结果后直接就返回了,不需要回到 ID 索引再去查一次。)
3)取 index1 索引树上刚刚查到的位置的下一条记录,发现已经不满足 email='zhangssxyz@xxx.com’的条件了,循环结束。
这个过程中,只需要回主键索引取一次数据,所以系统认为只扫描了一行。
二、如果使用的是 index2(即 email(6) 索引结构),执行顺序是这样的:
1)从 index2 索引树找到满足索引值是’zhangs’的记录,找到的第一个是 ID1;
2)到主键上查到主键值是 ID1 的行,判断出 email 的值不是’zhangssxyz@xxx.com’,这行记录丢弃;
(使用前缀索引就用不上覆盖索引对查询性能的优化)
3)取 index2 上刚刚查到的位置的下一条记录,发现仍然是’zhangs’,取出 ID2,再到 ID 索引上取整行然后判断,这次值对了,将这行记录加入结果集;
4)重复上一步,直到在 idxe2 上取到的值不是’zhangs’时,循环结束。
在这个过程中,要回主键索引取 N 次数据,也就是扫描了N 行。
但是使用前缀索引,如果能定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。我们在建立索引时关注的是区分度,区分度越高越好。因为区分度越高,意味着重复的键值越少。因此,我们可以通过统计索引上有多少个不同的值来判断要使用多长的前缀。
mysql> select count(distinct email) as L from SUser;
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;
针对字符串做索引还有没有其他方案呢,比如以身份证号建立索引:
第一种方式是使用倒序存储。
第二种方式是使用 hash 字段。
mysql>
select field_list from t
where id_card_crc=crc32('input_id_card_string')
and id_card='input_id_card_string'
这两种方法相同点是:都不支持范围查询。倒序存储的字段上创建的索引是按照倒序字符串的方式排序的,已经没有办法利用索引方式查出身份证号码在[ID_X, ID_Y]的所有市民了。同样地,hash 字段的方式也只能支持等值查询。
这两种方法不同点是:
1)从占用的额外空间来看,倒序存储方式在主键索引上,不会消耗额外的存储空间,而 hash 字段方法需要增加一个字段。当然,倒序存储方式使用 4 个字节的前缀长度应该是不够的,如果再长一点,这个消耗跟额外这个 hash 字段也差不多抵消了。
2)在 CPU 消耗方面,倒序方式每次写和读的时候,都需要额外调用一次 reverse 函数,而 hash 字段的方式需要额外调用一次 crc32() 函数。如果只从这两个函数的计算复杂度来看的话,reverse 函数额外消耗的 CPU 资源会更小些。
3)从查询效率上看,使用 hash 字段方式的查询性能相对更稳定一些。因为 crc32 算出来的值虽然有冲突的概率,但是概率非常小,可以认为每次查询的平均扫描行数接近 1。而倒序存储方式毕竟还是用的前缀索引的方式,也就是说还是会增加扫描行数。




