@adamhand
2019-02-24T21:53:19.000000Z
字数 2877
阅读 1160
假设有一个支持邮箱登录的系统,用户表定义如下:
mysql> create table SUser(
ID bigint unsigned primary key,
email varchar(64),
...
)engine=innodb;
由于要使用邮箱登录,所以业务代码中一定会频繁出现类似于这样的语句:
mysql> select f1, f2 from SUser where email='xxx';
为了加快查询速度,应该在邮箱这个字段上加上索引,这个索引应该怎么加呢?
如果直接给整个邮箱字符串加上索引,如果这个字符串比较长,可能会占用比较大的空间。MySQL 支持前缀索引,也就是说,可以定义字符串的一部分作为索引。给邮箱字符串创建两种索引的方式如下:
mysql> alter table SUser add index index1(email); //直接创建索引
或
mysql> alter table SUser add index index2(email(6)); //创建前缀索引
这两种索引的存储结构如下:
前缀索引占用的空间会更小,这就是使用前缀索引的优势。但前缀索引也有弱点:可能会增加额外的记录扫描次数。
如果有一个查询语句如下:
select id,name,email from SUser where email='zhangssxyz@xxx.com';
如果使用index1
,查询步骤如下:
index1
索引树找到满足索引值是’zhangssxyz@xxx.com’
的这条记录,取得 ID2
的值;ID2
的行,判断 email
的值是正确的,将这行记录加入结果集;index1
索引树上刚刚查到的位置的下一条记录,发现已经不满足 email='zhangssxyz@xxx.com’
的条件了,循环结束。这个过程中,只需要回主键索引取1
次数据,所以系统认为只扫描了1
行。
如果使用的是 index2
,执行顺序是这样的:
index2
索引树找到满足索引值是’zhangs’
的记录,找到的第一个是 ID1
;ID1
的行,判断出 email
的值不是’zhangssxyz@xxx.com’
,这行记录丢弃;index2
上刚刚查到的位置的下一条记录,发现仍然是’zhangs’
,取出 ID2
,再到 ID
索引上取整行然后判断,这次值对了,将这行记录加入结果集;idxe2
上取到的值不是’zhangs’
时,循环结束。在这个过程中,要回主键索引取 4
次数据,也就是扫描了 4
行。
使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本,那么应该如何选择合适的长度?
实际上,在建立索引时关注的是区分度,区分度越高越好。因为区分度越高,意味着重复的键值越少。因此,可以通过统计索引上有多少个不同的值来判断要使用多长的前缀。所以,在选择前缀索引时,可以先使用下面这个语句,算出这个列上有多少个不同的值。
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%
的值即可。
使用前缀索引可能导致覆盖索引不能用。比如下面的sql语句:
select id,email from SUser where email='zhangssxyz@xxx.com';
这个语句只需要返回id
和email
,根据建表语句可知id
是主键,也就是所有二级索引的叶子节点存储的都是id
的值,email
这个索引也不例外。所以如果使用index
的话可以使用覆盖索引直接得到id
的值,但是使用index2
的话,就不得不回到 id
索引再去判断 email
字段的值。
即使将 index2
的定义修改为 email(18)
的前缀索引,这时候虽然 index2
已经包含了所有的信息,但 InnoDB
还是要回到 id
索引再查一下,因为系统并不确定前缀索引的定义是否截断了完整信息。
如果需要建立前缀索引的字符串的前缀区分度很低,比如说身份证号,那么该怎么办呢?
一般情况下有两种方法:
第一种方式是使用倒序存储。如果将字符串倒序后,区分度比较大,可以使用这种方法。比如存储身份证号的时候把它倒过来存,每次查询的时候可以这么写:
mysql> select field_list from t where id_card = reverse('input_id_card_string');
第二种方式是使用 hash 字段。可以在表上再创建一个整数字段,来保存身份证的校验码,同时在这个字段上创建索引。
mysql> alter table t add id_card_crc int unsigned, add index(id_card_crc);
然后每次插入新记录的时候,都同时用 crc32()
这个函数得到校验码填到这个新字段。由于校验码可能存在冲突,也就是说两个不同的身份证号通过 crc32() 函数得到的结果可能是相同的,所以查询语句 where
部分要判断 id_card
的值是否精确相同。
mysql> select field_list from t where id_card_crc=crc32('input_id_card_string') and id_card='input_id_card_string'
由于索引是整数字段,所以只需要占用四个字节。
倒序方式和hash
方式的相同点是都不支持范围查询。倒序存储的字段上创建的索引是按照倒序字符串的方式排序的,已经没有办法利用索引方式查出身份证号码在 [ID_X, ID_Y]
的所有市民了。同样地,hash
字段的方式也只能支持等值查询。
它们的不同点主要有:
hash
字段方法需要增加一个字段。当然,倒序存储方式使用 4
个字节的前缀长度应该是不够的,如果再长一点,这个消耗跟额外这个 hash
字段也差不多抵消了。CPU
消耗方面,倒序方式每次写和读的时候,都需要额外调用一次 reverse
函数,而 hash
字段的方式需要额外调用一次 crc32()
函数。如果只从这两个函数的计算复杂度来看的话,reverse
函数额外消耗的 CPU
资源会更小些。hash
字段方式的查询性能相对更稳定一些。因为 crc32
算出来的值虽然有冲突的概率,但是概率非常小,可以认为每次查询的平均扫描行数接近 1
。而倒序存储方式毕竟还是用的前缀索引的方式,也就是说还是会增加扫描行数。