表是关于特定实体的数据集合,这也是关系型数据库模型的核心。本章介绍InnoDB的逻辑存储及其实现分析数据在表中如何组织和存放


索引组织表

表都是根据主键顺序组织存放的,如果创建表时没有显示定义主键,InnoDB会按照下面方式选择或者创建主键。首先判断表中是否有非空的唯一索引(UNIQUE NOT NULL),如果有则为主键,如果没有InnoDB会创建一个6字节的指针

如果有多个非空唯一索引,InnoDB选择建表时第一个定义的非空唯一索引作为主键,而不是按照列的顺序

1
mysql> SELECT *,_rowid from test;

_rowid可以显示表的主键,但是该值只能用于查看单个列作为主键的情况,对于多列组成的主键就无能为力

逻辑存储结构

表空间

表空间看作是存储引擎逻辑结构的最高层,所有数据都放在表空间中

即使如上一章所述,启用innodb_file_per_table,每章单独的表也只保存数据、索引和插入Bitmap页,其他数据,如回滚(undo)信息、插入缓冲索引页、系统事务信息,二次写缓冲等还是存放在共享表空间中。这也意味着即使表单独放在一个表中,共享表依旧会增加大小

表空间由各个段组成,常见的段有数据段、索引段、回滚段。因为InnoDB的表是索引组织的,因此数据即索引,索引即数据。因此数据段即为B+树的叶子节点(Leaf node segment),索引段即为B+树的非索引节点(Non-leaf node segment

区(Extent)是连续页组成的空间,区的大小都为1MB。为了保证连续性,InnoDB存储引擎一次想磁盘申请4-5个区默认情况下,InnoDB的页大小都为16KB,因此一个区一共有64个连续的页。当然可以通过参数设置页的大小,如innodb_page_size,但是区的大小固定不变

页???

页是InnoDB磁盘管理的最小单位,每个页默认为16KB。常见页的类型如数据页、undo页、系统页、事务数据页等

行???

数据是按照行进行存放的,每个页存放的行记录都是有硬性定义的,最多存放16KB/2-100行,即7992记录

行记录格式

Compact

MySQL默认的格式是Compact,只要知道页中行记录的组织规则,也能自己编写工具读取记录

Compact行记录格式的首部是一个非NULL变长字段长度列表(变长字段如varchar,定长数据类型利用空格来填充字段里的空白,但如果某个变长字段的长度定义为10,而输入的字符串长度为5,那么这个值的总长度也就是5),并且按照列的顺序逆序放置

第二个部分是NULL标志位,指示该行数据是否有NULL值

第三个部分共40位,记录如下一条记录的相对位置、记录类型、是否删除等等信息

第四部分就是实际存储每个列的数据,NULL不占用该部分任何空间,只占有NULL标记位

另外还有两个隐藏列,事务ID列回滚指针列,如果没有指定主键,还有rowid

创建一个测试表

并使用命令hexdump -C -v mytest.ibd > mytest.txt就能查看表的信息

从地址0xc078开始就是记录数据库信息,即下面的数字
03 02 01 00 00 00 10 00 2c 00 00 00 00 04 00 00 00 00 00 13 08 a8 00 00 01 1c 01 10 61 62 62 62 62 20 20 20 20 20 20 20 20 63 63 63

  • 03 02 01:变长字段长度列表,逆序
  • 00:NULL标志,第一行没有NULL,因此为0,而对于有NULL的第三行,此值就为06,转换成二进制就是00000110,即表示第2、3列有NULL值
  • 00 00 10 00 2c:记录头信息,固定为5字节,最后两个字节2c表示下一条记录的偏移量,因此在页内部是通过链表来串联各个行记录的
  • 00 00 00 00 04 00:RowID
  • 00 00 00 00 13 08:TransactionID
  • a8 00 00 01 1c 01 10:Roll Pointer
  • 61 62 62:对应前两列的数据a bb
  • 62 62 20 20 20 20 20 20 20 20:由于第三列数据定义为定长,因此需要填充
  • 63 63 63:对应数据ccc

Redundant

结构大同小异,不做分析

行溢出???

一般情况下,InnoDB的数据都是放在页种类为B-tree Node中,当发送行溢出时,数据存放在类型为UNcompress BLOG页中

=

数据页结构???

约束

为了保证数据的完整性,InnoDB通常提供以下几种约束方式

  • Primary Key
  • Unique Key
  • Foreign Key
  • Default
  • NOT NULL

约束更像是逻辑的概念,用来保证数据的完整性,而索引是一个数据结构,具有逻辑上的概念,在数据库上还代表物理存储的方式

通过触发器也可以实现对数据的完整性。还可以实现对传统CHECK约束的支持、物化视图、高级复制、审计等高级特性

视图

View是一个命名的虚表,由一个SQL查询来定义,但其中没有实际的物理存储

视图的主要用途之一是被用作一个抽象装置,对于一些应用程序,程序不关心基表的结构,只要按照视图定义来获取更新数据,View在一定程序起到了保护层的作用

物化视图的数据存储在非易失的存储设备上,可以保存复杂查询的结果,从而直接查出结果

MySQL本身不支持物化视图,但可以通过一定机制实现物化视图的功能。即通过触发器来实现当表变化时,就更新物化视图的基表来实现

分区表

分区的过程是将一个表或索引分解为更小、更可管理的部分。在物理上这个表或索引可能由数十个物理分区组成。每个分区可以独立处理,也可以作为更大对象的一部分处理

MySQL仅支持水平分区(即将同一个表不同行的记录分配到不同的物理文件中)。同时还是局部分区索引,一个分区既存放数据也存放索引,而全局分区则是索引放在一个对象,数据存放在各个分区中

MySQL支持以下类型的分区

  • RANGE:行数据基于一个给定连续区间的列值被放入分区
  • LIST:和RANGE类似,但是LIST分区面向的是离散的值
  • HASH:根据用户自定义的表达式的返回值来分区
  • KEY:根据MySQL提供的哈希函数来进行分区

RANGE

1
2
3
4
5
6
7
mysql> create table t(
-> id int
-> )engine=innodb
-> partition by range(id)(
-> partition p0 values less than (10),
-> partition p1 values less than (20));
Query OK, 0 rows affected (0.01 sec)

查看表在磁盘上的物理文件,启动分区之后,就不止一个idb文件,如下所示

之后插入的数据就会根据id值,分配到不同的分区,如果插入不在定义范围的值,就会抛出异常

RANGE分区主要用于日期列的分区,根据年份来保存记录,这样查询特定年份的数据,就只需要查询特定分区,而不会搜索所有分区,这叫做Partition Pruning(分区修剪)

LIST

1
2
3
4
5
6
7
mysql> create table t(
-> a int,
-> b int)engine=innodb
-> partition by list(b)(
-> partition p0 values in (1,3,5),
-> partition p1 values in (0,2,4));
Query OK, 0 rows affected (0.01 sec)

同样插入未定义的值,也会抛出异常

HASH

在HASH分区中,只要指定列值或表达式,以及要分割的分区数量即可

1
2
3
4
5
6
mysql> create table t_hash(
-> a int,
-> b datetime)engine=innodb
-> partition by hash(year(b))
-> partitions 4;
Query OK, 0 rows affected (0.02 sec)

该hash函数其实就是对分区数量取余,例如表达式year(b)表示取出b的年份,如果b为2010-4-10,则hash函数得到的值就是2010%4 = 2,也就是说这条数据会被放到分区2

MySQL还提供另一种hash函数linear hash,对于增加删除合并拆分更加快捷,有利于处理含有大量数据的表,缺点是分布可能不太均衡

KEY

和HASH类似,不同之处在于KEY分区使用MySQL数据库提供的函数进行分区,而HASH分区使用用户定义的函数进行分区

COLUMNS

上述分区的数据都必须是整型,所以对于DATATIME,需要通过year()函数转化成整型,而COLUMNS分区可以直接使用非整型数据进行分区,而且支持对多个列的值进行分区

1
2
3
4
5
6
7
8
9
10
mysql> create table rxv(
-> a int,
-> b int,
-> c char(5),
-> d int)engine=innodb
-> partition by range columns(a,d,c)(
-> partition p0 values less than (5,10,'hhh'),
-> partition p2 values less than (10,20,'dasdf'),
-> partition p3 values less than (maxvalue,maxvalue,maxvalue));
Query OK, 0 rows affected (0.01 sec)

子分区

MySQL允许在RANGE和LIST分区上再进行HASH和KEY的子分区

1
2
3
4
5
6
7
8
9
10
11
12
mysql> create table ts(
-> a int,
-> b date)
-> partition by range(year(b))
-> subpartition by hash(to_days(b)) (
-> partition p0 values less than (1990)(
-> subpartition s0,
-> subpartition s1),
-> partition p1 values less than (2000)(
-> subpartition s2,
-> subpartition s3));
Query OK, 0 rows affected (0.04 sec)

对于RANGE分区,如果向分区列插入NULL值,MySQL会选择将其放在最左边的分区;对于LIST分区,要使用NULL值,必须显式指定放在哪个分区;对于HASH和KEY分区,hash函数将NULL值的记录看做是0,在选择分区

分区性能

并不是将数据分区,就能提供查询效率

对于需要频繁扫描一张很大的表的数据库,如上亿行数据,用户如果要查询某一年的数据,那按照时间戳分区,将会提高效率

此处涉及B+树,暂时略过

交换分区

MySQL还支持分区或子分区的数据与另一个非分区的表中的数据进行交换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mysql> create table t1(
-> a int,
-> b char(10))
-> partition by range(a)(
-> partition p0 values less than (10),
-> partition p1 values less than (20));
Query OK, 0 rows affected (0.01 sec)


mysql> insert into t1 values (4,"dasda"), (11,"dasda");
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0

mysql> select * from t1;
+------+-------+
| a | b |
+------+-------+
| 4 | dasda |
| 11 | dasda |
+------+-------+
2 rows in set (0.00 sec)

我们先创建一个表,并有两个分区,每个分区一条记录,之后我们创建一个相同表结构的表t2

1
2
3
4
mysql> create table t2(
-> a int,
-> b char(10));
Query OK, 0 rows affected (0.01 sec)

我们将分区p0的数据转移到t2表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mysql> alter table t1 exchange partition p0 with table t2;
Query OK, 0 rows affected (0.02 sec)

mysql> select * from t1;
+------+-------+
| a | b |
+------+-------+
| 11 | dasda |
+------+-------+
1 row in set (0.00 sec)

mysql> select * from t2;
+------+-------+
| a | b |
+------+-------+
| 4 | dasda |
+------+-------+
1 row in set (0.00 sec)

要交换的表和分区表需要有相同的表结构,并且不能含有分区