浅析 InnoDB 变长字段存储:从 Compact 行格式到 `rec_get_n_extern_new` 源码

张开发
2026/4/17 1:14:47 15 分钟阅读

分享文章

浅析 InnoDB 变长字段存储:从 Compact 行格式到 `rec_get_n_extern_new` 源码
浅析 InnoDB 变长字段存储从 Compact 行格式到rec_get_n_extern_new源码InnoDB行格式InnoDB 提供了 4 种行格式分别是 Redundant、Compact、Dynamic和 Compressed 行格式。Redundant格式现在几乎没人用了这里以 Compact 行格式为例弄懂了 Compact 行格式再去弄懂Dynamic和 Compressed 行格式很快就能完成因为都是基于 Compact 行格式改了一点东西。从MySQL 5.7 版本之后默认使用Dynamic格式Compact 行格式在 Compact 行格式下一条完整的记录分为存储的额外数据和存储的真实数据存储的额外数据变长字段长度列表(以字节为单位)NULL 值列表( NULL 值列表必须用整数个字节的位表示1字节8位如果使用的二进制位个数不足整个字节则在高位补0)记录头信息记录的真实数据三个隐藏字段row_id(只有在表没有定义主键时才会额外出现)、trx_id、roll_pointern个列的值我们现在只关注变长字段长度列表以下面的一张表举例子CREATETABLEuser(idINT(11)NOTNULL,accountVARCHAR(20)NOTNULLCOMMENT账号唯一登录凭证,passwordVARCHAR(30)NOTNULLCOMMENT加密后的密码,statusTINYINTDEFAULTNULLCOMMENT状态1启用0禁用)ENGINEInnoDBDEFAULTCHARSETascii ROW_FORMATCOMPACT往该表插入两条记录insertuservalues(1,mynameissaki,123456,0);insertuservalues(2,anon,lovesoyorin,1);来看第一条记录account 列的值为 mynameissaki真实数据占用的字节数为12字节十六进制0x0Cpassword 列的值为 123456真实数据占用的字节数为6字节十六进制0x06id 列和status 列不是变长字段所以不用管这些变长字段占用的字节数会按照列的顺序逆序存放变长字段长度列表以字节为单位如 0x0C 表明真实数据占用的字节数为12字节第一条行记录中的格式如下图同样道理第二条行记录的行格式中变长字段长度列表里的内容是[0B 04]如下图单个字段varchar()在变长字段长度列表占用空间核心判定逻辑InnoDB 会根据两个关键维度来做决定单个字段定义的最大字节数 (M X W)以及实际存储的数据字节数 (L)。变量定义M列定义时的字符数如VARCHAR(100)中的 100。W字符集中单个字符占用的最大字节数如utf8mb4的 W4ascii的 W1。M X W该列理论上能达到的最大字节长度。L当前这条记录中该字段实际存储的数据所占用的字节长度。总结表字符集最大长度 (M×W)实际数据长度 (L)单个字段在长度列表占用空间 255任意1 字节 255 1271 字节 255 1272 字节之前的第一条、第二条记录通过上表不难得出它们的account、password字段在变长字段长度列表占用空间都为1字节。通过前面的表格我们已经掌握了判定逻辑。但你可能会问1.为什么 InnoDB 要设 127 这个分界点判定逻辑以下在 InnoDB 的Compact行格式中1.为什么是 127InnoDB 在处理大于 255 字节定义的字段时会利用字节的**最高位Bit**作为标志位。如果字节的第一位是0说明这是 1 字节表示长度范围 0~127。如果字节的第一位是1说明这是 2 字节的一部分因为字节最高位是标志位用于识别是否为双字节次高位作为“溢出标志位”次高位作为溢出标志位的前提是该长度记录已经进入了“双字节模式”故可表示的长度范围为 0 ~ 16383。举个例子存入一段长文本 (L 300)300 的二进制00 0001 0010 1100原数InnoDB 存储表现会在最高位设为1因为超过了 127 字节必须用双字节记录长度次高位设为0没溢出所以为0。存储形式1000 0001 0010 1100解析看到第一个字节最高位是1心领神会“这是个大家伙得连着后一个字节看。”抹去最高位的1把剩下的位拼起来算出 L 300。再举个例子存入一段长文本 (L 2434)2434 的二进制00 1001 1000 0010原数InnoDB 存储表现会在最高位设为1次高位设为0存储形式1000 1001 1000 0010解析看到第一个字节最高位是1心领神会“这是个大家伙得连着后一个字节看。”抹去最高位的1把剩下的位拼起来算出 L 2434。2.既然 2 字节带上标志位只能表示到 16383那超过这个长度的字段怎么办MySQL 中一个行Row的最大长度限制是 65535 字节。而变长字段列表中单个字段能表示的长度范围0 ~ 16383假设表中只含单个字段如下图CREATETABLEmessage(messageVARCHAR(40000)NOTNULL)ENGINEInnoDBDEFAULTCHARSETascii ROW_FORMATCOMPACT往表中插入行记录时若message数据大小为25000字节 16383字节变长字段列表岂不是表示不了message的长度了实际上一个页的大小一般是16KB也就是16384字节包含message数据(25000字节)的行记录已经超出了16384字节这时一个页可能就存不了一条记录就会发生行溢出多的数据会存到另外的溢出页中。在记录的长度列表中只会记录该字段在当前页存储的长度再加上一个指向溢出页的 20 字节指针的长度(针对Compact格式)。举个例子存入一段超长文本L 25000MySQL官方文档For each non- variable-length field, the record header contains the length of the column in one or two bytes. Two bytes are only needed if part of the column is stored externally in overflow pages or the maximum length exceeds 255 bytes and the actual length exceeds 127 bytes. For an externally stored column, the 2-byte length indicates the length of the internally stored part plus the 20-byte pointer to the externally stored part. The internal part is 768 bytes,so the length is 76820. The 20-byte pointer stores the true length of the column.NULL25000 的二进制110 0001 1010 1000原数InnoDB 存储表现InnoDB发现这数太大了InnoDB 启动溢出机制在当前页只留768 字节 指向溢出页的20 字节指针剩下的 24232 字节挪到溢出页。此时长度列表里记录的是留在本页的长度788二进制00 0011 0001 0100最高位会设为1次高位也会设为1标记数据溢出了存储形式1100 0011 0001 0100解析读取到长度列表中的788且看到溢出标志位为 1提取前 788 - 20 768 字节作为字段的开头部分提取后 20 字节去寻找剩下的 25000 - 768 24232 字节证明最高位/次高位存在源码mysql-server/storage/innobase/rem/rem0rec.cc at trunk · mysql/mysql-server该函数作用为计算 InnoDB 的 Compact Format 记录中前n个列里有多少个列的数据是存储在外部页这段rec_get_n_extern_new()源码表明在 InnoDB compact 行格式中变长字段的长度前缀对于大字段采用特殊编码最高位0x80用于表示两字节长度格式次高位0x40用于表示该字段外部存储。/** Determine how many of the first n columns in a compact physical record are stored externally. return number of externally stored columns */ulintrec_get_n_extern_new(constrec_t*rec,/*! in: compact physical record */constdict_index_t*index,/*! in: record descriptor */ulint n)/*! in: number of columns to scan */{constbyte*nulls;constbyte*lens;ulint null_mask;ulint n_extern;ulint i;ut_ad(dict_table_is_comp(index-table));ut_ad(rec_get_status(rec)REC_STATUS_ORDINARY);ut_ad(nULINT_UNDEFINED||ndict_index_get_n_fields(index));if(nULINT_UNDEFINED){ndict_index_get_n_fields(index);}nullsrec-(REC_N_NEW_EXTRA_BYTES1);lensnulls-UT_BITS_IN_BYTES(index-n_nullable);null_mask1;n_extern0;i0;/* read the lengths of fields 0..n */do{constdict_field_t*fieldindex-get_field(i);constdict_col_t*colfield-col;ulint len;if(!(col-prtypeDATA_NOT_NULL)){/* nullable field read the null flag */if(UNIV_UNLIKELY(!(byte)null_mask)){nulls--;null_mask1;}if(*nullsnull_mask){null_mask1;/* No length is stored for NULL fields. */continue;}null_mask1;}if(UNIV_UNLIKELY(!field-fixed_len)){/* Variable-length field: read the length */len*lens--;/* 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(len0x80){/* 1exxxxxxx xxxxxxxx */if(len0x40){n_extern;}lens--;}}}}while(in);return(n_extern);}我们仔细观察下述代码if(UNIV_UNLIKELY(!field-fixed_len)){/* Variable-length field: read the length */len*lens--;/* 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(len0x80){/* 1exxxxxxx xxxxxxxx */if(len0x40){n_extern;}lens--;}}}len的值是从记录的变长字段长度列表中读取的一个字节0x80二进制是1000 00000x40的二进制是0100 0000看上述代码的if (DATA_BIG_COL(col)) {}中的两个if明显是先判断len的最高位是否为1再判断次高为是否为1如len的二进制为1000 0011进入判断逻辑if( len 0x80)因最高位为1len 0x80为真可进入if (len 0x40)因次高位为0len 0x40为假不会执行n_extern。len--读取下一个字节。

更多文章