MySQL之索引分享

索引本质

官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构
本质:索引是数据结构

Innodb B-Tree 索引

  1. B+树成因:
    我们知道,每一种数据结构的出现都是为了解决特定的问题,那么B+树的出现是为了解决什么问题,那就是:每次查找数据时把磁盘IO次数控制在一个很小的数量级,最好是常数数量级。在理解B+树结构之前,先提一下计算机的局部性原理:当计算机访问一个地址的数据的时候,与其相邻的数据也会很快被访问到。每一次IO读取的数据我们称之为一页(page)。具体一页有多大数据跟操作系统有关,一般为4k或8k,也就是我们读取一页内的数据时候,实际上才发生了一次IO,这个理论对于索引的数据结构设计非常有帮助。
  2. B+树结构
    B+树的定义可以参见B+树,我们先看一下B+树的结构:
    #pic_center
    浅蓝色的块我们称之为一个磁盘块,可以看到每个磁盘块包含几个数据项(深蓝色所示)和指针(黄色所示),如磁盘块1包含数据项17和35,包含指针P1、P2、P3,P1表示小于17的磁盘块,P2表示在17和35之间的磁盘块,P3表示大于35的磁盘块。真实的数据存在于叶子节点即3、5、9、10、13、15、28、29、36、60、75、79、90、99。非叶子节点只不存储真实的数据,只存储指引搜索方向的数据项,如17、35并不真实存在于数据表中。
  3. B+树的查找过程
    如图所示,如果要查找数据项29,那么首先会把磁盘块1由磁盘加载到内存,此时发生一次IO,在内存中用二分查找确定29在17和35之间,锁定磁盘块1的P2指针,内存时间因为非常短(相比磁盘的IO)可以忽略不计,通过磁盘块1的P2指针的磁盘地址把磁盘块3由磁盘加载到内存,发生第二次IO,29在26和30之间,锁定磁盘块3的P2指针,通过指针加载磁盘块8到内存,发生第三次IO,同时内存中做二分查找找到29,结束查询,总计三次IO。真实的情况是,3层的b+树可以表示上百万的数据,如果上百万的数据查找只需要三次IO,性能提高将是巨大的,如果没有索引,每个数据项都要发生一次IO,那么总共需要百万次的IO,显然成本非常非常高。
  4. B+树性质
    1.通过上面的分析,我们知道IO次数取决于b+数的高度h,假设当前数据表的数据为N,每个磁盘块的数据项的数量是m,则有h=㏒(m+1)N,当数据量N一定的情况下,m越大,h越小;而m = 磁盘块的大小 / 数据项的大小,磁盘块的大小也就是一个数据页的大小,是固定的,如果数据项占的空间越小,数据项的数量越多,树的高度越低。这就是为什么每个数据项,即索引字段要尽量的小,比如int占4字节,要比bigint8字节少一半。这也是为什么b+树要求把真实的数据放到叶子节点而不是内层节点,一旦放到内层节点,磁盘块的数据项会大幅度下降,导致树增高。当数据项等于1时将会退化成线性表。

疑问:我们都知道一个表不能存储太多数据,否则会造成查询性能下降,那么个MySQL的一个表大概存储多少行记录会导致性能下降?
这个问题的简单回答是:约2千万
那如何计算呢?首先我假设每行记录为1k,
首先假设树高为2,那么根节点存储的指针对应每个叶子节点。也就是说,最大的话,有多少个指针就有多少个叶子节点。
这棵B+树存储的总行数=根节点指针树*每个叶子节点的行记录树。
那么根节点能存储多少指针呢?
InnoDB最小单位为页,一页为16k,我们假设主键ID为bigint类型,长度为8字节,而指针大小在InnoDB源码中设置为6字节,这样一共14字节,那么一页的总指针树为16k/(8+6) = 1170。
每个指针对应1页,假设每行记录为1k(实际上现在很多互联网业务数据记录大小通常就是1K左右),那么一页大概能存16条行记录。
所以这棵B+树总行数为:1170 * 16 = 18720
三层的话,总指针数为1170 * 1170,故总行数为1170 * 1170 * 16=21902400
所以当单表数据超过千万级别后,就得考虑分表了,否则B+树的层级可能会超过3级,造成查询效率下降。

高性能索引策略

主键索引

  1. 介绍
    InnoDB中,表数据文件本身就是按B+Tree组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键。
  2. 主键索引结构
    在这里插入图片描述
    从上图可以看到叶节点包含了完整的数据记录,这种索引叫也做聚集索引。

辅助索引

  1. 介绍
    InnoDB的辅助索引与主键索引结构类似,不过data域存储相应记录主键的值而不是地址。
  2. 辅助索引结构
    在这里插入图片描述
    这里以英文字符的ASCII码作为比较准则。聚集索引这种实现方式使得按主键的搜索十分高效,但是辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。

联合索引

  1. 介绍
    联合索引是指对表上的多个列合起来做一个索引。
  2. 结构
    假定两个键值得名称分别为a、b如图:
    在这里插入图片描述
    可以看到这与我们之前看到的单个键的B+树并没有什么不同,键值都是排序的,通过叶子结点可以逻辑上顺序地读出所有数据,就上面的例子来说,即(1,1),(1,2),(2,1),(2,4),(3,1),(3,2),数据按(a,b)的顺序进行了存放。
    因此,对于查询select * from table where a=xxx and b=xxx, 显然是可以使用(a,b) 这个联合索引的,对於单个列a的查询select * from table where a=xxx,也是可以使用(a,b)这个索引的。
    但对于b列的查询select * from table where b=xxx,则不可以使用(a,b) 索引,其实你不难发现原因,叶子节点上b的值为1、2、1、4、1、2显然不是排序的,因此对于b列的查询使用不到(a,b) 索引。
    联合索引的第二个好处是在第一个键相同的情况下,已经对第二个键进行了排序处理,例如在很多情况下应用程序都需要查询某个用户的购物情况,并按照时间进行排序,最后取出最近三次的购买记录,这时使用联合索引可以帮我们避免多一次的排序操作,因为索引本身在叶子节点已经排序了

覆盖索引

覆盖索引,即从辅助索引中就可以得到查询所需要的所有字段值,而不需要查询聚集索引中的记录。覆盖索引的好处是辅助索引不包含整行记录的所有信息,故其大小要远小于聚集索引,因此可以减少大量的IO操作。

前缀索引

前缀索引:在对一个比较长的字符串进行索引时,可以仅索引开始的一部分字符,这样可以大大的节约索引空间,从而提高索引效率.但是这样也会降低索引的选择性.
索引的选择性: 不重复的值/所有的值. 可以看出索引的选择性为0-1,最高的就是该列唯一,没有重复值.所以唯一索引的效率是比较好的.
计算索引选择性:

select 
    count(distinct left(school_name,3))/count(*) as sch3, 
    count(distinct left(school_name,4))/count(*) as sch4,
    count(distinct left(school_name,5))/count(*) as sch5,
    count(distinct school_name)/count(*) as original
from 
    user;

其中查找到的original就是原本的选择性,sch3,sch4,sch5分别是取该列的前3,4,5个字符作为索引的时候的选择性.逐步增加这个数值,当选择性与原来相差不大的时候,就是一个比较合适的前缀索引的长度

实战

在做项目的时候,我遇到过一个用索引优化的例子是这个:

根据某号码后6位搜索数据。如果我们使用比如:

select * from t1 where fin like ‘%123456’;
或者select * from t1 where fin like ‘%123456%’;

这种是用不到索引的,因为字符串中间有’101’的字符串并没有排好序,所以只能全表扫描了。
优化的方式就是,在表里再存一列底盘号的反转数据,比如正常的是abc123456,那么反转的就是654321cba,后面查询的时候就可以转换为

select * from t1 where fin like '654321%';

这样就能用到索引了。

Optimizer Switch

mysql 5.1中开始引入optimizer_switch,在mysql优化语句过程中,可通过设置optimizer_switch控制优化行为。使用select @@optimizer_switch;可以查询当前MySQL支持的优化行为。下面介绍两个比较常用的优化行为。

ICP(索引下推)

1. 介绍
索引条件下推优化(Index Condition Pushdown (ICP) )是MySQL5.6添加的,用于优化数据查询。 它的作用针对基于辅助/第二索引的查询,过滤不必要的回表查询减少回表次数,提高查询效率。

2. 原理
图一:不使用ICP技术
要获取数据,首先读取索引信息,然后根据索引将整行数据读取出来。
然后通过where条件判断当前数据是否符合条件,符合返回数据。
在这里插入图片描述
图二:使用ICP技术

  1. 获取下一行的索引信息。
  2. 检查索引中存储的列信息是否符合索引条件,如果符合将整行数据读取出来,如果不符合跳过读取下一行。
  3. 用剩余的判断条件,判断此行数据是否符合要求,符合要求返回数据。
    在这里插入图片描述

索引下推的条件必须是覆盖索引,因为它的原理是利用覆盖索引中的数据来过滤条件,从而达到减少回表的次数。
比如建立覆盖索引(name,age),查询条件为(陈%,20),如果不适用覆盖索引,则是先将以陈开头的人的主键索引先查询出来(比如10个数据),然后根据这十个索引查询数据,然后再过滤年龄为20岁的。如果使用索引下推,则会在查询回表之前,先将年龄不是20的索引给过滤掉(因为覆盖索引中有年龄的数据,所以可以用到),再用过滤后的主键索引去回表查询,减少回表次数。

3.相关参数

索引下推优化是默认开启的。可以通过下面的脚本控制开关SET optimizer_switch =
‘index_condition_pushdown=off’; SET optimizer_switch = ‘index_condition_pushdown=on’;

MRR(多范围读)

1.介绍
Multi-Range Read 多范围读(MRR) 也是MySQL 5.6版本提供了很多性能优化的特性之一。
它的作用针对基于辅助/第二索引的查询,减少随机IO,并且将随机IO转化为顺序IO,提高查询效率。

2.原理
在没有MRR之前,或者没有开启MRR特性时,MySQL 针对基于辅助索引的查询策略:
第一步 先根据where条件中的辅助索引获取辅助索引与主键的集合,结果集为rest。
第二步 通过第一步获取的主键来获取对应的值。
在这里插入图片描述
由于MySQL存储数据的方式: 辅助索引的存储顺序并非与主键的顺序一致,从图中可以看出,根据辅助索引获取的主键来访问表中的数据会导致随机的IO . 不同主键不在同一个page 里面时必然导致多次IO 和随机读。

在使用MRR优化特性的情况下,MySQL 针对基于辅助索引的查询策略是这样的:
第一步 先根据where条件中的辅助索引获取辅助索引与主键的集合,结果集为rest
第二步 将结果集rest放在buffer里面(read_rnd_buffer_size 大小直到buffer满了),然后对结果集rest按照pk_column排序,得到结果集是rest_sort
第三步 利用已经排序过的结果集,访问表中的数据,此时是顺序IO.
在这里插入图片描述
从图示MRR原理,MySQL 将根据辅助索引获取的结果集根据主键进行排序,将乱序化为有序,可以用主键顺序访问基表,将随机读转化为顺序读,多页数据记录可一次性读入或根据此次的主键范围分次读入,以减少IO操作,提高查询效率。

3.相关参数:

我们可以通过参数 optimizer_switch的标记来控制是否使用MRR,当设置mrr=on时,表示启用MRR优化。mrr_cost_based 表示是否通过 cost base的方式来启用MRR.如果选择mrr=on,mrr_cost_based=off,则表示总是开启MRR优化。

后话:讲这两个优化器开关主要是扩展一下我们优化查询的思路,除了在索引上动脑子外,了解一下MySQL的优化器也是一个不错的优化方式吧。

补充:
B- 树
在这里插入图片描述
B+树:
它与 B- 树的不同之处在于:

  • 所有关键字存储在叶子节点出现,内部节点(非叶子节点并不存储真正的 data)

  • 为所有叶子结点增加了一个链指针
    在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章