char 和 varchar 的区别?
结论:CHAR 是定长存储,写入时右侧补空格到 n 个字符;VARCHAR 在数据前加 1~2 字节长度前缀,只存实际内容。差异不止「能不能变长」,真正影响性能的是行内空间占用、变长更新引发的页分裂、排序与临时表的内存分配。
一、一行数据在页里长什么样
CHAR(8) utf8mb4 写入 'abc'(存储层补 5 个空格)
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ a │ b │ c │ │ │ │ │ │ 固定 8 字符 = 32 字节
└───┴───┴───┴───┴───┴───┴───┴───┘
VARCHAR(8) utf8mb4 写入 'abc'
┌──────┬───┬───┬───┐
│ 0x03 │ a │ b │ c │ 1 字节长度前缀 + 3 字节数据
└──────┴───┴───┴───┘
CHAR 的补齐发生在存储层,读取时会去掉尾部空格;VARCHAR 原样保存,尾部空格属于数据本身。
二、逐项对比
| 维度 | CHAR(n) | VARCHAR(n) |
|---|---|---|
| 长度语义 | 固定 n 个字符 | 最多 n 个字符 |
| 空间占用 | n × 字符集单字符最大字节数 | 实际字节数 + 长度前缀 |
| 长度前缀 | 无 | 最大字节数 ≤ 255 用 1 字节,否则 2 字节 |
| 尾部空格 | 存储补齐、读取截断 | 完整保留 |
| 变长更新 | 位置固定,不移动 | 可能触发页内重排、行迁移或页分裂 |
| 比较语义 | 取决于排序规则的 PAD 属性 | 同左 |
| 典型场景 | 定长编码:手机号、MD5、状态码 | 变长文本:昵称、标题、地址 |
需要注意 n 在两种类型里都是字符数,不是字节数。utf8mb4 下一个字符最多 4 字节,所以 CHAR(10) 最多占 40 字节。
三、三个容易答错的细节
- 长度前缀不是恒为 1 字节。规则看的是这一列的最大字节数:
VARCHAR(255)在 latin1 下最大 255 字节,前缀 1 字节;在 utf8mb4 下最大 1020 字节,前缀就是 2 字节。 - 尾部空格在比较时的表现由排序规则决定。
utf8mb4_general_ci属于 PAD SPACE,'a' = 'a '成立;MySQL 8.0 默认的utf8mb4_0900_ai_ci属于 NO PAD,两者不相等。所以「CHAR 会自动忽略空格」这句话只在 PAD SPACE 排序规则下成立。 - 内部临时表会放大 VARCHAR 的内存开销。MySQL 5.7 的内部临时表使用 MEMORY 引擎,VARCHAR 会被转成定长 CHAR;8.0.16 之后默认的 TempTable 引擎才按变长存储。老版本上一条带
GROUP BY的大字段查询把内存打爆,往往就是这个原因。
四、建表与验证
CREATE TABLE `user_profile` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`phone` CHAR(11) NOT NULL COMMENT '手机号,定长 11 位',
`nickname` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '昵称',
`intro` VARCHAR(512) NOT NULL DEFAULT '' COMMENT '签名,更长就换 TEXT',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_phone` (`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO user_profile (phone, nickname) VALUES ('13800000000', '瑞林 ');
SELECT nickname,
CHAR_LENGTH(nickname) AS char_len, -- 4,尾空格被计入
LENGTH(nickname) AS byte_len -- 9,utf8mb4 下每字 3 字节 + 空格
FROM user_profile;
行长度也有硬上限:单行(不含 TEXT/BLOB)不能超过 65535 字节。
CREATE TABLE t_wide (a VARCHAR(16000), b VARCHAR(16000)) CHARSET=utf8mb4;
-- ERROR 1118 (42000): Row size too large.
五、对索引与排序的影响
- 索引键上限:InnoDB 单列索引键最多 3072 字节(
DYNAMIC行格式),utf8mb4 下VARCHAR(768)就到顶了;再长的列只能建前缀索引。 - 前缀索引:
KEY idx_name (nickname(12))只索引前 12 个字符,能省空间但无法用于覆盖索引,也无法支持ORDER BY nickname。 - 排序内存:
ORDER BY变长列时,sort_buffer_size按最大可能长度分配;把VARCHAR(2000)换成VARCHAR(64),同样的排序语句可能从落盘退化为内存排序。 - 更新膨胀:VARCHAR 从短值改成长值,当前页放不下就会触发页分裂,碎片率上升,需要定期
OPTIMIZE TABLE或调整innodb_fill_factor。
常见追问
- CHAR(10) 存 'abc' 占 40 字节,是不是浪费? 是,但定长意味着按偏移寻址,不需要读长度前缀;只有宽度很小、值确实等长时才划算。
- 为什么手机号、身份证号不推荐用数值类型? 数值会丢前导零、超范围后精度失真,且等值查询用字符串索引同样走 B+Tree,没有性能劣势。
- VARCHAR 越长越慢吗? 索引键越长,单页能放的键越少、树越高。索引列应尽量短,超长文本用前缀索引或另存哈希列。
- 什么时候用 TEXT? 超过行大小限制、或很少参与检索与排序的长文本。TEXT 溢出页不占主行空间,但每次读取多一次 I/O。
- CHAR 一定比 VARCHAR 快? 只在定长、频繁更新的窄列上成立。宽 CHAR 会让 buffer pool 命中率下降,反而更慢。
小结
CHAR 用空间换定长寻址,VARCHAR 用长度前缀换空间弹性。选型看三点:值是否等长、更新是否频繁改变长度、列是否要进索引。utf8mb4 场景下,长度前缀是 1 字节还是 2 字节由「最大字节数」决定,别背成固定 1 字节。