📌 关键词:字符集、utf8mb4、UTF-8、Unicode、排序规则、Collation、索引失效、emoji、数据库适配
大家好呀!我是数据库小学妹 👋
今天想和大家聊一个每个 DBA 都踩过的坑——字符集。
别划走!我知道这听起来很基础,但我敢打赌,你或者你身边的同事,一定在字符集上栽过跟头。
我自己就踩过三个大坑,今天把血泪经验都分享给你。而且不光讲踩坑,我还把背后的原理搞明白了,一起分享出来,帮你少走弯路少踩坑!
一、你以为的utf8不是真的utf8
先说第一个坑,也是我转行学数据库时最困惑的。
踩坑经历
刚接触数据库那会儿,我在项目里看到这样的配置:
-- 建表时这么写的
CREATE TABLE users (
id INT PRIMARY KEY,
nickname VARCHAR(50)
) CHARSET=utf8;
当时我觉得:嗯,utf8,标准配置,没问题
直到有一天,同事跟我说:"用户昵称里存不了表情包!"
我一查,好家伙,用户存了个😂,入库后变成了 ????。
问题出在哪?
MySQL 里的 utf8,其实不是真正的 utf8!
MySQL 的 utf8 最多支持3 个字节的字符。而真正的 utf8(也就是 utf8mb4)支持4 个字节。
啥概念?
- 3 字节 utf8:能存中文、日文、韩文,但存不了表情包和部分生僻字
- 4 字节 utf8mb4:完整的 Unicode 字符集,表情包、生僻字都能存
所以,MySQL 的 utf8mb4 才是真正的 utf8。
深入理解:Unicode、UTF-8 和 MySQL 的"残缺版"
踩完坑之后我就想搞明白:为啥 MySQL 的 utf8 是"残缺"的?这得从 Unicode 和 UTF-8 的关系说起。
Unicode 是什么?
Unicode 是一个字符编号表。它给世界上每个字符分配一个唯一的编号(叫 code point)。
比如:
A→ U+0041中→ U+4E2D😂→ U+1F602𠮷→ U+20BB7
Unicode 本身只管编号,不管怎么存储。
UTF-8 是什么?
UTF-8 是 Unicode 的一种编码方式——负责把编号变成实际的二进制字节。
它的核心规则是变长编码:
| Unicode 范围 | UTF-8 字节数 | 二进制格式 |
|---|---|---|
| U+0000 ~ U+007F | 1 字节 | 0xxxxxxx |
| U+0080 ~ U+07FF | 2 字节 | 110xxxxx 10xxxxxx |
| U+0800 ~ U+FFFF | 3 字节 | 1110xxxx 10xxxxxx 10xxxxxx |
| U+10000 ~ U+10FFFF | 4 字节 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
注意看:真正的 UTF-8 最多用 4 个字节,覆盖了 Unicode 的全部范围(U+0000 ~ U+10FFFF)。
MySQL 的 utf8 只截了前 3 字节
MySQL 当年设计时,Unicode 3.0 标准里所有字符确实都在 U+0000 ~ U+FFFF 范围内(也就是 BMP),3 字节够用。
但 Unicode 后来扩展到了辅助平面(SMP),新增了大量 4 字节字符,包括 emoji、生僻字、历史文字等。MySQL 的 utf8 没跟着升级,卡在了 3 字节。后来才出了 utf8mb4(mb = most bytes)补上这个缺口。
一句话总结:MySQL 的 utf8 是 UTF-8 的子集,只支持 BMP 平面字符;utf8mb4 才是完整的 UTF-8。
utf8mb4 对 VARCHAR 长度的影响
还有一个细节很多人忽略:字符集不同,VARCHAR 的长度限制也不同。
MySQL 的 VARCHAR(N) 中 N 是字符数,不是字节数。但底层存储有行最大长度限制 65535 字节。
VARCHAR(50) 在 utf8 下:最多 50 × 3 = 150 字节
VARCHAR(50) 在 utf8mb4 下:最多 50 × 4 = 200 字节
这意味着同样建一张表,utf8mb4 能存更少的 VARCHAR 列。对于宽表场景,需要计算行总字节数是否超限。
-- 这个在 utf8 下可能没问题,在 utf8mb4 下可能超限
CREATE TABLE wide_table (
col1 VARCHAR(200), -- 800 字节
col2 VARCHAR(200), -- 800 字节
col3 VARCHAR(200), -- 800 字节
-- ... 更多列
) CHARSET=utf8mb4;
-- 报错:Row size too large (> 65535)
解法:超长文本字段改用 TEXT 类型。TEXT 只占行内 20 字节指针,实际内容存溢出页。
正确做法
建表时直接这么写:
CREATE TABLE users (
id INT PRIMARY KEY,
nickname VARCHAR(50)
) CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
utf8mb4_0900_ai_ci 是 MySQL 8.0 的默认排序规则,支持更精准的排序和比较。
二、字符集不一致,索引直接失效
第二个坑,是我在一次 SQL 优化时踩的。
踩坑经历
有天业务反馈,某个查询突然变慢了。我一看 SQL:
SELECT * FROM orders WHERE order_no = 'ORD20240101001';
很简单的等值查询,order_no 字段也有索引,咋就慢了呢?
我用 EXPLAIN 一看:
+----+-------------+-------+------+---------------+------+
| id | select_type | table | type | key | rows |
+----+-------------+-------+------+---------------+------+
| 1 | SIMPLE | orders| ALL | NULL | 50000|
+----+-------------+-------+------+---------------+------+
type=ALL,key=NULL —— 全表扫描,索引根本没用到!
排查过程
我检查了表结构:
SHOW CREATE TABLE orders\G
发现 order_no 字段的字符集是 utf8。
然后我查了这个字段的值来源——是从另一个系统的表同步过来的。那个表的字符集是 utf8mb4。
字符集不一致,导致隐式类型转换,索引失效。
深入理解:MySQL 的字符集转换规则
为什么字符集不一致会导致索引失效?我翻了一下 MySQL 文档,搞明白了它的转换规则。
当两个字符串比较时,如果字符集不同,MySQL 需要做 coercibility(可转换性)决策——把谁转成谁的字符集。
规则如下:
| 优先级 | 操作数 | 被转换方向 |
|---|---|---|
| 1 | 显式 COLLATE 子句 | 不转换,对方转过来 |
| 2 | 列值(有字符集声明的) | 不转换,对方转过来 |
| 3 | 字符串常量 | 被转换为列的字符集 |
| 4 | 系统变量值 | 被转换 |
| 5 | NULL | 被转换 |
关键在第 2 和第 3 条:列的优先级高于字符串常量。所以常量会被转成列的字符集。
问题出在 JOIN 或子查询中两列字符集不同。比如:
-- 表 A 的 name 字段是 utf8
-- 表 B 的 name 字段是 utf8mb4
SELECT * FROM A JOIN B ON A.name = B.name;
这时 MySQL 必须把其中一个列的值转成另一个的字符集。对列值做函数转换 = 索引失效。
就像你拿着人民币去美元商店买东西,得先兑换——这一兑换,原本的速度就没了。
还有一种隐蔽场景:WHERE 常量与列字符集不同
-- 列是 utf8mb4
-- 但客户端连接字符集是 utf8
SELECT * FROM users WHERE nickname = '测试';
MySQL 会把常量 '测试' 从 utf8 转为 utf8mb4。这个转换发生在优化器阶段,索引不会失效——因为转换的是常量端,不是列端。
但如果反过来,列是 utf8,常量是 utf8mb4,那列值就要被转换,索引可能失效。
结论:列的字符集应该 ≥ 常量的字符集范围。utf8mb4 的列不会被转换,utf8 的列可能被转换。
正确做法
确保关联字段、比较字段的字符集一致:
-- 修改字段字符集
ALTER TABLE orders
MODIFY order_no VARCHAR(50)
CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
检查清单:
- 主键和外键字段字符集是否一致
- JOIN 关联字段字符集是否一致
- WHERE 条件字段与比较值字符集是否一致
三、库、表、列三层字符集优先级
第三个坑,是关于字符集优先级的。
我的困惑
刚学字符集时,我一直搞不懂:
- 数据库有字符集
- 表有字符集
- 列也有字符集
它们之间啥关系?优先级咋算?
实验验证
我做了个实验:
-- 1. 创建数据库,指定 utf8mb4
CREATE DATABASE mydb CHARSET=utf8mb4;
-- 2. 建表时不指定字符集
USE mydb;
CREATE TABLE t1 (id INT, name VARCHAR(50));
-- 表的字符集继承数据库的 utf8mb4
-- 3. 建表时指定不同字符集
CREATE TABLE t2 (id INT, name VARCHAR(50)) CHARSET=utf8;
-- 表的字符集是 utf8,覆盖了数据库的设置
-- 4. 列级别指定字符集
CREATE TABLE t3 (
id INT,
name VARCHAR(50) CHARSET=utf8
) CHARSET=utf8mb4;
-- name 字段用 utf8,表默认是 utf8mb4
深入理解:四层继承链 + 连接字符集
其实不只是库、表、列三层,MySQL 的字符集配置有四层继承 + 三个连接变量:
四层继承(定义时生效):
服务器默认 → 数据库 → 表 → 列
三个连接变量(运行时生效):
| 变量 | 作用 |
|---|---|
character_set_client |
客户端发送的 SQL 语句用什么字符集 |
character_set_connection |
服务器处理 SQL 时内部用什么字符集 |
character_set_results |
服务器返回结果时用什么字符集 |
这三个变量决定了数据在客户端和服务器之间怎么编码和解码。
SET NAMES utf8mb4 就是一次性设置这三个变量:
SET NAMES utf8mb4;
-- 等价于:
SET character_set_client = utf8mb4;
SET character_set_connection = utf8mb4;
SET character_set_results = utf8mb4;
一图看懂数据流向:
客户端发送 SQL → [client] → [connection] → 列字符集
列字符集 → [results] → 客户端收到结果
如果任何一环字符集不匹配,就会发生隐式转换,轻则乱码,重则索引失效。
优先级规则
列 > 表 > 数据库 > 服务器默认
- 列级别指定了,就用列的
- 列没指定,用表的
- 表没指定,用数据库的
- 数据库没指定,用服务器默认(MySQL 8.0 默认是
utf8mb4)
实践经验
我的建议是:四层统一用 utf8mb4 + 连接也设 utf8mb4
-- 创建数据库时明确指定
CREATE DATABASE mydb
CHARSET=utf8mb4
COLLATE=utf8mb4_0900_ai_ci;
-- 建表时继承即可,不用重复指定
CREATE TABLE users (
id INT PRIMARY KEY,
nickname VARCHAR(50)
);
-- 连接时也指定
SET NAMES utf8mb4;
这样最省心,全链路 utf8mb4,不会出现"表是 utf8,列是 utf8mb4,连接是 latin1"这种混乱情况。
四、排序规则(Collation):字符集的"另一半"
讲到字符集,不能不提排序规则。很多人只关注 CHARSET,忽略了 COLLATE,结果排序和比较出了问题。
什么是排序规则?
字符集决定"怎么存",排序规则决定"怎么比"。
同样的 utf8mb4,不同的排序规则,比较结果可能不同:
-- 试试不同的排序规则
SELECT 'a' = 'A' COLLATE utf8mb4_0900_ai_ci; -- 结果:1(不区分大小写)
SELECT 'a' = 'A' COLLATE utf8mb4_0900_as_cs; -- 结果:0(区分大小写)
ai = accent insensitive(不区分重音)as = accent sensitive(区分重音)ci = case insensitive(不区分大小写)cs = case sensitive(区分大小写)
排序规则不一致也会索引失效
跟字符集不一致一样,排序规则不一致也会导致隐式转换:
-- 表 A 的 name 用 utf8mb4_0900_ai_ci
-- 表 B 的 name 用 utf8mb4_general_ci
SELECT * FROM A JOIN B ON A.name = B.name;
-- ERROR 1267: Illegal mix of collations
或者更隐蔽的——不报错,但索引失效。
常见排序规则对比
| 排序规则 | 大小写 | 重音 | 版本 | 说明 |
|---|---|---|---|---|
utf8mb4_general_ci |
不区分 | 不区分 | MySQL 5.x | 速度快,精度低 |
utf8mb4_unicode_ci |
不区分 | 不区分 | MySQL 5.x | 基于 UCA 标准,更准确 |
utf8mb4_0900_ai_ci |
不区分 | 不区分 | MySQL 8.0 | 基于 UCA 9.0,最准确 |
我的建议:MySQL 8.0 直接用 utf8mb4_0900_ai_ci,5.7 用 utf8mb4_unicode_ci。不要混用。
五、字符集转换导致数据"丢失"
还有一个坑,是字符集转换时的数据截断。
踩坑场景
有个老系统,字符集是 utf8(3 字节)。现在要迁移到 utf8mb4。
听起来很简单吧?ALTER TABLE ... CONVERT TO CHARSET utf8mb4 不就完了?
但问题就来了。
深入理解:CONVERT TO 和 MODIFY 的区别
这里有个很多人搞混的细节。修改字符集有两种写法,效果完全不同:
-- 方式一:MODIFY——只改元数据,不改数据内容
ALTER TABLE t MODIFY name VARCHAR(50) CHARSET=utf8mb4;
-- 方式二:CONVERT TO——改元数据 + 重写数据内容
ALTER TABLE t CONVERT TO CHARSET utf8mb4;
MODIFY:只改字段的字符集声明。数据不动。如果数据里有 utf8 存不了的字符(但 somehow 存进去了,比如通过二进制写入),这些数据不会被转码。
CONVERT TO:先把数据从旧字符集解码,再用新字符集重新编码。这个过程中,如果发现无法表示的字符,会出现 ? 替换或报错。
utf8 转 utf8mb4 的安全步骤
utf8 → utf8mb4 是扩大范围,理论上不会丢数据。但要注意 VARCHAR 长度和索引长度:
-- 第一步:检查索引是否超限
-- utf8 下 VARCHAR(255) 的索引前缀 = 255 × 3 = 765 字节
-- utf8mb4 下 = 255 × 4 = 1020 字节
-- InnoDB 索引前缀最大 767 字节(默认)
-- 所以 VARCHAR(255) + utf8mb4 + 索引 = 可能报错!
-- 第二步:转换
ALTER TABLE t CONVERT TO CHARSET utf8mb4;
-- 如果索引超限,需要缩短前缀
ALTER TABLE t ADD INDEX idx_name (name(191));
-- 191 × 4 = 764 < 767,安全
反向转换(utf8mb4 → utf8)才是真危险
如果数据库里已经有 emoji 或生僻字,往 utf8 转会直接丢数据:
-- 危险操作!
ALTER TABLE t CONVERT TO CHARSET utf8;
-- emoji 变成 ??,生僻字变成 ??
我的建议:永远不要从 utf8mb4 往下转。如果要转,先确认数据里没有 4 字节字符。
-- 检查是否有 4 字节字符
SELECT * FROM t
WHERE HEX(name) REGEXP '^[0-9A-F]{8,}$';
-- 如果有结果,说明存在 4 字节字符,不能转 utf8
六、生僻字和 emoji 的正确存储方式
最后,说说大家最关心的——表情包和生僻字怎么存。
测试方法
你可以用这个 SQL 测试:
-- 创建测试表
CREATE TABLE emoji_test (
id INT PRIMARY KEY,
content VARCHAR(100)
) CHARSET=utf8mb4;
-- 插入测试数据
INSERT INTO emoji_test VALUES
(1, '今天心情很好😄'),
(2, '生僻字测试:𠮷野家'),
(3, '普通中文测试');
-- 查询验证
SELECT * FROM emoji_test;
如果查出来是正常的,说明配置没问题 ✅
如果变成 ????,逐层排查:
-- 1. 检查数据库字符集
SHOW CREATE DATABASE mydb;
-- 2. 检查表字符集
SHOW CREATE TABLE emoji_test;
-- 3. 检查服务器变量
SHOW VARIABLES LIKE 'character%';
-- 4. 检查连接字符集
SELECT @@character_set_client,
@@character_set_connection,
@@character_set_results;
常见出错点:
- 数据库、表、字段的字符集不全是
utf8mb4 - 客户端连接字符集不对(
SET NAMES utf8mb4) - 应用程序的 JDBC 连接字符串没指定
characterEncoding=utf8mb4 - MySQL 5.x 的 my.cnf 没配
character-set-server=utf8mb4
完整配置示例
MySQL 服务器(my.cnf):
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_0900_ai_ci
[client]
default-character-set=utf8mb4
数据库层面:
CREATE DATABASE mydb CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
表层面:
CREATE TABLE users (
id INT PRIMARY KEY,
nickname VARCHAR(50)
) CHARSET=utf8mb4;
应用层面(JDBC):
jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=utf8mb4
连接层面:
SET NAMES utf8mb4;
七、字符集检查清单
最后,送大家一份我整理的检查清单,每次建库建表前对照一下:
建库建表前
- [ ] 数据库字符集设为
utf8mb4 - [ ] 排序规则用
utf8mb4_0900_ai_ci(MySQL 8.0)或utf8mb4_unicode_ci(MySQL 5.7) - [ ] 表不单独指定字符集,继承数据库
- [ ] 字段不单独指定字符集,继承表
- [ ] 宽表注意 VARCHAR 累计字节数是否超 65535 限制
- [ ] 索引列 VARCHAR(255) 在 utf8mb4 下可能超前缀限制,改用 VARCHAR(191)
迁移/同步场景
- [ ] 源库和目标库字符集一致
- [ ] 关联字段、外键字段字符集一致
- [ ] 排序规则也必须一致(不只是字符集)
- [ ] 转换前先检查数据长度,避免截断
- [ ] utf8 → utf8mb4 安全,反向转换可能丢数据
问题排查
- [ ]
SHOW CREATE TABLE查看实际字符集和排序规则 - [ ]
SHOW VARIABLES LIKE 'character%'查看服务器配置 - [ ]
SELECT @@character_set_client/connection/results查看连接变量 - [ ] 检查应用连接字符串是否指定字符集
- [ ] EXPLAIN 发现全表扫描时,考虑字符集/排序规则不一致
小结
回到开头——字符集这个坑,真的每个 DBA 都踩过。
但现在回头看,搞懂了 Unicode 和 UTF-8 的关系、MySQL 的四层继承链、coercibility 转换规则、排序规则的差异之后,这些坑其实都有迹可循。
字符集不只是"选 utf8mb4 就完了"——它贯穿数据的存储、比较、排序、传输全链路。任何一个环节不匹配,都可能埋下隐患。
所以今天把原理和踩坑一起整理出来,希望能帮你少走点弯路。你在字符集上踩过什么坑?欢迎评论区聊聊,说不定你的经历能帮到更多人。
我是数据库小学妹,一个用设计师思维学数据库的转行人。我们一起,把复杂的技术变得简单有趣吧!💕
本文基于个人踩坑经历和实践总结,字符集配置可能因 MySQL 版本而异,建议先在自己的测试环境验证。