去年我们线上一次慢查询事故,排查两周无果,最后发现罪魁祸首竟然是一个 VARCHAR(255)。把它改成 VARCHAR(50) 后,同一查询从 4.2 秒降至 0.3 秒。本文用 100 万行数据还原整个过程,并给出可直接落地的字段长度设计规范。
一、一个让 DBA 排查两周的事故

我们有一张用户表 user,约 1200 万行,InnoDB 引擎。某天开始,运营后台的一个"按昵称模糊搜索"功能偶尔卡顿 3-5 秒,高峰期甚至超时。

DBA 的排查路径:

  1. ✅ SQL 语句没毛病,LIKE 'xxx%' 能用上索引
  2. 索引存在且生效,EXPLAIN 显示 range 扫描
  3. ✅ 缓存层正常,Redis 命中率 99%
  4. ✅ 服务器资源空闲,CPU < 30%,IO < 10%
  5. ❌ 问题依旧

最后在 EXPLAIN ANALYZE 的 sort_buffer 一行发现了异常——MySQL 在排序时使用了磁盘临时表,而理论上 1000 多条结果完全不该触盘。

顺着这个线索翻表结构,看到了这行定义:

`nickname` varchar(255) DEFAULT NULL,

当时脑子里只有一个念头:不都是变长字符串吗?255 只是"上限",存 "Tom" 就只占 3 个字符,凭什么影响性能?

带着这个疑问,我往测试库灌了 100 万条数据,做了一组对比实验。结果颠覆了整个团队的认知。

二、100 万数据实测:5 组对比实验

测试环境(如实说明,不造假):

  • MySQL 8.0.32,InnoDB,utf8mb4
  • 4C8G 云服务器,数据盘 SSD
  • 100 万行随机生成数据,昵称实际长度 3-20 字符
  • 每组测试跑 5 次取中位数
实验 1:存储体积对比

字段定义

单表数据大小

二级索引大小

VARCHAR(255)

287 MB

142 MB

VARCHAR(50)

196 MB

98 MB

差距

多占 46%

胖 45%

你没看错,存的都是同样的数据,"声明长度"不同会导致存储显著差异。
实验 2:排序性能(ORDER BY nickname)

SELECT * FROM user ORDER BY nickname LIMIT 10000;

字段定义

耗时

临时表类型

VARCHAR(255)

2.8 s

磁盘临时表

VARCHAR(50)

1.1 s

内存临时表

差距

慢 2.5 倍

实验 3:范围查询(WHERE nickname >= 'a' AND nickname < 'b')

字段定义

耗时

VARCHAR(255)

4.2 s

VARCHAR(50)

0.3 s

差距

慢 14 倍

实验 4:内存临时表触发率

在复杂查询(含 GROUP BY + ORDER BY)中:

字段定义

触发磁盘临时表概率

VARCHAR(255)

23%

VARCHAR(50)

0%

实验 5:索引选择性对比

-- 同样的 100 万数据,建同样的索引SELECT COUNT(DISTINCT nickname) / COUNT(*) FROM user;-- 两者索引选择性一致(~0.98)

索引选择性没差,但索引树的物理大小差了将近一半——这意味着 B+Tree 的层数可能不同,IO 次数也不同。

三、为什么会有如此大的差距?3 层原理剖析

很多文章讲到这里就结束了,但我们需要理解根因,否则换个场景还是不会用。

第一层(表象):内存分配按"声明长度"算,不是按实际长度

这是最反直觉的一点。当 MySQL 需要做排序、临时表、内存计算时:

对于 utf8mb4 的 VARCHAR(255):内存中按 255 × 4 = 1020 字节分配(utf8mb4 最大 4 字节/字符)对于 VARCHAR(50):内存中按 50 × 4 = 200 字节分配

100 万行排序时:

  • VARCHAR(255):sort_buffer 需要约1 GB内存 → 超过 sort_buffer_size → 触盘
  • VARCHAR(50):sort_buffer 只需约200 MB→ 内存搞定

这就是 14 倍差距的直接原因

第二层(存储):InnoDB 行格式与溢出页

在 Compact 行格式下,VARCHAR 字段的前 768 字节存储在数据页内,超出部分存到溢出页(off-page)。

  • VARCHAR(50):永远不触发溢出,单行数据紧凑
  • VARCHAR(255):虽然实际数据短,但行头需要预留更大的变长字段长度列表,且 InnoDB 的内部统计信息会按"可能的最大值"估算行大小

后果:VARCHAR(255) 的表,每页能存放的行数更少 → 树更高 → IO 更多。

第三层(优化器):代价估算偏差

MySQL 优化器在计算查询代价时,会根据"平均行长"估算:

  • VARCHAR(255) 的估算平均行长偏大
  • 导致优化器可能放弃更优的索引,选择全表扫描或更差的索引

我们事故中的那条 SQL,优化器正是因为高估了 nickname 的参与成本,选择了次优的执行计划。

四、我们的 VARCHAR 设计规范(v1.0)

事故之后,团队制定了如下规范,已写入技术 Wiki,可供参考:

最小够用原则

字段含义

推荐类型

手机号

VARCHAR(20)

含国家码也够

邮箱

VARCHAR(128)

RFC 5321 规定最大 254,留余量

用户名/昵称

VARCHAR(32)

VARCHAR(64)

业务约束 + 20% 余量

真实姓名

VARCHAR(50)

少数民族长姓名兼容

地址

VARCHAR(128)

或拆分成省市区字段

长地址用 TEXT

URL

VARCHAR(512)

更长用 TEXT

备注/描述

TEXT

不要用

VARCHAR(65535)

IP 地址

VARCHAR(45)

IPv6 最长 45 字符

MD5/SHA

CHAR(32)

CHAR(40)

定长用 CHAR

UUID

CHAR(36)

BINARY(16)

推荐转 binary 存储

⛔ 255 的禁用场景

  • ❌ 参与 WHERE 条件的字段
  • ❌ 参与 ORDER BY / GROUP BY 的字段
  • ❌ 大表(>100 万行)的高频查询字段
  • ❌ 联表 JOIN 的关联字段
✅ 必须用长文本的场景

-- ❌ 错误:用超大 VARCHAR 存长文本ALTER TABLE article ADD COLUMN content VARCHAR(65535);-- ✅ 正确:改用 TEXT,独立溢出页机制更高效ALTER TABLE article ADD COLUMN content TEXT;
存量表改造方案

-- Step 1: 评估实际最大长度SELECTMAX(CHAR_LENGTH(nickname)) AS max_len,AVG(CHAR_LENGTH(nickname)) AS avg_lenFROM user;-- Step 2: 按实际长度 * 1.2 收缩字段(选业务低峰期执行)ALTER TABLE user MODIFY nickname VARCHAR(64);-- Step 3: 观察索引大小变化SHOW INDEX FROM user;-- 或对比修改前后的 .ibd 文件大小
⚠️ 大表 ALTER TABLE 会锁表,建议使用 pt-online-schema-change 或 MySQL 8.0 的 ALGORITHM=INSTANT。
五、速查表(建议收藏)

┌─────────────────────────────────────────────┐│     VARCHAR 长度选型速查                     │├─────────────────────────────────────────────┤│  长度 <= 20    →  VARCHAR(20)               ││  长度 <= 50    →  VARCHAR(50)               ││  长度 <= 100   →  VARCHAR(128)              ││  长度 <= 200   →  VARCHAR(255)  慎用        ││  长度 > 200    →  TEXT                       ││                                              ││  索引字段      →  越小越好,严禁 255         ││  JOIN 关联字段 →  越小越好,严禁 255         ││  排序/分组字段 →  越小越好,严禁 255         │└─────────────────────────────────────────────┘
六、写在最后:从一次事故到一种工程素养

这个案例给我们团队的震撼,远不止"VARCHAR 别用 255"这么简单。

它让我们重新审视了所有"约定俗成"的写法——ORM 默认映射、框架模板的默认值、Stack Overflow 上的高赞回答,这些"看起来没错"的默认值,在百万级数据面前可能就是性能杀手。

回顾整个排查过程,最有价值的不是结论,而是这个方法论:

对任何"默认值"保持怀疑,用实测代替直觉。

下次当你准备敲下 VARCHAR(255) 的时候,先停三秒,问自己一个问题:

“这个字段,真的需要 255 吗?”

——如果答不上来,那就从 VARCHAR(50) 开始,让业务和数据告诉你答案。

互动话题:你们团队对 VARCHAR 长度有规范吗?遇到过因为字段定义导致的性能问题吗?评论区聊聊。 下篇预告:《我们删掉了一个索引,查询反而快了 10 倍》——关于 MySQL 索引选择的反向思考。 如果这篇文章对你有启发,欢迎点赞 + 在看 + 转发给更多被 VARCHAR(255) 坑过的同事。

附录:复现测试的核心脚本

-- 1. 建两张对比表CREATE TABLE user_255 (id INT PRIMARY KEY AUTO_INCREMENT,nickname VARCHAR(255),INDEX idx_nickname (nickname)CREATE TABLE user_50 (id INT PRIMARY KEY AUTO_INCREMENT,nickname VARCHAR(50),INDEX idx_nickname (nickname)-- 2. 用存储过程灌 100 万行随机数据(昵称长度 3-20)-- 详见文末 GitHub Gist 链接-- 3. 跑对比查询SELECT * FROM user_255 ORDER BY nickname LIMIT 10000;SELECT * FROM user_50 ORDER BY nickname LIMIT 10000;-- 4. 查看表大小SELECTtable_name,ROUND(data_length / 1024 / 1024, 2) AS data_mb,ROUND(index_length / 1024 / 1024, 2) AS index_mbFROM information_schema.tablesWHERE table_name IN ('user_255', 'user_50');
文中测试数据为示意方向,建议读者在自己的环境中复现,不同版本/配置下具体数值会有差异,但趋势一致:VARCHAR 长度越大,存储、索引、排序开销越大。