表里有这么两个字段:
user_name varchar(50)
user_name varchar(500)
如果实际都只存一个 "zhangsan",varchar(500) 会不会比 varchar(50) 多占 10 倍空间?
不会。
这个问题我见过不少次,有人甚至为了“以后方便扩展”,建表时统一 varchar(500)、varchar(1000)。这地方我一般不这么干。
不是因为 varchar(500) 一定性能差,而是字段长度这个东西一旦随手放大,后面真正容易出问题的是索引、行大小,还有 Java 这一层的数据约束。
先看 MySQL 到底怎么存。
VARCHAR 是变长字段。
比如都是 utf8mb4:
create table customer_profile (
id bigint primary key,
short_name varchar(50),
long_name varchar(500)
) engine = InnoDB
default charset = utf8mb4;
两列都存:
dongge
MySQL 并不会给 short_name 提前留 50 个字符,也不会给 long_name 提前留 500 个字符。
存多少,主要就放多少。
VARCHAR 实际存储大致是:
长度信息 + 实际字符串
MySQL 官方文档也是这么定义的:VARCHAR 保存实际数据,并额外使用 1 或 2 个字节记录长度。最大可能值不超过 255 字节时使用 1 个长度字节,可能超过 255 字节时使用 2 个。
这里就有个容易漏掉的小区别。
utf8mb4 一个字符最多可能占 4 字节。
所以:
varchar(50)
最大数据长度约为 50 × 4 = 200 字节
长度信息:1 字节
varchar(500)
最大数据长度约为 500 × 4 = 2000 字节
长度信息:2 字节
也就是说,同样存 "dongge",两者实际存储空间差距非常小,不是什么 10 倍。
但这不代表:
varchar(50)
和:
varchar(500)
可以随便选。
真正的差距在后面。
第一个坑:索引
我看到一个字符串字段长度比较大时,第一反应一般不是磁盘空间,而是先看看它有没有进索引。
比如:
create table order_search (
id bigint primary key,
buyer_name varchar(500) not null,
biz_tag varchar(500) not null,
key idx_buyer_tag (buyer_name, biz_tag)
) engine = InnoDB
default charset = utf8mb4;
这时候就不能只看“实际业务里名字一般就十几个字”。
索引建的是字段定义允许的键值范围。
两个 varchar(500),按照 utf8mb4 最坏情况算:
500 × 4 + 500 × 4 = 4000 字节
而 InnoDB 在常见的 16KB page、DYNAMIC/COMPRESSED 行格式下,索引键长度上限是 3072 字节;页大小更小时限制还会继续下降。
这种表结构,我第一眼就不太信它能老老实实按你的想法工作。
所以经常能看到有人最后改成前缀索引:
create index idx_buyer_tag
on order_search (buyer_name(80), biz_tag(40));
能不能这么改,还得看数据区分度,不能闭着眼截 80 个字符。
至少得先查:
select
count(*) as total_rows,
count(distinct left(buyer_name, 40)) as p40,
count(distinct left(buyer_name, 80)) as p80
from order_search;
我一般宁愿多跑一次这种 SQL,也不愿意看到个长字符串就随手给它前缀索引。
第二个区别:整行大小
MySQL 对一行能够定义的数据大小是有限制的,所有列不是想写多长就写多长。
比如这种表:
create table import_record (
id bigint primary key,
source_name varchar(5000),
source_path varchar(5000),
error_detail varchar(5000),
raw_message varchar(5000)
) charset = utf8mb4;
一眼看过去就有点别扭。
四个字段如果都按照 utf8mb4 最大容量估:
5000 × 4 × 4
已经 80000 字节了。
MySQL 本身对一行所有列有 65535 字节的限制,VARCHAR 的长度字节同样要算进去;InnoDB 自己还有页内行存储和页外存储机制。
所以建表时很可能直接给你:
ERROR 1118 (42000): Row size too large
这种字段如果真的是错误详情、原始报文、第三方返回内容,我反而会重新判断它是不是应该用 TEXT,而不是一路:
varchar(1000)
varchar(5000)
varchar(10000)
往上加。
字段类型不是俄罗斯套娃。
回到 Java 的实际问题
Java 里的:
private String userName;
它不知道数据库后面到底是:
varchar(50)
还是:
varchar(500)
你完全可以往里面塞一个 300 个字符的字符串。
真正执行 INSERT 时数据库才发现:
insert into customer_profile(user_name)
values (?)
结果字段只有 varchar(50)。
生产环境一般都会开严格 SQL 模式,这时候超过长度会直接报错;非严格模式下则可能发生截断并产生 warning。MySQL 官方对这两种行为都有明确说明。
所以我更喜欢在入口把长度挡掉,而不是一路让数据跑到 DAO,最后等数据库告诉我:
Data too long for column 'user_name'
Spring 项目里可以这么写:
public record RenameRequest(
String userName
){
public String checkedName(){
String value = userName == null ? "" : userName.strip();
int chars = value.codePointCount(0, value.length());
if (chars == 0 || chars > 50) {
throw new IllegalArgumentException("用户名长度不合法");
}
return value;
}
}
为什么这里没直接写:
userName.length() > 50
因为 Java 的 String.length() 算的是 UTF-16 code unit。
碰到某些 emoji,一个你肉眼看到的字符,在 Java 里可能占两个 char。
而 MySQL 的:
varchar(50)
这个 50 表示的是字符数,不是字节数。
你也可以自己在库里看:
select
char_length('程序员东哥') as chars,
length('程序员东哥') as bytes;
CHAR_LENGTH() 看字符数。
LENGTH() 看字节数。
这两个函数我排字符集问题时经常一起用,光看 Java 日志里的字符串,很多时候根本看不出来数据库到底按多少字节存了。
如果项目用 JPA,还有一个地方别搞混:
@Column(name = "user_name", length = 50)
private String userName;
这里的 length = 50 属于持久化映射里的列长度定义,尤其会影响 Schema Generation;它不是一个可靠的接口参数运行时校验器。Jakarta Persistence 的 @Column.length 默认值本身就是 255。
想在请求入口做校验,可以再配 Bean Validation:
@Size(max = 50)
private String userName;
但再抠细一点,@Size 对 CharSequence 检查的是字符序列长度。
如果你的业务允许大量 emoji,又要求 Java 校验规则和 MySQL 字符语义尽量一致,我还是会自己用 codePointCount() 控一遍。
最后再回到最开始的问题。
如果只存同样的 "dongge":
varchar(50)
varchar(500)
实际数据占用不会差 10 倍,网络传输也不会因为声明成 500 就平白多传几百个字符。
但我仍然不会把能确定 50 个字符的字段写成 500。
用户昵称,就按业务允许的昵称长度来。
订单号能确定 64,就别写 500。
外部流水号最大 128,就写 128。
真不知道上限、而且内容本来就是一坨不可控文本,再考虑 TEXT。
VARCHAR 后面这个数字,看着只是一个长度。
表小的时候确实没什么感觉。
等它进了联合索引,旁边又塞了十几个大 VARCHAR,Java 接口还完全不做长度校验,这个数字就没那么随便了。