“MySQL 里 utf8mb4 字符集,字母 a 占几个字节?一个汉字占几个字节?换成 utf8mb3 呢?”这个问题我来来回回被问过很多次,有准备面试突击的,也有线上乱码排查到这里的。问题看着简单,但背后牵着的其实是 Unicode 编码规则、MySQL 字符集实现、建表字段长度设计、索引字节上限、容量估算这一整串知识点。我把答案、推导过程、实测方法一次性拆开讲清楚:在 utf8mb4 和 utf8mb3 下,ASCII 字母 a 都是 1 字节;基本平面的汉字(比如“中”“文”)都是 3 字节。两者真正的区别在别处——utf8mb4 还能装下 4 字节字符(常见于 emoji、部分生僻字),而 utf8mb3 最多只能处理 3 字节。这篇文章适合刚接触 MySQL 的开发者理解编码基础,也适合正在做表结构设计、索引优化的同学对照避坑。
1. 先给结论:a 和汉字到底占几个字节
1.1 一张表看清楚各字符的字节占用
先把结论摆在最前面,后面再讲原理。下面这个表我建议收藏,遇到问题时直接查。
| 字符集 | 字母 a | 汉字“中” | 4 字节字符(emoji、扩展区生僻字) | 单字符最大字节数 |
|---|---|---|---|---|
| utf8mb4 | 1 字节 | 3 字节 | 4 字节 | 4 |
| utf8mb3(即 utf8) | 1 字节 | 3 字节 | 不支持,插入会报错 | 3 |
所以这题的“标准答案”是:字母 a 在 utf8mb4 和 utf8mb3 下都占 1 字节,汉字在两者下面都占 3 字节。如果你去面试,先把这个答案干净利落地给出来,再补一句“utf8mb4 的真正优势在于还能处理 4 字节字符”,这一题基本就稳了。
1.2 “最大 4 字节”不等于“每个字符都 4 字节”
这是最常见的误区。很多人一看到 utf8mb4 里的 4,就以为里面的所有字符都固定占 4 字节,估算表容量时直接按 4 字节翻倍,结果预算大幅超支。实际上,utf8mb4 是一个变长编码字符集,这个名字的意思是单个字符“最多”占 4 字节,而不是“必须”占 4 字节。
举几个具体例子:字母 a 的 Unicode 码位是 U+0061,UTF-8 编码后是 0x61,正好 1 字节;汉字“中”的码位是 U+4E2D,编码后是 0xE4 0xB8 0xAD,共 3 字节;而一个辅助平面的字符,码位在 U+10000 以上,编码后才是 4 字节。换句话说,不是字符集名字带 4,就所有数据都涨到 4 倍,实际占用完全取决于你存的是什么内容。
顺带补充一个容易忽略的点:中文标点“,”“。”以及全角数字、全角字母,它们的码位都在 U+0800 到 U+FFFF 这个区间,所以同样是 3 字节。而像希腊字母“α”、带音标的拉丁字符“é”这类码位在 U+0080 到 U+07FF 范围的字符,UTF-8 编码是 2 字节。真正稳定占 1 字节的,只有原来的 ASCII 字符集那 128 个字符。
1.3 汉字为什么是 3 字节而不是 2 字节
这是面试里最常被追问的“为什么”。要解释清楚,得看 UTF-8 的编码区间分配。UTF-8 把 Unicode 码位按区间切成四段,每段用不同的字节模板表示:
- U+0000 ~ U+007F:1 字节,模板为 0xxxxxxx,和 ASCII 完全兼容。
- U+0080 ~ U+07FF:2 字节,模板为 110xxxxx 10xxxxxx。
- U+0800 ~ U+FFFF:3 字节,模板为 1110xxxx 10xxxxxx 10xxxxxx。
- U+10000 ~ U+10FFFF:4 字节,模板为 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx。
汉字的常用区落在 U+4E00 到 U+9FFF,绝大部分常用汉字,包括“中”“文”“字”,码位都大于 U+0800,同时小于 U+10000,落进的是 3 字节区间。所以汉字在 UTF-8 下固定按 3 字节编码,这和字符集叫 utf8mb4 还是 utf8mb3 没有关系,是 Unicode 码位本身决定的。
这里可以手算一下“中”字:U+4E2D = 二进制 0100 1110 0010 1101,共 15 位有效数据。按 3 字节模板切分,得到 11100100 10111000 10101101,也就是十六进制 0xE4 0xB8 0xAD。这个字节序列在 utf8mb4 和 utf8mb3 下完全一致。
2. 拆开 UTF-8 编码,看字节数由什么决定
2.1 变长编码设计是兼容性和空间利用的平衡
UTF-8 为什么要设计成变长,而不是所有字符统一用 2 字节或者 3 字节?这和 ASCII 的兼容性有直接关系。如果所有字符都固定占 2 字节,那字母 a 也要浪费一个字节;如果固定占 3 字节,整个互联网上的 ASCII 文本体积直接变三倍。变长编码的最大好处就是:原有 ASCII 数据继续按 1 字节存储,新扩展的字符按需增加字节数,空间紧凑,还能被旧系统部分识别。
实际解析 UTF-8 字节流时,靠首字节的最高位前缀就能判断一个字符占几个字节。以 0 开头的是 1 字节字符,以 110 开头表示这是 2 字节字符的第一个字节,以 1110 开头是 3 字节字符的开头,以 11110 开头则是 4 字节字符的开头。后面的连续字节都以 10 开头,作为数据载荷部分。这套设计非常精巧,即使字节流中间断了,也能很快定位到字符边界。
2.2 从字符集命名看 MySQL 的设计思路
MySQL 的 utf8mb4 和 utf8mb3,名字里的 mb 是 maximum bytes 的缩写,数字代表的是排序规则和存储层面允许的单字符最大字节数。utf8mb4 允许单字符最多 4 字节,utf8mb3 则限制在 3 字节。
这里有个特别坑的历史遗留:MySQL 里的 utf8 其实一直是 utf8mb3 的别名,并不是完整意义上的 UTF-8。早期 Unicode 规范里,绝大多数常用字符都在基本平面内,当时认为 3 字节足够用了,所以 MySQL 把 utf8 做到了 3 字节上限。后来 Unicode 扩展到辅助平面,emoji 和各种生僻字大量出现,3 字节装不下了,MySQL 才在 5.5.3 版本引入 utf8mb4。也就是说,你现在写CHARACTER SET utf8,在 MySQL 8.0 之前等于用的是 utf8mb3,而不是你以为的完整 UTF-8。
2.3 4 字节场景:生僻字和扩展字符
很多人以为只有 emoji 才会触发 4 字节,其实所有码位在 U+10000 到 U+10FFFF 之间的字符都需要 4 字节。这里面既包括 emoji,也包括很多扩展区的生僻汉字,比如码位 U+20000 的汉字,UTF-8 编码是 0xF0 0xA0 0x80 0x80。这类字符在 utf8mb3 下直接无法存储,插入时会报Incorrect string value: '\xF0\xA0\x80\x80...',这也是为什么现在新建表几乎清一色推荐 utf8mb4。
有一点值得注意:即使某个字段声明成 utf8mb4,也不是所有汉字都刚好 3 字节。基本区汉字是 3 字节,扩展到辅助平面的汉字是 4 字节。所以严谨地说,题目里“一个汉字占几个字节”的答案应该是“常用汉字 3 字节,扩展区生僻字可能 4 字节”。日常交流中默认是常用汉字,答 3 字节没问题,但能把边界情况点出来,说明你底子是扎实的。
3. 动手实测:在 MySQL 里验证字节数
3.1 建一个测试表,分别存 utf8mb4 和 utf8mb3
理论讲再多,不如直接在 MySQL 里跑一遍。准备一个测试表,两列分别用 utf8mb4 和 utf8mb3。注意:MySQL 8.0 的 information_schema 里能直接看到 utf8mb3,写 SQL 时也可以用这个字符集名;如果你用的是 5.7 或更早版本,把第二列的utf8mb3改成utf8效果一样。
CREATE TABLE t_char_bytes ( c_mb4 VARCHAR(20) CHARACTER SET utf8mb4, c_mb3 VARCHAR(20) CHARACTER SET utf8mb3 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;插入字母 a 和汉字“中”:
INSERT INTO t_char_bytes (c_mb4, c_mb3) VALUES ('a', 'a'); INSERT INTO t_char_bytes (c_mb4, c_mb3) VALUES ('中', '中');接着用LENGTH()和CHAR_LENGTH()分别查字节数和字符数:
SELECT c_mb4, CHAR_LENGTH(c_mb4) AS char_cnt, LENGTH(c_mb4) AS byte_cnt, HEX(c_mb4) AS hex_val FROM t_char_bytes;结果很直观:字母 a 的char_cnt是 1,byte_cnt也是 1,HEX是 61;汉字“中”的char_cnt是 1,byte_cnt是 3,HEX是 E4B8AD。两列的结果完全一致,说明在这个层面 utf8mb4 和 utf8mb3 对 ASCII、常用汉字的存储没有任何区别。
3.2 用二进制字面量测 4 字节字符
再测一下 4 字节场景。这里故意不直接贴 emoji,而是用它的字节序列来测,避免不同终端字体显示差异。U+1F600 的 UTF-8 编码是F0 9F 98 80:
INSERT INTO t_char_bytes (c_mb4) VALUES (CONVERT(X'F09F9880' USING utf8mb4)); SELECT c_mb4, CHAR_LENGTH(c_mb4) AS char_cnt, LENGTH(c_mb4) AS byte_cnt, HEX(c_mb4) AS hex_val FROM t_char_bytes WHERE CHAR_LENGTH(c_mb4) = 1 ORDER BY byte_cnt DESC;在 utf8mb4 列上,这条数据会正常插入,byte_cnt返回 4,hex_val是F09F9880。如果你把同一份字节插到 c_mb3 列,MySQL 会直接报Incorrect string value。这一步能直观证明:utf8mb4 和 utf8mb3 对 a、汉字的字节数一样,真正的分水岭在 4 字节字符上。
3.3 连接层字符集也会影响结果
有个很容易被忽略的变量:LENGTH()返回的是当前连接字符集解释后的字节数吗?准确地说,它返回的是存储层实际字节数,但如果你在查询前执行了SET NAMES或者连接层字符集和表字符集不一致,某些客户端显示层面会出现乱码,甚至写入时发生字符替换。排查字节数相关问题时,先确认三处一致:表字符集、连接字符集、客户端工具字符集。
SHOW VARIABLES LIKE 'character_set_client'; SHOW VARIABLES LIKE 'character_set_connection'; SHOW VARIABLES LIKE 'character_set_results';连接字符集对写入的校验影响很大。举个实际场景:表列是 utf8mb4,但连接的character_set_client是 utf8mb3,你通过连接插入一个 4 字节字符,MySQL 会先按客户端字符集解析,再转换到列字符集存储,这一步就可能因为中间环节不支持而报错或者把字符替换成问号。所以实际生产环境里,应用层 JDBC 或各类驱动配置里都要显式声明characterEncoding=utf8mb4或等价的参数。
4. 这对建表设计意味着什么
4.1 VARCHAR(N) 的 N 是字符数,不是字节数
这是字节数知识落地时最直接的一个坑。VARCHAR(20)里的 20,指的是最多能存 20 个字符,不是 20 个字节。所以VARCHAR(20) CHARACTER SET utf8mb4这一列,能装 20 个汉字,也能装 20 个字母。区别在于最大字节占用:全汉字是 60 字节,全字母是 20 字节,混排时介于两者之间。
设计字段长度时,应该按“业务上最多可能出现多少个字符”来规划,而不是按字节数来规划。比如用户昵称字段,业务规定最多 30 个字符,那就写VARCHAR(30),不管它是 utf8mb4 还是 utf8mb3,都能容纳 30 个汉字。但如果看到某些老系统里把用户名定义成VARCHAR(255),你就要多想一层:这个 255 当年可能是按字节数拍脑袋定的,换到 utf8mb4 后,这个字段理论上最多可以占 1020 字节,在索引和内存中的开销会显著增大。
4.2 索引长度上限与 4 字节的碰撞
字节数对索引的影响极其直接。InnoDB 的索引键有字节上限:COMPACT 和 REDUNDANT 行格式下,单个索引键最长 767 字节;DYNAMIC 和 COMPRESSED 行格式下,上限放宽到 3072 字节。utf8mb4 因为单字符最多 4 字节,很容易撞到这条线。
举一个实际遇到过的报错场景:某个表用 utf8mb4,对一个VARCHAR(200)字段建普通索引,计算公式是200 × 4 = 800字节。在旧版本默认 COMPACT 行格式、且未开启大前缀支持的实例上,直接报Specified key was too long; max key length is 767 bytes。解决方式一般是三种:把列长度缩短、对索引列做前缀索引(例如INDEX idx_name (colname(50)),按字符数截取前 50 个字符做索引)、或者升级实例行格式到 DYNAMIC 并确认innodb_large_prefix开启。在 5.7 之后的默认配置里,200 字节不会超限,但如果你建联合索引,几个VARCHAR(255)字段加起来轻松超过 3072 字节,依然会被拒。
所以我的建议是:大字段不要无脑加索引,能用前缀索引就用前缀索引;对几个大 varchar 列做联合索引前,先心算一遍总字节数。索引不是越宽越好,磁盘和内存的代价都在那里。
4.3 容量估算别再拍脑袋
知道单字符字节数,对容量估算很有帮助。但要注意,不能拿“最大字节数”去估算整表。举个例子:业务表预计 1000 万行,其中有一个title VARCHAR(100)字段,中文标题平均 30 个汉字。估算时按平均长度算,一条记录的 title 大约占30 × 3 = 90字节,1000 万行就是 900MB 左右的数据量。如果你偷懒按100 × 4 = 400字节去算,直接变成 4GB,工程判断就会出大问题。
当然,真实容量还要叠加 InnoDB 的行开销、块内碎片、二级索引、事务信息等,通常在纯数据字节数上再乘一个系数。我在做容量规划时,一般先按“平均字节数 × 行数”算出基础值,再乘 1.3 到 1.5 的膨胀系数,预留 20% 余量。这套方式比“每行按 4 字节 × 字段上限”要靠谱得多。
5. utf8mb3 和 utf8mb4 怎么选
5.1 历史包袱:utf8 其实是 utf8mb3
MySQL 中utf8这个名字已经用了很多年,很多老项目的建表语句里写的就是DEFAULT CHARSET=utf8。这个 utf8 在 MySQL 里从来就不是完整 UTF-8,而是最多 3 字节的版本。早期这么设计有历史原因:Unicode 基本平面内的字符,3 字节足够覆盖,当时辅助平面用的人太少。后来 emoji 普及,扩展区汉字需求也上来,utf8mb4 才成为事实标准。
现在新项目里如果还写utf8,等于主动选择了残缺版本。一旦某天业务需要保存 emoji 或者生僻字,要么被 MySQL 拒之门外,要么出现字符被替换成?的线上事故。更麻烦的是,这种问题往往不是一开始就暴露,而是数据积累到一定程度才被发现,那时候再改表,成本远高于建表时顺手写 utf8mb4。
5.2 平滑迁移步骤
如果你手里已经有 utf8mb3 的老表,建议尽早规划迁移。常规流程是:
- 先备份,至少导出表结构和全量数据,并确认备份可恢复。
- 修改表字符集:
ALTER TABLE t CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;如果实例是 5.7,没有utf8mb4_0900_ai_ci,用utf8mb4_unicode_ci。 - 调整应用层连接配置,确保连接、客户端、结果集的字符集都改成 utf8mb4。
- 重新验证应用功能,重点测试含生僻字、emoji、各种语言的写入和检索。
这里要特别提醒两点。第一,CONVERT TO CHARACTER SET会重建表,表数据量大时耗时不短,而且会锁表,务必在业务低峰期执行,或者使用在线改表工具分批操作。第二,如果表里已经有乱码或者不规范的二进制数据,直接转换可能把问题固化下来,建议先清洗数据再转换。
5.3 排序规则顺带说清楚
字符集和排序规则是两个层次的东西,字节数由字符集决定,排序比较由排序规则决定。在建表语句里,二者常一起出现,比如CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci。常见排序规则对比:
| 排序规则 | 特点 | 适用建议 |
|---|---|---|
| utf8mb4_general_ci | 旧默认,排序速度略快,但比较规则较粗糙 | 老项目兼容场景 |
| utf8mb4_unicode_ci | 基于 Unicode 排序规则,比较更准确 | 5.7 时代的稳妥选择 |
| utf8mb4_0900_ai_ci | 8.0 默认,支持更完整的 Unicode 排序和大小写规则 | 新项目首选 |
排序规则不影响字节数,但会影响同一条 SQL 在不同排序规则下的索引可用性和查询结果。例如中文排序、大小写不敏感匹配等场景,0900_ai_ci表现更符合直觉。你不需要背下所有细节,只要记住一句话:新库选 utf8mb4 +utf8mb4_0900_ai_ci,老库迁移时根据现有数据选择兼容的排序规则。
6. 常见问题与面试追问
6.1 高频问题速查表
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 为什么 utf8mb4 下字母 a 只占 1 字节 | ASCII 字符在 UTF-8 中就是单字节编码 | 正常现象,不是 bug |
| utf8mb4 是不是一定比 utf8mb3 占空间 | 不一定,不存 4 字节字符时二者字节数完全一致 | 按实际内容估算容量 |
| 为什么 utf8mb3 也能正常存汉字 | 常用汉字在基本平面,3 字节足够 | 不乱码时短期可用,长期仍建议迁移 |
| 插入 emoji 报 Incorrect string value | 表列用了 utf8mb3 或连接层字符集不支持 4 字节 | 表和连接都改成 utf8mb4 |
| 为什么 LENGTH 返回值和预想不一致 | LENGTH 返回字节数,不是字符数 | 要数个数用 CHAR_LENGTH |
6.2 面试官不会写在题面上的细节
这个题目看起来只有一行,但稍微深挖一点,几个衍生问题都很值得准备。
第一,“一个VARCHAR(255) CHARACTER SET utf8mb4的列,理论上最多占多少字节?”答案是 1020 字节,因为 255 个字符,按每个字符 4 字节上限计算。但实际如果全存汉字,只占 765 字节;全存字母,只有 255 字节。面试时能分开“理论最大”和“实际平均”两个维度回答,说明你真的理解了变长编码。
第二,“组合 emoji 算一个字符吗?”不算。像家庭成员类的组合 emoji 由多个码点组合而成,MySQL 按码点当字符处理,所以一个看起来像一个图形的 emoji 序列,可能被拆成 5 个甚至更多字符存储,字节数也成倍增加。这个问题在正常业务里不常见,但一旦遇到,很容易让字符数统计偏离预期。
第三,“UTF-8 为什么不干脆用 2 字节存所有汉字?”2 字节最多只能表达 U+07FF 之前的字符,汉字码位普遍在 U+4E00 之后,超过 2 字节上限。如果强行改变编码规则,又会影响和 ASCII 的兼容性,代价更大。所以 3 字节是 UTF-8 在“兼容性”和“空间效率”之间选定的折中方案。
第四,排序规则会不会改变字节数?不会。utf8mb4_unicode_ci和utf8mb4_general_ci的排序规则不同,但存储时的字节序列完全一致。排序规则只影响字符之间的比较结果,不影响LENGTH的返回值。
说一点我自己的体会。第一次把“a 占几字节、汉字占几字节”彻底弄明白,是在排查一次线上乱码的时候。当时发现老表用的是 utf8mb3,但应用连接已经改成 utf8mb4,有些特殊字符写进去后变成问号,查了好久才发现是字符集中间层不匹配。后来我把所有新表的默认字符集统一改成 utf8mb4,再遇到字节数相关的问题,就能很自然地答出来了。如果你正在准备面试,或者只是想在项目里少踩几个字符集的坑,建议把CHAR_LENGTH和LENGTH在本地随手测一测,亲手看到 1 字节、3 字节、4 字节的差异,这比背十遍结论都管用。