最后更新:

数据库 VARCHAR 长度设计 - 字符数限制最佳实践

11 分钟阅读

在数据库设计中,VARCHAR 列的长度如何确定往往容易被忽视,但与API 响应的字数设计一样,这是影响整个系统质量的重要设计决策。不要简单地设置 VARCHAR(255),而应根据数据的性质选择合适的长度。

VARCHAR(255) 神话的真相 - 为什么 255 成为了默认值

VARCHAR(255) 之所以成为"默认选择",与 MySQL 的旧版规范有关。在 MySQL 4.1 之前,VARCHAR 的长度前缀用 1 个字节管理,1 个字节能表示的最大值就是 255。MySQL 5.0 之后扩展为 2 字节前缀,理论上最大可存储 65,535 字节,但"255"这个数字作为惯例一直沿用至今。

这个惯例根深蒂固还有另一个原因。MySQL 中,存储值不会超过 255 字节的列,长度前缀只消耗 1 字节,可能超过的列则消耗 2 字节。容易被忽略的是,这条分界线是按字节数而不是字符数划定的。在单字节字符集下,VARCHAR(255) 和 VARCHAR(256) 之间确实每行相差 1 字节的开销。但 utf8mb4 中一个字符最多占 4 字节,VARCHAR(64) 的最大长度就已经达到 256 字节,用的也已经是 2 字节前缀。只要表里存的是中文或表情符号,"255 就能留在 1 字节一侧"这个前提根本不成立。"255 更高效"的认知,其实是把字符集还是单字节时代的隐含前提一路带到了今天。

各 RDBMS 的 VARCHAR 内部实现差异

即使同样是 VARCHAR(100),不同 RDBMS 的内部存储方式和内存分配行为也大不相同。不理解这些差异就进行设计,会对性能和存储效率产生意想不到的影响。

RDBMS最大长度单位内部存储方式内存分配
MySQL 8.0 (InnoDB)65,535 字节 (整行)字符数指定实际数据长度 + 1~2 字节长度前缀 (存储值可能超过 255 字节的列为 2 字节)。行内放不下的较长值转移到溢出页8.0 起默认的 TempTable 引擎即使在内部临时表中也把 VARCHAR 保持为可变长度 (不做填充)。旧的 MEMORY 引擎才会填充到声明长度
PostgreSQL约 1 GB字符数指定varlena 结构体。VARCHAR 和 TEXT 使用相同的存储格式。TOAST 机制会自动压缩较长的值,压缩后仍放不下的则移到后台的辅助表仅按实际数据长度分配。声明长度仅作为检查约束
SQL Server8,000 字节字符数指定行内存储。VARCHAR(MAX) 转移到 LOB 存储查询执行时以声明长度为依据估算并预留内存 (Memory Grant)
Oracle4,000 字节 (标准) / 32,767 字节 (扩展)字节或字符 (由 NLS_LENGTH_SEMANTICS 控制)行内存储。扩展模式下转移到 SecureFile LOB在 PGA 中按声明长度分配
SQLite无限制-动态类型。VARCHAR 声明被忽略,仅存储实际数据长度仅按实际数据长度

这里要分清的是"声明长度会不会变成运行时的成本"。SQL Server 在生成执行计划时就以声明长度为依据估算并预留内存,因此即使 VARCHAR(255) 的列实际只存了 10 个字符,排序或哈希连接时也可能预留过多内存,拖慢查询。MySQL 在 5.x 时代也有同样的问题,因为内部临时表由 MEMORY 引擎处理并被填充成定长。而 8.0 起默认的 TempTable 引擎把 VARCHAR 保持为可变长度,这方面的代价已经大幅缓解。换句话说,在 8.0 之后收紧声明长度的意义,重心已经从"节省内存"转向"不让不合规的数据写进来"。

UTF-8 可变长编码对 VARCHAR 的影响

即使以"字符数"指定 VARCHAR 长度的 RDBMS,内部也受到字节数的约束。理解Unicode 基础知识有助于更清楚地认识这一机制。UTF-8 是可变长编码,不同类型的字符消耗的字节数不同。

字符类型UTF-8 字节数示例VARCHAR(255) 可存储的最大字符数 (字节换算)
ASCII 英数字1 字节a, Z, 0, @255 字符 (255 字节)
拉丁扩展/西里尔字母2 字节é, ñ, Д255 字符 (510 字节)
中日韩文字3 字节中,あ,漢255 字符 (765 字节)
表情符号/特殊符号4 字节😀, 🎉, 𠮷255 字符 (1,020 字节)

在 MySQL 的 utf8mb4 中声明 VARCHAR(255) 时,最坏情况下这一列就要占 1,020 字节。InnoDB 的行大小上限在默认的 16 KB 页面下约为 8,000 字节 (略小于页面的一半),这样的列一多,行内空间很快就会吃紧。不过这并不意味着"排上 8 个列就建不出表"。行内放不下的较长值会被移到溢出页,表照样能建成。真正的影响是行内容纳不下的值变多以后,读取时要额外访问溢出页,参照成本随之上升。所以该问的不是"能不能存进去",而是"能不能留在行内"。

实际工作中,事先用字符计数器确认存放中文文本的列的字节消耗量,可以防止意外的数据截断。

VARCHAR 与 TEXT 的选择 - 性能与索引的实际情况

"长文本应该用 TEXT"是一般性建议,但 VARCHAR 和 TEXT 的选择因 RDBMS 而异。

角度MySQL (InnoDB)PostgreSQLSQL Server
存储方式差异VARCHAR 和 TEXT 的处理方式相同。8.0 默认的 DYNAMIC 行格式把较长的值整体移到溢出页,行内只留指针。旧的 COMPACT / REDUNDANT 格式则把前 768 字节留在行内无差异。VARCHAR(n) 和 TEXT 使用相同的 varlena 结构体VARCHAR 行内存储。TEXT (VARCHAR(MAX)) 使用 LOB 存储
索引VARCHAR:可建完整索引 (8.0 默认的 DYNAMIC 行格式下键长上限为 3,072 字节,旧的 COMPACT / REDUNDANT 为 767 字节)。TEXT:必须指定前缀长度才能建索引两者均可同等建立索引VARCHAR:可建完整索引。TEXT:仅支持全文索引
排序/GROUP BYVARCHAR:内存中处理。TEXT:可能使用磁盘临时表无差异VARCHAR:内存中。TEXT:使用 tempdb
默认值VARCHAR:可设置。TEXT:不能指定字面量默认值 (MySQL 8.0.13 以后可以用表达式指定默认值)两者均可设置VARCHAR:可设置。TEXT:可设置

PostgreSQL 中 VARCHAR(n) 和 TEXT 实质上没有差异。官方文档的说法是:character varying、text 和 character(n) 三者之间没有性能差异,只有会用空格补齐的 character(n) 因为多出存储开销而最慢,多数场合应当使用 text 或 character varying。换个角度说,在 PostgreSQL 中加长度限制并不是为了性能,而是作为保证取值合理的约束。而 MySQL 中 TEXT 列不指定前缀长度就无法建立索引,因此要作为检索对象的列应选择 VARCHAR。

包含表情符号 (4 字节 UTF-8) 数据的陷阱

现代应用程序需要以用户输入包含表情符号为前提进行设计。表情符号在 UTF-8 中消耗 4 字节,但问题不止于此。

VARCHAR 与 CHAR 的区别

选择字符串类型时,首先需要理解 VARCHAR 与 CHAR 的区别。

特性CHAR(n)VARCHAR(n)
存储方式定长 (用空格填充)变长 (仅占实际长度)
存储空间始终消耗 n 字节实际数据 + 1-2 字节
适用场景国家代码、邮政编码等定长数据姓名、邮箱地址等变长数据
检索速度因定长而略快因变长而有少量开销

大多数情况下 VARCHAR 是合适的选择。使用 CHAR 应限定于长度完全固定的数据,例如 ISO 国家代码 (CHAR(2)) 或货币代码 (CHAR(3))。另外,MySQL 的 InnoDB 中 CHAR 列也以变长方式存储 (去除末尾空格),因此存储空间上的差异已经很小。

常见 VARCHAR 设计错误与修复成本

VARCHAR 长度的设计错误在初期阶段不易察觉,数据积累后才会显现。修复成本与数据量成正比增长,因此设计阶段的审慎判断至关重要。

迁移时 VARCHAR 长度变更的风险与安全步骤

在生产环境中变更 VARCHAR 长度的 ALTER TABLE,其行为因 RDBMS 而有很大差异。为了安全地执行,需要事先理解各 RDBMS 的内部动作。

RDBMS长度扩展 (如 100→200)长度缩减 (如 200→100)注意事项
MySQL (InnoDB)最大字节长度维持在 255 字节以内:仅元数据变更 (瞬时)。从 255 字节以内跨到 256 字节以上:需要表重建需要表重建。现有数据超过新长度时报错推荐使用 pt-online-schema-change 或 gh-ost
PostgreSQL仅元数据变更 (瞬时)。不会重写表,但语句执行期间会短暂持有排他锁需要检查现有数据。有约束违反时报错加长是轻量操作,缩短则要扫描现有值,两者并不等价
SQL Server仅元数据变更 (瞬时)检查现有数据后进行元数据变更VARCHAR→VARCHAR(MAX) 的变更需要表重建
Oracle仅元数据变更 (瞬时)检查现有数据后进行元数据变更BYTE→CHAR 语义的变更可通过 ALTER TABLE MODIFY 实现
  1. 变更前用 SELECT MAX(CHAR_LENGTH(column_name)) FROM table_name; 确认现有数据的最大长度。
  2. 判断变更是否跨越 255 字节边界 (跨越时会发生表重建)。
  3. 大型表 (超过 100 万行) 应使用 pt-online-schema-change 或 gh-ost,实现无停机变更。
  4. 变更后执行 ANALYZE TABLE,更新优化器的统计信息。

列长度设计最佳实践

下面整理决定合适 VARCHAR 长度的指导原则。

数据项目推荐长度依据
邮箱地址VARCHAR(254)实务中作为邮箱地址上限被广泛采用的长度
姓名 (中文)VARCHAR(50)姓名合计 50 字符足够
姓名 (国际化)VARCHAR(100)适应包含中间名和长姓氏的文化圈
电话号码VARCHAR(20)E.164 含国家代码最多 15 位数字,再为加号和分隔符留出余量
URLVARCHAR(2048)实务中广泛沿用的参考值。浏览器和服务器都能处理更长的 URL,原样保存第三方 URL 时要留余量
地址VARCHAR(200)中文地址通常在 100 字符以内。国际化则 200 较安全
商品名称VARCHAR(200)电商网站的一般上限
用户显示名VARCHAR(100)足以容纳含表情符号的显示名,多语言环境下也留有余量
密码哈希VARCHAR(60) / CHAR(60)bcrypt 哈希长度固定为 60 字符。CHAR(60) 最合适
UUIDCHAR(36) / BINARY(16)含连字符 36 字符。二进制存储则 16 字节更高效
  1. 数据有规范或标准 (RFC、ISO 等) 时,按其上限设置。
  2. 没有规范时,在实际数据的最大长度上加 20-50% 的余量。
  3. 预留未来扩展空间的同时,避免过大的值。MySQL 中要注意 255 字节边界。
  4. 应用层也实现相同的字数限制验证,防止与数据库产生偏差。
  5. 需要国际化的列,不以中文为基准,而应以最长的语言圈为基准来设计。

总结

VARCHAR 长度的设计应综合考虑数据性质、编码方式和 RDBMS 的内部实现来决定。不要"先设 255 再说",而是设置有依据的长度,这样才能同时提升存储效率、查询性能和数据质量。特别是 MySQL 中要注意 255 字节边界带来的前缀长度差异、较长的值何时溢出到页外,以及对索引大小的影响。在设计阶段使用字符计数器测量预期数据的字符数,推导出合适的列长度。

分享这篇文章