浅入浅出——MySQL索引

简介: 本文介绍了数据库索引的概念和各种索引结构,如哈希表、B+树、InnoDB引擎的索引运作原理等。还分享了覆盖索引、联合索引、最左前缀原则等优化技巧,以及如何避免索引误用,提高数据库性能。

我家有一本《现代汉语词典》,比我大10岁。小时候我爸经常带着我一起查字、查词。但是我们俩最大的差别是,他总是先查检字表,而我总是直接翻。

image.png

我当时觉得他太麻烦,而我,却总在现实面前被打脸——我总是比他慢,很多。

现在我们知道,他在走索引,而我在全表扫描。

image.png

一、几个前提认知

1.1 硬盘&内存

image.png

作为大批量的结构化数据,MySQL 选择将数据存储在硬盘上,这就要求必须要想办法解决读写速度问题。


1.2 快慢

快慢,我们一般说的是 I/O。关于 I/O,同样的存储介质,我们一般认为:

  • 一次 I/O 的时间是差不多的;
  • I/O 的时间远大于其他的计算时间;


二、索引(index)

回到我们最初的目标,解决 MySQL 作为硬盘存储技术,要解决读写速度的问题。再简化一下,就是尽量减少 I/O 次数。


选择的手段,就是通过索引,来帮我们快速找到特定值,提高查找效率。

索引的本质,就是像目录一样的,一种排好序的数据结构


三、索引结构

3.1 哈希表

我们最为熟悉的一种数据结构,面试的时候被问的最多的一种数据结构。

image.png

哈希表是一种以键 - 值(key-value)存储数据的结构,输入要查找的 key,通过哈希运算,得到在数组中的位置,然后再遍历链表,找到要查询的具体值。


所以这个查询效率极高,一个哈希运算加上一个遍历链表的时间,基本上是 O(1),是单条记录查询效率最高的存储结构。

image.png

但是,无论是哈希运算对应的数组下标,还是后面的链表,都是乱序的,这就意味着你没有办法做范围查找。如果你的查询条件是一个 [a, b] 区间或者 >c 的查询,就必须要扫描全表。


所以,哈希表只能支持等值查询的场景,在 Memcached、Redis 等 NoSQL 数据库中使用比较多。


3.2 有序数组

image.png

有序数组就解决了哈希表的这个问题,在等值查询和范围查询场景中的性能就都非常优秀。同样是范围查找 [a, b] ,通过二分查找定位到 a,然后往后遍历到 b,就能够完成,时间复杂度 O(log(N))。


如果只考虑查询效率,那么有序数组基本上就是最好的数据结构了。但是,如果你想插入一条数据可就麻烦了,必须要挪动后面所有的记录,成本极高。


所以,有序数组索引只适合用于静态存储引擎,比如一个班级的学生信息,通过学号排序。


3.3 二叉树

二叉树也是很经典的数据结构,我们学习树的时候,第一个学的就是这个。

image.png

二叉树的特点:

  • 一个节点只能有两个子节点,即度不超过 2;
  • 左子树所有结点的值小于父节点的值,右子树所有结点的值大于父节点的值;

image.png

如果要查 User101 这个用户的话,按照图中的顺序就是 User9 → User50 → User70 → User101这个路径得到。时间复杂度是 O(log(N))。


但是这个并不是稳定的,二叉树如果发生链化,也就是节点一直在某一边增加。那么时间复杂度就会降低到 O(N)。

image.png

为了维持 O(log(N)) 的查询复杂度,避免链化,就需要保持这棵树是平衡二叉树。我们常听到的树好多都是这种,包括平衡二叉搜索树、红黑树、数堆、伸展树等。为了达到这个目的,就要在每次写入的时候做调整,那么更新的时间复杂度就也是 O(log(N))。


平衡二叉树的查询效率稳定了,但是,它是一个好选择吗?

image.png

当我们有100万条数据,树高20,那么一次查询需要访问20个数据块,假设读取一个数据块需要10ms的寻址时间,那么查询一次数据需要需要200ms。相对比的,我们的 dwork_park 数据库,95% 以上的查询中 10ms 以内。


所以,继续优化……既然树的高度影响了效率,那就降低树的高度。


3.4 B树

针对同样的数据,如果我们把二叉树改成 M 叉树(M>2)呢?

image.png

当 M 增大,树的高度会快速降低。这个时候,B树就出现了。


B树的英文是 Balance Tree,也就是平衡的多路搜索树,它的高度远小于平衡二叉树的高度。在文件系统和数据库系统中的索引结构经常采用B树来实现。

image.png

B树的每一个节点最多可以包括M(图中 M=3)个子节点,M称为B树的阶。同时每个磁盘块中可以又多个关键字,还另外多了子节点的指针。如果一个磁盘块中包括了 x 个关键字,那么指针数就是 x+1。对于一个100阶的B树来说,4层最多可以存储约100万的索引数据。对于大量的索引数据来说,采用B树的结构是非常适合的。


一个M阶的B树(M>2)有以下的特性:


  • 根节点的儿子数的范围是[2,M]
  • 每个中间节点包含 k-1 个关键字和 k 个孩子,孩子的数量 = 关键字的数量 +1,k 的取值范围为[ceil(M/2), M]
  • 叶子节点包括 k-1 个关键字,k 的取值范围为[ceil(M/2), M]
  • 所有叶子节点位于同一层


对于B树,如果是等值查询,效率是很高的;但是对于范围查找,效率有所下降,因为要找到左右两个区间值,并且要遍历中间的所有子树。为了解决这个问题,B+树登场了。


3.5 B+树

image.png

B+树,是基于B树进行的改造,主要变化点有:


  • 父节点的关键字在子节点也有,最终叶子节点有所有的关键字
  • 非叶子节点只用作索引,不记录数据,所有跟记录有关的信息都存放在叶子节点中
  • 叶子节点构成了一个有序链表,叶子节点本身也是按照从小到大排序


这带来了两个好处:


  • 非叶子节点每个索引信息占的空间非常小(只有关键字信息),这样一个数据块就可以存放更多的数据,也就是M(树的阶)可以更大,进而让树的高度更低
  • 所有的叶子节点都是排序的,那么对于范围查找只需要定位起始点,然后往后遍历即可,极大减少了读不同节点的I/O次数


MySQL的InnoDB引擎,使用的就是B+树这种索引模型,也就是说,我们平时在数据库中创建的索引,都是存储在B+树中的,每一个索引对应了一棵树。


四、InnoDB 的索引

4.1 主键索引

image.png

● 主键索引的叶子节点存的是整行数据。在 InnoDB 里,主键索引也被称为聚簇索引(clustered index)。


● 非主键索引的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为二级索引(secondary index)。


面试的时候经典问题又来了:


● select * from Table where id = 00001,即主键查询方式


● select * from Table where year = 2012,即普通索引查询方式


这两条查询语句在性能上有什么不同?我们通过上图可以知道,基于主键查询,只需要检索 ID 这棵 B+树,就可以直接得到结果数据;而普通索引查询,需要先搜索 year 这棵 B+树,得到 ID 的值为 00001,再到 ID 索引树搜索一次,这个过程也被称为回表。也就是说,基于非主键索引的查询需要多扫描一次索引树。所以,我们在应用中应该尽量使用主键查询。


4.2 覆盖索引

我们说,尽量使用主键索引,而不是一般索引,是为了避免回表,减少一次 B+ 树的扫描。那么,是不是有另外一种可能的方式,就是只扫描一般索引树,不扫描主键索引树呢?这就是覆盖索引


如果执行的语句是 select ID, year from T able where year = 2012,这时只需要查 ID 的值,而 ID 的值已经在 year 索引树上了,因此可以直接提供查询结果,不需要回表。也就是说,在这个查询里面,索引 year 已经“覆盖了”我们的查询需求,我们称为覆盖索引。


覆盖索引,是一个非常常用的性能优化方式。但是,我们并不总是只返回 ID 就够了,我们可能还需要其他字段,比如 name。


这个时候,就出现了另外一个技术:联合索引。


4.3 联合索引

image.png

在我们的业务场景中,parkCode 是园区的唯一标识。也就是说,如果通过 parkCode 查询园区信息,只需要建立 parkCode 索引就可以了。这个时候建立(parkCode_name) 联合索引,是不是在浪费空间呢?


这个时候还是要分析具体业务,看查询园区名是不是高频请求,如果需要根据parkCode 查询园区名称,那么这个索引就非常必要,因为可以用到覆盖索引,不需要回表就可以获得查询结果。但是如果查询园区名称并不高频,或者说更多的时候需要和其他字段一起查询,那么就没有必要建立联合索引。


因为索引字段的维护是要付出代价的:


  • 时间代价:因为联合索引意味着索引的值会变得更细更多,那么每增加一条数据进行的索引调整就会更加频繁


  • 空间代价:因为每一个数据块的大小是固定的,每一条索引占用的空间越大,每个数据块可容纳的索引条数就更小,带来的结果就是 M 变小,B+ 树的高度就会变大,进一步降低查询效率


4.4 最左前缀原则

B+树的索引,遵循最左前缀原则。也就是说,并不需要匹配索引的全部关键字信息,只要满足最左前缀,就可以利用索引来加速检索。这个最左前缀可以是联合索引的最左N个字段,也可以是字符串索引的最左M个字符。这个“最左”包含了两层意思:


  • 字符串索引的最左侧 M 个字符:比如使用  where name like ' 南%'就可以在字符串中进行匹配,而'%京'就无法进行匹配。


  • 联合索引的最左 N 个字段:比如对一个(area,name,year) 索引,如果查询条件是where area = '中区' and name = '武汉江夏' 就可以匹配到 area 与 name;而如果查询条件是 where area = '中区' and year = '2014',由于跳过了 name,就无法匹配 year,只能匹配到 area。
  • 这里面又引申出来一个原则,要充分发挥索引的复用能力,如果建立了(area,name,year) 联合索引,就没有必要单独建立(area)索引了,因为前一个联合索引可以兼容后面的独立索引。
  • 但是要注意,(a,b)索引可以覆盖(a) 索引,但是不能覆盖(b)索引,如果有比较高频的 b 的筛选条件,就需要建立(a,b)(b) 两个索引。


再次强调,索引是要付出代价的,所以:


  • 索引不是越多越好


  • 联合索引不是字段越多越好


小思考:假设一个表有两个字段:name、age,在查询的时候可能筛选 name、可能筛选 age、也可能筛选 name和 age 两个字段,如何设计索引?
参考答案:(name,age)和(age)两个索引,因为 age 所占的空间比 name 小。


4.5 给字符串加索引

image.png

不知道大家在创建索引的时候,有没有注意到后面这个字段,就是前缀长度。比如我们对用户的 email 创建索引,一种方式是不设置长度 idx_email_1(email),一种是设置长度为5, idx_email_2(email(5)),这两种在存储和查询的时候,会有什么区别呢?

image.png

我们查询语句 select * from table where email = 'wangwei3@cainiao.com'  会发生什么?


在 index_email_1(email) 下比较简单,就是直接匹配索引,得到 id 00003,回表查询全部信息;而在 idx_email_2(email(5))下呢,你会发现可以匹配到的索引非常多,先是得到 00001 回表匹配 email,发现不是,再继续得到 00002,回表匹配还不是,继续得到 00003 回表匹配成功,这个时候才最终返回,比前面的要多两次回表。也就是说,使用前缀索引,可能导致查询次数增加,效率降低。


那为什么还有这么个配置可以选择呢?因为全量字符串会浪费空间,前面讲到过,空间也很带来时间的浪费。所以,怎么选择是个艺术活。


还是这个例子,对于我们公司的员工而言,邮箱的后缀基本都是@cainiao.com,而前缀是两到三个字的人名,那么如果我设置 idx_email_3(email(8)) 呢? 那么索引就会变成'wangwei3',也是可以唯一定位到查询数据的。


所以,使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。


前缀索引可能带来多次回表,而我们接触到“回表”这个概念是在讲覆盖索引的时候。那么我们看这样一个查询:


select id, email from table where email = 'wangwei3@cainiao.com',分别使用index_email_1(email) 和 idx_email_3(email(8)) 会有什么差别?


对于 index_email_1(email),由于可以使用覆盖索引,所以不需要回表就可以返回结果了;而对于idx_email_3(email(8)) 虽然也是可以唯一匹配,但是数据库并不知道前缀是否被截断了,所以还是需要回表查询确认。


也就是说,使用了前缀索引就无法使用覆盖索引做性能优化了。


此外,对于字符串类型的索引,还可以考虑:


● 倒序存储,再前缀索引,比如身份证这种前面几位是区域信息,如果说本地出生的数据库,大家都一样


● 使用 hash 字段,对于较长的字符串做 hash 运算,转成较短的字符串,可以节省存储空间,缺点是需要额外存储一个字段


4.6 order by

SELECT id, park_code, park_name, biz_type, biz_id, approve_type, approve_status, assignee, assignee_name, creator_name, data_before, data_after, data_update, attachment, reason, opinion, process_order, assignee_code_list, assignee_code_user_list, deleted, creator, operator, extra, gmt_create, gmt_modified FROM approval_list WHERE (park_code IN ('P_1', 'P_2', 'P_3', 'P_4', 'P_5', 'P_6', 'P_7', 'P_8', 'P_9', 'P_10', 'P_11', 'P_12', 'P_13', 'P_14', 'P_15', 'P_16', 'P_17', 'P_18', 'P_19', 'P_20', 'P_21', 'P_22', 'P_23', 'P_24', 'P_25', 'P_26', 'P_27', 'P_28', 'P_29', 'P_30', 'P_31', 'P_32', 'P_33', 'P_34', 'P_35', 'P_36', 'P_37', 'P_38', 'P_39', 'P_40') AND deleted = 0) ORDER BY approve_status ASC, id DESC LIMIT 10
  • 平均查询耗时:22.1s
  • 平均扫描行数:3,544,023


上面这个语句我优化了一天,修改索引、修改查询逻辑,都没啥用。观晓说:“你把approve_status的排序删掉吧”,然后降到了6ms


发生了啥?就算 park_code 的 in 查询几乎包含了所有的值,limit10 也不至于扫描全表吧。


我们使用 explain 看一下,发现了这么一个信息:“Using filesort”,

image.png

也就是需要排序,MySQL会给每个线程分配一块内存用于排序,称为sort_buffer。

image.png


我们按照这个图来分析一下上面这条语句:


1)先是通过 park_code 索引,查询符合条件的数据,由于几乎所有的 parcCode 都包含了,所以这里需要扫描全表

2)回表查询各个字段,由于字段太多,丢弃一些,把需要排序的字段放到 sort_buffer

3)由于数据量太大,无法在内存中完成,需要拆分临时文件排序,再合并

4)limit10,截取个 id

5)由于前面丢弃了一些字段,所以需要回表查询补充所需字段

6)返回结果集


当我把 ORDER BY approve_status ASC, id DESC 去掉 approve_status 排序,只剩下  ORDER BY id DESC 后呢?


我们使用 explain 再看一下,发现了已经没有了:“Using filesort”,

image.png

也就是说,没有经过 sort_buffer 排序!


为什么会这样呢?我们还是有 order by,为何不需要排序了呢?


因为数据库判断需要全表扫描,没有走索引;而order by id,只需要走主键索引排序取10个值即可。


还有一种情况,如果我们的索引是 (a,b) 联合索引,如果 order by b 或者是唯一索引也是排序字段,那么在读取索引的时候就也可以直接获得排序结果,并不需要排序过程。


所以,我们回到前面的覆盖索引,索引上的信息不仅可以满足查询需求,还可以满足排序需求。


在此产生一个建议,排序字段尽量放到索引里(且大部分情况放在后面)。


4.7 选错索引

我们建立了索引就万事大吉了吗?No,还有很多时候 MySQL“很傻”,会选错索引。


MySQL 优化器在选择索引的时候,需要找到一个“最优解”,用最小的代价去执行语句。而寻找这个“最优解”的目标,就是更快,更少的 I/O,更少的扫描行数。但是,优化期并不知道真实的扫描行数,那样就执行完了,它只能根据统计数据来获取这个信息,而这个统计数据还是抽样统计。


算基数


一个索引上不同的值越多,这个索引的区分度就越好,也就是我们要寻找的索引。一个索引上不同的值的个数,称之为“基数”(cardinality),也就是说,这个基数越大,索引的区分度越好。


这个基数的计算,InnoDB 会选择 N 个数据页,统计这些页面上的不同值,得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数。也就是 (count(1) + count(2) + ... + count(N)) / N * page。选取的这 N 页并不均匀,所以导致最后的结果存在偏差。


算行数


索引统计只是一个输入,对于一个具体的语句来说,优化器还要判断,执行这个语句本身要扫描多少行。

而这个数据,也是统计数据。


需要额外注意的一点是,数据库不仅会计算所有的扫描行数,还会计算回表的行数。如果发现加一起和全量数据差不多,就会直接全表扫描。


优化手段


  • 执行analyze table t,重新统计索引信息
  • 使用 force index 强行选择一个索引
  • 修改语句,引导 MySQL 使用我们期望的索引,比如我们order by b limit 1,会大概率采用 b 的索引,但是也许 a 更合适,这个时候可以改成 order by b, a limit 1,逻辑上一致,但是可能就会选择 a 索引
  • 新建一个更合适的索引,来提供给优化器做选择,或删掉误用的索引


4.8 多用 explain

image.png

五、几点建议和忠告

  • 在数据量非常大的情况下,没有 WHERE 条件过滤是非常可怕的。
  • 数据量很少的情况下(比如少于 1000 条),是没必要使用索引的。
  • 数据重复度大(区分度小)的,也没必要使用索引。几千万条数据,deleted = 1 的一共没几条,并且几乎不会查(代码里面大部分默认 deleted = 0),所以创建(deleted) 索引出了让引擎误会,选错索引导致全表扫描外,起不到任何作用。
  • order by 不要乱用,尤其是对于分页表格,已经有了筛选项,就没必要按照分类排序了。
  • 不要给每个字段都创建一个单独索引,好多是被联合索引覆盖了,另外一些可能没有区分度。
  • 没有任何一条优化规则是可以解决所有问题的(否则就被引擎内置了),你能做的是了解原理,根据实际业务场景做出更优的选择。


六、巨人的肩膀

做这次分享主要是因为最近团队内部出现了很多慢sql,发现部分同学对于索引不是很理解,就整理了一下。在这个过程中,参考了很多前辈技术大牛的分享,在此表示感谢!大家有时间建议去看下这些分享,更加系统的学习相关知识。


  • 《MySQL实战45讲》——林晓斌
  • 《数据结构与算法之美》——王争
  • 《检索技术核心 20 讲》——陈东
  • 《SQL 必知必会》——陈旸
  • 《MySQL 索引原理,一篇从头到尾讲清楚》——HollisChuang





来源  |  阿里云开发者公众号

作者  |  虚一

相关文章
|
14天前
|
供应链 监控 安全
对话|企业如何构建更完善的容器供应链安全防护体系
阿里云与企业共筑容器供应链安全
171331 12
|
17天前
|
供应链 监控 安全
对话|企业如何构建更完善的容器供应链安全防护体系
随着云计算和DevOps的兴起,容器技术和自动化在软件开发中扮演着愈发重要的角色,但也带来了新的安全挑战。阿里云针对这些挑战,组织了一场关于云上安全的深度访谈,邀请了内部专家穆寰、匡大虎和黄竹刚,深入探讨了容器安全与软件供应链安全的关系,分析了当前的安全隐患及应对策略,并介绍了阿里云提供的安全解决方案,包括容器镜像服务ACR、容器服务ACK、网格服务ASM等,旨在帮助企业构建涵盖整个软件开发生命周期的安全防护体系。通过加强基础设施安全性、技术创新以及倡导协同安全理念,阿里云致力于与客户共同建设更加安全可靠的软件供应链环境。
150295 32
|
25天前
|
弹性计算 人工智能 安全
对话 | ECS如何构筑企业上云的第一道安全防线
随着中小企业加速上云,数据泄露、网络攻击等安全威胁日益严重。阿里云推出深度访谈栏目,汇聚产品技术专家,探讨云上安全问题及应对策略。首期节目聚焦ECS安全性,提出三道防线:数据安全、网络安全和身份认证与权限管理,确保用户在云端的数据主权和业务稳定。此外,阿里云还推出了“ECS 99套餐”,以高性价比提供全面的安全保障,帮助中小企业安全上云。
201962 14
对话 | ECS如何构筑企业上云的第一道安全防线
|
2天前
|
机器学习/深度学习 自然语言处理 PyTorch
深入剖析Transformer架构中的多头注意力机制
多头注意力机制(Multi-Head Attention)是Transformer模型中的核心组件,通过并行运行多个独立的注意力机制,捕捉输入序列中不同子空间的语义关联。每个“头”独立处理Query、Key和Value矩阵,经过缩放点积注意力运算后,所有头的输出被拼接并通过线性层融合,最终生成更全面的表示。多头注意力不仅增强了模型对复杂依赖关系的理解,还在自然语言处理任务如机器翻译和阅读理解中表现出色。通过多头自注意力机制,模型在同一序列内部进行多角度的注意力计算,进一步提升了表达能力和泛化性能。
|
7天前
|
存储 人工智能 安全
对话|无影如何助力企业构建办公安全防护体系
阿里云无影助力企业构建办公安全防护体系
1252 8
|
7天前
|
人工智能 自然语言处理 程序员
通义灵码2.0全新升级,AI程序员全面开放使用
通义灵码2.0来了,成为全球首个同时上线JetBrains和VSCode的AI 程序员产品!立即下载更新最新插件使用。
1304 24
|
9天前
|
机器学习/深度学习 自然语言处理 搜索推荐
自注意力机制全解析:从原理到计算细节,一文尽览!
自注意力机制(Self-Attention)最早可追溯至20世纪70年代的神经网络研究,但直到2017年Google Brain团队提出Transformer架构后才广泛应用于深度学习。它通过计算序列内部元素间的相关性,捕捉复杂依赖关系,并支持并行化训练,显著提升了处理长文本和序列数据的能力。相比传统的RNN、LSTM和GRU,自注意力机制在自然语言处理(NLP)、计算机视觉、语音识别及推荐系统等领域展现出卓越性能。其核心步骤包括生成查询(Q)、键(K)和值(V)向量,计算缩放点积注意力得分,应用Softmax归一化,以及加权求和生成输出。自注意力机制提高了模型的表达能力,带来了更精准的服务。
|
7天前
|
消息中间件 人工智能 运维
1月更文特别场——寻找用云高手,分享云&AI实践
我们寻找你,用云高手,欢迎分享你的真知灼见!
590 23
1月更文特别场——寻找用云高手,分享云&AI实践
|
7天前
|
机器学习/深度学习 人工智能 自然语言处理
|
13天前
|
人工智能 自然语言处理 API
阿里云百炼xWaytoAGI共学课DAY1 - 必须了解的企业级AI应用开发知识点
本课程旨在介绍阿里云百炼大模型平台的核心功能和应用场景,帮助开发者和技术小白快速上手,体验AI的强大能力,并探索企业级AI应用开发的可能性。