char 和 varchar 的区别?

#中间件#面试八股

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. 长度前缀不是恒为 1 字节。规则看的是这一列的最大字节数:VARCHAR(255) 在 latin1 下最大 255 字节,前缀 1 字节;在 utf8mb4 下最大 1020 字节,前缀就是 2 字节。
  2. 尾部空格在比较时的表现由排序规则决定。utf8mb4_general_ci 属于 PAD SPACE,'a' = 'a ' 成立;MySQL 8.0 默认的 utf8mb4_0900_ai_ci 属于 NO PAD,两者不相等。所以「CHAR 会自动忽略空格」这句话只在 PAD SPACE 排序规则下成立。
  3. 内部临时表会放大 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 字节。

笔记加载中…