1、数据页

MySQL中有很多的页
1、B-Tree Node(数据页,索引页)
2、Undo Log Page (undo页)
3、System Page(系统页)
4、Transaction System Page(事务数据页)
5、Insert Buffer Bitmap(插入缓冲位图页)
6、Insert Buffer Free List(插入缓冲位图列表页)
7、Uncompressed Blob Page (未压缩的二进制大对象页)
本文主要讲解最常见的数据页

在MySQL中,数据页是存储数据的最小单位,默认大小为16k,可以进行修改,但是16k是经过测试之后,在性能方面一个最优的结果,一般是不会去修改它的

mysql数据页里是索引还是数据 mysql数据页 16k_mysql


数据页既是最小存储单位,也是innodb加载数据到内存的最小单位。

innodb每次最少加载一个数据页到Buffer Pool中,再到内存中,从这一页数据里面找到具体的数据。

因为I/O操作的代价是十分昂贵的,所以操作系统任务,所需要查找的数据,附近的数据也可能会被访问到,所以每次查询时,会把需要查找的数据的整个数据页都加载到内存中

数据页由七个部位组成

mysql数据页里是索引还是数据 mysql数据页 16k_数据_02

  1. File Header
    记录页的一些头部通用信息

属性名

字节大小

描述

FIL_PAGE_SPACE_OR_CHKSUM

4

存储检查状态

FIL_PAGE_OFFSET

4

当前页的page no,每个表空间从0开始,既这个值乘以数据页的大小就可以得到数据页在文件中的起始偏移位量。fil_io函数读取以及写入数据页的时候依赖这个规则

FIL_PAGE_PREV

4

指向上一个数据页,记录上一个数据页的Page no,通常用于维护B+Tree叶子结点的双向链表

FIL_PAGE_NEXT

4

指向下一个数据页,记录下一个数据页的Page no,通常用于维护B+Tree叶子结点的双向链表

FIL_PAGE_LSN

8

最近一次修改该page的LSN(Log Sequence Number):这个字段非常重要,InnoDB redolog幂等性就依赖此字段。在崩溃恢复应用日志阶段,如果发现redolog的lsn小于等于这个值,就不需要再次应用redolog了,也就是一个版本字段,跟redolog日志中保持一致

FIL_PAGE_TYPE

2

Page类型,上面有说过的7个类型,例如索引页就是FIL_PAGE_INDEX

FIL_PAGE_FILE_FLUSH_LSN

8

“文件已刷新到磁盘至少到此 lsn”(日志序列号),仅在文件的第一页有效

FIL_PAGE_SPACE

4

页面所在的空间ID,必要的标识符,因为不同的页面可能属于同一文件中的不同(表)空间。“空间”一词是“日志”或“表空间”的通用术语。

数据页类型

mysql数据页里是索引还是数据 mysql数据页 16k_链表_03


mysql数据页里是索引还是数据 mysql数据页 16k_mysql数据页里是索引还是数据_04


从FIL_PAGE_PREV和FIL_PAGE_NEXT已经可以看出,B+树中叶子节点,就是一个双向链表

  1. Page Header
    记录页的状态

名称

字节

描述

PAGE_N_DIR_SLOTS

2

Page Directory中的slot(槽)个数(见下文Page Directory详细描述),一个新建的空数据页,就有2个目录,分别指向最大(supremum)和最小记录(Infimun)

PAGE_HEAP_TOP

2

指向当前Page内已使用的空间的末尾偏移位置,既free space的开始位置(空闲空间的起始地址)。大于这个地址的且小于数据目录的空间都是未分配的,可以被后续使用。指向堆中第一条记录的记录指针

PAGE_N_HEAP

2

Page内所有记录个数,包含用户记录,系统记录以及标记删除的记录,在创建新的空页的时候,默认被置为2,既最大和最小记录。此外当第一个bit(最高位)设置为1时,表示这个page内是以新格式Compact格式存储的

PAGE_FREE

2

指向标记删除的记录链表的第一个记录

PAGE_GARBAGE

2

被删除的记录链表上占用的总的字节数,属于可回收的垃圾碎片空间

PAGE_LAST_INSERT

2

指向最近一次插入的记录偏移量(指针),主要用于优化顺序插入操作

PAGE_DIRECTION

2

用于指示当前记录的插入顺序以及是否正在进行顺序插入,每次插入时,PAGE_LAST_INSERT会和当前记录进行比较,用以确定插入方向,要么PAGE_LEFT, PAGE_RIGHT或 PAGE_NO_DIRECTION据此进行插入优化

PAGE_N_DIRECTION

2

当前以相同方向的顺序插入记录次数,同一方向连续插入的次数,例如“最后 5 个都在左边”

PAGE_N_RECS

2

Page上未被标记删除的用户记录个数

PAGE_MAX_TRX_ID

2

最近一次修改该Page记录的事务ID,主要用于辅助判断二级索引记录的可见性

PAGE_LEVEL

2

该Page所在的B+Tree Level,根节点的level最大,叶子结点的level为0

PAGE_INDEX_ID

8

该Page归属的索引ID

PAGE_BTR_SEG_LEAF

10

“B树中叶子节点的文件段头”,仅在 B+ 树的 Root 页定义

PAGE_BTR_SEG_TOP

10

“B树中非叶子节点的文件段头”,仅在 B+ 树的 Root 页定义

通过PAGE_LEVEL可以看出,B+树上的所有节点,都是数据页,因为每个数据页的Page header中都有一个PAGE_LEVEL用来记录当前数据页所在的B+树的高度。

  1. PAGE_FREE
    已删除的记录位于单向链表中。PAGE_FREE 页头中的指针指向列表中的第一条记录。记录头中的“下一个”指针(特别是记录的额外字节中)指向列表中的下一个记录。
    也就是说,数据页中的数据,是不会被删除的,在每行数据的头信息中,会有一个字段标识数据是否被删除(下文会详细解释),在数据页中,被标记为删除的数据,也会形成一个单向链表。
  2. Infimum+supremun
    最大(supremum)最小(Infimum)记录
    数据页中的上确界(supremum)和下确界(infimum)
    这两条数据,组成了数据页的边界,下限是最大下限,因此它低于可能的最低键值
    由于这两条记录不是我们自己定义的记录,所以它们并不存放在页的User Records部分,他们被单独放在一个称为Infimum + Supremum的部分,如图所示:
  3. mysql数据页里是索引还是数据 mysql数据页 16k_链表_05

  4. 从图中我们可以看出来,最小记录和最大记录的heap_no值分别是0和1,也就是说它们的位置最靠前。
    最大记录是这个数据页中逻辑上最大的记录,所有用户的记录都小于它。最小记录是数据页上最小的记录,所有用户记录都大于它。他们在数据页被创建的时候创建,而且不能被删除。它们为导航设置了一个有用的障碍,因此“ get-prev ”不会通过开头而“ get-next ”不会通过结尾。此外,infimum 记录可以是临时记录锁定的虚拟目标。
    还有一个作用就是在遍历数据的时候,通过槽slot从前往后找到数据的过程中,只要找到最小或者最大记录,就意味着已经到了本页面的边界,数据已经遍历完了。
    在不同的行格式中,Infimum和supremum所占用的字节数也不一样
    表的行格式决定了其行的物理存储方式,这反过来又会影响查询和 DML 操作的性能。随着单个磁盘页面中容纳更多行,查询和索引查找可以更快地工作,缓冲池中需要的缓存内存更少,写出更新值所需的 I/O 也更少。
    每个表中的数据分为页。构成每个表的页面排列在称为 B 树索引的树数据结构中。表数据和二级索引都使用这种类型的结构。表示整个表的 B 树索引称为聚集索引,它根据主键列进行组织。聚集索引数据结构的节点包含行中所有列的值。二级索引结构的节点包含索引列和主键列的值。
    可变长度列是列值存储在 B 树索引节点中的规则的一个例外。太长而不适合 B 树页面的可变长度列存储在单独分配的磁盘页面上,称为溢出页面。此类列称为页外列。页外列的值存储在溢出页面的单向链接列表中,每个这样的列都有自己的一个或多个溢出页面列表。根据列长度,可变长度列值的全部或前缀存储在 B 树中,以避免浪费存储空间和读取单独的页面。
  5. User Records
    实际的用户记录,也就是我们存储的实际数据行
    InnoDB存储引擎有两种文件格式
    Antelope: compact与redundant两种行记录格式
    Barracuda: compress与dynamic两种行记录格式


    MySQL官网-InnoDB 文件格式管理InnoDB支持四种行格式
    Version5.0之前使用Redundant
    Version 5.6 已经默认使用 Compact
    Version 5.7+ 默认使用Dynamic
    可以在创建表时,指定ROW_FORMAT=Compact更改行格式
    MySQL官网-InnoDB 行格式
  • REDUNDANT(冗余行格式)
  • COMPACT(紧凑行格式)
  • DYNAMIC(动态行格式)
  • COMPRESSED(压缩行格式)
    5.7默认是Dynamic,所有的行数据,都会有六个固定的隐藏列

    delete_mask:表示数据被删除时,并没有直接从磁盘上移除,而是被标记为了删除,在有新数据插入时,会覆盖这条数据,被标识为删除的数据,next_record,也会指向下一条被删除的数据,从而形成一条单向链表
    next_record:从这个就可以看出,数据页中的数据,是一条单向链表

这里简单的介绍一下四种格式

  1. REDUNDANT
    该REDUNDANT格式提供与旧版本 MySQL 的兼容性。是MySQL5.0之前Innodb使用的行记录存储方式,格式如图所示:

    Redundant行格式会把该条记录中所有列(包括隐藏列)的长度信息都按照逆序存储到‘字段长度偏移列表’中
    存储的值为两个相邻数值的差值
    所有数据逆序存放
不管该列使用的字符集是什么,只要是使用CHAR类型,占用的真实数据空间就是该字符集表示一个字符最多需要的字节数和字符串长度
 例如使用utf-8字符集的CHAR(10)列占用的真实数据空间始终是30个字节(10表示10个字符,一个中文字符占3个字节),不够的字节会用空格填充
  1. COMPACT
    Compact中一条完整的记录可以被分成记录的额外信息记录的真实信息两部分
    1、记录的额外信息
    Compact中记录的额外信息包含三类:变成字段长度列表、NULL值列表、记录头信息
    变成字段列表
    MySQL支持一些变长的数据类型(VARCHAR),这些数据在存储时不仅要存储数据内容,还要将占用的字节数存储起来,定长数据类型(CHAR),在以一些变长字符集存储数据时,由于存储长度为变长,所以也需要在该列表中存储其长度,在记录的开头部位就是各个变长字段占用的字节数,这些数据逆序存放
    如果表中没有变长字段,就没有变长字段长度列表
    NULL值列表
    NULL值列表存储表中所有NULL值,可以节约许多空间。
    值为1时,代表NULL;值为0时,代表非NULL。当位数不够整数个字节时,在高位补0。
    所有数据逆序存放
    如果表中没有运行存储NULL值的列,就没有NULL值列表
  2. mysql数据页里是索引还是数据 mysql数据页 16k_数据库_06

  3. 记录真实的数据
    隐藏列
    除了一些自定义列的数据外,MySQL还会为每个记录默认添加一些隐藏列:
  4. mysql数据页里是索引还是数据 mysql数据页 16k_mysql_07

  5. 储存数据
    当定长数据类型 (CHAR) 没有储存满时,剩下的位都由空格 (0x20) 填满
    当使用变成字符集时,由于储存字节长度不一定,当占用字节数少的字符串变为占用字节数多的字符串时需要新开辟一个空间,会产生存储碎片
  6. DYNAMIC & COMPRESSED
    这两个行格式与 Compact 只有在处理行溢出数据 时有不同:新的两种格式对于存放BLOB的数据采用了完全的行溢出的方式,在数据页中只存放20个字节的指针,实际的数据都存放在BLOB Page中,而之前的Compact和Redundant两种格式会存放768个前缀字节。
    Compressed行记录格式的另一个功能就是,存储在其中的行数据会以zlib的算法进行压缩,因此对于BLOB、TEXT、VARCHAR这类大长度类型的数据能进行非常有效的存储。
    具体可以看看MySQL行溢出数据在记录头信息之后的数据视具体情况而定:
    1、对于聚集索引记录,数据包含了事务id,回滚段指针;
    2、对于二级索引记录,数据包含了二级索引键值以及聚集索引键值。如果二级索引键值和聚集索引有重合,则只保留一份重合的,例如pk(col1,col2),sec key(col2,col3),在二级索引就只会包含(col1,col2,col3)。
    3、对于非叶子节点页的记录,聚集索引上包含了其子节点的最小记录键值及对应的page no;二级索引上有所不同,除了二级索引键值外,还包含了聚集索引键值,再加上page no三部分组成。
    包含子节点的最小记录键值的page no是为了在子节点所在的页进行查找时,可以从最小键值开始遍历
  7. mysql数据页里是索引还是数据 mysql数据页 16k_mysql_08

  1. Free Records
    数据页空闲空间,同样也是一个链表结构,当User Records中新增一行记录,Free Records就会少一行
    从PAGE_HEAP_TOP开始,到最后一个数据目录,这之间的空间就是空闲空间,都被重置为0,插入数据的过程中,如果需要插入页面,系统就会从这个页面的heap申请所需要的空间。
    以上是分配空间,下面说下空间的删除
    如果记录被删除了,则系统会把这个记录锁对应的空间,通过PAGE_FREE管理。每次在页面删除记录后,都会把删除的记录对应空间的NEXT指向原来PAGE_FREE指向的空间,然后再将PAGE_FREE指向新删除的记录空间的首地址,这就通过链表管理起来了。因为在页面记录中,都会在记录首地址的前两个字节位置存储当前记录的下一个记录,用来将记录之间形成一个单向链表,那么自然被删除的空间也可以通过这个指针串联起来,最终通过PAGE_FREE进行管理。

当对再次插入数据时,则会覆盖之前被删除的数据的空间,达到空间的复用。

  • Page Directory
    数据目录(槽),在数据页中,数据的查找并不是直接遍历整个数据页,而是通过一个叫槽(slot)的东西进行分组,每六行记录就会有一个槽,页中的数据是按键值的顺序存放的,比如“A,D,F,B”四条数据,实际存放顺序是,“A,B,C,D”,在存放了六条数据后,就会把六条数据的最小的那一条数据(也就是头部位置的第一条)。
    槽的作用就是页面内搜索数据的,可以理解为在页内构建的一个很小的索引来辅助二分查找。为了管理记录,把多条数据对应一个槽。
    我们将User Record中分的每段的记录数称为相应槽的记录数,而这个数字也就对应了行格式中的Record Header中的4字节的n_owned段。对于n_owned的规定:
  • 虚记录Infimum的n_owned值固定为1
  • 虚记录Supremum的n_owned值可取[1,8]
  • 槽指向的普通记录的n_owned可取[4,8]
  • 不是槽的普通记录的n_owned为0
    也就是说,infimum独占一组,supremum跟其他1-8条数据占一组

所有的数据,总共14条数据,被分为了4组,每组最大的一条数据的相对偏移量会被抽出来放置到组中对应的空间,infimum独占一组,supremum+四条数据占了一组,其他8条数据分为两组,按从0-4的顺序存放四个组,每个组中有一个2字节的空间,存放了每组对应数据的相对偏移量,通过这个偏移量可以找到具体的数据,而具体的数据上有一个n_owned字段,存储了这一组有多少条数据,往上查询对应数量就可以得到对应的数据

那么,如果要搜索主键为6的数据,过程是什么样的呢?
因为infimum独占了一组,而infimum是一条虚拟记录,所以可以不用查找,那么二分查找过程是这样的
start=1,stop=4
1、(4+1) / 2 = 2,取出槽2的相对偏移量对应的数据,主键为9,9大于6,start不变,stop为2
2、(2+1)/2=1,取出槽1的相对偏移量对应的数据,主键为5,小于6
3、结果为槽1<6<槽2,表示主键6应该在槽2对应的组中
4、取出槽2的相对偏移量,找到主键9数据,得到n_owned值
5、然后从这一段数据中找到最小记录,再通过单向链表遍历的方式找到主键6的数据
这里有个问题,槽中存放的都是最大记录,如何通过最大记录找到最小记录?
方法:
1、通过槽2,找到槽1(位置是相连的,槽2很容易就找到槽1)
2、槽1的最大记录的下一条,就是槽2的最小记录
在数据页中查找数据可以分为两步:
1、通过二分查找在槽中找到数据所属的分组对应的槽,再找到槽的最小记录
2、通过单向链表遍历找到具体数据

  1. File Trailer
    数据页尾部,这个部分处于数据页的最尾部,只有8个字节。低地址的四个字节存储checksum的值,高地址的四个字节存储FIL_PAGE_LSN的低位四字节。
    它的存在是因为InnoDB架构师担心完整性。一个页面不可能只写了一半,或者被崩溃损坏,因为日志恢复机制会恢复到一致的状态。但是如果真的出了问题,那么最好有一个校验,并且在页面的最后有一个值,该值必须与页面最开始的值相同。
    当一个页面在内存中修改之后,在同步之前需要先计算出校验和
    File Header 在页面的前面,所以校验和会先进行同步,File Trialer 在最后进行同步,如果两次校验和不同说明数据改变了
    插入数据申请内存流程:
    1、获取记录的长度(所需要的内存空间)。
    2、首先从PAGE_FREE链表中尝试获取足够的空间,仅仅只比较链表头的一个记录,如果这个记录的空间大于需要插入的记录的空间,则复用这块空间(包括heap_no),否则就从PAGE_HEAP_TOP分配空间。如果这两地方都没有,则返回空。这里需要注意一下,由于只判断PAGE_FREE链表的第一个头元素,所以算法对空间的利用率不是很高。假设,某个数据页先删了几条大的记录,但是最后一条删除的是比较小的记录大小为4K,3K,5K,2K,PAGE_FREE链表为2K->5K->4K->3K,那么只有当插入的记录小于2K时,这些被删除的记录才会被复用,如果插入的记录只需要0.5K,那么PAGE_FREE头的第一个元素,也就是2K,可以被复用,但是只用了前面0.5K,还有1.5K依然会被浪费,下次插入只能利用5K记录所占的空间,并不会把剩下的1.5K利用起来。这些特性从底层解释了,为什么InnoDB那么容易产生碎片,经常需要进行空间整理
    3、如果PAGE_FREE链表不够,就从PAGE_HEAP_TOP分配,如果分配成功,需要递增PAGE_N_HEAP
    4、如果这个数据页有足够的空间,则拷贝记录到指定的空间
    5、修改新插入记录前驱上的next指针,同时修改这条新插入记录的指针next指针。这两步主要是保证记录上链表的连续性。
    6、递增PAGE_N_RECS。设置heap_no。设置owned值为0
    7、更新PAGE_LSN_INSERT,PAGE_DIRECTION,PAGE_N_DIRECTION,设置这些参数后,可以一定程度上提高连续插入的性能,因为插入前需要先定位插入的位置,有了这些信息可以加快查找。
    8、修改数据目录。因为增加了一条新的记录,可能有些目录own的记录数量超过了最大值(目前是8条),需要重新整理一下这个数据页的目录。算法也比较简单,就是找到中间节点,然后用这个中间节点重新构建一个新的目录,为了给这个新的目录腾空间,需要把后续的所有目录都平移。
    9、写redo_log日志,持久化操作。
    10、如果有blob字段,则处理独立的off-page

2、Buufer Pool

缓冲池是内存中的一个区域,用于在InnoDB访问时缓存数据页。缓冲池允许直接从内存中访问经常使用的数据,从而加快读写速度。

比如在进行数据的查询时,当MySQL Server调用了InnoDB的api接口进行数据查询时,首先会先查看数据所在的数据页是否有存在于Buffer Pool中,如果有,则直接读取Buffer Pool中的,没有才到磁盘中读取数据,在磁盘中读取到数据后,同样也是先把数据页加载到Buffer Pool,然后再进行遍历,查询具体的数据。

Buffer Pool是一个非常大的缓冲区,如果是独立专用的服务器,通常会占用服务器百分之80的内存。

当进行全表遍历时,所有的数据页都会被加载到Buffer Pool中,磁盘的空间肯定要比内存大,如此大量的数据在进行加载时,肯定是存不下的,就需要使用LRU算法,将最近最少使用的数据从Buffer Pool中抛弃。
官网介绍-Buffer Pool

缓冲池列表

mysql数据页里是索引还是数据 mysql数据页 16k_链表_09

LRU算法,会将最近访问的数据放到LRU链表的头部,而InnoDB则是将数据放在一个MidPoint的位置,所以InnoDB的LRU算法,算是一个变体。

基于LRU的淘汰策略都存在一个瞬时访问命中率下降的问题。例如,MySQL在进行全表扫描时,会将大量的数据页读入内存,这些数据页几乎都不会再被上层查询所使用,因此又会快速的成为Old,被淘汰出内存。所以全表扫描的开销非常大,并且会污染Buffer Pool中的热点数据。

当有新的数据页要进入Buffer Pool时,会淘汰尾部的数据页,将新的移至头部
或者当Buffer Pool中的数据页被访问时,也会将它移至头部

3、Change Pool

Change Pool是Buffer Pool中的一块空间,用来缓存MySQL Server做更改操作的数据

对数据库而言SQL只有两种,查询和更新

insert,update,delete是更新

select是查询

我们在进行一个更新操作时,在innodb中会有一个预读的操作,会先将数据从磁盘中读取出,然后返回给MySQL Server进行更新操作,再写入到Buffer Pool中,然后InnoDB会开启一个守护线程,将Buffer Pool中的数据写入到实际的数据页中。

mysql数据页里是索引还是数据 mysql数据页 16k_数据库_10


mysql数据页里是索引还是数据 mysql数据页 16k_mysql数据页里是索引还是数据_11

4、索引和二分查找

  1. 索引
    索引是一个能够在数据库中快速查找到数据的有序的数据结构
  2. 二分查找
    二分查找,也叫折半查找,每找一次,都会把候选数据的长度缩短一半。在已经排好序的数组中,能够快速的找到指定的数据。时间复杂度为O(log2n)或者O(logn)两者没有区别,2是n的对数,可以省略。
    例如有一组数据,arr = [2,3,4,5,6,7,8,9,10,23,25,56,78]
    目标数据为78
    如果靠传统的遍历搜索,则需要遍历arr.length次,在大量数据的情况下,这种方式太慢了
    而使用二分查找,
    第一次直接从数组的中间判断78>arr[arr.lenght/2],也就是78>8,结果是true
    那可以得知,78肯定是在数组的后半部分,这时,把起点设置为8这个数据的下标+1
    因为不需要8了,从8的下一个开始,到数组的最后,再次进行折半
    起始下标7,结束下标12
    78>arr[(12+7)/2],78>23
    再次计算,78>arr[(12+10)/2],78>56,
    最后再次计算78>arr[(12+12)/2],78=78

5、二叉树搜索树(Binary Search Tree)

二叉搜索树是一个最简单的树,特点是左字节点永远小于父节点,右子节点永远大于父节点。将树展开,就是一个有序的链表

数据结构在线演示

mysql数据页里是索引还是数据 mysql数据页 16k_数据_12


展开二叉搜索树

mysql数据页里是索引还是数据 mysql数据页 16k_链表_13


二叉树既能实现快速插入,也能实现快速查询。

但是二叉树有一个很大的问题:

它的查找耗时是跟这棵树的深度相关的,在最坏的情况下时间复杂度会退化成O(n)。

按照上面那一批数据,从小到大按顺序插入,就会出现二叉树退化成链表的情况,例如下图。

树所有元素都在右边,变成了一棵斜树(右倾树),如果是倒序插入,则会变成左倾树

mysql数据页里是索引还是数据 mysql数据页 16k_mysql数据页里是索引还是数据_14

在这种情况下,是不能使用二分查找的,所以查询效率并没有提升,和顺序查找效率是一样的。

造成这种问题的原因:

因为左右自述深度差太大,这棵树的左子树根本没有节点,也就是它不够平衡。

为了解决这个问题,就演变出了平衡二叉树

6、AVL树(平衡二叉树)

平衡二叉树,对于树的平衡非常严格,左右子树的深度差绝对值不能1。

平衡二叉树在二叉搜索树的基础上,新增了一个自旋转的功能,也就是左旋和右旋

比如左子节点深度是2,右子节点深度就只能是1或者3.

这时再用上面的一组数据进行演示,同样按顺序插入。

在线演示平衡二叉树

mysql数据页里是索引还是数据 mysql数据页 16k_数据_15


可以看到,树现在处于一个平衡的状态,每个节点上都存储了当前节点的最大深度,当跟自己同级节点的深大相差超过1时,就会进行旋转,比如这时再插入11和12

mysql数据页里是索引还是数据 mysql数据页 16k_mysql_16


可以看到未旋转时,节点是向右倾斜的,这时就需要把11往左旋转,这个过程就叫左旋。

所以,为了保持平衡,AVL树在插入和更新数据的时候执行了一系列的计算和调整的操作。

平衡二叉树作为索引,如何查询数据?

在平衡二叉树中,一个节点是一个固定的单位,作为索引应该存储什么内容?

它应该存储三块的内容:

1、索引的键值。比如我们在id上面创建了一个索引,我在用where id = 1的条件查询的时候就会找到索引里面的id和这个键值。

2、数据的磁盘地址(物理地址),因为索引的作用就是取查找数据的存放地址。

3、左右子节点的指针,这样才能找到下一个节点。比如大于当前节点时,就走右边,小于当前节点时,就走左边。

mysql数据页里是索引还是数据 mysql数据页 16k_数据库_17


当我们用树的结构来存储索引的时候,因为拿到一块数据就要在Server蹭比较是不是要访问的数据,如果不是的话,就要再读一次磁盘。

访问一个节点就要跟磁盘之间发生一次I/O。InnoDB操作磁盘的最小的单位是一页(或者叫一个磁盘块),大小是固定的16k(16384字节)。

那么一个树的节点就是16k大小。

但是,从上图可以看到,我们一个节点只存了一个键值,一个地址,两个指针,这些数据的大小远远达不到16k的,所以,会浪费大量的磁盘空间,在一次I/O加载节点时,花了16k的空间,实际存储的数据可能才几十字节。浪费了大量的磁盘和内存空间。

而且平衡二叉树的深度是不可控的,一个节点只能存一条数据,那当数据量很大时,整个树的深度将会到达一个很恐怖的量,如果需要查找树最底下的数据,跟磁盘交互的次数就会无限增多。
这样会导致数据在检索和插入时,效率变的非常的慢,因为要不停的旋转,树的高度越高旋转的效率就越低。

为了解决这个问题,就需要在每个节点上,存放更多的数据,以此来达到,降低树的高度,以及将每个节点16k的空间利用起来的目的,从而演变出了B树(B-Tree)。

7、B-Tree(B树,多路平衡查找树 Balanced Tree)(分裂,合并)

跟AVL树一样,B树在枝节点(非叶子节点)和叶子节点上存储键值、数据地址、节点引用。

它有一个特点:分叉数(路数)永远要比比关键字树多1。比如我们画的这棵树,每个节点存储两个关键字,那么就会有三个指针指向三个子节点。

mysql数据页里是索引还是数据 mysql数据页 16k_数据_18


B Tree的查找规则:

例如查询目标是15

从根节点开始,15小于17走左边

判断15大于12,走右边

在磁盘块7中,找到了15,只用了三次I/O

效率相对AVL树来说,效率高了很多。

在线演示地址

B树实现单节点存储多数据原理,以及和AVL树的区别:
比如Max Degree(路数)是3的时候,我们插入数据1、2、3,在插入3的时候,本来应该在第一个磁盘块,但是如果一个节点有三个关键字的时候,意味着有四个指针,子节点会变成4路,所以这个时候必须进行分裂。把中间的数据2提上去,把1和3变成2的子节点。

如果删除节点,会有相反的合并操作。

主要这里是分裂和合并,跟AVL树的左旋和右旋不一样。

mysql数据页里是索引还是数据 mysql数据页 16k_mysql_19


如果继续插入4和5,B Tree又会出现分裂和合并操作。

mysql数据页里是索引还是数据 mysql数据页 16k_mysql_20


从这里也能看到,在更新索引的时候会有大量的索引的结构的调整,所以解释了为什么不要在频繁更新的列上创建索引,或者为什么不要更新主键。

节点的分裂和合并,其实就是InnoDB页的分裂和合并。

B Tree虽然解决了AVL树的查询效率问题,但是B Tree每个节点上,都存放了数据,这样每个节点能存放的键值也大大的减少了,而且同样也存在一个问题,就是全表扫描,范围查询的问题,当需要进行全表扫描时,需要遍历整个B Tree,比如用前序遍历,中序遍历,后序遍历,这样效率是非常的慢的。所以,由此演变出了B+Tree(B Tree升级版)

8、B+Tree(B+树)

B+树是B树的改良版本,也是InnoDB目前使用的底层索引数据结构。
特点:
1、分为两个类型的B+树索引,聚集索引和非聚集索引
2、聚集索引,非叶子只存放主键值和子节点的指针
3、聚集索引,叶子节点存放具体的数据页,而且叶子节点是一个双向链表连接的关系
4、非聚集索引,非叶子节点存放索引键值和节点的指针
5、非聚集索引,叶子节点存放的是索引键值和主键值

叶子节点,就是子节点树深度为0的节点,也就是整个树最底下那一层节点

可以看到只有叶子节点才存储数据,而且B+树每个节点在叶子节点上都会存在

mysql数据页里是索引还是数据 mysql数据页 16k_链表_21

mysql数据页里是索引还是数据 mysql数据页 16k_链表_22


MySQL中的B+树有两个特点:

1、它的关键字的数量跟路数相等。

2、B+树的根节点和枝节点(叶子节点)都不会存放数据,只有叶子节点才存放数据。前面的三种类型的数据,叶子节点和非叶子节点都是存放的数据的物理地址,而在B+树中,叶子节点存放的是真正的数据,从innodb存储引擎的表的物理文件来看,.ibd结尾的就是数据和索引的文件,innoDB只有两个文件

mysql数据页里是索引还是数据 mysql数据页 16k_mysql数据页里是索引还是数据_23


一个是表结构文件,一个是数据跟索引的文件,而MyISAM是索引跟数据分离的,所以他有三个文件

mysql数据页里是索引还是数据 mysql数据页 16k_数据_24


从这里就能看出,innodb的B+树,叶子节点,就是真正的数据所在的数据页。

那么从那些地方可以看出InnoDB中B+树的特点。

在上面讲数据页时,有说明过,在User Records中,有一个record_type,记录的类型,里面就有一个非叶子节点类型和普通记录类型,从这个其实也不难看出,数据页既可以存我们自己的用户的普通数据,还能用来存放非叶子节点的数据,可以得出B+树的每个节点,都是一个数据页。

而且,B+树非叶子节点数据页,只存放索引键值和子节点的指针,所以相对B树来说,可以存放的数据更多,树的高度也更低,这样查询的效率就非常的快了,一棵B+树只需要3层,就大概能够存放2千万的数据(具体计算公式可以百度)。

InnoDB中,只有聚集索引的叶子节点才会存放实际的数据,那么什么样的键值才能用做聚集索引。
1、如果表中有主键,就以主键的值做聚集索引
2、如果表中没有主键,就以第一个不为空的唯一索引做聚集索引
3、如果以上两个条件都不满足,InnoDB就会生成一个隐藏列,_row_id用做聚集索引
row_id是自增的列。

回表:

mysql数据页里是索引还是数据 mysql数据页 16k_mysql_25

B+树在搜索过程中,并不能具体定位要某一行数据,只能知道数据大概所在的数据页,所以在检索某条数据时,会将数据所在的数据页加载到Buffer Pool中,然后再到槽中进行二分查找,找到数据所在的组,然后拿到最小记录,再遍历链表找到具体数据。

为什么要这么做的原因:
因为,Innodb读取一次数据,就等于一个I/O操作,一次I/O操作的代价是非常的昂贵的,如果从数据页中一条条数据读取再检索,那么整个查询的效率将会十分缓慢,所以会先定位到数据所在的数据页,然后将数据页加载到Buffer Pool中,再进行查询。

B+树具体查询过程:
每个表在创建时,就会按照上面说的规则创建一个聚集索引,聚集索引的根节点数据页(page offset = 3),是固定不会变化的。只要表创建了聚簇索引,它的根节点号就被记录到了某个地方了。

比如要查询到id=15的数据。
会直接从根节点开始遍历,定位到数据所在的数据页,然后加载到内存。

因为B+树聚集索引的叶子节点是双向链表,所以在进行全表遍历时,只需要把双向链表拿出来遍历就可以了,不用遍历整个索引树,所以B+树的范围查询和排序,都是非常高效吧,树本身就是天然有序的。

B+树的磁盘读写能力相对于B树来说,更强,非叶子节点存放的只有主键值和子节点指针,能存放的数据就更多,I/O的次数就更少,因为一次可以把一大批的主键都加载到Buffer Pool中。

B+树相对B树来说更加的稳定,所有的查询,最终都要到叶子节点拿数据,数据被统一管理,不会分散。

B+树的叶子节点为什么说是双向链表,在上面说页的结构组成时,有提到File Header中,有一个FIL_PAGE_PREV和FIL_PAGE_NEXT,叶子节点的数据页,就是靠这两个属性串联起来的。

9、页分裂

在MySQL中,大家都知道建议使用自增的列作为主键,但是为什么要这么做,原因在于以下几点:

数据页要求,后一页的主键值,要比前一页大

1、数据页的数据存放是按照主键排序的,如果,主键是自增的,那么当一页满了之后,只需要新增一个数据页,然后继续插入数据,如果数据是随机的,那么可能出现后面一条数据比前面的小,就会出现页分裂

mysql数据页里是索引还是数据 mysql数据页 16k_链表_26


触发页分裂逻辑,经过调整后,得到如下结果

mysql数据页里是索引还是数据 mysql数据页 16k_链表_27


2、主键建议使用数字自增,不建议使用字符串也同样的道理,因为底层会把字符串的hashCode计算出来,排序,结果可想而知,相当于是随机值了,同样也会出现页分裂。

3、当一个数据页的数据,存放的容量到达了15/16时,就会新增一页,把新的数据放到新的页中,然后用前后页指针串联起来。