InnoDB表

索引组织表


在 InnoDB 存储引擎中,表都是根据主键顺序组织存放的,这种存储方式的表称为索引组织表(index organized table)。 在InnoDB存储引擎表中,每张表都有个主键(Primary Key),如果在创建表时没有显式地定义主键,则 InnoDB 存储引警会按如下方式选择或创建主键:

  • 首先判断表中是否有非空的唯一索引(Unique NOT NULL),如果有,则该列即为主键。

  • 如果不符合上述条件,InnoDB 存储引擎自动创建一个 6 字节大小的指针。

注:当表中有多个非空唯一索引时,InnoDB 存储引擎将选择建表时第一个定义的非空唯一索引为主键。这里需要非常注意的是,主键的选择根据的是定义索引的顺序,而不是建表时列的顺序。

CREATE TABLE z (
    a INT NOT NULL,
    b INT NULL,
    c INT NOT NULL,
    d INT NOT NULL,
    UNIQUE KEY (b),
    UNIQUE KEY (d), UNIQUE KEY (c));
 
INSERT INTO z SELECT 1, 2, 3, 4;
INSERT INTO z SELECT 5, 6, 7, 8;
INSERT INTO z SELECT 9, 10, 11, 12;

上述示例创建了一张表z,有 a、b、c、d 四个列。b、c、d 三列上都有唯一索引,不同的是 b 列允许 NULL 值。由于没有显式地定义主键,因此会选择非空的唯一索引,可以通过下面的 SQL 语句判断表的主键值:

mysql> SELECT a, b, c, d, _rowid FROM z;
+---+------+----+----+--------+
| a | b    | c  | d  | _rowid |
+---+------+----+----+--------+
| 1 |    2 |  3 |  4 |      4 |
| 5 |    6 |  7 |  8 |      8 |
| 9 |   10 | 11 | 12 |     12 |
+---+------+----+----+--------+
3 rows in set (0.00 sec)

_rowid 可以显示表的主键,因此通过上述查询可以找到表 z 的主键。此外,虽然 c、d 列都是非空唯一索引,都可以作为主键的候选,但是在定义的过程中,由于 d 列首先定义为唯一索引,故 InnoDB 存储引擎将其视为主键。

另外需要注意的是,_rowid 只能用于查看单个列为主键的情况,对于多列组成的主键就显得无能为力了,如:

CREATE TABLE a (
    a INT,
    b INT,
    PRIMARY KEY(a,b)
)ENGINE=InnoDB;

INSERT INTO a SELECT 1, 1;

SELECT a, _rowid FROM a;

mysql> SELECT a, _rowid FROM a;
ERROR 1054 (42S22): Unknown column '_rowid' in 'field list'

InnoDB 逻辑存储结构


从 InnoDB 存储引擎的逻辑存储构看,所有数据都被逻辑地存放在一个空间中,称之为表空间(tablespace)。表空间又由段(segment)、区(extent)、页(page)组成。页在一些文档中有时也称为块(block),InnoDB 存储引擎的逻辑存储结构大致图 4-1 所示。

表空间


表空间可以看做是 InnoDB 存储引擎逻辑结构的最高层,所有的数据都存放在表空间中。默认情况下,InnoDB 存储引擎有一个共享表空间 idbdata1,即所有数据都存放在这个表空间内。如果用户启用了参数 innodb_file_per_table,则每张表内的数据可以单独放到一个表空间内。

如果启用了 innodb_file_per_table 参数,需要注意的是每张表的表空间内存放的只是数据、索引和插入缓冲 Bitmap 页,其他类的数据,如回滚(undo)信息,插入缓冲索引页、系统事务信息,二次写缓冲(Double write buffer)等还是存放在原来的共享表空间内。这同时也说明了另一个问题,即使在启用了参数 innodb_file_per_table 之后,共享表空间还是会不断地增加其大小。


上图显示了表空间是由各个段组成的,常见的段有数据段、索引段、回滚段等。因为前面已经介绍过了 InnoDB 存储引擎表是索引组织的(index organized),因此数据即索引,索引即数据。那么数据段即为 B+ 树的叶子节点(图 4-1 的 Leaf node segment),索引段即为 B+ 树的非叶子引节点(图 4-1 的 Non-leaf node segment)。回滚段较为特殊,将会在后面的章节进行单独的介绍。

在 InnoDB 存储引擎中,对段的管理都是由引擎自身所完成,DBA 不能也没有必要对其进行控制。这和 Oracle 数据库中的自动段空间管理(ASSM)类似,从一定程度上简化了 DBA 对于段的管理。


区是由连续页组成的空间,在任何情况下每个区的大小都为 1MB。为了保证区中页的连续性,InnoDB 存储引擎一次从磁盘申请 4~5 个区。在默认情况下,InnoDB 存储引擎页的大小为 16KB,即一个区中一共有 64 个连续的页。

InnoDB 1.0.x 版本开始引人压缩页,即每个页的大小可以通过参数 KEY BLOCK SIZE 设置为 2K、4K、8K,因此每个区对应页的数量就应该为 512、256、128。

InnoDB 1.2.x 版本新增了参数 innodb_page_size,通过该参数可以将默认页的大小设置为 4K、8K,但是页中的数据库不是压缩。这时区中页的数量同样也为 256、128。总之,不论页的大小怎么变化,区的大小总是为 1M。


同大多数数据库一样,InnoDB 有页(Page)的概念(也可以称为块),页是 InnoDB 磁盘管理的最小单位。在 InnoDB 存储引擎中,默认每个页的大小为 16KB。而从 InnoDB 1.2.x 版本开始,可以通过参数 innodb_page_size 将页的大小设置为 4K、8K、16K。若设置完成,则所有表中页的大小都为 innodb_page_size,不可以对其再次进行修改。除非通过 mysqldump 导入和导出操作来产生新的库。

在 InnoDB 存储引擎中,常见的页类型有:

  • 数据页(B-tree Node)

  • undo 页(undo Log Page)

  • 系统页(SystemPage)

  • 事务数据页(Transaction system Page)

  • 插入缓冲位图页(Insert Buffer Bitmap)

  • 插入缓冲空闲列表页(Insert Buffer Free List)

  • 未压缩的二进制大对象页(Uncompressed BLOB Page)

  • 压缩的二进制大对象页(compressed BLOB Page)


InnoDB 存储引擎是面向列的(row-oriented),也就说数据是按行进行存放的。每个页存放的行记录也是有硬性定义的,最多允许存放 16KB / 2 - 200 行的记录,即 7992 行记录。这里提到了row-oriented 的数据库,也就是说,存在有 column-oriented 的数据库。MySQL infobright 存储引擎就是按列来存放数据的,这对于数据仓库下的分析类 SQL 语句的执行及数据压缩非常有帮助。类似的数据库还有 Sybase IQ、Google Big Table。

注意:16KB / 2 - 200 = 16 * 1024 / 2 - 200 = 7992

InnoDB 数据页结构


页是 InnoDB 存储引擎管理数据的最小磁盘单位,页类型为 B-tree Node 的页存放的即是表中行的实际数据了。下面将从底层具体介绍 InnoDB 数据页的内部存储结构。InnoDB 数据页由以下 7 部分组成,如图 4-6 所示。

  • File Header(38 字节):文件头,描述通用的状态信息;

  • Page Header(56 字节):页头,描述页特有的状态信息;

  • Infimun 和 Supremum Records:虚拟记录,标识页的最小记录和最大记录;

  • User Records:用户记录,即行记录;

  • Free Space:页中剩余的空闲链表,记录删除后,其空间会加入到空闲链表中;

  • Page Directory:页目录,存放 slot,每个 slot 存放记录在页中的偏移量;

  • File Trailer:文件结尾信息,checksum,用于检查页的完整性;

其中,File Header、Page Header、File Trailer 的大小是固定的,分别为 38、56、8 字节,这些空间用来标记该页的一些信息,如 Checksum,数据页所在 B+ 数索引的层数等。User Records、Free Space、Page Directory 这些部分为实际的行记录存储空间,因此大小是动态的。在接下来的各个小节中将具体分析各个组成部分。

下面主要介绍 Infimun 和 Supremum Records、User Records、Free Space、Page Directory。

Infimun 和 Supremum Records


在 InnoDB 存储引擎中,每个数据页中有两个虚拟的行记录,用来限定记录的边界。Infimum 记录是比该页中任何主键值都要小的值,Supremum 指比任何可能大的值还要大的值。这两个值在页创建时被建立,并且在任何情况下不不会被删除。在 Compact 行格式和 Redundant 行格式下,两者占用的字节数各不相同。图 4-7 显示了 Infimum 和 Supremum 记录。

User Records 和 Free Space


User Record 就是之前讨论过的部分,即实际存储行记录的内容。再次强调,InnoDB 存储引擎表总是 B+ 树索引组织的。

这里我们以「compact行格式」的数据记录为例来展开介绍下:

Free Space 很明显指的就是空闲空间,同样也是个链表数据结构。在一条记录被删除后,该空间会被加入到空闲链表中。

Page Directory


数据页中的记录以单向链表的形式,Infimum 记录、Supremum 记录分别为表头、表尾(如下图所示);而链表的查找效率非常低,每次都需要从表头开始遍历。故需要一个索引结构 Page Directory。

Page Directory 存储结构

1)将整个链表分为若干个部分(分组),每个组对应 Page Directory 中的一个 Slot(每个 Slot 占 2 字节)。

2)将分组内最后一条记录(组内最大记录)的地址(即该记录在数据页中的中的地址偏移量)存放在一个 Slot 中。

3)各 Slot 根据其指向记录的主键按从大到小的顺序在 Page Directory 中排列。

4)分组中记录的数量存储在该分组内最后一条记录(即该分组对应 Slot 所指向的记录)的 n_owned 字段。

注:分组和 Slot 是两个概念,且 Slot 中存放的是分组中最后一条记录的地址。

Page Directory 存储结构示意图如下:

注1:上图中分组 1、分组 2 实际上有四条记录,分组 3 实际上有 5 条记录,为了简单,这里做了简化,省略了中间的几条记录。

注2:

1)Slot 中存放的是记录在页中的相对位置,不是字节偏移量。

2)InnoDB 采用稀疏目录(Sparse Directory),并不是每个记录都对应一个 Slot。

3)一个分组可能包含多条记录,记录数存放在 Slot 所指向记录的 n_owned 字段;(即分组内的最后一条记录)

  • Infimum 记录的 n_owned 字段取值总为 1

  • Supremum 记录的 n_owned 字段取值为 [1, 8]

  • 用户记录的 n_owned 字段取值为 [4, 8]

4)当插入或者删除记录时,可能产生页分裂、页合并,此时需要对分组进行分裂、合并等平衡维护操作。

5)Slots 在 Page Directory 中按照各 Slot 指向记录的主键值从大到小排列,所以可通过二分查找快速定位到对应 Slot。

6)由于 Slots 采用稀疏目录,故定位到 Slot 后,还要根据 record header 的 next_record 继续遍历,查找所需记录。

Page Directory 的意义:如果需要根据主键查找某条记录,可先利用二分查找从 Slots 中定位到具体的 Slot,然后再对 Slot 对应分组中的所有记录进行遍历(分组中的记录仍以链表形式存在)。大大缩小了对链表遍历的范围,从而提高效率。

如何对数据页进行分组

1)数据页初始化后,只有 Infimum 记录、Supremum 记录,且分别属于两个分组。

2)在把一条新记录插入到页中后,确定 Slot 的方法是:

  • 从 Slot 0 槽开始遍历(该 Slot 所指向记录是所有 Slot 指向记录中的最小值,即 Infimum 记录)

  • 直至找到第一个 Slot 所指向记录的主键比新记录主键大的 Slot(即Supremum所在Slot)

  • 将新记录插入该 Slot 对应的分组中,并将该 Slot 所指向记录的 n_owned 字段值加 1

3)为了避免某个分组内记录数量过多,当分组内记录数达到 8 时,此时如果再向该分组插入记录,会导致此分组被拆分为两个组,一个分组内 4 条记录,另一个分组内 5 条记录。

4)增加了一个新分组后,Page Directory 中的 Slot 数据也需要适当调整、维护,以保证 Page Directory 的有序性和准确性。

如何确定每个分组中的记录数

1)Infimum 记录所在分组只能有一条记录,即只有它自己(n_owned 字段的值总为 1)。

2)Supremum 记录所在分组的记录数只能在 1~8 之间,即 n_owned 字段取值范围为 [1, 8]。

3)其它分组的记录数只能在 4~8 之间,即 n_owned 字段取值范围为 [4, 8]。

记录查找过程

1)InnoDB 基于 Page 管理磁盘数据,即 B+ 树本身并不能找到一条具体记录,只能找到记录所在页。

2)在找到记录所在 Page 后,根据二分查找,在 Page Directory 中找到对应 Slot。

3)根据 Slot 指向记录(record header),通过 next_record 指针继续遍历,最终找到目标记录。

Slot 和记录的存放顺序

在 Page 中 Slot 和记录的存放顺序如下:

1)Slot 从前往后存。

2)记录从后往前存。

示意图如下:

问:记录的存放顺序为什么要从后往前存?

1)方便空间分配,且不影响 Page Head Slot 偏移量的存放。

2)页中记录数不固定,因此 Slot Array 不是定长。

3)若从前往后存放记录,则 Slot Array 和记录分配空间互相影响,可能需要挪动数据来调整空间布局;若从后往前存放,则无此影响,Slot Array 向后扩展,记录向前扩展,同时从空间区域的两端分配空间,互不干扰。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

数据库内核

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值