【MySQL高阶】16.行结构

文章目录

  • [4. 行结构](#4. 行结构)
    • [4.1 InnoDB支持的数据行格式都有哪些?](#4.1 InnoDB支持的数据行格式都有哪些?)
      • [4.1.1 如何查看当前数据库或表应用了哪种行格式?](#4.1.1 如何查看当前数据库或表应用了哪种行格式?)
      • [4.1.2 如何指定行格式?](#4.1.2 如何指定行格式?)
      • [4.1.3 DYNAMIC 格式由哪些部分组成?](#4.1.3 DYNAMIC 格式由哪些部分组成?)
    • [4.2 数据区是怎么存储真实数据的?](#4.2 数据区是怎么存储真实数据的?)
      • [4.2.1 新问题](#4.2.1 新问题)
    • [4.3 额外(管理)信息区包含了关于行的哪些信息?](#4.3 额外(管理)信息区包含了关于行的哪些信息?)
      • [4.3.1 新问题](#4.3.1 新问题)
    • [4.4 头信息区域包含了哪些信息?](#4.4 头信息区域包含了哪些信息?)
      • [4.4.1 删除一行记录时在InnoDB内部执行了哪些操作?](#4.4.1 删除一行记录时在InnoDB内部执行了哪些操作?)
    • [4.5 Null列表有啥作用?列表中的值是什么?](#4.5 Null列表有啥作用?列表中的值是什么?)
    • [4.6 变长字段列表有啥作用?列表中的值是什么?](#4.6 变长字段列表有啥作用?列表中的值是什么?)
      • [4.6.1 如何记录变长字段的实际长度?](#4.6.1 如何记录变长字段的实际长度?)
      • [4.6.2 读取长度时如何处理粘包问题?](#4.6.2 读取长度时如何处理粘包问题?)
    • [4.7 其他的行格式与DYNAMIC有什么区别?](#4.7 其他的行格式与DYNAMIC有什么区别?)
      • [4.7.1 REDUNDANT 冗余格式](#4.7.1 REDUNDANT 冗余格式)
      • [4.7.2 COMPRESSED 压缩格式](#4.7.2 COMPRESSED 压缩格式)
      • [4.7.3 COMPACT 紧凑格式](#4.7.3 COMPACT 紧凑格式)
    • [4.8 总结](#4.8 总结)

4. 行结构

真实的数据在表空间以数据行的形式存储,也就是说每一条数据都对应着表中的一行,数据行在页中的位置如下图所示:

InnoDB支持四种行格式,不同的行格式数据的存储上有所不同,接下来我们介绍关于行结构的内容,首先提出第一个问题:


4.1 InnoDB支持的数据行格式都有哪些?

InnoDB支持四种行格式,分别是:

  1. REDUNDANT 冗余格式
  2. COMPACT 紧凑格式
  3. DYNAMIC 动态格式
  4. COMPRESSED 压缩格式

默认是 DYNAMIC 格式。


4.1.1 如何查看当前数据库或表应用了哪种行格式?

  • 可以使用以下SQL查看行格式:
mysql 复制代码
# 查看系统变量中设置的行格式
mysql> SHOW VARIABLES LIKE 'innodb_default_row_format';
+---------------------------+---------+
| Variable_name             | Value   |
+---------------------------+---------+
| innodb_default_row_format | dynamic | # 当前使用的行格式
+---------------------------+---------+
1 row in set (0.00 sec)

# 使用SHOW table STATUS查看数据库中的所有表
*************************** 4. row ***************************
           Name: student
         Engine: InnoDB
        Version: 10
     Row_format: Dynamic # 指定数据库使用的行格式
           Rows: 5
 Avg_row_length: 3276
    Data_length: 16384
Max_data_length: 0
   Index_length: 0
      Data_free: 0
 Auto_increment: 6
    Create_time: 2026-05-28 13:13:23
    Update_time: 2026-05-28 13:13:23
     Check_time: NULL
      Collation: utf8mb4_general_ci
       Checksum: NULL
 Create_options: row_format=DYNAMIC
        Comment: 
*************************** 5. row ***************************

# 通过查询INFORMATION_SCHEMA.INNODB_TABLES表查看指定表的行格式
mysql> SELECT NAME, ROW_FORMAT FROM INFORMATION_SCHEMA.INNODB_TABLES WHERE NAME='test_db/student';
+-----------------+------------+
| NAME            | ROW_FORMAT |
+-----------------+------------+
| test_db/student | Dynamic    | # 指定表使用的行格式
+-----------------+------------+
1 row in set (0.00 sec)

mysql> 

4.1.2 如何指定行格式?

可以通过全局变量设置行格式,也可以在创建表中通过 ROW_FORMAT 子句指定行格式:

mysql 复制代码
# 通过全局变量设置
SET GLOBAL innodb_default_row_format=DYNAMIC;
# 在创建表时明确的指定行格式
CREATE TABLE t1 (c1 INT) ROW_FORMAT=DYNAMIC;

4.1.3 DYNAMIC 格式由哪些部分组成?

一个 DYNAMIC 格式的数据行会被分为两部分:

  1. 存储真实数据的区域
  2. 存储额外信息的区域

在页结构的小节已经对行做了简单介绍,下面来详细讲解一下行的组成结构


4.2 数据区是怎么存储真实数据的?

数据区在数据行中的位置如下图所示:

  • 从分隔线向右第一个字段存储真实数据的主键值,对于主键值有以下几种情况:
    • 如果表中定义了主键,则直接存储主键的值;
    • 如果是复合主键会根据列定义的顺序依次排列在这里;
    • 如果没有主键,会优先使用第一个不允许为NULLUNIQUE 唯一列作为主键;
    • 如果既没有主键也没有唯一键,那么InnoDB会构建一个6字节的字段 DB_ROW_ID 作为行的唯一标识,存储在真实数据的头部
  • 紧接着是在事务运行中两个非常重要的固定字段
    • 6字节的事务ID字段 DB_TX_ID ,记录创建或最后一次修改该记录的事务ID
    • 7字节的回滚指针字段 DB_ROLL_PTR ,如果在事务中这条记录被修改,指向这条记录的上一个版本
  • 接下来就是除了主键和值为NULL的列之外,其他列的真实数据,按照顺序从左到右依次排列
  • 至于为什么不存储NULL值,原因很简单,就是为了节少空间,所有允许为NULL的列都会在行额外信息区的NULL值列表中进行标识,后面我们会详细详解,以上就是数据行对真实数据的存储方式。

回滚指针示意:


4.2.1 新问题

DB_TX_IDDB_ROLL_PTR 这两个字段的作用是什么?


4.3 额外(管理)信息区包含了关于行的哪些信息?

额外信息区在数据行中的位置如下图所示:

额外信息区从右向左分别为:头信息,Null值列表,变长字段列表。


4.3.1 新问题

头信息,Null值列表,变长字段列表分别存储了哪些信息?


4.4 头信息区域包含了哪些信息?

分隔线向左是额外信息区,第一个是固定占5Byte40Bit的头信息区域,头信息区由右向左主要包含以下信息:

  • 下一行地址偏移量: next_record16bit,通过这个信息将所有的行链接成一个单向链表
  • 行类型: record_type3bit,包括四种类型:
    • 0:普通数据行(这个页的第一个数据行)
    • 1:索引目录行(这个页的最后一数据行)
    • 2:页内最小行infimun
    • 3:页内最大行supremun
  • 行在整个页中的位置: heap_no13bit
  • 分组的行数: n_owned4bit,只在该行是分组最后一行才有值,这样就可以快速查询行数,而不用一条条的累加了
  • B+树索引树每层最小值标记: min_rec_flag1bit,如果当前行的类型是目录行也就是record_type=1 ,同时也是B+索引树某层的最小值,则会置为1,会在索引查询时用到,后面我们讲索引时再介绍
  • 删除标记: delete_mask1bit ,从页中删除数据行时,并不会直接移除,而是修改这个删除标记为 1
  • 预留区:占2bit

4.4.1 删除一行记录时在InnoDB内部执行了哪些操作?

从页中删除数据行时,并不会直接移除,而是修改 delete_mask 这个删除标记为 1 ,并将next_record 改为 0 ,同时将上一行的 next_record 指向后续的行,从而把该行从链表中断开。

如果执行事务提交后,则将这行的 next_record 指向一个被称为垃圾链表的区域,这个链表会被用在事务回滚中,后续在事务中详细介绍

这个图的意思是,加到这个垃圾链表的尾部,实际上的引用是从垃圾链表的头部开始的

之前的首个被删行的地址,其实就是这里的垃圾链表头部,哪个先被删,就被链接到这里。


4.5 Null列表有啥作用?列表中的值是什么?

  • 头信息区再向右就是NULL值列表的可变区域,用来存储数据行中所有列允许为Null的值从而节省空间,具体的实现方式是,用1BIT的大小来表示行中某一列是否为空,这样空列就不需要记录在真实数据区域中了。

  • 为每个没有定义 NOT NULL 约束的,也就是可以为NULL的列,在NULL值列表中都安排了一个bit位,按列序号从小到大的顺序从右至左依序安排,这就是常说的逆序排列。

    NULL值列表最小1字节即8bit,如果没有那么多可以为NULL的列,则会用0补满8bit,如果为值为NULL的列超过8个,则新开辟1字节的空间,依此类推。

  • 如果某列为空,则NULL值列表中对应的bit设置为1,这样只用了一bit就存储了NULL列,非常节省空间。

以下是一个建表的SQL语句

mysql 复制代码
CREATE TABLE test_student (
 `id` bigint NOT NULL AUTO_INCREMENT,
 `sn` char(10) NOT NULL,
 `name` varchar(50) NOT NULL,
 `age` int NOT NULL,
 `mail` varchar(100) NULL,
 `remark` varchar(255) NULL,
 PRIMARY KEY (`id`)
);

默认:

插入一条值:

mysql 复制代码
1,1,张三,18,123@qq.com,NULL

再插入一条值:

mysql 复制代码
2,1,李四,18,NULL,NULL

每一个插入行前面都有一个NULL值列表


4.6 变长字段列表有啥作用?列表中的值是什么?

查看编码集所占的字节数

mysql 复制代码
mysql> show charset;
+----------+---------------------------------+---------------------+--------+
| Charset  | Description                     | Default collation   | Maxlen |
+----------+---------------------------------+---------------------+--------+
| armscii8 | ARMSCII-8 Armenian              | armscii8_general_ci |      1 |
| ascii    | US ASCII                        | ascii_general_ci    |      1 |
| big5     | Big5 Traditional Chinese        | big5_chinese_ci     |      2 |
| binary   | Binary pseudo charset           | binary              |      1 |
| cp1250   | Windows Central European        | cp1250_general_ci   |      1 |
| cp1251   | Windows Cyrillic                | cp1251_general_ci   |      1 |
| cp1256   | Windows Arabic                  | cp1256_general_ci   |      1 |
| cp1257   | Windows Baltic                  | cp1257_general_ci   |      1 |
| cp850    | DOS West European               | cp850_general_ci    |      1 |
| cp852    | DOS Central European            | cp852_general_ci    |      1 |
| cp866    | DOS Russian                     | cp866_general_ci    |      1 |
| cp932    | SJIS for Windows Japanese       | cp932_japanese_ci   |      2 |
| dec8     | DEC West European               | dec8_swedish_ci     |      1 |
| eucjpms  | UJIS for Windows Japanese       | eucjpms_japanese_ci |      3 |
| euckr    | EUC-KR Korean                   | euckr_korean_ci     |      2 |
| gb18030  | China National Standard GB18030 | gb18030_chinese_ci  |      4 |
| gb2312   | GB2312 Simplified Chinese       | gb2312_chinese_ci   |      2 |
| gbk      | GBK Simplified Chinese          | gbk_chinese_ci      |      2 |
| geostd8  | GEOSTD8 Georgian                | geostd8_general_ci  |      1 |
| greek    | ISO 8859-7 Greek                | greek_general_ci    |      1 |
| hebrew   | ISO 8859-8 Hebrew               | hebrew_general_ci   |      1 |
| hp8      | HP West European                | hp8_english_ci      |      1 |
| keybcs2  | DOS Kamenicky Czech-Slovak      | keybcs2_general_ci  |      1 |
| koi8r    | KOI8-R Relcom Russian           | koi8r_general_ci    |      1 |
| koi8u    | KOI8-U Ukrainian                | koi8u_general_ci    |      1 |
| latin1   | cp1252 West European            | latin1_swedish_ci   |      1 |
| latin2   | ISO 8859-2 Central European     | latin2_general_ci   |      1 |
| latin5   | ISO 8859-9 Turkish              | latin5_turkish_ci   |      1 |
| latin7   | ISO 8859-13 Baltic              | latin7_general_ci   |      1 |
| macce    | Mac Central European            | macce_general_ci    |      1 |
| macroman | Mac West European               | macroman_general_ci |      1 |
| sjis     | Shift-JIS Japanese              | sjis_japanese_ci    |      2 |
| swe7     | 7bit Swedish                    | swe7_swedish_ci     |      1 |
| tis620   | TIS620 Thai                     | tis620_thai_ci      |      1 |
| ucs2     | UCS-2 Unicode                   | ucs2_general_ci     |      2 |
| ujis     | EUC-JP Japanese                 | ujis_japanese_ci    |      3 |
| utf16    | UTF-16 Unicode                  | utf16_general_ci    |      4 |
| utf16le  | UTF-16LE Unicode                | utf16le_general_ci  |      4 |
| utf32    | UTF-32 Unicode                  | utf32_general_ci    |      4 |
| utf8mb3  | UTF-8 Unicode                   | utf8mb3_general_ci  |      3 |
| utf8mb4  | UTF-8 Unicode                   | utf8mb4_0900_ai_ci  |      4 |
+----------+---------------------------------+---------------------+--------+
41 rows in set (0.00 sec)

mysql> 

以下是一个建表的SQL语句

mysql 复制代码
CREATE TABLE test_student (
 `id` bigint NOT NULL AUTO_INCREMENT,
 `sn` char(10) NOT NULL,
 `name` varchar(50) NOT NULL,
 `age` int NOT NULL,
 `mail` varchar(100) NOT NULL,
 `remark` varchar(255) NULL,
 PRIMARY KEY (`id`)
);

比如我插入一行数据,char,int类型的值的长达大小是固定的,但是varchar这种的变长字段存储的值的长度是不固定的,就需要存储起来,便于在真实数据区根据列的长度进行列和列之间的分割。

总结:

  1. 行结构的最左侧是变长字段列表,也叫可变字段长度列表,在这个列表中记录了数据行中所有变长字段的实际长度,这样做的目的,是为了在真实数据区域,可以根据列的长度进行列与列之间的分割;
  2. 需要记录的变长字段类型常见的有varcharvarbinarytextblob,以及当使用了例如utf-8、gbk等变长字符集的char类型,当char类型的字节数可能超过768个字节时,比如使用utf8mb4字符集时定义了char(255),这个字段的最大字节数是4*255=1020
  3. 每个变长字段分配1 ~ 2个字节来存放这些字段的真实大小,放置顺序也是按表中字段的顺序从右至左逆序排列;

例子:

mysql 复制代码
mysql> CREATE TABLE test_varchar (
    ->  `id` bigint NOT NULL AUTO_INCREMENT,
    ->  `name` varchar(20000) NULL,
    ->  PRIMARY KEY (`id`)
    -> );
ERROR 1074 (42000): Column length too big for column 'name' (max = 16383); use BLOB or TEXT instead
mysql> 
  1. 2个字节最大可以表示65535个字节,按照最大长度字符串,比如 utf8mb4,一个字符占用最多4个字节计算,2个字节最多可以表示65535/4=16383个字符,列数据类型varchar的长度上限16383就是根据这个计算来的;

  2. 需要特别说明的是,如果textblob存储的内容过大,一个页已经不够放了,就会把这个列放入一个叫"溢出页"的独立空间中,在这个数据行对应的真实数据处,只使用20个字节来标记这个溢出页的位置信息


4.6.1 如何记录变长字段的实际长度?

  • 不同的字符集在处理字符对应的最大字节长度不同,比如 ascii 最大1个字节, utf8mb3 最大3个字节, utf8mb4 最大4个字节,如下所示:
mysql 复制代码
+----------+---------------------------------+---------------------+--------+
| Charset  | Description                     | Default collation   | Maxlen |
+----------+---------------------------------+---------------------+--------+
| ascii    | US ASCII                        | ascii_general_ci    | 1      |
| utf8mb3  | UTF-8 Unicode                   | utf8mb3_general_ci  | 3      |
| utf8mb4  | UTF-8 Unicode                   | utf8mb4_0900_ai_ci  | 4      |
+----------+---------------------------------+---------------------+--------+

utf8mb4最多用4个字节表示一个字符

  1. 英文只占一个字节
  2. 大部分的中文占3个字节
  3. 复杂的符号与emoji表情占4个字节
  • 当使用varchar( M )指定一个字段的最大字符数时,该字段真实使用的字节数与建表时指定的字符集有关,如果指定的字符集单个字符最大占 W 个字节,从理论上讲,该列最多使用的字节数 M * W ,如果 M * W <= 255 则用一个字节记录这个变长字段的长度就足够了
  • 如果 M * W > 255 可能分为两种情况,假设当前变长字段实现占用了 L 个字节:
    • L <= 127 用一个字节表示长度
    • L > 127 用两个字节表示长度

4.6.2 读取长度时如何处理粘包问题?

  • 即在读取变长字段长度时,如何确定读取一个字节还是两个字节?

    我们插入一行数据,多个变长类型数据一起来的时候,计算机如何分辨哪个数据长度对应哪个呢?

    例如:我有两个变长类型:emailid,前后者大小都可能是1-2字节。计算机总不能按固定的字节数去读取吧。

  • 在任何时候都是先读一个字节,然后判断这个字节的高位是否为0,如果是0则表示当前用一个字节表示长度,如果是1则表示当前用两个字节表示长度

  • 1时再读一个字节,然后合并在一起进行解析得到该字段真实的使用的字节数,而且第二个BIT位表示是否使用溢出页默认数据页大小为16KB,数据页中一个数据行的大小最大为8KB

上面的描述可能不如下面的图直观,可以看一下下面的图:

如果长度L<=127,那么换成二进制最大也才:0111 1111,这个字节的高位是0则表示当前用一个字节表示长度

如果长度L>127,那么换成二进制就是1开头的:1??? ????,我们用两个字节来存储,即使前面全为0,从一开始往后读取的值也不会变。

1??? ????=0000 0000 1??? ????

如果前面不为0,因为第7位是1,也可以连着读取数据:

0000 0011 1??? ????=1??? ????+256+512

如果全为1,刚好是8KB大小:

所以才这么设计:

不过,如果是1000 00001111 1111那么多的那个字节不就浪费了吗?

确实,不过这个是必要的浪费,是结构性开销。

假如1000 00001111 1111,不存在前面的0000 0000,那么我们看一下下面的例子:

接收方字节流:[1000 0000] [0000 0101] [0100 0001]

  1. A:[1000 0000] B:[0000 0101] [0100 0001]
  2. A:[1000 0000] [0000 0101] B:[0100 0001]

你觉得应该是1还是2呢?
假如1000 00001111 1111,存在前面的0000 0000,那么我们看一下下面的例子:

接收方字节流:[0000 0000] [1000 0000] [0000 0101] [0100 0001]

A:[0000 0000] [1000 0000] B:[0000 0101] [0100 0001]

是不是很直观?
小结:

  1. InnoDB支持四种行格式,分别是:
    1. REDUNDANT 冗余格式
    2. COMPACT 紧凑格式
    3. DYNAMIC 动态格式(默认)
    4. COMPRESSED 压缩格式
  2. DYNAMIC 格式的数据行由两部分组成,分别是真实数据区和额外信息区
  3. 真实数据区存储的是真实数据,有三个隐藏字段分别是:
    1. DB_ROW_ID 作为行的唯一标识
    2. DB_TX_ID 事务ID字段
    3. DB_ROLL_PTR 回滚指针字段
  4. 额外信息区从右向左分别为:
    1. 头信息
    2. Null值列表
    3. 变长字段列表

4.7 其他的行格式与DYNAMIC有什么区别?

4.7.1 REDUNDANT 冗余格式

已被淘汰,之所以存在是为了与旧版本 MySQL 兼容,不建议使用,这里不再讨论。


4.7.2 COMPRESSED 压缩格式

行结构与 DYNAMIC 完全相同,只是会对数据进行压缩,以减少对空间的占用。


4.7.3 COMPACT 紧凑格式

在结构上与 DYNAMIC 相同,只是对超长字段的处理上有些区别,它不会把所有超长数据都放在溢出页中,而是会在本行中保留前768个字节的数据,多出的部分放在溢出页中,溢出页的地址额外用20个字节表示,那么在本行的列中就会占用768+20个字节。


4.8 总结

相关推荐
javachen__1 小时前
mysql-1015 - Can‘t lock file (errno: 165 - Table is read only)
mysql
逍遥德1 小时前
PostgreSQL --- 自增主键【序列】的避坑指南
数据库·后端·sql·mysql·postgresql·sqlserver
HQL_seven1 小时前
Android 四大组件解析
android
Digitally1 小时前
适用于安卓、iOS 和电脑的最新文件共享应用评测指南
android·ios·电脑
土狗TuGou1 小时前
SQL进阶笔记 · 第1篇:存储引擎
java·数据库·笔记·后端·sql·mysql
Database_Cool_2 小时前
MySQL 数据分析慢怎么办?迁移到阿里云 AnalyticDB MySQL 实现百倍加速
数据仓库·mysql·阿里云·数据分析
青春之我_XP2 小时前
深度解析 SQL 经典面试题:如何优雅地计算连续登录天数?
数据库·sql·mysql
承渊政道2 小时前
【MySQL数据库学习】MySQL表的约束(上)
数据库·c++·学习·mysql·bash·数据库架构·数据库系统
梓䈑2 小时前
【MySQL】一文梳理MySQL 8.0常用数据类型:含存储范围、对比差异与实操案例
数据库·mysql