MySQL实战45讲学习笔记:第十一讲

1、如何在邮箱这样的字段上创建合理的索引

如今,几乎全部的系统都支持邮箱登陆,如何在邮箱这样的字段上创建合理的索引,是咱们今天要讨论的问题。mysql

假设,你如今维护一个支持邮箱登陆的系统,用户表是这么定义的:sql

mysql> create table SUser(
ID bigint unsigned primary key,
email varchar(64), 
... 
)engine=innodb; 

因为要使用邮箱登陆,因此业务代码中必定会出现相似于这样的语句:数据库

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

从第 4 和第 5 篇讲解索引的文章中,咱们能够知道,若是 email 这个字段上没有索引,那么这个语句就只能作全表扫描。bash

2、直接建立完整索引,这样可能比较占用空间

同时,MySQL 是支持前缀索引的,也就是说,你能够定义字符串的一部分做为索引。默认地,若是你建立索引的语句不指定前缀长度,那么索引就会包含整个字符串。session

好比,这两个在 email 字段上建立索引的语句:

mysql> alter table SUser add index index1(email);数据结构

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

第一个语句建立的 index1 索引里面,包含了每一个记录的整个字符串;而第二个语句建立的 index2 索引里面,对于每一个记录都是只取前 6 个字节。函数

那么,这两种不一样的定义在数据结构和存储上有什么区别呢?如图 2 和 3 所示,就是这两个索引的示意图。性能

 图 1 email 索引结构优化

 图 2 email(6) 索引结构spa

从图中你能够看到,因为 email(6) 这个索引结构中每一个邮箱字段都只取前 6 个字节(即:zhangs),因此占用的空间会更小,这就是使用前缀索引的优点。
但,这同时带来的损失是,可能会增长额外的记录扫描次数。

接下来,咱们再看看下面这个语句,在这两个索引定义下分别是怎么执行的。

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

一、若是使用的是 index1执行顺序是这样的:

若是使用的是 index1(即 email 整个字符串的索引结构),执行顺序是这样的:

  • 1. 从 index1 索引树找到知足索引值是’zhangssxyz@xxx.com’的这条记录,取得 ID2的值;
  • 2. 到主键上查到主键值是 ID2 的行,判断 email 的值是正确的,将这行记录加入结果集;
  • 3. 取 index1 索引树上刚刚查到的位置的下一条记录,发现已经不知足email='zhangssxyz@xxx.com’的条件了,循环结束。

这个过程当中,只须要回主键索引取一次数据,因此系统认为只扫描了一行。

二、若是使用的是 index2执行顺序是这样的:

若是使用的是 index2(即 email(6) 索引结构),执行顺序是这样的:

  • 1. 从 index2 索引树找到知足索引值是’zhangs’的记录,找到的第一个是 ID1;
  • 2. 到主键上查到主键值是 ID1 的行,判断出 email 的值不是’zhangssxyz@xxx.com’,这行记录丢弃;
  • 3. 取 index2 上刚刚查到的位置的下一条记录,发现仍然是’zhangs’,取出 ID2,再到ID 索引上取整行而后判断,此次值对了,将这行记录加入结果集;
  • 4. 重复上一步,直到在 idxe2 上取到的值不是’zhangs’时,循环结束。

在这个过程当中,要回主键索引取 4 次数据,也就是扫描了 4 行。

经过这个对比,你很容易就能够发现,使用前缀索引后,可能会致使查询语句读数据的次数变多。

可是,对于这个查询语句来讲,若是你定义的 index2 不是 email(6) 而是 email(7),也就是说取 email 字段的前 7 个字节来构建索引的话,即知足前缀’zhangss’的记录只有
一个,也可以直接查到 ID2,只扫描一行就结束了。

也就是说使用前缀索引,定义好长度,就能够作到既节省空间,又不用额外增长太多的查询成本。

三、当要给字符串建立前缀索引时,有什么方法可以肯定我应该使用多长的前缀呢?

因而,你就有个问题:当要给字符串建立前缀索引时,有什么方法可以肯定我应该使用多长的前缀呢?

实际上,咱们在创建索引时关注的是区分度,区分度越高越好。由于区分度越高,意味着重复的键值越少。所以,咱们能够经过统计索引上有多少个不一样的值来判断要使用多长的前缀。

首先,你可使用下面这个语句,算出这个列上有多少个不一样的值:

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% 的值,假设这里 L六、L7
都知足,你就能够选择前缀长度为 6。

2、前缀索引对覆盖索引的影响

前面咱们说了使用前缀索引可能会增长扫描行数,这会影响到性能。其实,前缀索引的影响不止如此,咱们再看一下另一个场景。
你先来看看这个 SQL 语句:

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

与前面例子中的 SQL 语句

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

相比,这个语句只要求返回 id 和 email 字段。

因此,若是使用 index1(即 email 整个字符串的索引结构)的话,能够利用覆盖索引,从 index1 查到结果后直接就返回了,不须要回到 ID 索引再去查一次。而若是使用

index2(即 email(6) 索引结构)的话,就不得不回到 ID 索引再去判断 email 字段的值。即便你将 index2 的定义修改成 email(18) 的前缀索引,这时候虽然 index2 已经包含了
全部的信息,但 InnoDB 仍是要回到 id 索引再查一下,由于系统并不肯定前缀索引的定义是否截断了完整信息。

也就是说,使用前缀索引就用不上覆盖索引对查询性能的优化了,这也是你在选择是否使用前缀索引时须要考虑的一个因素。

3、其余方式

对于相似于邮箱这样的字段来讲,使用前缀索引的效果可能还不错。可是,遇到前缀的区分度不够好的状况时,咱们要怎么办呢?

好比,咱们国家的身份证号,一共 18 位,其中前 6 位是地址码,因此同一个县的人的身份证号前 6 位通常会是相同的。

假设你维护的数据库是一个市的公民信息系统,这时候若是对身份证号作长度为 6 的前缀索引的话,这个索引的区分度就很是低了。

按照咱们前面说的方法,可能你须要建立长度为 12 以上的前缀索引,才可以知足区分度要求。

可是,索引选取的越长,占用的磁盘空间就越大,相同的数据页能放下的索引值就越少,搜索的效率也就会越低。

那么,若是咱们可以肯定业务需求里面只有按照身份证进行等值查询的需求,还有没有别的处理方法呢?这种方法,既能够占用更小的空间,也能达到相同的查询效率。
答案是,有的。

一、第一种方式是使用倒序存储

第一种方式是使用倒序存储。若是你存储身份证号的时候把它倒过来存,每次查询的时候,你能够这么写:

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

因为身份证号的最后 6 位没有地址码这样的重复逻辑,因此最后这 6 位极可能就提供了足够的区分度。固然了,实践中你不要忘记使用 count(distinct) 方法去作个验证。

二、第二种方式是使用 hash 字段

第二种方式是使用 hash 字段。你能够在表上再建立一个整数字段,来保存身份证的校验码,同时在这个字段上建立索引。

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

而后每次插入新记录的时候,都同时用 crc32() 这个函数获得校验码填到这个新字段。因为校验码可能存在冲突,也就是说两个不一样的身份证号经过 crc32() 函数获得的结果可能
是相同的,因此你的查询语句 where 部分要判断 id_card 的值是否精确相同

这样,索引的长度变成了 4 个字节,比原来小了不少。

三、使用倒序存储和使用 hash 字段这两种方法的异同点。

接下来,咱们再一块儿看看使用倒序存储和使用 hash 字段这两种方法的异同点。

首先,它们的相同点是,都不支持范围查询。倒序存储的字段上建立的索引是按照倒序字符串的方式排序的,已经没有办法利用索引方式查出身份证号码在 [ID_X, ID_Y] 的全部市
民了。一样地,hash 字段的方式也只能支持等值查询。

它们的区别,主要体如今如下三个方面:

1. 从占用的额外空间来看,倒序存储方式在主键索引上,不会消耗额外的存储空间,而hash 字段方法须要增长一个字段。固然,倒序存储方式使用 4 个字节的前缀长度应该
    是不够的,若是再长一点,这个消耗跟额外这个 hash 字段也差很少抵消了。

2. 在 CPU 消耗方面,倒序方式每次写和读的时候,都须要额外调用一次 reverse 函数,而 hash 字段的方式须要额外调用一次 crc32() 函数。若是只从这两个函数的计算复杂
    度来看的话,reverse 函数额外消耗的 CPU 资源会更小些。

3. 从查询效率上看,使用 hash 字段方式的查询性能相对更稳定一些。由于 crc32 算出来的值虽然有冲突的几率,可是几率很是小,能够认为每次查询的平均扫描行数接近 1。
    而倒序存储方式毕竟仍是用的前缀索引的方式,也就是说仍是会增长扫描行数

4、小结

在今天这篇文章中,我跟你聊了聊字符串字段建立索引的场景。咱们来回顾一下,你可使用的方式有:

1. 直接建立完整索引,这样可能比较占用空间;
2. 建立前缀索引,节省空间,但会增长查询扫描次数,而且不能使用覆盖索引;
3. 倒序存储,再建立前缀索引,用于绕过字符串自己前缀的区分度不够的问题;
4. 建立 hash 字段索引,查询性能稳定,有额外的存储和计算消耗,跟第三种方式同样,都不支持范围扫描。

在实际应用中,你要根据业务字段的特色选择使用哪一种方式。好了,又到了最后的问题时间。

若是你在维护一个学校的学生信息数据库,学生登陆名的统一格式是”学号@gmail.com", 而学号的规则是:十五位的数字,其中前三位是所在城市编号、第四到第
六位是学校编号、第七位到第十位是入学年份、最后五位是顺序编号。系统登陆的时候都须要学生输入登陆名和密码,验证正确后才能继续使用系统。就只考虑
登陆验证这个行为的话,你会怎么设计这个登陆名的索引呢?

你能够把你的分析思路和设计结果写在留言区里,我会在下一篇文章的末尾和你讨论这个问题。感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一块儿阅读。

5、上期问题时间

上篇文章中的第一个例子,评论区有几位同窗说没有复现,你们要检查一下隔离级别是否是 RR(Repeatable Read,可重复读),建立的表 t 是否是 InnoDB 引擎。我把复现过
程作成了一个视频,供你参考

https://time.geekbang.org/45ba9ec1-70fa-4ab0-baa0-61b4537cc25e

在上一篇文章最后,我给你留的问题是,为何通过这个操做序列,explain 的结果就不对了?这里,我来为你分析一下缘由。

delete 语句删掉了全部的数据,而后再经过 call idata() 插入了 10 万行数据,看上去是覆盖了原来的 10 万行。

可是,session A 开启了事务并无提交,因此以前插入的 10 万行数据是不能删除的。这样,以前的数据每一行数据都有两个版本,旧版本是 delete 以前的数据,新版本是标记为
deleted 的数据。

这样,索引 a 上的数据其实就有两份。

而后你会说,不对啊,主键上的数据也不能删,那没有使用 force index 的语句,使用explain 命令看到的扫描行数为何仍是 100000 左右?(潜台词,若是这个也翻倍,也
许优化器还会认为选字段 a 做为索引更合适)

是的,不过这个是主键,主键是直接按照表的行数来估计的。而表的行数,优化器直接用的是 show table status 的值。

这个值的计算方法,我会在后面有文章为你详细讲解

6、精选留言

一、封建的风

 

二、某人

回答下今天老师的问题:1.在user创建索引,因为学号的最后7位才能肯定到某个学生.不知足最左前缀,那么select from where '%1234567%'没法使用索引,是全表扫描。可是这种状况也有优化的办法,若是该表上的字段比较多,能够这样改写select password from t join (select id from where user like '%1234567%') as a on a.id=t.id经过全扫描二级索引获得惟一id值.再用id值与t表关联的时候,就能迅速的定位到某一行了,避免全表扫描不过在本次问题里,这种方式效果很差  2.hash索引,创建(hashuser,user,password)索引,不用回表,覆盖索引,可是索引占用长度长。或者创建(hashuser)索引,由于hashuser基本上能肯定到惟一值,虽然回表可是扫描的行数也就两行,效率也挺高。可是hash索引对于insert和update操做要多作一些额外的操做。要嘛经过程序计算出hash值,插入表里,要嘛就经过触发器来作。3.创建前缀索引,因为后面是固定email,能够考虑只存学号.因为学号后面7位就能肯定到某一个学生,能够用倒序存储+前缀索引。不过因为前缀索引不能在int类型上创建,只能用varchar类型。虽然前缀索引没法用到覆盖索引,不过回表扫描的行数也就一行,效率也挺高。这种方式来讲,对insert和update相对还好。还有前缀索引还有个影响是不能用于排序。

相关文章
相关标签/搜索