关于MySQL中InnoDB的B+树可存储记录数估算
在InnoDB存储引擎中,每个数据页中有两个虚拟的行记录,用来限定记录的界限;Infimum记录是比该页中任何主键值都要小的值,Supremem记录比任何可能大的值都大的值还要大的值;这两个值在页创建时被建立,并且在任何情况下不会被删除,在Compact行格式和Redundant行格式下,两者占用的字节数各不相同;
InnoDB数据页结构
InnoDB中的各个数据页可以组成一个双向链表,而每个数据页的记录会按照主键值从小到到大顺序组成一个单向链表,每个数据页都会存储在它里面的记录生成一个页目录,再通过主键查找某条记录的时候可以在页目录中使用二分查找定位到对应的槽,然后再遍历该槽位对应的记录即可快速找到指定的记录;
数据页直接通过双向链表相关联,如下;
B+树的演化
B+树和二叉树,平衡二叉树一样,都是经典的数据结构,B+树由B树和索引顺序访问方法演化而来的;
二叉查找树
二叉树是每个节点最多只有两个分支(即不存在分支度大于2的节点)的树结构;
当元素按大小顺序插入时,构建出树的节点变成线性结构的,如按顺序插入[1, 2, 3, 4, 5, 6, 7, 8],从形式上看,更像一个单链表,它不能发挥BST的优势,极大地降低查询的效率;
如果想把这棵树的查询效率提高,需要这棵树是平衡的,从而引出平衡二叉树(AVL);
参考:[https://en.wikipedia.org/wiki/Binary_search_tree]
[https://www.cs.usfca.edu/~galles/visualization/BST.html]
平衡二叉树(AVL)
平衡二叉树也又被称为AVL树, 它可以保证查询效率较高,它具有以下特点:
将上面的二叉树的元素,按插入顺序生成的平衡二叉树如下;
平衡二叉树采用平衡算法可以让数据均匀的分布到树里的各个节点,避免树的高度相差太多,从而解决二叉树的层级不稳定问题;
首先操作系统从磁盘读取数据到内存时是以磁盘块为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么;
磁盘I/O操作的效率很低,如果利用二叉树作为索引结构,在大量数据存储中,查询时不能一下子将所有数据查出,并加载到内存中,只能逐节点加载(一个节点一次I/O),那么磁盘的I/O次数和索引树的高度是相关的,平衡二叉树由于树深度过大而造成磁盘I/O读写过于频繁,进而导致效率低下;
为了提高查询效率,就需要减少磁盘I/O次数,减少磁盘I/O次数,需要尽量将树的高度降低,把原来“瘦高”的树结构变的“矮胖”,树的每层的分叉越多越好;
如果能够在一个节点中存放更多的数据,还可以进一步减少节点的数量,从而降低树的高度,这就是多叉树;
参考:[https://www.cs.usfca.edu/~galles/visualization/AVLtree.html]
[https://en.wikipedia.org/wiki/AVL_tree]
平衡多路查找树(B-Tree)
B-Tree就是B树,它与平衡二叉树不同之处是:B树是一种多叉树(即每个非叶子结点可以有多个孩子),又称平衡多路查找树(查找路劲不止两个),它是为磁盘等外部存储设备设计的一种平衡查找树;
一棵M阶的B树的特性如下:
- 排序方式:所有节点关键字是按递增次序排列,并遵循左小右大原则;
- 子节点数:非叶子节点(根节点和枝节点)的子节点数 >1、且子节点数量<=M 、且M>=2,空树除外;(注:M阶代表一个树节点最多有多少个查找路径,M=M路,当M=2则是2叉树,M=3则是3叉)
- 关键字数的个数n满足:ceil(M/2) - 1 <= n <= M - 1(注:ceil是个向上取整的函数,如ceil(1.1)结果为2)
- 所有叶子节点均在同一层、叶子节点除了包含了关键字 和 关键字记录的指针外,也有指向其子节点的指针,只不过其指针地址都为null;
B树种每个节点根据实际情况可以包含大量的关键字信息和分支,如下为一个3阶的B树:
每个节点占用一个磁盘块空间,一个节点上有两个升序排序的关键字和三个指向子树根节点的指针,指针存储的是子节点所在磁盘块的地址;两个关键字划分出三个范围,分别对应三个指针指向的子树的数据的范围;以根节点为例,关键字为17和35,P1指向的子树的数据范围为小于17的,P2指针指向的子树数据范围是(17, 35),P3指针指向的子树的数据范围是大于35的;
模拟查找关键字29的过程:
根据根节点找到磁盘块1,读入内存,磁盘I/O操作第一次,比较关键字29在区间(17, 35),找到磁盘块1指向的指针P2;
根据P2指针找到磁盘块3,读入内存,磁盘I/O操作第二次,比较关键字29在区间(26, 30),找到磁盘块3指向的指针P2;
根据P2指针找到磁盘块8,读入内存,磁盘I/O操作第三次,在磁盘块8的关键字列表中找到关键字29;
I/O次数最多是B树的高度,每次深度加1就会进行一次磁盘IO的查询,将当前高度的数据加到内存中,再进行数值比较;B树相对平衡二叉树在节点空间的利用率上进行改进,B树在每个节点保存更多的数据,减少了树的高度,从而提升了查找的性能;
参考:[https://en.wikipedia.org/wiki/M-ary_tree]
[https://en.wikipedia.org/wiki/B-tree]
B+树
B+树是在B树的基础上的一种优化,其主要对两个方面进行了提升,一方面是查询的稳定性,另外一方面是在数据排序方面更友好,InnoDB存储引擎就是使用B+树实现其索引结构;
在MySQL中,InnoDB存储引擎中有页(Page)的概念,页是InnoDB磁盘管理的最小单位(一页对应一个磁盘块),默认每个页的大小为16KB,可以通过命令查看页的大小:
show variables like 'innodb_page_size';
InnoDB把磁盘数据读入到磁盘时以页为基本单位,在查询时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘的I/O次数,提高查询效率;
注意:这里的说的“块”是一个逻辑逻辑单位,不是指磁盘扇区的物理块,块是InnoDB读写磁盘的基本单位,InnoDB每操作一次磁盘I/O,读取的都是16KB的整数倍的数据;
在B树结构中,每个节点不仅包含数据的key值,还包含data值,而每一个节点的存储空间是有限的,如果data数据较大时,将会导致每个节点能存储的key数量很小,当存储的数量很大时,同样会导致B树的深度较大,增大查询的I/O次数,从而影响查询效率;
B+树的非叶子节点不保存具体的数据,保存关键字的索引,而所有的数据最终都会保存到叶子节点;因为所有数据必须要到叶子节点才能获取到,所以每次数据查询的次数都一样,这样一来B+树的查询速度也就会比较稳定,而B树的查找过程中,不同的关键字查找的次数很有可能都是不同的(有的数据可能在根节点,有的数据可能在最下层的叶节点),所以在数据库的应用层面,B+树就显得更合适;
B+树叶子节点的关键字从小到大有序排列,左边结尾数据都会保存右边节点开始数据的指针,因为叶子节点都是有序排列的,所以B+树对于数据的排序有着更好的支持;
- 在叶子节点一层,所有记录的主键按照从小到大的顺序排列,并且形成了一个双向链表,叶子节点的每一个Key指向一条记录;
- 非叶子节点取的是叶子节点里面Key的最小值,这意味着所有非叶子节点的Key都是冗余的叶子节点,同一层的非叶子节点也互相串联,形成了一个双向链表;
基于这样一个数据结构,可以很容易实现如下这几个特性:
- 范围查询
例如要查主键在[1,17]之间的记录,需要二次查询,先查找1所在的叶子节点的记录位置,再查找17所在的叶子节点记录的位置(就是16所处的位置),然后顺序地从1遍历链表直到16所在的位置;
- 前缀匹配模糊查询
假设主键是一个字符串类型,要查询where Key like 'abc%',其实可以转化成一个范围查询Key in ['abc','abcz'],如果是后缀匹配模糊查询,或者诸如where Key like '%abc%'这样的中间匹配,则没有办法转化成范围查询,只能挨个遍历;
- 排序与分页
叶子节点天然是排好序的,支持排序和分页;
基于B+树的特性,对于offset这种特性,其实是用不到索引的;例如每页显示10条数据,要展示第101页,通常会写成select xxx where xxx limit 1000,10,从offset=1000的位置开始取10条,虽然只取了 10 条数据,但实际上数据库要把前面的1000条数据都遍历才能知道offset=1000的位置在哪;对于这种情况,合理的办法是不要用offset,而是把offset=1000的位置换算成某个max_id,然后用where语句实现,就变成了select xxx where xxx and id>max_id limit 10,这样就可以利用B+树的特性,快速定位到max_id所在的位置,即是offset=1000所在的位置;
参考:[https://en.wikipedia.org/wiki/B%2B_tree]
[https://stackoverflow.com/questions/4481388/why-does-mysql-higher-limit-offset-slow-the-query-down]
[https://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/]
MySQL中InnoDB的B+树可存储记录数估算
在前面提到,对于磁盘来说,不可能一条条地读写,而是以“块”为单位进行读写,页是InnoDB磁盘管理的最小单位(一页对应一个磁盘块,这里的说的“块”是一个逻辑逻辑单位,不是指磁盘扇区的物理块,块是InnoDB读写磁盘的基本单位,InnoDB每操作一次磁盘I/O,读取的都是16KB的整数倍的数据),默认每个页的大小为16KB,无论叶子节点还是非叶子节点,都会装载到Page里;
表中的数据都是存在在页中,假设一条的记录大小为1KB,对于InnoDB一个页就可以存(16KB / 1KB = 16)条记录,如果用来装叶子节点,一个Page大概可以装16条记录;
计算非叶子节点可存放多少键值
假设主键ID为bigint类型,长度为8字节,指针类型一般为4或8个字节,一个页大概可以存储16KB / (8B + 8B) = 1K个键值,这里为了估值的计算方便,则这里取一页大概可以存储1K个键值,即1000个分叉;
第一层,一个节点就是一个Page,里面存了1000个Key,对应1000个分叉,最多存放16条记录;
第二层,1000个节点,对应1000个Page,每个节点里存储1000个Key,最多16 * 1000 = 16000条记录;
第三层,1000 * 1000个节点(Page),每个Page里面装16条记录,即16 * 1000 * 1000 = 16000000条记录;
把第一层和第二层的索引全装载到内存,即 (1 + 1000) * 16KB,即约为16MB的内存;
在Compact行格式存储记录中,Compact记录头信息有两个字段,一个是record_type,记录头信息的一项属性,表示记录的类型(0-普通的用户记录,1-B+树节点指针,2-最小记录,3-最大记录),另一个是记录头信息的一项属性,表示下一条地址相对于本条记录的地址偏移量;
Page与Page之间组成双向链表,每一个Page头部有两个关键字段:前一个Page的编号,后一个 Page 的编号;Page 里面存储一条条的记录,记录之间用单向链表串联;对于记录来说,定位到了Page,也就定位到了Page里面的记录,因为Page会一次性读入内存,同一个Page里面的记录可以在内存中通过二分法查找;