mysql 索引原理以及优化

mysql

参考 参考

系统从磁盘内读取数据都是一个磁盘块(block)读取的,innodb引擎读取是按照页(page)读取的,show variables like '%innodb_page_size%'; 可以查看,默认为16k,linux 默认页大小为4K
而一个磁盘块肯定没这么大,innodb读取的时候以page页为单位,会读取连续若干磁盘块的数据达到page的大小。而在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,
这就减少了磁盘I/O次数,提高查询效率。而B+Tree结构的数据就可以让系统高效的找到数据所在的磁盘块。
(磁盘是按 block 分的,一般为 512 Byte。磁盘 IO 一次读取若干个 block,我们称为一页,具体大小和操作系统有关,一般为 4 k,8 k或 16 k)
​
为什么不用二叉树呢?
平衡二叉树是通过旋转来保持平衡的,而旋转是对整棵树的操作,若部分加载到内存中则无法完成旋转操作。其次平衡二叉树的高度相对较大为 log n(底数为2),这样逻辑上很近的节点实际可能非常远,无法很好的利用磁盘预读(局部性原理)

b树(b-tree)

B-Tree结构的数据可以让系统高效的找到数据所在的磁盘块。为了描述B-Tree,首先定义一条记录为一个二元组[key, data] ,key为记录的键值,对应表中的主键值,data为一行记录中除主键外的数据。对于不同的记录,key值互不相同

一棵m阶的B-Tree有如下特性:

  • 每个节点最多有m个孩子。

  • 除了根节点和叶子节点外,其它每个节点至少有Ceil(m/2)个孩子。

    比如一个3阶的树至少有2个孩子

  • 若根节点不是叶子节点,则至少有2个孩子

  • 所有叶子节点都在同一层,且不包含其它关键字信息

  • 每个非叶子节点包含n个关键字信息和n+1个指针组成(P0,P1,…Pn, k1,…kn)

  • 关键字的个数n满足:ceil(m/2)-1 <= n <= m-1

 比如一个3阶的树至少有2个孩子,n等于2
  • ki(i=1,…n)为关键字,且关键字升序排序。
说的是k的保存顺序,从跟节点开始查找,从左边找,每个节点的关键字从左到由,由小到大排列参考下图
  • Pi(i=1,…n)为指向子树根节点的指针。P(i-1)指向的子树的所有节点关键字均小于ki,但都大于k(i-1)
这个说的可能绕点,我分析下,p0对应k1,P1对应K2 ,参考每个非终端节点包含n个关键字信息(P0,P1,…Pn, k1,…kn)这个
​那个以关键字为8和12来说,有3个指针,P1,P2,P3,那么对应的关键字2个分别为K2->8,K3->9,那么当i=2的时候根据公式套用
p(i-1)指向的是3和5,k2为8,那么P(i-1)指向的子树的所有节点关键字均小于ki结论成立,当i=3的时候根据公式继续套用
p(i-1)指向的是9和10,k3为12,那么P(i-1)指向的子树的所有节点关键字均小于ki结论成立,k(i-1)为8,
P(i-1)指向的子树的所有节点关键字都大于k(i-1)成立。
其实这段话说明了一件事就是非叶子节点里的关键字指向的节点左边的关键字始终小于右边的,中间的大于左边的,小于右边的,举个栗子:
​
每个节点占用一个盘块的磁盘空间,一个节点上有两个升序排序的关键字和三个指向子树根节点的指针,指针存储的是子节点所在磁盘块的地址。
两个关键词划分成的三个范围域对应三个指针指向的子树的数据的范围域。以根节点为例,关键字为17和35,P1指针指向的子树的数据范围为小于17,
P2指针指向的子树的数据范围为17~35,P3指针指向的子树的数据范围为大于35。
imgae

查找关键字29的过程

  1. 根据根节点找到磁盘块1,读入内存。【磁盘I/O操作第1次】

  2. 比较关键字29在区间(17,35),找到磁盘块1的指针P2。

  3. 根据P2指针找到磁盘块3,读入内存。【磁盘I/O操作第2次】

  4. 比较关键字29在区间(26,30),找到磁盘块3的指针P2。

  5. 根据P2指针找到磁盘块8,读入内存。【磁盘I/O操作第3次】

  6. 在磁盘块8中的关键字列表中找到关键字29

    分析上面过程,发现需要3次磁盘I/O操作,和3次内存查找操作。由于内存中的关键字是一个有序表结构,可以利用二分法查找提高效率。而3次磁盘I/O操作是影响整个B-Tree查找效率的决定因素。 B-Tree相对于AVLTree(平衡二叉树)缩减了节点个数,使每次磁盘I/O取到内存的数据都发挥了作用,从而提高了查询效率。

B+Tree

B+Tree是在B-Tree基础上的一种优化,使其更适合实现外存储索引结构,InnoDB存储引擎就是用B+Tree实现其索引结构。
​
从上一节中的B-Tree结构图中可以看到每个节点中不仅包含数据的key值,还有data值。而每一个页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,
当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。在B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上
只存储key值信息,这样可以大大加大每个节点存储的key值数量,(这也可以解释为什么mysql innodb 引擎3阶的可以存储很多数据)降低B+Tree的高度。

B+Tree相对于B-Tree有几点不同

  1. 非叶子节点只存储键值信息。

  2. 所有叶子节点之间都有一个链指针(每个叶子节点组成一个链表)。

  3. 数据记录都存放在叶子节点中。

  4. 如果查找的key就在根节点,那么B树只需要O(1)就可以找到,最大也不过是O(N),而B+TREE则至少需要O(N)才可以找到,因为data域存储在叶子节点,一个3阶的树至少也需要3次才能找到 B+树内节点不存储数据,所有 data 存储在叶节点导致查询时间复杂度固定为 log n。而B-树查询时间复杂度不固定,与 key 在树中的位置有关,最好为O(1)

  5. B+TREE支持范围查找,是因为叶子节点之间形成一个链表,而B-TREE不支持,每个节点的KEY和data域保存在一起,没法进行范围查找。 根据空间局部性原理:如果一个存储器的某个位置被访问,那么将它附近的位置也会被访问,B+树范围查找的时候可以很好的利用局部性原理。相邻的叶子节点利用磁盘预读原理提前将这些数据读入内存,减少了磁盘 IO 的次数

  6. B+TREE更适合外部存储。由于内节点无 data 域,每个节点能索引的范围更大更精,由于B-tree节点内部每个key都带着data域,而B+树节点只存储key的副本,真实的key和data域都在叶子节点存储。 前面说过磁盘是分 block 的,一次磁盘IO会读取若干个block,具体和操作系统有关,那么由于磁盘IO数据大小是固定的,在一次IO中,单个元素越小,量就越大。这就意味着B+树单次磁盘IO的信息量大于B-树,从这点来看B+树相对B-树磁盘IO次数少。 ​

B+Tree的非叶子节点只存储键值信息,假设每个磁盘块能存储4个键值及指针信息,则变成B+Tree后其结构
image
通常在B+Tree上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。因此可以对B+Tree进行两种查找运算:
一种是对于主键的范围查找和分页查找,另一种是从根节点开始,进行随机查找

可能上面例子中只有22条数据记录,看不出B+Tree的优点,下面做一个推算:

InnoDB存储引擎中页的大小为16KB,一般表的主键类型为INT(占用4个字节)或BIGINT(占用8个字节),指针类型也一般为4或8个字节,也就是说一个页(B+Tree中的一个节点)中大概
存储16KB/(8B+8B)=1K个键值(因为是估值,为方便计算,这里的K取值为〖10〗^3)。也就是说一个深度为3的B+Tree索引可以维护10^3 * 10^3 * 10^3 = 10亿 条记录

mysql的InnoDB存储引擎在设计时是将根节点常驻内存的,也就是说查找某一键值的行记录时最多只需要1~3次磁盘I/O操作。

数据库中的B+Tree索引可以分为聚集索引(clustered index)和辅助索引(secondary index)。上面的B+Tree示例图在数据库中的实现即为聚集索引,聚集索引的B+Tree中的叶子节点
存放的是整张表的行记录数据。辅助索引与聚集索引的区别在于辅助索引的叶子节点并不包含行记录的全部数据,而是存储相应行数据的聚集索引键,即主键。当通过辅助索引来查询数据时,InnoDB
存储引擎会遍历辅助索引找到主键,然后再通过主键在聚集索引中找到完整的行记录数据。

为什么使用B-/B+ Tree

一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个
数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的渐进复杂度。换句话说,索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数。

红黑树等数据结构也可以用来实现索引,但是文件系统及数据库系统普遍采用B-/+Tree作为索引结构。MySQL 是基于磁盘的数据库系统,索引往往以索引文件的形式存储的磁盘上,索引查找过程中
就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级,索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数。为什么使用B-/+Tree,还跟磁盘存取原理有关。

主存存取原理

局部性原理与磁盘预读

由于磁盘的存取速度与内存之间鸿沟,为了提高效率,要尽量减少磁盘I/O.磁盘往往不是严格按需读取,而是每次都会预读,磁盘读取完需要的数据,会顺序向后读一定长度的数据放入内存。而这样做的
理论依据是计算机科学中著名的局部性原理:

1. 当一个数据被用到时,其附近的数据也通常会马上被使用
2. 程序运行期间所需要的数据通常比较集中  

由于磁盘顺序读取的效率很高(不需要寻道时间,只需很少的旋转时间),因此对于具有局部性的程序来说,预读可以提高I/O效率.预读的长度一般为页(page)的整倍数。
MySQL(默认使用InnoDB引擎),将记录按照页的方式进行管理,每页大小默认为16K(这个值可以修改).linux 默认页大小为4K。

B-/+Tree索引的性能分析

每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了一个结点只需一次I/O。
假设 B-Tree 的高度为 h,B-Tree中一次检索最多需要h-1次I/O(根节点常驻内存),渐进复杂度为O(h)=O(logdN)O(h)=O(logdN)。一般实际应用中,出度d是非常大的数字,通常超过100,
因此h非常小(通常不超过3)。
而红黑树这种结构,h明显要深的多。由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,所以红黑树的I/O渐进复杂度也为O(h),效率明显比B-Tree差很多。

为什么使用 B+树

  1. B+树更适合外部存储,由于内节点无 data 域,一个结点可以存储更多的内结点,每个节点能索引的范围更大更精确,也意味着 B+树单次磁盘IO的信息量大于B-树,I/O效率更高。

  2. Mysql是一种关系型数据库,区间访问是常见的一种情况,B+树叶节点增加的链指针,加强了区间访问性,可使用在范围区间查询等,而B-树每个节点 key 和 data 在一起,则无法区间查找。

MyISAM

  1. MyISAM引擎使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址.

  2. MyISAM的索引文件仅仅保存数据记录的地址。在MyISAM中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复.

    MyISAM中索引检索的算法为首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录。 MyISAM的索引方式也叫做“非聚集”的,之所以这么称呼是为了与InnoDB的聚集索引区分。

InnoDB

  1. InnoDB的数据文件本身就是索引文件,MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。而在InnoDB中,表数据文件本身就是按B+Tree组织的一个索引结构, 这棵树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。这种索引叫做聚集索引,因为InnoDB的数据文件本身要按主键聚集, 所以InnoDB要求表必须有主键(MyISAM可以没有),如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则MySQL自动为InnoDB表 生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整形。

  2. InnoDB的辅助索引data域存储相应记录主键的值而不是地址。也就是说,InnoDB的所有辅助索引都引用主键作为data域。 辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。 这也是为什么不建议使用过长的字段作为主键,因为所有辅助索引都引用主索引,过长的主索引会令辅助索引变得过大。还有为什么主键不能重复,也是因为节点分裂调整效率低下。

索引使用策略及优化

MySQL的优化主要分为结构优化(Scheme optimization)和查询优化(Query optimization)。

  • 最左前缀原理与相关优化

    1. 索引最多用于一个范围列,因此如果查询条件中有两个范围列则无法全用到索引。

    2. 函数,参与计算等,以及隐式转换都用不到索引,因为在查找的过程当中需要查找一次KEY就得计算一次,所以优化器认为不合理,直接走全表扫描。

    3. like "%aaa" 这个用不到索引,因为不知道KEY的前缀是什么,没法在节点里面查找,like 'aaa%'这个可以用到索引。

    4. 联合索引比如a,b,c的索引 a,ab,abc都可以用到。参考数据结构,会根据联合索引的a去查找到主键,然后查找相应的数据,所以a为前缀的都可以用到,索引类型为req否则就没法找到,但explain分析工具分析即使顺序不对,比如c,b,a也是可以用到索引的,只不过索引的类型是index,index类型的索引会扫描整个索引树,而req可以直接根据索引树的有序性直接查找到数据。

    5. 前缀索引,如果有一个表的字段的前几个值都不会重复或者重复的几率小,这个时候建议建立前缀索引,这样节点存储的KEY就小,查找起来会更快。公式参考高性能MYSQL这本书。 前缀索引兼顾索引大小和查询速度,但是其缺点是不能用于ORDER BY和GROUP BY操作,也不能用于Covering index(即当索引本身包含查询所需全部数据时,不再访问数据文件本身)

    6. 覆盖索引,对于二级索引来说,如果索引树上的列刚好满足查询的列,就可以走覆盖索引,不用回表再查一次真正的数据。

    7. order by 参考mysql order by 排序

  • 索引选择性与前缀索引

    索引可以加快查找的速度,但会增加索引文件的体积,数据插入的时候可能导致节点分裂,索引重排等,导致删除,插入速度减慢,另外,MySQL在运行时也要消耗资源维护索引。 数据少的表不建议建立索引,还有就是根据索引的选择性(Selectivity),是指不重复的索引值(也叫基数,Cardinality)与表记录数(#T)的比值。Index Selectivity = Cardinality / #T 显然选择性的取值范围为[0, 1],选择性越高的索引价值越大,这是由B+Tree的性质决定的,比如:SELECT count(DISTINCT(title))/count(*) AS Selectivity FROM title;

InnoDB的主键选择与插入优化

  1. 尽量用innodb引擎所带的自增的建作为主键,InnoDB使用聚集索引,数据记录本身被存于主索引(一颗B+Tree)的叶子节点上。这就要求同一个叶子节点内(大小为一个内存页或磁盘页) 的各条数据记录按主键顺序存放,因此每当有一条新的记录插入时,MySQL会根据其主键将其插入适当的节点和位置,如果页面达到装载因子(InnoDB默认为15/16),则开辟一个新的页(节点)。 如果表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页。 这样就会形成一个紧凑的索引结构,近似顺序填满。由于每次插入时也不需要移动已有数据,因此效率很高,也不会增加很多开销在维护索引上。

  2. 如果使用非自增主键(如果身份证号或学号等),由于每次插入主键的值近似于随机,因此每次新纪录都要被插到现有索引页得中间某个位置, 此时MySQL不得不为了将新记录插到合适位置而移动数据,甚至目标页面可能已经被回写到磁盘上而从缓存中清掉,此时又要从磁盘上读回来,这增加了很多开销,同时频繁的移动、 分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续不得不通过OPTIMIZE TABLE来重建表并优化填充页面。因此,只要可以,请尽量在InnoDB上采用自增字段做主键。

你可能感兴趣的:(mysql 索引原理以及优化)