☰
MySQL字符集详解:utf8mb4与utf8mb3的字节数差异与实践
2026/10/21 6:27:17 网站建设 项目流程

在开发群里经常看到有人问MySQL字符集的问题,尤其是“为什么utf8mb4下字母a只占1个字节,汉字却占3个字节”“那utf8mb3是不是一样的”这类问题。实话实说,这问题看着简单,背后牵扯到的却是Unicode编码规则、MySQL历史包袱、以及实际业务里的存储和索引设计,一个点没搞明白,生产环境里就可能踩出大坑。今天我就把这块彻底讲透,先从最底层的编码规则说起,再对比两个字符集的实际差异,最后落到建表、写数据、算索引长度这些真实场景里,保证你看完能自己算字节数,也能避开那些常见的坑。

1. 先搞懂字节数是怎么算出来的:从Unicode到UTF-8

要搞明白字母a和汉字在MySQL里各占几个字节,不能光记结论,得先理解字节数是怎么来的。字符集(Charset)定义的是“字符到数字”的映射关系,而这个映射出来的数字,在计算机里最终要用“编码规则”转成字节序列。MySQL里常用的utf8mb4和utf8mb3,底层编码都是UTF-8,只是实现范围不同。

1.1 Unicode编码表:每个字符先映射成一个码点

Unicode做的事情很简单,就是给世界上几乎所有字符分配一个唯一的数字编号,这个编号叫“码点”(Code Point)。比如:

  • 字母a的Unicode码点是 U+0061,十进制是97。
  • 汉字汉的Unicode码点是 U+6C49,十进制是27713。
  • Emoji表情😀的码点是 U+1F600,十进制是128512。

码点本身只是一个数字,至于这个数字怎么存进计算机、占几个字节,那是UTF-8、UTF-16这些编码方案要解决的事。也就是说,字符集决定了“哪个编号对应哪个字符”,而编码方式决定了“这个编号用几个字节怎么存”。

1.2 UTF-8编码规则:按码点大小分段处理

UTF-8是一种变长编码,它会根据码点的大小决定用1到4个字节来存储,具体规则如下:

码点范围(十六进制)二进制形式字节数
U+0000 ~ U+007F0xxxxxxx1字节
U+0080 ~ U+07FF110xxxxx 10xxxxxx2字节
U+0800 ~ U+FFFF1110xxxx 10xxxxxx 10xxxxxx3字节
U+10000 ~ U+10FFFF1111xxxx 10xxxxxx 10xxxxxx 10xxxxxx4字节

这套规则的巧妙之处在于,第一个字节的高位连续“1”的个数,直接表示这个字符总共占几个字节,后续字节统一用“10”开头。比如看到字节1110xxxx,就知道后面还有两个“10”开头的续字节,所以这是个3字节字符。

1.3 手动推演两个例子

拿字母a(U+0061)来说,因为码点在 U+0000 ~ U+007F 之间,直接用1字节表示,二进制就是01100001,十六进制0x61。所以不管在哪个字符集下,只要是UTF-8编码,字母a永远只占1个字节。

再拿汉字汉(U+6C49)来说,这个码点在 U+0800 ~ U+FFFF 之间,属于3字节段。它的二进制为0110 1100 0100 1001,按3字节模板拆分:

  • 第一字节:1110 + 0110 =11100110,即0xE6
  • 第二字节:10 + 110001 =10110001,即0xB1
  • 第三字节:10 + 001001 =10001001,即0x89

所以汉在UTF-8编码下就是三个字节E6 B1 89。这个拆解过程很直观也很有用,以后你手头有十六进制工具,可以自己验证任何字符的字节占用。

1.4 为什么会有“字母1字节、汉字3字节”的差异

很多人不理解为什么汉字要比字母多占2个字节,本质上是因为Unicode码点的区域划分。ASCII字符全部集中在U+0000到U+007F这128个编号内,1个字节能覆盖;而常用汉字被分配在U+4E00到U+9FFF这个区间,属于3字节段。这个设计是历史原因和空间规划共同作用的结果,同时也决定了在UTF-8体系下,汉字是“标配”3字节。

理解了以上规则,就可以进入正题,看MySQL里两个字符集的区别了。

2. utf8mb4与utf8mb3的本质区别:不只是名字差一个字母

MySQL里的utf8mb4和utf8mb3,名字上都带着“utf8”,而且编码规则都是UTF-8,但实际支持的字符范围完全不同。明确说一句:utf8mb3就是MySQL旧版本里的utf8,它最多支持3字节的UTF-8字符,存不了Emoji和生僻字;utf8mb4是真正的完整UTF-8,支持1到4字节所有字符。这也是标题中两个问题答案有差异的核心原因。

2.1 utf8mb3是MySQL的历史包袱

在MySQL早期的版本里,utf8这个字符集被定义为“最多3字节”的UTF-8,但它依然觉得自己叫“utf8”,导致很长一段时间里大家都以为用了utf8就能存所有字符。实际上一碰到Emoji、部分生僻字、古汉字,就会报Incorrect string value或直接存成乱码。后来MySQL在5.5.3版本引入utf8mb4,才真正补齐了4字节能力。直到MySQL 8.0,官方才把默认字符集从utf8mb3改成utf8mb4,算是把这个历史坑填上了。

2.2 字节占用对比表:同样字符在不同字符集下的表现

字符utf8mb3(旧utf8)utf8mb4
字母a1字节1字节
汉字汉3字节3字节
Emoji😀不支持(报错)4字节
生僻字(如U+2A6A5级别的)不支持(报错)4字节

从表格可以得出关键结论:在“字母a和汉字各占几字节”这个问题上,utf8mb4和utf8mb3的回答完全一样——字母1字节、汉字3字节。差异只体现在对4字节字符的支持上。

2.3 为什么汉字在utf8mb4里依然是3字节

有个常见误解是“用了utf8mb4,所有字符都变4字节了”,这是错的。utf8mb4是“允许最长4字节”,不是“全部字符强制4字节”。编码长度始终由字符的码点范围决定,汉字码点落在U+0800到U+FFFF之间,就是3字节,不会因为是utf8mb4就多占空间。这一点在评估存储空间时特别重要。

2.4 排序规则(Collation)也要一起看

字符集决定了怎么存,排序规则决定了怎么比较和排序。utf8mb4最常见的排序规则是utf8mb4_unicode_ci和utf8mb4_general_ci,在MySQL 8.0里默认是utf8mb4_0900_ai_ci。utf8mb3对应的则是utf8_general_ci这类。你在建表时最好字符集和排序规则成对指定,否则可能因为排序规则不匹配导致关联查询报错。从MySQL 8.0开始,utf8mb4_0900_ai_ci基于Unicode 9.0标准,排序更准确,但和旧版本的排序规则在索引比较上有细微差异,跨版本迁移时需要留意。

3. 实操验证:在真实MySQL环境里确认字节数

光讲理论还不够,我建议你动手在真实环境里跑一遍。下面给出可以直接执行的步骤和SQL,验证方式不止一种,可以交叉确认,避免自己理解偏差。

3.1 方式一:用HEX函数看存储内容

SELECT HEX('a'); -- 输出 61,说明字母a占1个字节 SELECT HEX('汉'); -- 输出 E6B189,说明汉字占3个字节 SELECT HEX('😀'); -- 输出 F09F9880,说明Emoji占4个字节(仅utf8mb4)

HEX函数会直接返回字符串的十六进制编码,每两个十六进制字符代表1个字节。看到61就说明只存了1字节,看到E6B189就说明存了3字节。这是最直观的验证方法。

3.2 方式二:用CHAR_LENGTH和LENGTH函数对比

SELECT CHAR_LENGTH('a'), LENGTH('a'); -- 结果都是1,说明占1字节 SELECT CHAR_LENGTH('汉'), LENGTH('汉'); -- 结果是1和3,说明占3字节 SELECT CHAR_LENGTH('😀'), LENGTH('😀'); -- 结果是1和4,说明占4字节

CHAR_LENGTH返回字符个数,LENGTH返回字节数。两者一除,就能得到平均每个字符占多少字节。这个方法在排查乱码问题时经常用,如果你的数据里LENGTH和CHAR_LENGTH比值异常,就要警惕是不是混合了不同编码的数据。

3.3 方式三:实际建表写入测试

CREATE TABLE charset_test ( id INT PRIMARY KEY AUTO_INCREMENT, c_utf8mb4 VARCHAR(20) CHARACTER SET utf8mb4, c_utf8mb3 VARCHAR(20) CHARACTER SET utf8mb3 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO charset_test (c_utf8mb4) VALUES ('😀'); -- 这条能成功 INSERT INTO charset_test (c_utf8mb3) VALUES ('😀'); -- 这条会报错:Incorrect string value

一个表里同时建两个不同字符集的字段,这种设计不推荐用于生产,但用来做验证非常直观。通过对比成功和报错的情况,你就彻底理解两个字符集的能力边界了。

3.4 关于存储空间的计算细节

实际问题中大家更关心的是“一张表要占多大空间”。这里有个容易忽略的点:VARCHAR(20)这个20指的是字符个数上限,不是字节数上限。所以:

  • VARCHAR(20)在utf8mb4下,最多能存20个汉字,但有可能占用60字节。
  • InnoDB的单个索引最大长度限制是3072字节(对应innodb_large_prefix开启时),如果用utf8mb4,索引前缀最多能覆盖的字符数就是3072 ÷ 4 = 768个字符。注意这里除以4是因为要按最大可能字节数预留空间,不是按实际写入内容算。
  • 如果用utf8mb3,同样的索引长度限制下,最多是3072 ÷ 3 = 1024个字符。

这个“按最大字节数预留”的机制特别关键,它意味着即使你实际存的全是英文字母,建索引时MySQL依然会按最坏情况算空间。所以字符串字段别动不动就VARCHAR(255),在utf8mb4下它可能占用1020字节,多个这样的字段建联合索引很容易顶到索引长度红线。

4. 业务选择指南:到底该用utf8mb4还是utf8mb3

既然utf8mb4是完整版,是不是无脑选它就行?大体上是的,但结合历史系统和存储成本约束,还是有几个实际场景值得单独拎出来分析。

4.1 新项目首选utf8mb4,没有悬念

新项目直接选utf8mb4,搭配utf8mb4_unicode_ci或MySQL 8.0的默认排序规则。原因很简单:现在用户输入内容的来源五花八门,手机输入法自带Emoji,第三方接口返回的数据里可能夹杂特殊符号,如果你的库不支持4字节字符,数据入库时就会因为一个表情符号直接报错,尤其在做数据同步或接口对接时,这种问题极其隐蔽,排查起来还费劲。与其将来被动修改字符集,不如一开始就选完整版。

4.2 存量utf8mb3库怎么平滑迁移

存量系统如果要迁移,我建议分三步走:

  1. 先检查现有数据里有没有4字节字符。可以用一条SQL排查:
SELECT COUNT(*) FROM your_table WHERE LENGTH(column_name) > CHAR_LENGTH(column_name) * 3;

这条SQL的逻辑是:如果某行的字节数比字符数的3倍还多,说明里面很可能有4字节字符。注意这个条件会误伤一些多字节组合的情况,但作为初步筛选已经足够。

  1. 没有4字节字符的表可以直接改成utf8mb4:
ALTER TABLE your_table CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

转换时锁表时间和表大小直接相关,大表务必在低峰期操作,或者用在线DDL工具分批次做。

  1. 转换完成后,检查所有关联表的字符集是否已经统一,避免出现表A是utf8mb4、表B还是utf8mb3,导致关联查询时索引失效。

4.3 什么情况下可以考虑保留utf8mb3

如果说你的业务是纯内部系统,用户群体固定,输入内容完全可控,几千张表都是存量utf8mb3且运行稳定,那确实没必要为了“用新版本”而去做全量迁移。尤其是数据量在亿级以上的大表,做一次字符集转换带来的成本和风险可能远大于收益。真正需要评估的是业务未来的扩展性——“会不会接入用户生成内容”“会不会对接第三方系统”——如果这些是必然的,早迁移比晚迁移好。

4.4 连接层的字符集参数也要设置

经常有人建表用了utf8mb4,写入还是乱码,最后发现是连接层没配置对。通过SET NAMES utf8mb4指定客户端连接的字符集,或在连接串里加上characterEncoding=utf8(Java侧)等参数,确保“客户端→连接层→服务端→存储层”这整条链路都是utf8mb4。只有表字符集对,连接层不对,照样会做编码转换,甚至损坏数据,这一点在真实环境里踩过的人才懂。

5. 常见问题速查:一天能碰到八遍的字符集坑

这里整理一份高频问题清单,都是我在实际支持开发和运维过程中遇到的真实案例,直接按场景对照排查。

现象可能原因解决办法
写入Emoji报错表字符集是utf8mb3或旧版utf8ALTER TABLE转换到utf8mb4,并确认连接层也设置正确
查询返回问号(?)连接层character_set_client与服务端不一致执行SET NAMES utf8mb4,并检查连接池配置
字符串长度超限报错VARCHAR定义的是字符数,实际字节数超限改成TEXT,或重新评估字段长度设计
联合索引创建失败InnoDB索引长度超3072字节减少字段个数、改用前缀索引、或调大innodb_large_prefix(注意不同版本默认值不同)
字符集转换后乱码转换前数据已被错误解码,转码发生二次破坏先备份,用HEX确认原始编码,按原始字节做无损转换
排序结果和预期不符排序规则选错统一使用utf8mb4_unicode_ci或按业务需求选_bin

5.1 一个典型案例:乱码是“解释”出来的,不是“存坏”的

很多人以为乱码是存储环节把数据破坏掉了,其实绝大多数情况不是。字符在存储层就是一堆字节,所谓“乱码”,是显示或读取时用了错误的字符集去解释这些字节。比如一个汉字E6B189被当成latin1解释,就会显示成汉。如果你只改表字符集却不改连接层,或者反过来,都会出现这种“解释错位”。所以排查乱码问题的第一原则是:先查字节序列,再查各层字符集设置,不要一上来就改数据。

5.2 前缀索引是个好东西,但别乱用

在超长字符串字段上建索引,为了控制索引长度,经常会用前缀索引,比如INDEX idx_name (name(10))。但注意,这里的10是字符数,在utf8mb4下最多预留40字节。如果业务查询是WHERE name = '完整值',前缀索引能用上,不过回表次数会增加,需要在查询性能和索引体积之间做权衡。前缀索引没法用于排序(ORDER BY)和分组(GROUP BY)场景,这些限制设计时要提前想清楚。

5.3 大小写和重音敏感场景要用_bin排序规则

默认的utf8mb4_unicode_ci是不区分大小写的,也就是a和A在比较时相等。如果业务需要严格区分,或者要做精确匹配去重,应该使用utf8mb4_bin。这个排序规则按二进制比较,效率和精确性都有保证。顺便说一下,如果你在utf8mb4下做SELECT DISTINCT,默认会把a和A合并成一个,这在某些业务场景下是隐患。

6. 写在最后:字符集这件事值得花半小时彻底搞明白

字符集是数据库里最不起眼、却最容易让人栽跟头的部分。一个小小的问题,可能会在某个深夜的线上告警里突然跳出来。我在实际项目中见过不少因为字符集没有统一而引发的数据事故,从表情符号存不进去,到迁移后大量乱码,再到联合索引创建失败,每一个问题背后都对应着今天聊到的基础原理。如果非要总结一条最实用的经验,那就是:新库无脑utf8mb4,连接层同步设置,排序规则和表字符集成对指定,存数据前先想清楚这个字段将来会不会出现4字节字符。把这套习惯养成了,能省掉大量日后排查的精力。

另外建议每个开发者都跑一遍今天说的验证SQL,花不了几分钟,但亲手看到HEX('汉')输出E6B189、看到Emoji在utf8mb3下报错,这些直观经验比看十篇文章都管用。等你自己能算清楚字节数、能一眼看出乱码的根源、能设计合理的索引长度时,字符集这块就基本不会再坑你了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询