字符串字段是指哪些字段?应该就是char,varchar数据类型的字段,那么在这些字段上该怎么建立索引呢?既然这些字段中存放的都是字符串,而字符串也是可以排序的,这个地方就有一个疑问,字符串的b+树是什么样子的?汉字的排序规则是什么?那就直接在字段上建立索引呗,有什么值得单独说的吗?既然专门提到在字符串字段上怎么建立索引,那么一定是有一些小技巧能够帮助我们节省索引所占用的空间,或者是别的一些优点,那么有哪些优点呢?
    在实际应用场景中,经常会遇到邮箱登录,那么怎么在邮箱字段上建立最合适的索引呢?
    假如现在我们要实现一个支持邮箱登录的系统,其中存放用户信息的表的创建语句如下所示。

    1. create table SUser(
    2. ID bigint unsigned primary key,
    3. email varchar(64),
    4. ...
    5. )engine=innodb;

    由于会实现邮箱登录的功能,所以我们在写业务代码的时候一定会出现类似下面的语句。

    select f1, f2 from SUser where email='xxx';
    

    如果我们在email字段上不建立索引的话,那么每次执行上面的查询语句都会进行全表扫描,显然这样做查询语句的执行速度会很慢,所以我们一定会在email字段上建立索引,那么我们会怎么建立索引呢?
    直接在整个字段上建立索引肯定是可以的,那么我们还有别的更好的建立索引的方法吗?既可以加快查询语句执行的速度,也能够减少索引占用的时间。从前面的内容我们知道Mysql可以只在字段的一部分上建立索引,但是只能在字段的前半部分上建立索引,既然可以在字段的前半部分上建立索引,那么我们在多少长度的前半部分上建立索引呢?在字段的前半部分上建立索引有什么好处呢?
    假设我们分别使用下面的两条语句在email字段上建立索引,那么这两条建立索引的语句有什么区别呢?

    alter table SUser add index index1(email);
    或
    alter table SUser add index index2(email(6));
    

    如果使用第一条语句建立索引的话,每一个节点中保存的值则是整个字符串,如果是使用第二条语句建立的索引,那么每一个节点中保存的值只是每个字符串的前6个字符,所以在字段的前半部分上建立索引的优点就是建立的索引占用的空间更小,那么这种做法有什么缺点呢?缺点在于字段的前半部分的区分度可能不高,这样就会导致执行器需要扫描更多的数据行。为什么前半部分的区分度不高时,就需要扫描更多的数据行呢?
    为了体会这一点,我们来看下面的查询语句在两种建立索引方法下各自的执行过程。

    select id,name,email from SUser where email='zhangssxyz@xxx.com';
    

    如果我们是使用第一条语句建立的索引,那么我们就可以直接在索引树上定位到email的值为”zhangssxyz@xxx.com”的叶子节点,从叶子节点中我们可以取得对应的主键值,再根据主键的值回到主键的索引树上取出id,name,email的值,然后再回到email字段的索引树上取最开始定位叶子节点的下一个叶子节点,验证该节点的email的值是否满足语句中的条件,如果满足,那就重复上述过程,直到取到email值不满足条件的叶子节点时停止。那么在这个过程中Mysql扫描了多少行?Mysql扫描了多少行就是存储引擎向执行器返回了多少行数据,就是存储引擎调用了多少次接口。这么解释好像和丁奇说的不一样。
    如果我们使用的是第二条语句建立的索引,也就是在email字段的前6个字符上建立的索引,那么我们会在email索引树上先定位到第一个值为”zhangs”的叶子节点,然后取出该叶子节点中的主键值,回到主键的索引树上检查该主键值的email是不是”zhangssxyz@xxx.com”,如果不是,就把这个数据行丢弃,把数据行丢弃是什么意思?就是不保存这个数据行中的数据,如果是,就把这个数据行添加到结果集中,那么结果集是怎么样的一个结构呢?再回到email字段的索引树上,取最开始定位到的叶子节点的下一个节点,如果这个叶子节点的值还是”zhangs”,就重复上面的过程,直到取到值不为”zhangs”的叶子节点时停止重复。那么在这个过程中Mysql扫描了多少行?
    从上面的例子中可以看到,在字段的前半部分上建立索引,会增加查询语句在执行时的扫描行数,在整个字段上建立索引只需要扫描一行,而在字段的前6个字符上建立索引就需要扫描四行。但是这个结论并不是绝对的,还是以这个查询语句、这张表为例,如果我们在email字段的前7个字符上建立索引,也只需要扫描一行。
    所以我们可以得到这么一个结论,如果我们能够在字段的合适长度的前半部分上建立索引,那么既能节省索引占用的空间,又能够避免额外扫描太多的数据行。
    那么什么样的长度才是合适的长度呢?如果字段的某个长度的前半部分的区分度比较高,并且这个长度尽可能地短,那么这个长度就是合适的长度。字段的某个长度的前半部分的区分度比较高是什么意思呢?意思就是当我们截取字段的这个长度的前半部分时,它们的值大部分都互不相同。那么我们可以通过什么方法来获取字段不同长度的前半部分的区分度呢?我们可以通过下面这个sql语句来获取。

    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;
    

    那么怎么获取整个字段的区分度呢?

    select count(distinct email) as L from SUser;
    

    为什么区分度比较高的长度才是合适的长度呢?因为区分度比较高意味着
    但是在使用前缀索引时不可避免的会损失一定的区分度,这时我们可以提前设置一个可以接受的比例,比如设置为5%,我们就可以从上面的L4,L5,L6,L7中找出不小于L*95%的值,然后选择最短的长度来建立索引。
    前缀索引除了会增加扫描次数这一个缺点之外,还有别的缺点吗?前缀索引还有可能会影响到覆盖索引的使用,比如下面这个sql语句。

    select id,email from SUser where email='zhangssxyz@xxx.com';
    

    如果我们是在整个字段上建立索引的话,那么就可以利用覆盖索引,在email字段的索引树上搜索就可以了,不需要进行回表,而如果我们是在字段上建立前缀索引的话,那么就必须要回表并且判断email的值是否符合条件了。
    即使我们在整个字段上建立了前缀索引,比如说在char(18)数据类型的身份证号identity字段上建立了identity(18)的前缀索引,虽然此时在字段的前缀索引树中已经包含了这个字段的全部内容,但是Mysql并不知道这个前缀索引已经包含了字段的所有内容,所以还是会回表取出identity字段的值并判断该值是否符合查询语句中的条件。
    所以我们在使用前缀索引的同时还要去考虑前缀索引对覆盖索引的排斥特性。
    如果是前半部分区分度不高的字符串字段,我们可以怎么建立索引呢?
    比如说在身份证号字段上该怎么建立索引呢?我们都知道我国的身份证号的前六位每个地区都是相同的,如果对身份证号字段做长度为6的前缀索引,区分度只有1,显然这种做法不可取。
    如果非要建立前缀索引的话,那么就至少要创建长度12以上的前缀索引,才能够满足我们对区分度的要求,但是建立这么长的前缀索引对空间占用的节省效果也很有限。
    使用前缀索引的优点除了节省索引树的空间占用,还有什么?如果索引的长度越小,那么在一个磁盘页上就可以放的下更多的节点,也就是说b+树的分支更多,那么一个同样层高的b+树中就可以存放更多行的数据。
    如果我们在业务中只会使用身份证号的等值查询,那么我们有没有什么好的建立索引的方法呢?为什么要强调等值查询呢?
    首先我们可以将身份证号倒序存放到表中,我们在业务中总是先对身份证号进行倒序处理之后再插入到字段中,那么我们在查询的时候sql语句该怎么写呢?

    select field_list from t where id_card = reverse('input_id_card_string');
    

    将身份证号倒序存放到表中有什么好处呢?因为身份证号的后六位区分度很高,这样就可以在保证区分度的同时,尽可能的在更短的长度上建立索引。
    我们还可以使用hash字段,什么是hash字段?hash字段就是存放想要加快查询速度但是不适合直接建立索引的字段计算出的hash函数值的字段,这种方法需要我们在表上再创建一个整数字段,int数据类型会占用4个字节,这个字段中保存的是身份证的hash值,然后我们再在这个字段上建立索引。
    上面的过程对应的sql语句是什么呢?

    alter table t add id_card_crc int unsigned, add index(id_card_crc);
    

    我们会在业务中向表中插入新记录,在向表中插入新记录的时候,需要使用crc32()函数计算出身份证号的hash值并填充到新创建的整数字段中,不同的身份证号使用crc32()函数计算出来的hash值有可能相同,所以我们不能只通过crc的值来查询用户信息,在查询条件中还必须要带上身份证号的验证。在查询时使用的sql语句如下所示。

    select field_list from t where id_card_crc=crc32('input_id_card_string') and id_card='input_id_card_string'
    

    由于存储crc值的字段是整数字段,而int数据类型只占用4个字节,所以我们在crc字段上建立的索引长度只有4个字节,远远小于整个身份证号字段的18个字节。
    crc32()函数和hash值有什么关系?crc32()函数是hash函数的一种,crc32()函数计算出来的值就是hash值。
    但是我们使用crc字段来加快查询速度的执行过程仍然要进行回表操作。使用crc字段真的能加快查询速度吗?肯定比在整个字段上建立索引节省占用空间,使用crc字段的目的主要是节省占用空间而不是加快查询速度,想要加快查询速度那直接在整个字段上建立索引就可以了。
    那么使用倒序建立索引和hash字段上建立索引有什么相同的地方和不同的地方呢?
    它们的相同点在于都不支持范围查询,假如现在我们要查询身份证号在[identity1,identity2]之间的用户,如果我们使用倒序建立索引,那么b+树中叶子结点的顺序是什么?是身份证号倒序后从小到大的排列,这个排列和身份证号在正序时从小到大的排列是完全无关的,假设我们最开始搜索到了identity1所在的节点,但是这个节点的前一个节点或者后一个节点,其中的身份证号都不一定在[identity1,identity2]之间,也就是说,我们在搜索到第一个节点后,下一个符合条件的节点的位置我们不知道在哪里。如果我们是在hash字段上建立的索引呢?hash字段的索引树的叶子节点的顺序是hash值的顺序,hash值的顺序和身份证号的顺序是无关的,所以我们在搜索到第一个符合条件的叶子节点后,同样没办法确定应该去检查哪一个节点是否符合条件。所以说倒序建立索引和hash字段建立索引都是不支持范围查询的,如果进行的是等值查询,倒序建立的索引和hash字段上建立的索引在查询语句的执行过程中都是会使用的。
    那么它们的区别在什么地方呢?