Dreamer Dreamer
首页
  • 分类
  • 标签
  • 归档
关于
GitHub (opens new window)

lycpan233

白日梦想家
首页
  • 分类
  • 标签
  • 归档
关于
GitHub (opens new window)
  • Mysql

    • 浅谈char与varchar尾随空格对比
    • utf8存储emoji处理
    • Mysql 查询每个班级的前几名
    • Text为什么不支持设置默认值
    • Mysql 事务级别与差异
    • Mysql SQL 优化思路
    • 10w牌谱数据存储方案
    • InnoDB行格式详解
      • 目录
      • 行格式定义
        • 行格式枚举
        • 表标志位定义
        • 判断函数
      • VARCHAR 长度存储规则
        • COMPACT/DYNAMIC 格式的长度编码
        • 规则 1:列最大长度 ≤ 255 字节
        • 规则 2:列最大长度 > 255 字节
        • ⚠️ 重要说明:VARCHAR 数据的存储方式
        • 关键代码
      • 列最大长度计算
        • field_length 的含义
        • 计算方式
        • VARCHAR 类型
        • 其他类型
        • 字符长度与字节长度的转换
        • 示例
      • EXPLAIN key_len 计算
        • 计算公式
        • store_length 计算方式
        • 对于 VARCHAR 字段
        • 示例
      • COMPACT vs DYNAMIC 格式对比
        • 文本数据(BLOB/TEXT)存储差异
        • 关键常量
        • 代码说明
      • DYNAMIC 格式完整记录结构
        • 记录布局(从高地址到低地址)
        • 关键常量
        • VARCHAR 长度标识 vs BLOB 指针引用
        • VARCHAR 长度标识(1-2 字节)
        • BLOB 指针引用(20 字节)
        • 关键区别总结
        • 存储示例
        • 存储布局:
      • 关键文件位置
        • 定义文件
        • 实现文件
      • 总结
  • Node

  • Go

  • 运维

  • 后端
  • Mysql
lycpan233
2026-01-04
目录

InnoDB行格式详解

# InnoDB 行格式详解

本文档详细说明 MySQL InnoDB 存储引擎的行格式相关概念和实现细节。

# 目录

  1. 行格式定义
  2. VARCHAR 长度存储规则
  3. 列最大长度计算
  4. EXPLAIN key_len 计算
  5. COMPACT vs DYNAMIC 格式对比
  6. DYNAMIC 格式完整记录结构

# 行格式定义

# 行格式枚举

在 storage/innobase/include/rem0types.h 中定义:

enum rec_format_enum {
  REC_FORMAT_REDUNDANT = 0,  /*!< REDUNDANT row format */
  REC_FORMAT_COMPACT = 1,    /*!< COMPACT row format */
  REC_FORMAT_COMPRESSED = 2, /*!< COMPRESSED row format */
  REC_FORMAT_DYNAMIC = 3     /*!< DYNAMIC row format */
};
1
2
3
4
5
6

# 表标志位定义

在 storage/innobase/include/dict0mem.h 中定义:

/** dict_table_t::flags bit 0 is equal to 0 if the row format = Redundant */
constexpr uint32_t DICT_TF_REDUNDANT = 0;
/** dict_table_t::flags bit 0 is equal to 1 if the row format = Compact */
constexpr uint32_t DICT_TF_COMPACT = 1;
1
2
3
4

# 判断函数

在 storage/innobase/include/dict0dict.ic 中:

static inline rec_format_t dict_tf_get_rec_format(uint32_t flags) {
  if (!DICT_TF_GET_COMPACT(flags)) {
    return (REC_FORMAT_REDUNDANT);
  }

  if (!DICT_TF_HAS_ATOMIC_BLOBS(flags)) {
    return (REC_FORMAT_COMPACT);
  }

  if (DICT_TF_GET_ZIP_SSIZE(flags)) {
    return (REC_FORMAT_COMPRESSED);
  }

  return (REC_FORMAT_DYNAMIC);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# VARCHAR 长度存储规则

# COMPACT/DYNAMIC 格式的长度编码

对于 VARCHAR 字段,实际长度在记录中的存储规则(COMPACT 和 DYNAMIC 格式相同):

# 规则 1:列最大长度 ≤ 255 字节

  • 存储方式:始终使用 1 字节 存储实际长度
  • 范围:0-255

# 规则 2:列最大长度 > 255 字节

根据实际长度决定:

  • 实际长度 0-127:

    • 使用 1 字节 存储
    • 格式:0xxxxxxx(最高位为 0)
  • 实际长度 ≥ 128 或字段存储在外部:

    • 使用 2 字节 存储
    • 格式:1exxxxxxx xxxxxxxx
      • 第 1 字节的最高位(bit 7)= 1,表示使用 2 字节格式
      • 第 1 字节的第 6 位(bit 6,标记为 e)= 1 表示外部存储(EXTERNAL)
      • 第 1 字节的低 6 位(bits 0-5)和第 2 字节的 8 位共 14 位表示实际长度
      • 长度范围:0-16383(2^14 - 1)

# ⚠️ 重要说明:VARCHAR 数据的存储方式

VARCHAR 数据通常直接存储在记录的数据区域中,不是存储引用(指针)!

  • 正常情况:VARCHAR 字段的实际数据直接存储在记录的数据区域(DATA AREA)中
  • 特殊情况:只有在记录太大需要外部存储时,VARCHAR 字段才可能被存储到外部页,但这种情况非常罕见
  • 与 BLOB 的区别:
    • VARCHAR (DATA_VARCHAR, DATA_VARMYSQL, DATA_BINARY):数据直接存储在记录中
    • BLOB/TEXT (DATA_BLOB):在 DYNAMIC 格式中,数据存储在外部页,记录中只存储 20 字节的指针引用

DYNAMIC 格式的特殊规则:

  • 对于 VARCHAR 字段,如果最大长度 ≤ 255 字节,强制存储在行内(无法存储外部)
  • 如果最大长度 > 255 字节,且记录太大需要压缩,才可能考虑外部存储

# 关键代码

位置:storage/innobase/rem/rec.h

/* If the maximum length of the field is up
to 255 bytes, the actual length is always
stored in one byte. If the maximum length is
more than 255 bytes, the actual length is
stored in one byte for 0..127.  The length
will be encoded in two bytes when it is 128 or
more, or when the field is stored externally. */
if (DATA_BIG_COL(col)) {
  if (len & 0x80) {
    /* 1exxxxxxx xxxxxxxx */
    len <<= 8;
    len |= *lens--;

    offs += len & 0x3fff;
    if (UNIV_UNLIKELY(len & 0x4000)) {
      ut_ad(index->is_clustered());
      any_ext = REC_OFFS_EXTERNAL;
      len = offs | REC_OFFS_EXTERNAL;
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 列最大长度计算

# field_length 的含义

对于 VARCHAR 字段,field_length 表示字段的最大字节长度,而不是字符长度。

# 计算方式

# VARCHAR 类型

col->len = field->pack_length() - field->get_length_bytes()
1

其中:

  • field->pack_length():MySQL 字段的总打包长度(字符串数据 + 长度字段)
  • field->get_length_bytes():长度字段的字节数(1 或 2)
    • VARCHAR 最大长度 ≤ 255 时,为 1 字节
    • VARCHAR 最大长度 > 255 时,为 2 字节
  • col->len:InnoDB 中存储的字符串数据的最大字节长度(不包含长度字段)

# 其他类型

col->len = field->pack_length()
1

# 字符长度与字节长度的转换

// 字节长度转字符长度
char_length() = field_length / charset()->mbmaxlen

// 字符长度转字节长度
field_length = char_length * charset()->mbmaxlen
1
2
3
4
5

# 示例

  • VARCHAR(255) CHARACTER SET utf8mb4:

    • pack_length() = 255 + 1 = 256
    • get_length_bytes() = 1
    • col->len = 256 - 1 = 255 字节
  • VARCHAR(500) CHARACTER SET utf8mb4:

    • pack_length() = 500 + 2 = 502
    • get_length_bytes() = 2
    • col->len = 502 - 2 = 500 字节

# EXPLAIN key_len 计算

# 计算公式

在 EXPLAIN 输出中,key_len 表示索引键使用的字节长度,计算方式:

key_len = Σ(每个使用的索引键部分的 store_length)
1

# store_length 计算方式

对于每个索引键部分(KEY_PART_INFO),store_length 计算如下:

store_length = field->key_length()              // 字段的字节长度
             + (字段可为NULL ? 1 : 0)           // HA_KEY_NULL_LENGTH
             + (VARCHAR/BLOB/GEOMETRY ? 2 : 0)  // HA_KEY_BLOB_LENGTH
1
2
3

其中:

  • HA_KEY_NULL_LENGTH = 1 字节(NULL 标记)
  • HA_KEY_BLOB_LENGTH = 2 字节(VARCHAR/BLOB/GEOMETRY 的长度前缀)

# 对于 VARCHAR 字段

对于 VARCHAR 字段在索引中:

  • store_length = field->key_length() + (NULL 标记: 1 字节,如果可为 NULL) + 2 字节(长度前缀)
  • field->key_length() 是字段的最大字节长度(字符数 × mbmaxlen)
  • 只有实际使用的索引键部分会被累加到 key_len 中

# 示例

假设有索引:INDEX idx_name_email (name VARCHAR(100) NOT NULL, email VARCHAR(200) NULL)

  • name 字段:

    • field->key_length() = 100 × 4 = 400 字节(假设 utf8mb4)
    • NULL 标记:0 字节(NOT NULL)
    • 长度前缀:2 字节
    • store_length = 400 + 0 + 2 = 402 字节
  • email 字段:

    • field->key_length() = 200 × 4 = 800 字节
    • NULL 标记:1 字节(可为 NULL)
    • 长度前缀:2 字节
    • store_length = 800 + 1 + 2 = 803 字节

如果查询使用了这两个字段,key_len = 402 + 803 = 1205 字节


# COMPACT vs DYNAMIC 格式对比

# 文本数据(BLOB/TEXT)存储差异

特性 COMPACT 格式 DYNAMIC 格式
行内存储 768 字节数据 + 20 字节指针 = 788 字节 20 字节指针(无数据前缀)
页外存储 总长度 - 768 字节(超过部分) 全部数据(原子存储)
存储方式 分片存储(前缀在行内,剩余在页外) 原子存储(全部在页外)
优点 小 BLOB 查询可能更快(前缀在行内) 行更紧凑,大 BLOB 更高效
缺点 行体积较大,大 BLOB 效率较低 小 BLOB 需要额外页访问

# 关键常量

  • DICT_ANTELOPE_MAX_INDEX_COL_LEN = 768 字节(COMPACT 格式的前缀长度)
  • BTR_EXTERN_FIELD_REF_SIZE = 20 字节(BLOB 指针大小)
  • FIELD_REF_SIZE = 20 字节

# 代码说明

// COMPACT 格式
if (!dict_table_has_atomic_blobs(index->table)) {
  /* up to MySQL 5.1: store a 768-byte prefix locally */
  local_len = BTR_EXTERN_FIELD_REF_SIZE + DICT_ANTELOPE_MAX_INDEX_COL_LEN;
  // = 20 + 768 = 788 字节
}

// DYNAMIC 格式
else {
  /* new-format table: do not store any BLOB prefix locally */
  local_len = BTR_EXTERN_FIELD_REF_SIZE;
  // = 20 字节
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# DYNAMIC 格式完整记录结构

# 记录布局(从高地址到低地址)

┌─────────────────────────────────────────────────────────────┐
│                    RECORD HEADER (5 bytes)                   │
├─────────────────────────────────────────────────────────────┤
│ [2 bytes] Next record pointer (指向页内下一条记录)           │
│ [3 bits]  Record type                                        │
│           000 = 常规记录                                      │
│           001 = 节点指针 (B-tree内部节点)                    │
│           010 = infimum                                      │
│           011 = supremum                                     │
│ [13 bits] Heap number (记录在页内的序号)                    │
│ [4 bits]  Number of records owned (拥有的记录数)            │
│ [4 bits]  Info bits (删除标记、最小记录标记等)              │
├─────────────────────────────────────────────────────────────┤
│                    OPTIONAL FIELDS                           │
├─────────────────────────────────────────────────────────────┤
│ [1-2 bytes] Number of fields (如果表有 instant ADD COLUMN)   │
├─────────────────────────────────────────────────────────────┤
│                    NULL BITMAP                              │
├─────────────────────────────────────────────────────────────┤
│ [N bytes] NULL位图 (每个可空字段1位,向上取整到字节)         │
├─────────────────────────────────────────────────────────────┤
│                    LENGTH ARRAY                              │
├─────────────────────────────────────────────────────────────┤
│ [1-2 bytes] 最后一个非NULL可变长字段的长度                  │
│ [1-2 bytes] 倒数第二个非NULL可变长字段的长度                │
│ ...                                                          │
│ [1-2 bytes] 第一个非NULL可变长字段的长度                    │
│                                                              │
│ 编码格式:                                                    │
│ - 0xxxxxxx (1字节, 长度0-127)                               │
│ - 1exxxxxxxxxxxxxx (2字节, 长度128-16383, e=外部存储标志)   │
├─────────────────────────────────────────────────────────────┤
│                    DATA AREA (ORIGIN)                        │
│                    ┌──────────────────────┐                  │
│                    │  Field 0 的数据      │                  │
│                    ├──────────────────────┤                  │
│                    │  Field 1 的数据      │                  │
│                    ├──────────────────────┤                  │
│                    │  ...                 │                  │
│                    ├──────────────────────┤                  │
│                    │  VARCHAR字段:        │                  │
│                    │  - 实际数据直接存储  │                  │
│                    │  - 例如:"Hello"     │                  │
│                    │    (5字节数据)      │                  │
│                    ├──────────────────────┤                  │
│                    │  BLOB/TEXT字段:      │                  │
│                    │  (DYNAMIC格式)       │                  │
│                    │  - 仅存储20字节指针  │                  │
│                    │    [4 bytes] space_id                   │
│                    │    [4 bytes] page_no                    │
│                    │    [4 bytes] offset                     │
│                    │    [8 bytes] length + flags             │
│                    │  - 实际数据存储在外部页                 │
│                    └──────────────────────┘                  │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

# 关键常量

  • REC_N_NEW_EXTRA_BYTES = 5 字节(记录头大小)
  • BTR_EXTERN_FIELD_REF_SIZE = 20 字节(BLOB 指针大小)
  • REC_OFFS_EXTERNAL = 1 << 30(外部存储标志位)

# VARCHAR 长度标识 vs BLOB 指针引用

# VARCHAR 长度标识(1-2 字节)

  • 位置:存储在长度数组(length array)中
  • 大小:1 字节或 2 字节(可变)
  • 用途:表示该字段实际存储的数据字节数
  • 编码:
    • 1 字节:0xxxxxxx(长度 0-127)
    • 2 字节:1exxxxxxxxxxxxxx(长度 128-16383,e 位表示外部存储)
  • 数据存储:VARCHAR 的实际数据直接存储在数据区域中(不是指针)

# BLOB 指针引用(20 字节)

  • 位置:存储在数据区域(data area)中,作为字段数据的一部分
  • 大小:固定 20 字节
  • 用途:指向外部存储的 BLOB 数据
  • 结构:
    [0-3]   space_id (4 bytes)
    [4-7]   page_no (4 bytes)
    [8-11]  offset (4 bytes)
    [12-19] length + flags (8 bytes)
    
    1
    2
    3
    4
  • 数据存储:BLOB 的实际数据存储在外部页,记录中只存储指针

# 关键区别总结

特性 VARCHAR 长度标识 VARCHAR 数据 BLOB 指针引用 BLOB 数据
位置 长度数组 数据区域(直接存储数据) 数据区域(存储指针) 外部页
大小 1-2 字节(可变) 实际数据长度 20 字节(固定) 实际数据长度
内容 长度数值 实际字符串数据 空间 ID、页面号等 实际二进制数据
存储方式 元数据 直接存储在记录中 指针引用 存储在外部页

# 存储示例

假设有一条记录:

  • id INT NOT NULL (4 字节)
  • name VARCHAR(100) NOT NULL (实际存储 "Hello", 5 字节)
  • content TEXT NULL (大文本,存储在外部)

# 存储布局:

[记录头 5字节]
  - Next pointer: 2 bytes
  - Record type, Heap no, etc.: 3 bytes

[NULL位图 1字节]
  - content字段为NULL时,对应位为1;否则为0

[长度数组]
  - content字段长度: 2字节 (1e0000000014, e=1表示外部存储)
    或如果content为NULL,则不在长度数组中
  - name字段长度: 1字节 (05, 表示5字节)

[数据区域]
  - Field 0 (id): 4字节数据
  - Field 1 (name): 5字节数据 "Hello" (实际数据直接存储)
  - Field 2 (content):
    如果为NULL: 无数据
    如果不为NULL: 20字节BLOB指针 (指向外部页的数据)
      [4 bytes] space_id
      [4 bytes] page_no
      [4 bytes] offset
      [8 bytes] length + flags

注意:name 字段的 "Hello" 是实际数据,直接存储在记录中;
      content 字段存储的是指针,实际数据在外部页。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 关键文件位置

# 定义文件

  • storage/innobase/include/rem0types.h - 行格式枚举类型定义
  • storage/innobase/include/dict0mem.h - 表标志位常量定义
  • storage/innobase/include/dict0dict.ic - 格式判断函数
  • storage/innobase/rem/rec.h - 记录结构定义
  • storage/innobase/rem/rec.cc - 记录解析实现
  • storage/innobase/include/btr0types.h - BLOB 指针相关常量
  • storage/innobase/include/lob0lob.h - LOB 模块定义

# 实现文件

  • storage/innobase/rem/rem0rec.cc - 记录格式说明和实现
  • storage/innobase/data/data0data.cc - 大记录转换(BLOB 处理)
  • sql/field.h 和 sql/field.cc - MySQL 字段定义
  • sql/create_field.cc - 字段创建和长度计算
  • sql/opt_explain.cc - EXPLAIN 输出实现

# 总结

  1. VARCHAR 长度存储:

    • COMPACT 和 DYNAMIC 格式相同,根据字段最大长度和实际长度,使用 1 或 2 字节存储在长度数组中
    • 最大长度 ≤ 255 字节:始终 1 字节
    • 最大长度 > 255 字节:0-127 用 1 字节,≥128 用 2 字节
  2. VARCHAR 数据存储:

    • 通常直接存储在记录的数据区域中,不是存储引用
    • 只有在记录太大需要压缩的极少数情况下,才可能存储到外部
    • DYNAMIC 格式中,最大长度 ≤ 255 字节的 VARCHAR 强制存储在行内
  3. 列长度计算:对于 VARCHAR,col->len = pack_length() - get_length_bytes()

  4. EXPLAIN key_len:累加所有使用的索引键部分的 store_length

  5. COMPACT vs DYNAMIC(BLOB/TEXT):

    • COMPACT:存储 768 字节前缀+指针(788 字节)
    • DYNAMIC:仅存储指针(20 字节),所有数据在外部页
  6. VARCHAR vs BLOB 存储:

    • VARCHAR:数据直接存储在记录中
    • BLOB/TEXT:数据存储在外部页,记录中只存储 20 字节指针
编辑 (opens new window)
上次更新: 2026/01/04, 09:46:17
10w牌谱数据存储方案
npm为什么父项目指定依赖版本后,可以影响到子项目的依赖

← 10w牌谱数据存储方案 npm为什么父项目指定依赖版本后,可以影响到子项目的依赖→

最近更新
01
Gemini 出了点问题解决方案
12-02
02
10w牌谱数据存储方案
11-24
03
k3s + Gitea Action 实现 CI/CD 流程
10-17
更多文章>
Theme by Vdoing | Copyright © 2023-2026 Dreamer | MIT License
粤ICP备2025379918号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式