⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 http://www.iocoder.cn/Fight/MySQL-zhong-Varchar50-he-varchar500-qubieshishenme/ 「芋道源码」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

一个被忽视的设计规范:能短就别长

很多团队的建表规范里都有这一条:

「对可变长度字段,在满足业务的前提下,尽可能使用较短的长度」

但具体短多少?为什么要短?大部分人答不上来。最常见的「直觉式回答」是:

  • 「VARCHAR(50) 比 VARCHAR(500) 节省存储」 —— 错。
  • 「短一点查得快」 —— 对,但只在特定 SQL 模式下成立。

要回答清楚,得做实验。下面这套实验从存储、索引查询、全表排序三个维度走一遍,最后会发现:90% 时间没区别,但有一种 SQL 模式差距 3 倍以上——这才是规范要短的真正原因。

实验 1:存储空间——确实没区别

建两张结构完全一样、只差 name 字段长度的表:

CREATE TABLE category_info_varchar_50 (
id BIGINT NOT NULL AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
is_show TINYINT(4) NOT NULL DEFAULT 0,
sort INT NOT NULL DEFAULT 0,
-- 其他业务字段省略
PRIMARY KEY (id),
KEY idx_name (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE category_info_varchar_500 (
id BIGINT NOT NULL AUTO_INCREMENT,
name VARCHAR(500) NOT NULL,
-- ... 其他字段同上
PRIMARY KEY (id),
KEY idx_name (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

各灌 100 万条同样数据(写入数据的存储过程略,实际数据完全一致),然后查 information_schema.TABLES:

SELECT table_name AS "表名",
table_rows AS "记录数",
TRUNCATE(data_length / 1024 / 1024, 2) AS "数据(MB)",
TRUNCATE(index_length / 1024 / 1024, 2) AS "索引(MB)"
FROM information_schema.TABLES
WHERE table_schema = 'test_mysql_field';

VARCHAR(50) 表:

VARCHAR(500) 表:

两张表的数据容量、索引容量完全相同——VARCHAR 是变长的,实际存储看的是写入内容长度,不是声明的最大长度。「存储空间不一样」的直觉第一关就废。

实验 2:索引查询——也基本没区别

走索引的等值查询、IN 查询、ORDER BY 索引列查询:

-- 等值
SELECT name FROM category_info_varchar_50 WHERE name = 'name100000'; -- 0.012s
SELECT name FROM category_info_varchar_500 WHERE name = 'name100000'; -- 0.012s

-- ORDER BY 走索引
SELECT name FROM category_info_varchar_50 ORDER BY name; -- 0.370s
SELECT name FROM category_info_varchar_500 ORDER BY name; -- 0.379s

-- IN 查询(45 个值)
SELECT * FROM category_info_varchar_50 WHERE name IN (...); -- 0.011 ~ 0.014s
SELECT * FROM category_info_varchar_500 WHERE name IN (...); -- 0.012 ~ 0.014s

走索引的 SQL 性能差距在 1ms 量级——可以认为没差。原因很简单:InnoDB 在 DYNAMIC / COMPRESSED 行格式下,索引 key 的单字段上限是 3072 字节——VARCHAR(500) utf8mb4 满打满算 2000 字节,完整能进索引;查询走 B+Tree,比较时按实际值长度来。索引这条路上 50 和 500 没毛病。

仅在老的 REDUNDANT / COMPACT 行格式下,索引 key 上限是 767 字节——这种情况下 VARCHAR(500) utf8mb4 才会被截断成 prefix 索引,但MySQL 5.7+ 默认就是 DYNAMIC,绝大多数项目不用担心。

实验 3:全表 + 排序——差距 3 倍出来了

不走索引的全表扫描 + 排序,差距瞬间出现:

无排序,两张表表现一致:

加 ORDER BY name:

SELECT * FROM category_info_varchar_50  ORDER BY name;  -- 1.498s
SELECT * FROM category_info_varchar_500 ORDER BY name; -- 4.875s

VARCHAR(50) 全表+排序耗时:

VARCHAR(500) 全表+排序耗时:

VARCHAR(500) 慢了 3.25 倍。一个看着无所谓的字段长度选择,在排序场景下变成了真问题。

真凶:sort_buffer 内存预估按声明长度算

为什么会差这么多?看 SQL 执行 profile。

VARCHAR(50) 的 profile——86% 时间在数据传输(Sending data):

关注两个状态变量:Created_tmp_files 和 sort_merge_passes:

  • Created_tmp_files = 3
  • sort_merge_passes = 95

VARCHAR(500) 的 profile——多了「临时表排序」环节:

  • Created_tmp_files = 4
  • sort_merge_passes = 645 —— 比 50 的版本暴涨 6.8 倍

sort_merge_passes 是 MySQL 归并排序的次数——越大说明 sort_buffer 塞不下,越要靠磁盘临时文件归并,磁盘 IO 上来性能就崩。

关键来了:MySQL 在做排序前,会根据字段声明的最大长度估算每条记录在 sort_buffer 里的占位——VARCHAR(500) 单条估到 500 字节 + 元数据开销,VARCHAR(50) 估到 50 字节。同样 8MB 的 sort_buffer_size:

字段长度 估算单条 装得下行数 100 万行需要归并次数
VARCHAR(50) ~58 字节 ~144,000 ~7 轮
VARCHAR(500) ~558 字节 ~15,000 ~67 轮

实际数据明明都是 name123456 这种十几字节的字符串,但 MySQL 不看这个——它只看声明长度。这就是规范让你「能短就短」的真正动机:在排序、分组、临时表场景下,声明长度直接决定内存利用率。

决策矩阵:常见字段该给多少长度

不是越短越好,太短了改起来更痛苦。给个参考:

字段类型 推荐长度 理由
用户名 / 昵称 VARCHAR(64) 主流业务 32-50 够用,留点余量
邮箱 VARCHAR(128) RFC 5321 上限 254,但实际 < 128
手机号 CHAR(20) 定长且固定,CHAR 比 VARCHAR 更适合
URL VARCHAR(512) 或 VARCHAR(1024) 长 URL 现实存在,但避免 VARCHAR(2000)
标题 / 名称 VARCHAR(128) 业务分类、文章标题、商品名都够
描述 / 富文本 TEXT 一旦超过 1KB 用 TEXT,别拿 VARCHAR(8000) 死撑
UUID / 雪花 ID CHAR(36) / BIGINT 定长用 CHAR,雪花用 BIGINT
极不确定的开放字段 JSON 或拆出独立表 别用 VARCHAR(N) 当万能桶

为什么不无脑用 VARCHAR(255)? 历史遗留——MySQL 5.0.3 之前 VARCHAR 上限是 255 字节,模板默认 255。现在 InnoDB 上 VARCHAR 最长 65535 字节,但越长在排序场景代价越大。

4 个真实生产坑,按踩到概率从高到低

坑 1:分页查询带排序,TP99 突然劣化(最常见)

ORDER BY name LIMIT 100, 10 看起来很轻——前端列表分页谁不写?但只要 ORDER BY 字段没走索引、且字段是 VARCHAR(500+),sort_buffer 直接爆。监控里 TP99 飙升、慢查询日志炸出一堆 Using filesort; Using temporary。

修法:要么给 ORDER BY 字段加索引,要么把字段长度收紧,要么调大 sort_buffer_size(但调大全实例都受影响,要慎重)。

坑 2:临时表 GROUP BY 拼字段(常见)

SELECT category, COUNT(*) FROM ... GROUP BY category;

GROUP BY 也走 sort_buffer 或临时表,逻辑同 ORDER BY。长 VARCHAR 字段做 GROUP BY 同样吃亏。

坑 3:UNION 把多张表的字段长度并起来(少见但破坏力大)

SELECT name FROM table_a   -- VARCHAR(50)
UNION
SELECT name FROM table_b; -- VARCHAR(500)

UNION 结果集字段长度按最大的那个算——本来 50 就够,UNION 一接,结果集里全按 500 估。复杂报表 SQL 把多个表的字段拼起来,性能突然崩就是这种问题。

坑 4:JSON 字段当 VARCHAR 用(高级场景)

「业务字段不确定,先放 JSON」——三年后字段平均 5KB,涉及它的查询、排序、临时表全炸。JSON 比 VARCHAR(N) 对 sort_buffer 更不友好。该拆表必须拆,别拿 JSON 顶。

说到底

VARCHAR(50) 和 VARCHAR(500) 在存储、走索引的查询上没区别——这条规范的真正意义在 sort_buffer 上。好的字段长度设计不是「能存就行」,是「让 MySQL 在排序时不被声明长度欺骗到磁盘上」。

文章目录
  1. 1. 一个被忽视的设计规范:能短就别长
  2. 2. 实验 1:存储空间——确实没区别
  3. 3. 实验 2:索引查询——也基本没区别
  4. 4. 实验 3:全表 + 排序——差距 3 倍出来了
  5. 5. 真凶:sort_buffer 内存预估按声明长度算
  6. 6. 决策矩阵:常见字段该给多少长度
  7. 7. 4 个真实生产坑,按踩到概率从高到低
  8. 8. 说到底