MySQL 索引设计概要

在关系型数据库中设计索引其实并非复杂的事情,不少开发者都以为设计索引可以提高数据库的性能,相关的知识必定很是复杂。数据库

然而这种想法是不正确的,索引其实并非一个多么高深莫测的东西,只要咱们掌握必定的方法,理解索引的实现就能在不须要 DBA 的状况下设计出高效的索引。缓存

本文会介绍 数据库索引设计与优化 中设计索引的一些方法,让各位读者可以快速的在现有的工程中设计出合适的索引。性能

磁盘 IO

一个数据库必须保证其中存储的全部数据都是能够随时读写的,同时由于 MySQL 中全部的数据其实都是以文件的形式存储在磁盘上的,而从磁盘上随机访问对应的数据很是耗时,因此数据库程序和操做系统提供了缓冲池和内存以提升数据的访问速度。优化

除此以外,咱们还须要知道数据库对数据的读取并非以行为单位进行的,不管是读取一行仍是多行,都会将该行或者多行所在的页所有加载进来,而后再读取对应的数据记录;也就是说,读取所耗费的时间与行数无关,只与页数有关。编码

在 MySQL 中,页的大小通常为 16KB,不过也多是 8KB、32KB 或者其余值,这跟 MySQL 的存储引擎对数据的存储方式有很大的关系,文中不会展开介绍,不过索引或行记录是否在缓存池中极大的影响了访问索引或者数据的成本spa

随机读取

数据库等待一个页从磁盘读取到缓存池的所须要的成本巨大的,不管咱们是想要读取一个页面上的多条数据仍是一条数据,都须要消耗 10ms 左右的时间:操作系统

10ms 的时间在计算领域实际上是一个很是巨大的成本,假设咱们使用脚本向装了 SSD 的磁盘上顺序写入字节,那么在 10ms 内能够写入大概 3MB 左右的内容,可是数据库程序在 10ms 以内只能将一页的数据加载到数据库缓冲池中,从这里能够看出随机读取的代价是巨大的。设计

这 10ms 的一次随机读取是按照每秒 50 次的读取计算获得的,其中等待时间为 3ms、磁盘的实际繁忙时间约为 6ms,最终数据页从磁盘传输到缓冲池的时间为 1ms 左右,在对查询进行估算时并不须要准确的知道随机读取的时间,只须要知道估算出的 10ms 就能够了。code

内存读取

若是在数据库的缓存池中没有找到对应的数据页,那么会去内存中寻找对应的页面:排序

当对应的页面存在于内存时,数据库程序就会使用内存中的页,这可以将数据的读取时间下降一个数量级,将 10ms 下降到 1ms;MySQL 在执行读操做时,会先从数据库的缓冲区中读取,若是不存在与缓冲区中就会尝试从内存中加载页面,若是前面的两个步骤都失败了,最后就只能执行随机 IO 从磁盘中获取对应的数据页。

顺序读取

从磁盘读取数据并非都要付出很大的代价,当数据库管理程序一次性从磁盘中顺序读取大量的数据时,读取的速度会异常的快,大概在 40MB/s 左右。

若是一个页面的大小为 4KB,那么 1s 的时间就能够读取 10000 个页,读取一个页面所花费的平均时间就是 0.1ms,相比随机读取的 10ms 已经下降了两个数量级,甚至比内存中读取数据还要快。

数据页面的顺序读取有两个很是重要的优点:

  1. 同时读取多个界面意味着总时间的消耗会大幅度减小,磁盘的吞吐量能够达到 40MB/s;
  2. 数据库管理程序会对一些即将使用的界面进行预读,以减小查询请求的等待和响应时间;

小结

数据库查询操做的时间大都消耗在从磁盘或者内存中读取数据的过程,因为随机 IO 的代价巨大,如何在一次数据库查询中减小随机 IO 的次数每每可以大幅度的下降查询所耗费的时间提升磁盘的吞吐量。

查询过程

在上一节中,文章从数据页加载的角度介绍了磁盘 IO 对 MySQL 查询的影响,而在这一节中将介绍 MySQL 查询的执行过程当中以及数据库中的数据的特征对最终查询性能的影响。

索引片(Index Slices)

索引片其实就是 SQL 查询在执行过程当中扫描的一个索引片断,在这个范围中的索引将被顺序扫描,根据索引片包含的列数不一样,数据库索引设计与优化 书中对将索引分为宽索引和窄索引:

主键列 id 在全部的 MySQL 索引中都是必定会存在的。

对于查询 SELECT id, username, age FROM users WHERE username="draven" 来讲,(id, username) 就是一个窄索引,由于该索引没有包含存在于 SQL 查询中的 age 列,而 (id, username, age) 就是该查询的一个宽索引了,它包含这个查询中所须要的所有数据列

宽索引可以避免二次的随机 IO,而窄索引就须要在对索引进行顺序读取以后再根据主键 id 从主键索引中查找对应的数据:

对于窄索引,每个在索引中匹配到的记录行最终都须要执行另外的随机读取从汇集索引中得到剩余的数据,若是结果集很是大,那么就会致使随机读取的次数过多进而影响性能。

过滤因子

从上一小节对索引片的介绍,咱们能够看到影响 SQL 查询的除了查询自己还与数据库表中的数据特征有关,若是使用的是窄索引那么对表的随机访问就不可避免,在这时如何让索引片变『薄』就是咱们须要作的了。

一个 SQL 查询扫描的索引片大小实际上是由过滤因子决定的,也就是知足查询条件的记录行数所占的比例:

对于 users 表来讲,sex=”male” 就不是一个好的过滤因子,它会选择整张表中一半的数据,因此在通常状况下咱们最好不要使用 sex 列做为整个索引的第一列;而 name=”draven” 的使用就能够获得一个比较好的过滤因子了,它的使用能过滤整个数据表中 99.9% 的数据;固然咱们也能够将这三个过滤进行组合,建立一个新的索引 (name, age, sex) 并同时使用这三列做为过滤条件:

当三个过滤条件都是等值谓词时,几个索引列的顺序实际上是无所谓的,索引列的顺序不会影响同一个 SQL 语句对索引的选择,也就是索引 (name, age, sex) 和 (age, sex, name) 对于上图中的条件来讲是彻底同样的,这两个索引在执行查询时都有着彻底相同的效果。

组合条件的过滤因子就能够达到十万分之 6 了,若是整张表中有 10w 行数据,也只须要在扫描薄索引片后进行 6 次随机读取,这种直接使用乘积来计算组合条件的过滤因子其实有一个比较重要的问题:列与列之间不该该有太强的相关性,若是不一样的列之间有相关性,那么获得的结果就会比直接乘积得出的结果大一些,好比:所在的城市和邮政编码就有很是强的相关性,二者的过滤因子直接相乘其实与实际的过滤因子会有很大的误差,不过这在多数状况下都不是太大的问题。

对于一张表中的同一个列,不一样的值也会有不一样的过滤因子,这也就形成了同一列的不一样值最终的查询性能也会有很大差异:

当咱们评估一个索引是否合适时,须要考虑极端状况下查询语句的性能,好比 0% 或者 50% 等;最差的输入每每意味着最差的性能,在平均状况下表现良好的 SQL 语句在极端的输入下可能就彻底没法正常工做,这也是在设计索引时须要注意的问题。

总而言之,须要扫描的索引片的大小对查询性能的影响相当重要,而扫描的索引记录的数量,就是总行数与组合条件的过滤因子的乘积,索引片的大小最终也决定了从表中读取数据所须要的时间。

匹配列与过滤列

假设在 users 表中有 name、age 和 (name, sex, age) 三个辅助索引;当 WHERE 条件中存在相似 age = 21 或者 name = “draven” 这种等值谓词时,它们都会成为匹配列(Matching Column)用于选择索引树中的数据行,可是当咱们使用如下查询时:

 

1

2

3

SELECT * FROM users

WHERE name = "draven" AND sex = "male" AND age > 20;

 

虽然咱们有 (name, sex, age) 索引包含了上述查询条件中的所有列,可是在这里只有 name 和 sex 两列才是匹配列,MySQL 在执行上述查询时,会选择 name 和 sex 做为匹配列,扫描全部知足条件的数据行,而后将 age 当作过滤列(Filtering Column):

过滤列虽然不可以减小索引片的大小,可是可以减小从表中随机读取数据的次数,因此在索引中也扮演着很是重要的角色。

索引的设计

做者相信文章前面的内容已经为索引的设计提供了充足的理论基础和知识,从整体来看如何减小随机读取的次数是设计索引时须要重视的最重要的问题,在这一节中,咱们将介绍 数据库索引设计与优化 一书中概括出的设计最佳索引的方法。

三星索引

三星索引是对于一个查询语句可能的最好索引,若是一个查询语句的索引是三星索引,那么它只须要进行一次磁盘的随机读及一个窄索引片的顺序扫描就能够获得所有的结果集;所以其查询的响应时间比普通的索引会少几个数量级;根据书中对三星索引的定义,咱们能够理解为主键索引对于 WHERE id = 1 就是一个特殊的三星索引,咱们只须要对主键索引树进行一次索引访问而且顺序读取一条数据记录查询就结束了。

为了知足三星索引中的三颗星,咱们分别须要作如下几件事情:

  1. 第一颗星须要取出全部等值谓词中的列,做为索引开头的最开始的列(任意顺序);
  2. 第二颗星须要将 ORDER BY 列加入索引中;
  3. 第三颗星须要将查询语句剩余的列所有加入到索引中;

三星索引的概念和星级的给定来源于 数据库索引设计与优化 书中第四章三星索引一节。

若是对于一个查询语句咱们依照上述的三个条件进行设计,那么就能够获得该查询的三星索引,这三颗星中的最后一颗星每每都是最容易得到的,知足第三颗星的索引也就是上面提到的宽索引,可以避免大量的随机 IO,若是咱们遵循这个顺序为一个 SQL 查询设计索引那么咱们就能够获得一个完美的索引了;这三颗星的得到其实也没有表面上这么简单,每一颗星都有本身的意义:

  1. 第一颗星不仅是将等值谓词的列加入索引,它的做用是减小索引片的大小以减小须要扫描的数据行;
  2. 第二颗星用于避免排序,减小磁盘 IO 和内存的使用;
  3. 第三颗星用于避免每个索引对应的数据行都须要进行一次随机 IO 从汇集索引中读取剩余的数据;

在实际场景中,问题每每没有这么简单,咱们虽然能够总可以经过宽索引避免大量的随机访问,可是在一些复杂的查询中咱们没法同时得到第一颗星和第二颗星。

 

1

2

3

4

5

SELECT id, name, age FROM users

WHERE age BETWEEN 18 AND 21

  AND city = "Beijing"

ORDER BY name;

 

在上述查询中,咱们总能够经过增长索引中的列以得到第三颗星,可是若是咱们想要得到第一颗星就须要最小化索引片的大小,这时索引的前缀必须为 (city, age),在这时再想得到第三颗星就不可能了,哪怕在 age 的后面添加索引列 name,也会由于 name 在范围索引列 age 后面必须进行一次排序操做,最终获得的索引就是 (city, age, name, id):

若是咱们须要在内存中避免排序的话,就须要交换 age 和 name 的位置了,在这时就能够获得索引 (city, name, age, id),当一个 SQL 查询中同时拥有范围谓词和 ORDER BY 时,不管如何咱们都是没有办法得到一个三星索引的,咱们可以作的就是在这二者之间作出选择,是牺牲第一颗星仍是第二颗星。

总而言之,在设计单表的索引时,首先把查询中全部的等值谓词所有取出以任意顺序放在索引最前面,在这时,若是索引中同时存在范围索引和 ORDER BY 就须要权衡利弊了,但愿最小化扫描的索引片厚度时,应该将过滤因子最小的范围索引列加入索引,若是但愿避免排序就选择 ORDER BY 中的所有列,在这以后就只须要将查询中剩余的所有列加入索引了,经过这种固定的方法和逻辑就能够最快地得到一个查询语句的二星或者三星索引了。

总结

在单表上对索引进行设计其实仍是很是容易的,只须要遵循固定的套路就能设计出一个理想的三星索引,在这里强烈推荐 数据库索引设计与优化 这本书籍,其中包含了大量与索引设计与优化的相关内容;在以后的文章中读者也会分析介绍书中提供的几种估算方法,来帮助咱们经过预估问题设计出更高效的索引。

相关文章
相关标签/搜索