varchar 存储谜团:1 字节还是 2 字节?深度解析

导读

在数据库领域,我们清楚地知道varchar属于变长字段,其长度并非固定不变,正因如此,需要额外的空间来存储它的长度信息。那么,具体要用多少字节来存储这个长度呢?

首先,我们要了解mysql中对varchar的限制:它的最大长度为65535字节。在utf8mb3字符集环境下,varchar的限制是varchar(21845) ;而在utf8mb4字符集环境下,限制为varchar(16383) 。一旦超过这个限度,你将会收到如下报错信息:

ERROR 1074 (42000): Column length too big for column 'aa' (max = 21845); use BLOB or TEXT instead
ERROR 1074 (42000): Column length too big for column 'aa' (max = 16383); use BLOB or TEXT instead

鉴于varchar最大仅65535字节,从理论上来说,用2字节就足以表示其长度。那么,是不是遇到varchar类型,就统一用2字节来表示其长度,事情就解决了呢?

这种做法在逻辑上的确行得通,而且十分简便。但这仅仅是从最浅显的层面,也就是所谓“站在第一层”的思考方式。在实际存储场景中,情况远非如此简单。

数据行结构

在深入分析“varchar究竟使用1字节还是2字节来存储长度”这个问题之前,我们有必要先来探究一下数据行在磁盘中的存储机制(对于已经熟悉这部分内容的读者,可以选择跳过此部分)。

这里我们聚焦于主键索引的叶子节点,因为数据正是存储于此。具体结构如下图所示:

记录变量长度的部分位于“variable length”区域。当后续的主键或者数据部分存在变量时,便会从“variable length”中读取相应的长度信息。

分析

首先,查询权威资料是深入了解技术细节的关键一步。我们前往官网 ,官网通常涵盖了最为全面和准确的信息。

不出所料,在数据类型的相关章节中,我们找到了与varchar存储长度相关的内容。

其描述如下:

In contrast to CHAR, VARCHAR values are stored as a 1-byte or 2-byte length prefix plus data. The length prefix indicates the number of bytes in the value. A column uses one length byte if values require no more than 255 bytes, two length bytes if values may require more than 255 bytes.

这段话的核心意思是:与char类型不同,varchar类型在存储时,会使用1到2字节作为长度前缀,再加上实际的数据。这个长度前缀代表了该值所占用的字节数。如果varchar类型的值不超过255字节,那么会使用1字节来记录长度;若超过255字节,则会使用2字节来存储长度。

这种存储方式相较于简单地统一使用2字节存储长度,虽然看似增加了一些复杂性,但从存储空间的高效利用角度来看,实则更加合理(能使用1字节存储的情况,就无需占用2字节的空间)。

看到这里,你或许会认为既然官网都如此明确说明了,这个问题就已经有了定论。但实际上,这也只是从一个相对深入的层面,即“站在第二层”的视角来理解这个问题,背后还有更深层次的考量。

深度分析

或许有人会说:“官网都已经写得明明白白了,还有什么可分析的!”然而,秉持严谨的学习态度,我们仍需对这个问题进行深入剖析。

假设存在如下表结构和数据:

create table t20250319(c1 varchar(300) primary key) default character set utf8mb4;
insert into t20250319 values('1234567890');

从表的元数据来看,字段 c1 被定义为 varchar(300),这意味着理论上它最多可存储 300 个字符,在 utf8mb4 字符集下即 1200 字节。但实际上,插入的数据仅为 10 字节。那么,此时还有必要使用 2 字节来存储其长度吗?

直观上看,似乎没有必要。然而,在解析数据时,由于我们事先并不知道实际数据的大小,该如何确定是使用 1 字节还是 2 字节来存储长度呢?

我们不妨做这样一个假设:通过查看第 1 字节的第 1 位(bit)来判断。若该位为 1,则表示使用 2 字节来记录长度;若为 0,则表示使用 1 字节来记录长度。如此一来,当数据长度小于 128 字节时,只需 1 字节就能表示其长度;当数据长度大于 128 字节时,再使用 2 字节(计算时需排除第 1 字节的第 1 位)。

那么,对于元数据长度不超过 255 字节的字段,是否也采用这种存储方式呢?显然不需要。我们可以先查看元数据信息,判断其长度是否大于 255 字节。若不大于 255 字节,直接读取 1 字节即可;若大于 255 字节,再查看第 1 字节的第 1 位,判断是否为 1。

需要注意的是,由于我们牺牲了第 1 字节的第 1 位来记录是否使用 2 字节,那么 2 字节所能表示的范围就变为 2^15 = 32768 字节。在 InnoDB 存储引擎中,PAGE_SIZE 最大为 64KB,且每页至少要存储 2 行数据,因此每行数据(页内)的长度不超过 32KB,仍在 2 字节所能表示的范围之内,所以这种设计在理论上是可行的。

虽然上述假设在理论上没有问题,但当第 1 字节的第 1 位为 1,需要使用 2 字节来表示长度时,这 2 字节具体是如何表示长度的呢?

参考下面这张图:

其解析方法如下:

先读取右边的 1 字节,如果该字节的值大于 128,则再读取 1 字节。由于右边的这 1 字节的第 1 位已被用于标记是否使用 2 字节,所以它只能作为高位(低位必须能够表示 1 - 255)。此时,高位的大小为 (右边数据 - 128) * 256,再加上左边 1 字节的值,就得到了数据的实际长度。

这种解析方式看起来有些复杂,不过我们可以使用 Python 代码来清晰地表达这个过程:

size = readint_reverse(1) # 读取1字节
if maxsize <= 255:
    return size
elif size > 128:
    size = readint_reverse(1) + (size - 128) * 256
return size

其中,maxsize 表示元数据的长度(字节),readint_reverse 表示读取 1 字节的 variable length 数据。

通过 Python 代码,这个过程是不是变得清晰易懂了呢?到这里,我们已经从更深层次对这个问题进行了分析,也就是所谓的“站在第三层”。

但这是否就是最终的真相呢?我们还需要通过实际验证来进一步确认。

验证

当元数据信息的长度小于 255 字节时,毫无疑问会使用 1 字节来记录数据长度。因此,我们将重点放在元数据信息长度大于 255 字节的情况进行验证。

首先,准备如下数据:

create table t20250319_varchar(c1 varchar(300) primary key) default character set utf8mb4;
insert into t20250319_varchar values('1234567890');
insert into t20250319_varchar values(repeat('x',129));

这里需要说明的是,表中仅设置一个字段且将其作为主键,目的是为了简化数据解析过程。因为若存在多个字段,解析时还需考虑 trxidnullbitmask 等因素,会使问题变得极为复杂。

接着,我们使用以下 Python 脚本来解析数据文件:

#!/usr/bin/env python
# write by ddcw
# 验证varchar使用1字节还是2字节的脚本

import struct
import sys
filename = sys.argv[1]
f = open(filename,'rb')
f.seek(4*16384,0) # 如果是5.7, 则改为f.seek(3*16384,0), 因为没得sdi
data = f.read(16384)
f.close()

# 开始解析数据了
offset = 99

offset += struct.unpack('>h',data[offset-2:offset])[0]
size = struct.unpack('>B',data[offset-5-1:offset-5])[0]
if size > 128: # 不需要考虑元数据信息小于255的情况, 因为我们只验证大于255的情况...
    size = struct.unpack('>B',data[offset-5-1-1:offset-5-1])[0] + (size-128)*256
print('第1行数据:',data[offset:offset+size].decode())

offset += struct.unpack('>h',data[offset-2:offset])[0]
size = struct.unpack('>B',data[offset-5-1:offset-5])[0]
if size > 128:
    size = struct.unpack('>B',data[offset-5-1-1:offset-5-1])[0] + (size-128)*256
print('第2行数据:',data[offset:offset+size].decode())

然后,使用该脚本对数据进行解析,结果如图所示:

从解析结果来看,按照我们之前的猜想,脚本确实正确地解析出了数据。这表明我们的猜想是正确的,同时也反映出官方文档在这方面或许还有待进一步完善。

总结

综上所述,我们可以得出如下结论:当元数据信息的长度小于等于 255 字节时,使用 1 字节来记录数据长度;当元数据信息的长度大于 255 字节时,使用 1 - 2 字节来记录数据长度。

这里不妨再深入思考一下:对于定长的 char 类型,其存储长度的记录方式又是怎样的呢?这就需要我们站在更高的层面,即所谓的“站在第 4 层”去探索了。

虽然这类细节问题对我们从宏观上理解 InnoDB 存储机制影响不大,但在编写相关工具时,却很容易让人陷入困境。目前,我已经向官方反馈了这个问题,后续若有相关反馈,我会及时同步更新。

相关BUG地址: https://bugs.mysql.com/117736

参考

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件举报,一经查实,本站将立刻删除。

文章由技术书栈整理,本文链接:https://study.disign.me/article/202513/2.mysql-varchar-bug.md

发布时间: 2025-03-26