我最早注意到 VARCHAR(255) 和 VARCHAR(256) 的差异,是在一次线上大表变更踩坑之后。当时业务方说要给用户昵称字段扩长度,从 VARCHAR(255) 改成 VARCHAR(256),我心想不就多了 1 个字符容量嘛,结果一条 ALTER 语句把一张几百万行的表卡了二十分钟,业务超时告警一片。回滚之后查资料才搞清楚,在 MySQL 的存储和 DDL 机制里,255 到 256 并不是“容量多 1”这么简单,而是一个跨越存储元数据边界的临界值。
这篇文章想聊的就是这个看似基础、实际开发中又很容易被忽略的问题:VARCHAR(255) 和 VARCHAR(256) 在底层到底差在哪,为什么 255 会成了行业里的“默认万能长度”,以及我们在真实项目里到底应该按什么原则去选字段长度。无论你是后端开发、DBA,还是刚入门没多久的新手,读完应该都能避开我当年踩过的那个坑。
1. VARCHAR(255) 和 VARCHAR(256) 在 MySQL 存储层差的不是“1”
1.1 真正的变量:长度前缀,以及那个“255 = 2^8 - 1”的边界
MySQL 的 InnoDB 存储引擎在保存变长字段时,并不是只存字符串内容,它还得在行记录里额外存一段“这个字段实际用了多少字节”的长度信息,这段信息叫作长度前缀。InnoDB 约定:如果该字段的实际数据不超过 255 字节,就用 1 个字节来记录长度;一旦超过 255 字节,就得用 2 个字节来记录。
这背后是二进制的基础知识:1 个字节最多能表示 0 到 255 这 256 个值。255 就是单字节长度前缀的极限,而 256 恰好越过了这个极限,逼着存储层为它多准备 1 个字节。所以大家经常听到“VARCHAR(255) 是 1 字节长度前缀,VARCHAR(256) 是 2 字节长度前缀”,这个说法的源头就在这。
但实际情况远没有这句顺口溜这么简单。MySQL 里 VARCHAR(n) 的 n 是字符数,不是字节数。如果你用的是 utf8mb4 字符集,一个字符最多占 4 个字节,那么一个 VARCHAR(255) 字段单行最大可以到 1020 字节,早就超过 255 字节了,这种情况下如果实际真的存了那么长,它照样要用 2 字节长度前缀。也就是说,“VARCHAR(255) 一定比 VARCHAR(256) 省字节”是个误解,省不省取决于你实际存进去的数据有多长,而不是声明长度。
1.2 声明长度、实际字节数和字符集的三角关系
把字符集、声明长度、实际存储长度这三件事放在一起看,才能真正理解 255 和 256 的分界线。
| 维度 | VARCHAR(255) | VARCHAR(256) |
|---|---|---|
| 单字符最大字节数(utf8mb4) | 4 字节 | 4 字节 |
| 字段声明的最大字符数 | 255 | 256 |
| 理论最大字节数(utf8mb4) | 1020 字节 | 1024 字节 |
| 单行实际数据 ≤255 字节时 | 长度前缀 1 字节 | 长度前缀 1 字节 |
| 单行实际数据 >255 字节时 | 长度前缀 2 字节 | 长度前缀 2 字节 |
| 从另一个长度跨越 255 阈值变更时 | 可能触发全表重建 | 可能触发全表重建 |
所以严格来说,VARCHAR(255) 和 VARCHAR(256) 在日常存储开销上可能没有任何差别:如果你的业务数据从来不超过 255 字节,两者都是 1 字节长度前缀;如果业务数据经常超过 255 字节,两者都得用 2 字节。它们真正拉开差距的场景,是数据库要按“声明长度”去计算元数据、判断 DDL 算法、评估行大小时,这时候 255 和 256 就分道扬镳了。
在实际开发里还有一层容易忽略的影响:字段声明越长,优化器在预估排序、去重、临时表大小时,就越可能高估内存需求。虽然 VARCHAR 不像 CHAR 那样固定占满声明长度,但 MySQL 在构造内存临时表或排序缓冲区时,常常需要按字段的最大可能长度来预留空间。你如果在一个表里堆了十几个 VARCHAR(256)、VARCHAR(512),即使实际每行数据都很短,执行 ORDER BY 或 DISTINCT 时仍然可能因为“看起来太大”而把临时表放到磁盘上,一个原本几十毫秒的查询就变成了几百毫秒。
2. 为什么全行业都爱用 255:一个被代代相传的“伪标准”
2.1 三个历史巧合把它推上了神坛
VARCHAR(255) 这么流行,不是因为 255 有什么物理意义上的优越性,而是三个历史因素叠加的结果。
第一个因素是早期 MySQL 版本对 VARCHAR 长度的限制。在 MySQL 4.x 以及更早的版本里,VARCHAR 最大就是 255,超过 255 的字符串你只能选 TEXT。后来版本虽然放宽了限制,但大量老项目、老文档、老框架的默认值已经固定下来,“字符串就用 VARCHAR(255)”成了很多人写建表语句时的肌肉记忆。
第二个因素来自 InnoDB 旧版索引键长度限制。早期 InnoDB 一条索引键最长只能有 767 字节,而当时主流字符集 utf8mb3 一个字符最多占 3 字节,VARCHAR(255) × 3 字节 = 765 字节,刚好卡在 767 字节之内,可以正常建索引;如果声明成 VARCHAR(256),256 × 3 = 768 字节,就直接超限建不了索引。在这个历史背景下,255 成了“既能建索引又尽可能大”的黄金分割点。
第三个因素是 ORM 框架的默认值。我见过太多人拿着 JPA 的 @Column 注解不写 length 参数,生成出来的表字段默认就是 length=255;Django 的文档示例里 CharField(max_length=255) 也到处都是;很多数据库建模工具的默认模板同样是 varchar(255)。框架默认值加上网上的教程互相抄,255 就成了行业里心照不宣的标准。
2.2 “全员 255”不是防洪堤,反而削弱了数据库层的约束
无脑用 255 最大的问题在于:它让字段长度彻底失去了“业务约束”的意义。
我举个例子。你的用户昵称业务规则是最多 30 个字符,结果建表时图省事写成了 VARCHAR(255)。这个字段在数据库层面就几乎不设防了,任何 200 个字符的脏数据都能成功写入。你以为自己是在“预留足够的扩展空间”,实际上是把本该有的一道防线给拆了。
更麻烦的是,表结构里全是 255 之后,你没法通过 DDL 看出任何一个字段的真实业务语义。做数据治理或排查慢查询的人拿到表结构,看到十来个字段全是 varchar(255),只能一脸问号:这个字段到底存口令还是存备注?该不该给索引?字段实际最长数据有多少?一切都无从判断。
还有一个隐藏成本是内存与元数据层面的。前面提过,优化器会参考字段声明长度来估算临时表、排序缓冲区的开销。一张表如果有几十个 VARCHAR(255),即使每行实际数据只有几十字节,在数据库规划执行计划时仍然会按更坏的情况去估算,这会影响内存临时表的大小、连接缓冲区的分配,甚至在极端情况下逼着优化器放弃索引、选择全表扫加文件排序。
3. 实际开发中怎么选长度:把它当业务约束,而不是容量规划
3.1 先回答“这个字段最长的合法值是多少”
每次设计字段长度,我建议先问自己一个问题:这个字段在真实业务里,最长的合法值到底是多少?带着这个问题去定长度,就不会陷入“255 还是 256”的纠结。
按这个思路,字段可以分成几类。第一类是固定长度的业务标识,比如身份证号、手机号、订单号、各种编号,这类字段的值域是确定的,直接精确设定长度,甚至可以用 CHAR。第二类是开放输入但有合理上限的字段,比如昵称、邮箱、地址、URL,你需要根据产品规则和常见技术规范去定一个“能接受的最大值”。第三类是没有明确上限的文本,比如备注、长描述、文章正文,这种就别硬塞 VARCHAR,直接考虑 TEXT 或对应的长文本类型。
在真实项目里,预留扩展空间和约束数据质量是一对矛盾。我的思路是:只对真正可能增长的字段留余量,并且余量不要大到离谱。比如地址从省市区街道到详细门牌号,正常情况下几十个字符足够,你定 128 已经是很大的余量;如果未来真的需要存几百字的完整地址,那是业务形态变了,到时候正常走一次表结构变更,而不是今天就用 VARCHAR(1024) 把所有情况都兜住。
3.2 一份可以直接抄的字段长度清单
下面这份清单是我在实际项目里常用的长度参考,覆盖了大部分高频场景,可以直接抄作业:
| 业务字段 | 建议类型和长度 | 说明 |
|---|---|---|
| 用户昵称 | VARCHAR(30) 或 VARCHAR(50) | 主流平台通常限制 20~30 个字符,50 已很宽松 |
| 邮箱 | VARCHAR(254) | RFC 标准定义邮箱最大 254 字符,255 反而超了 |
| 手机号 | CHAR(11) 或 VARCHAR(20) | 中国大陆手机号固定 11 位,留一点国家码余量 |
| 身份证号 | CHAR(18) | 固定 18 位(旧版 15 位已淘汰) |
| 文件 URL | VARCHAR(512) 或 VARCHAR(1024) | CDN 签名 URL 很容易超过 255,别卡太死 |
| 哈希值(bcrypt) | CHAR(60) | bcrypt 输出固定 60 字符 |
| 哈希值(SHA-256 hex) | CHAR(64) | 64 位十六进制字符串 |
| 通用备注/摘要 | VARCHAR(255) | 一般够用,但需确认业务上限 |
| 长文本正文 | TEXT / MEDIUMTEXT | 超过几百字符就换长文本,别硬用 VARCHAR |
你会发现,处理完真实业务之后,真正需要 255 的地方反而不多。倒是 URL、签名串这类容易超长的字段,如果按“255 主义”去建,很快就会撞上长度上限,到时候又得走一遍 ALTER,这就是我在下一章要聊的事故场景。
3.3 跨过 255 之后,是升级 VARCHAR 还是换 TEXT/JSON
当某个业务字段确实需要超过 255 字符时,就需要做一次选择了。我的建议是分场景处理。
如果只是偶尔超长,比如 URL、短备注,数据本身参与 WHERE 条件、排序、去重,或者需要建普通索引,那么 VARCHAR(512)、VARCHAR(1024) 是合适的。要注意的是,MySQL 里 VARCHAR 单行最大字节数是 65535,而且要扣除其他列的开销,所以在 utf8mb4 下想声明特别大的 VARCHAR,要提前算好与行内其他字段的字节数总和,别贪多。
如果字段是长文本,本身不参与复杂查询,只是存进去、偶尔取出来展示,那么 TEXT 是更优的选择。MySQL 的 TEXT 类型在 InnoDB COMPACT 行格式下,会额外用一部分空间存储指向溢出页的指针,查询时如果需要读取完整内容可能触发额外的页加载,但它避免了 VARCHAR 把整行塞得太大、导致其他字段也跟着被排挤到溢出页的问题。长文本要检索时,该走全文索引或 ES 就走,不要指望 VARCHAR 硬扛。
还有一个容易被忽略的迁移场景:如果你已经在线上用了 VARCHAR(255),但预感到未来数据很可能超过 255 字符,不要等真的超了再改。因为从 255 改到 256 这一步,代价可能比你从 255 改成 1024 还要大,原因同样和存储元数据有关,这就进入下一章的内容了。
4. 一次把 255 改成 256 的线上事故复盘
4.1 事故现场:只有一条 ALTER,却把业务卡了二十分钟
那次事故的背景是这样的:线上有一张订单扩展表,大概几百万行,其中有个字段存的是带签名的 CDN 访问 URL,建表时用了 VARCHAR(255)。后来 CDN 服务商升级了签名规则,URL 长度经常会超过 255 字符,导致写入报错。业务方临时把写入逻辑里做了截断,同时提了工单要求 DBA 把这个字段扩到 VARCHAR(256)。
当时执行变更的同事没太当回事,直接执行了:
ALTER TABLE order_ext MODIFY COLUMN cdn_url VARCHAR(256) NOT NULL DEFAULT '';执行之后,前几秒看起来一切正常,但很快 SHOW PROCESSLIST 里出现大量会话处于Waiting for table metadata lock状态,业务写入全部被堵住,监控面板上的写延迟一路飙升。更麻烦的是,这条 ALTER 迟迟不结束,导致后续所有访问这张表的请求都在排队,最终引发了多个线程互相等待,线上开始报死锁相关的错误。
回滚后我们从头排查,发现整件事就是“255 改成 256”这个看似人畜无害的操作引起的。
4.2 根因:跨过 255 阈值后 MySQL 退回到 COPY 重建
MySQL 官方文档里对修改 VARCHAR 列长度有一个很关键的限制:如果列的长度从 0~255 范围改成 256 或更大,或者反过来从 256 及以上改成 0~255 范围,那么由于变长字段的长度前缀字节数发生了变化,这个操作不支持使用ALGORITHM=INPLACE,也就是说不能原地修改,必须退回ALGORITHM=COPY。
COPY 算法意味着什么?MySQL 会新建一张结构相同的临时表,把原表数据逐行复制进去,复制完成后删掉旧表、重命名新表。这张表几百万行,每个行都要读出来、按新结构写进去,期间要么锁写,要么需要额外处理增量数据。磁盘 IO 高、redo 日志暴涨、临时表占用大量空间,都是 COPY 算法的典型副作用。
我们的那条 ALTER 因为没加任何提示,MySQL 直接选用了最保守的策略,进一步加剧了业务阻塞。而根子上的原因,就是 256 这个数字跨过了“1 字节长度前缀”的边界,数据库必须重建整张表来适配新的元数据。
值得强调的一点是:如果这次变更是从 VARCHAR(255) 改成 VARCHAR(300)、VARCHAR(1024),底层同样大概率是重建,因为新声明的最大长度前缀需求变了。所以不要以为“我只多扩一点”就能省事,关键不是扩多少,而是你有没有跨过 255 这条线。
4.3 如果你非改不可:在线变更工具的注意事项
遇到大表字段长度变更,正确姿势是使用专门的在线 DDL 工具。我常用的方案是 gh-ost 或 pt-online-schema-change,它们的核心思路类似:创建一个影子表,在影子表上执行结构变更,同时通过触发器或 binlog 同步增量数据,最后在某个时间点做表切换。这样业务写入不会被长时间锁住,变更过程对线上影响小很多。
但用这类工具也有讲究。第一,变更前务必确认磁盘空间足够,因为工具会复制一份全量数据,临时表大小接近原表大小。第二,控制好复制速度,比如 gh-ost 的chunk-size、max-load参数要按线上负载调整,否则复制太快会把 IO 和主从延迟打满。第三,务必在业务低峰期操作,并提前演练回滚方案,一旦发现异常要及时暂停或终止。
那次事故之后,我把这条心得写进了团队规范:大表变更字段长度,一律先查表行数和大小,再决定是直接 ALTER 还是走在线工具;字段长度变更跨越 255 阈值时,默认按“必然重建表”来评估。
5. 站在其他数据库和 ORM 的视角,重新看“255 vs 256”
5.1 Oracle、PostgreSQL、SQL Server 的差异
MySQL 的这个“255 边界”问题,放到其他数据库里完全是另一回事,这点在团队内部做跨数据库迁移时特别容易踩坑。
Oracle 里常见的是 VARCHAR2,它和 MySQL 最大的不同在于长度单位默认是字节而不是字符。VARCHAR2(255) 在默认设置下允许最多 255 字节,而不是 255 个字符,在 AL32UTF8 字符集下一个汉字最多占 3 字节,所以 VARCHAR2(255) 实际只能存 85 个左右汉字。如果你想让表结构表达“255 个字符”的约束,必须写成 VARCHAR2(255 CHAR)。Oracle 的字节语义是很多从 MySQL 迁到 Oracle 的团队最容易搞错的地方。
PostgreSQL 则更宽松一些,VARCHAR(255) 和 VARCHAR(256) 在存储层几乎没有本质区别。PG 的变长字段头部开销取决于实际数据长度而非声明长度,实际长度小于 127 字节时用 1 字节头,更大时用 4 字节头。声明长度主要承担“写入时校验是否超长”的角色,255 和 256 只是两道不同高度的栏杆,不存在 MySQL 那种“跨过 255 就要重建表”的元数据边界。
SQL Server 则要区分 varchar 和 nvarchar。varchar(n) 的 n 是字节数,nvarchar(n) 的 n 是字符数,nvarchar 每个字符固定 2 字节。SQL Server 在 255 和 256 之间不存在 MySQL 那样的重建分界线,但如果数据需要超过 8000 字节,就得换 varchar(max)、nvarchar(max),这个 8000 边界才是它真正要注意的坎。
这些差异告诉我们,讨论 VARCHAR(255) 还是 VARCHAR(256),一定得先说清楚是在哪个数据库里讨论。跨库迁移时如果只照搬字段类型定义,很容易在字符集、字节语义、存储上限上踩一圈坑。
5.2 ORM 默认值才是“全员 255”的真正推手
日常开发里我们很少直接手写 DDL,表结构大多由 ORM 的实体类生成或迁移工具维护。而很多主流 ORM 的默认行为,就天然把字段推向了 255。
JPA 里@Column不指定 length 时,默认值恰好是 255,Hibernate 自动生成的 DDL 里字符串字段基本全是 varchar(255)。Django 的 CharField 必须显式指定 max_length,但大量教程示例用的都是 255。MyBatis 这类半自动框架虽然不主动生成 DDL,但很多团队的建表脚本模板、数据库建模工具默认也是 255。
这带来的直接后果是:表结构里的长度并不是开发人员深思熟虑后的业务约束,而是框架默认值的搬运工。我自己现在做项目时,会在编码规范里要求:所有字符串字段必须显式写明 length,禁止依赖 JPA 默认值;代码评审时看到新增字段是 varchar(255) 但业务上限不到 50,会直接打回。
如果你正在维护一个老系统,表里全是 255,也别急着全部改掉。改字段长度涉及数据迁移、索引重建、应用层校验联动,风险不小。更务实的做法是:存量字段保持不动,新表、新字段按业务真实约束来定义,慢慢消化历史债务。
6. 高频数据场景下的长度作业答案:直接抄
6.1 昵称、邮箱、URL、哈希值怎么定长
聊完原理,最后给一份可以抄的字段长度清单,按真实场景而非“业界传统”来定。
用户昵称,我一般定 VARCHAR(30) 或 VARCHAR(50),具体取决于产品规则。如果产品没想好,默认 30 就够,主流平台几乎没有超过 30 个字符的昵称。邮箱,RFC 标准上限是 254 字符,所以定 VARCHAR(254) 反而是有据可依的,不要因为“255 好听”就定 255,邮箱字段用 255 会超标准上限。手机号在国内是固定 11 位,用 CHAR(11) 没问题,但考虑海外号码或国家码,定 VARCHAR(20) 更保险。身份证号定 CHAR(18),因为它是固定长度的业务标识。
URL 是重灾区。普通 URL 可能 100 字符以内就够,但带签名参数、跳转参数、CDN 鉴权信息的 URL 很容易突破 255。我建议文件存储类的 URL 字段直接定 VARCHAR(512) 起步,如果确认会存很长的签名链路,用 VARCHAR(1024) 也不为过。哈希值类字段要注意它是固定长度的:bcrypt 输出固定 60 字符,定 CHAR(60);SHA-256 的十六进制串固定 64 字符,定 CHAR(64)。哈希摘要用 CHAR 会比 VARCHAR 更贴切,因为它的值是定长的。
这里额外说一句,定长字符串在 InnoDB 里实际上也是按变长方式处理的,CHAR(n) 如果没填满,存储时会自动去尾部空格,所以不要担心 CHAR 白白浪费空间。但 CHAR 相比 VARCHAR 确实有更明确的语义:它表达了“这个字段的值应该是固定长度”,代码评审时一眼就能看出字段特性。
6.2 长文本、结构化 JSON 不该硬塞进 VARCHAR
最后再提醒一个常见的过度使用问题:把本该用长文本类型或 JSON 类型的数据,硬塞进 VARCHAR。
比如业务文档、文章正文、多行备注,这些内容动不动几千上万字符,如果塞进 VARCHAR(255) 或 VARCHAR(1024),要么频繁超长报错,要么把单行数据撑得很大,引发 InnoDB 行溢出。行溢出后,后续 UPDATE 触发页分裂、行迁移的概率会增加,甚至可能让一些热点行的更新互相等待,产生本不该出现的锁竞争。
再比如结构化数据,MySQL 5.7 之后有原生的 JSON 类型,PostgreSQL 有 JSONB,直接用对应类型就好,不要为了图省事把 JSON 序列化后塞进 VARCHAR(8192) 这种字段。JSON 类型有专门的校验、索引和查询支持,用 VARCHAR 存 JSON 除了能得到一个“看起来不太好看”的字段,没有任何好处。
我在团队里经常说一句话:字段长度是你在数据库层面给业务规则上的一道锁,锁的尺度取决于真实业务,而不是网上流传的所谓“最佳实践”。255 这个数字承载了太多历史包袱,但它不应该成为你偷懒的理由。下次建表再遇到“用 255 还是 256”的问题,先翻翻产品文档,看看这个字段到底最长能有多长,答案往往就出来了。
关于这个主题,我自己操作中最大的体会是:把字段长度当约束而不是容量,在数据库设计阶段多想一步,能省掉后期大量的 ALTER、数据迁移和线上事故复盘。如果非要给一个小技巧的话——给 URL、备注这类长度不可控的字段留足余量,给昵称、手机号这类业务明确的值精确锁定,遇到跨 255 的变更,先想想有没有必要,再想想改起来有多痛。