前缀索引,在性能和空间中寻找平衡

标签: mysql java java-ee java-web | 发表时间:2023-04-24 11:03 | 作者:江南一点雨
出处:https://segmentfault.com/blogs

@[toc]
我们在项目的具体实践中,有时候会遇到一些比较特殊的字段,例如身份证号码。

松哥之前有一个小伙伴做黑龙江省的政务服务网,里边有一些涉及到用户身份证存储的场景,由于存储的数据大部分都是当地的,此时如果想给身份证号码建立索引的话,小伙伴们知道,身份证前六位是地址码,在这样的场景下,给身份证字段建立索引的话,前六位的区分度是很低的,甚至前十位的区分度都很低(因为出生年份毕竟有限,一个省份上千万人口,出生年份重复率是很高的),不仅浪费存储空间,查询性能还低。

那么有没有办法解决这个问题呢?我们今天就来聊一聊前缀索引,聊完之后相信大家自己就有答案了。

1.什么是前缀索引

有时候为了提升索引的性能,我们只对字段的前几个字符建立索引,这样做既可以节约空间,还能减少字符串的比较时间,B+Tree 上需要存储的索引字符串更短,也能在一定程度上降低索引树的高度,提高查询效率。

MySQL 中的前缀索引有点类似于 Oracle 中对字段使用 Left 函数来建立函数索引,只不过 MySQL 的这个前缀索引在查询时是内部自动完成匹配的,并不需要使用 Left 函数。

不过前缀索引有一个缺陷,就是有可能会降低索引的 选择性

2.什么是索引选择性

关于索引的选择性(Index Selectivity),它是指不重复的索引值(也称为基数 cardinality)和数据表的记录总数的比值,取值范围在 (0,1] 之间。索引的选择性越高则查询效率越高,因为选择性高的索引可以让 MySQL 在查找时过滤掉更多的行。

那有小伙伴要问了,是不是选择性越高的索引越好呢?当然不是!索引选择性最高为 1,如果索引选择性为 1,就是唯一索引了,搜索的时候就能直接通过搜索条件定位到具体一行记录!这个时候虽然性能最好,但是也是最费空间的, 这不符合我们创建前缀索引的初衷

我们一开始之所以要创建前缀索引而不是唯一索引, 就是希望能够在索引的性能和空间之间找到一个平衡,我们希望能够选择足够长的前缀以保证较高的选择性(这样在查询的过程中就不需要扫描很多行),但是又希望索引不要太过于占用存储空间。

那么我们该如何选择一个合适的索引选择性呢?索引前缀应该足够长,以便前缀索引的选择性接近于索引的整个列,即前缀的基数应该接近于完整列的基数。

首先我们可以通过如下 SQL 得到全列选择性:

  SELECT COUNT(DISTINCT column_name) / COUNT(*) FROM table_name;

然后再通过如下 SQL 得到某一长度前缀的选择性:

  SELECT COUNT(DISTINCT LEFT(column_name, prefix_length)) / COUNT(*) FROM table_name;

在上面这条 SQL 执行的时候,我们要注意选择合适的 prefix_length,直至计算结果约等于全列选择性的时候,就是最佳结果了。

3.创建前缀索引

3.1 一个小案例

举个例子,我们来创建一个前缀索引看看。

松哥这里使用的数据样例是网上找的一个测试脚本,有 300W+ 条数据,做 SQL 测试优化是够用了,小伙伴们在公众号后台回复 mysql-data-samples 获取脚本下载链接。

我们来大致上看下这个表结构:

这个表有一个 user_uuid 字段,我们就在这个字段上做文章。

Git 小伙伴们应该都会用吧?不同于 Svn,Git 上的版本号不是数字而是一个 Hash 字符串,但是我们在具体应用的时候,比如你要做版本回退,此时并不需要输入完整的的版本号,只需要输入版本号前几个字符就行了,因为根据前面这一部分就能确定出版本号了。

那么这张表里边的 user_uuid 字段也是这意思,如果我们想给 user_uuid 字段建立索引,就没有必要给完整的字符串建立索引,我们只需要给一部分字符串建立索引。

可能有小伙伴还是不太明白,我举一个例子,比如说我现在想按照 user_uuid 字段来查询,但是查询条件我没有必要写完整的 user_uuid,我只需要写前面一部分就可以区分出我想要的记录了,我们来看如下一条 SQL:

大家看到,user_uuid 我只需要给出一部分就能唯一锁定一条记录。

当然,上面这个 SQL 是松哥测试过的,给定的 '39352f%' 条件不能再短了,再短就会查出来两条甚至多条记录。

通过上面这个例子我们就可以看出来,如果给 user_uuid 字段建立索引,可能并不需要给完整的字符串建立索引,只需要给一部分前缀字符串建立索引。

那么给前面几个字符串建立索引呢?这个可不是拍脑门,需要科学计算,我们继续往下看。

3.2 前缀索引

首先我们通过如下 SQL 来看一下 user_uuid 全列索引选择性是多少:

  SELECT COUNT(DISTINCT user_uuid) / COUNT(*) FROM system_user;

可以看到,结果为 1。全列选择性为 1 说明这一列的值都是唯一不重复的。

接下来我们先来试几个不同的 prefix_length,看看选择性如何。

松哥这里一共测试了 5 个不同的 prefix_length,大家来看看各自的选择性:

8 和 9 的选择性是一样的,因为在 uuid 字符串中,第 9 个字符串是 -,所有的 uuid 第九个字符串都一样,所以 8 个字符和 9 个字符串的区分度就一样。

当 prefix_length 为 10 的时候,选择性就已经是 1 了,意思是,在这 300W+ 条数据中,如果我用 user_uuid 这个字段去查询的话,只需要输入前十个字符,就能唯一定位到一条具体的记录了。

那还等啥,赶紧创建前缀索引呗:

  alter table system_user add index user_uuid_index(user_uuid(10));

查看刚刚创建的前缀索引:

  show index from system_user;

可以看到,第二行就是我们刚刚创建的前缀索引。

接下来我们分析查询语句中是否用到该索引:

  select * from system_user where user_uuid='39352f81-165e-4405-9715-75fcdf7f7068';

可以看到,这个前缀索引已经用上了。

具体搜索流程是这样:

  1. user_uuid_index 索引中找到第一个值为 39352f81-1 的记录(user_uuid 的前十个字符)。
  2. 由于 user_uuid 是二级索引,叶子结点保存的是主键值,所以此时拿到了主键 id 为 1。
  3. 拿着主键 id 去回表,在主键索引上找到 id 为 1 的行的完整记录,返回给 server 层。
  4. server 层判断其 user_uuid 是不是 39352f81-165e-4405-9715-75fcdf7f7068(所以执行计划的 Extra 为 Using where)。

    1. 如果不是,这行记录丢弃。
    2. 如果是,将该记录加入结果集。
  5. 索引叶子结点上数据之间是有单向链表维系的,所以接着第一步查找的结果,继续向后读取下一条记录,然后重复 2、3、4 步,直到在 user_uuid_index 上取到的值不为 39352f81-1 时,循环结束。

如果我们建立了前缀索引并且前缀索引的选择性为 1,那么就不需要第 5 步了,如果前缀索引选择性小于 1,就需要第五步。

从上面的案例中,小伙伴们看到,我们既节省了空间,又提高了搜索效率。

3.3 一个问题

使用了前缀索引后,我们来看一个问题,大家来看如下一条查询 SQL:

  select user_uuid from system_user where user_uuid='39352f81-165e-4405-9715-75fcdf7f7068';

这次不是 select *,而是 select user_uuid,小伙伴们知道,这里应该是要用到覆盖索引,我们来看看执行计划:

咦,说好的索引覆盖呢?(注意看 Extra 是 Using where 不是 Using index)。

大家想想,前缀索引中,B+Tree 里保存的就不是完整的 user_uuid 字段的值,必须要回表才能拿到需要的数据。 所以,用了前缀索引,就用不了覆盖索引了。

4. 回到开始的问题

在本文一开始,松哥抛出了一个问题,如何给身份证建立索引更高效?

由于身份证前六位区分度太低,所以我们可以考虑将身份证倒序存储,倒序存储之后,为前六位或者前八位(可以自行计算选择性)建立前缀索引,这样的建立的索引选择性就会比较高,同时对空间的占用也会比较小。在查询的时候使用 reverse 反转身份证号码即可,像下面这样:

  explain select * from user where id_card=reverse('正序的身份证号码');

5.小结

好啦,这就是前缀索引,感兴趣的小伙伴赶紧体验一把吧~

相关 [前缀 索引 性能] 推荐:

前缀索引,在性能和空间中寻找平衡

- - SegmentFault 最新的文章
我们在项目的具体实践中,有时候会遇到一些比较特殊的字段,例如身份证号码. 松哥之前有一个小伙伴做黑龙江省的政务服务网,里边有一些涉及到用户身份证存储的场景,由于存储的数据大部分都是当地的,此时如果想给身份证号码建立索引的话,小伙伴们知道,身份证前六位是地址码,在这样的场景下,给身份证字段建立索引的话,前六位的区分度是很低的,甚至前十位的区分度都很低(因为出生年份毕竟有限,一个省份上千万人口,出生年份重复率是很高的),不仅浪费存储空间,查询性能还低.

mongodb索引讲解与性能调优

- - haohtml's blog
mongodb索引规则基本上与传统的关系库一样,大部分优化MySQL/Oracle/SQLite索引的技巧也适用于mongodb. 当查询中用到某些条件时,可以对该键建立索引,以提高查询速度. 如果数据量很多且查询多于更新时,可以用索引提高查询的速度. a)         查询索引:. 查询索引很简单,比如说需要查询mailaccess数据库中的Mail collection上的索引时:.

复合索引性能问题初探

- - CSDN博客推荐文章
在《品悟性能优化》一书,4.4.3章节里介绍了复合索引的两个特点:前缀性,可选性. 何为前缀性,该书阐述为排除skip scan index的情况,约束条件如果不包含复合索引的第一列,则该复合索引不会被用到;何为可选性,该书阐述为字段值越多,可选性越强,定位记录越少,查询效率越高. 即查询返回记录少的列应该放在复合索引的前面.

《高性能 MySQL》 — 第五章 创建高性能的索引

- - 码蜂笔记
索引的使用:现在索引中找到对应值,然后根据匹配的索引记录找到对应的数据行. 对于有多列的索引,MySQL 只能高效地使用索引的最左前缀列. MySQL 的唯一限制和主键限制都是通过索引实现. MySQL 中,索引是在存储引擎层而不是服务器层实现的,所以没有统一的索引标准. B-Tree 通常意味着所有的值都是按顺序存储的,并且每一个叶子页到根的距离相等,适合范围查询.

SQL Server 查询性能优化——堆表、碎片与索引(一)

- - 博客园_首页
      SQL Server在堆表中查询数据时,是不知道到底有多少数据行符合你所指定的查找条件,它将根据指定的查询条件把数据表的全部数据都查找一遍. 如果有可采用的索引,SQL Server只需要在索引层级查找每个索引分页的数据,再抓出所需要的少量数据分页即可. 访问数据表内数以万计的数据分页与只访问少数索引的分页两者间的差异,让索引变成效能调校的最佳工具.

数据库查询性能优化之利器—索引(一)

- - 博客园_首页
                   数据库查询性能优化之利器—索引(一).   最近在做基于Android的公交查询系统的过程中,遇到一个很棘手的问题:换乘算法效率低. 在直达查询和一次换乘查询的时候,问题体现的还不是很明显,能够在1s之内查询出乘车方案,而当进行二次查询的时候,基本要等一两分钟才能查询出换乘方案,这对于公交查询系统是绝对无法容忍的.

不同SSD盘组合搜索引擎单机性能测试

- - 搜索技术博客-淘宝
测试不同SSD盘组合策略HA3引擎性能,寻找到较好的SSD盘组合策略. a) IO调度策略:deadline(sudo echo deadline > /sys/block/sdb/queue/scheduler). b) 预读策略:关闭系统预读(sudo blockdev –setra 0 /dev/sdb1).

Linux常用性能调优工具索引

- - 系统技术非业余研究
原创文章,转载请注明: 转载自 系统技术非业余研究. Linux常用性能调优工具索引. 前段时间看到brendangregg的 Linux Performance Analysis and Tools PPT里面提到Linux常用性能调优工具, 见下图:. 其中提到了的工具,大部分我之前有推荐过或者在实践的案例里面使用过,这里方便大家索引下:.

SQL Server 查询性能优化——创建索引原则(一)

- - 博客园_首页
索引是提高查询性能的一个重要工具,索引就是把查询语句所需要的少量数据添加到索引分页中,这样访问数据时只要访问少数索引的分页就可以. 但是索引对于提高查询性能也不是万能的,也不是建立越多的索引就越好. 索引建少了,用WHERE子句找数据效率低,不利于查找数据. 索引建多了,不利于新增、修改和删除等操作,因为做这些操作时,SQL SERVER除了要更新数据表本身,还要连带地立即更新所有的相关索引,而且过多的索引也会浪费硬盘空间.

电商搜索引擎的架构设计和性能优化

- - SegmentFault 最新的文章
「 OneAPM 技术公开课」由应用性能管理第一品牌. OneAPM 发起,内容面向 IT 开发和运维人员. 云集技术牛人、知名架构师、实践专家共同探讨技术热点. 本文系「OneAPM 技术公开课」第一期演讲嘉宾前当当网高级架构师吴英昊的演讲整理:. 首先,非常感谢 OneAPM 技术公开课举办的这次活动.