跳到主要内容
预计阅读 20 分钟

字符的秘密 —— 编码与字符集的那些事儿

你有没有遇到过从数据库里查出来的数据变成了一堆”???”或者乱码方块?这几乎是每个数据库新手都会踩的坑。今天我们来彻底搞懂字符集与编码,让乱码从此远离你。

📋 开篇自测:你已经知道多少?

  1. “字符集”和”编码”是同一回事吗?它们之间有什么关系?
  2. utf8utf8mb4在MySQL中有什么区别?你应该用哪一个?
  3. MySQL中有多少个层级可以设置字符集?它们的优先级是怎样的?

一、从烽火台到Unicode——人类如何让机器理解文字

计算机本质上只认识0和1。让计算机”认字”,就需要一套约定:用哪个数字代表哪个字符。

字符集与编码的区别

这两个概念经常被混用,但严格来说:

  • 字符集(Character Set):定义了一个字符的”花名册”。比如,“这个集合里包含了A、B、C、中、国、人……每个字符对应一个编号”
  • 编码(Encoding):定义了如何把这个编号变成计算机能存储的二进制。编号是一个抽象的数字,编码决定了这个数字在内存/磁盘中占多少字节、每个字节是什么

打个比方:字符集就像一本字典——列出所有收录的字和对应的页码(编号)。编码则是这本字典的装帧方式——是用大32开精装本(固定4字节),还是用口袋书(变长字节)。同一本字典可以有不同的装帧方式。

ASCII:最初的约定

最早的字符集是ASCII,由美国人设计。它只收录了128个字符——26个英文字母(大小写)、数字0-9、常用标点、以及一些控制字符(如换行、退格)。每个字符用7位二进制表示,实际存储占1个字节。

对英语国家来说ASCII够用了。但世界上还有中文、日文、韩文、阿拉伯文……128个坑位根本不够。

各国的”土办法”

于是各国开始各自扩展:

  • 中国出了GB2312(收录6763个汉字)和后来的GBK(收录21003个汉字)
  • 日本出了Shift_JIS
  • 韩国出了EUC-KR

这些本地化编码在各自国家内部好用,但互相之间不兼容。一段GBK编码的中文,用Shift_JIS去解码,得到的就是乱码。

Unicode:一统天下

为了解决”编码割据”的问题,Unicode诞生了。它的野心很大——要给全世界所有文字系统的每一个字符都分配一个唯一编号(码点,Code Point)。截至2025年,Unicode已经收录了超过15万个字符,覆盖了世界上几乎所有的书写系统,甚至包括表情符号。

Unicode只是字符集(花名册),它对应的编码方式有多种:

  • UTF-32:每个字符固定4字节。简单粗暴,但浪费空间
  • UTF-16:大多数字符2字节,生僻字符4字节
  • UTF-8:ASCII字符1字节,欧洲文字2字节,中日韩文字3字节,生僻字符4字节。变长编码,兼容ASCII,是互联网的主流编码

🤔 想一想 为什么UTF-8在互联网上比UTF-16更流行?提示:想想全世界网页中英文内容的占比。


二、MySQL中的字符集——四个层级的套娃

MySQL的字符集设置是分层的,就像一个套娃——外层设置是默认值,内层设置可以覆盖外层。

四个层级

服务器级(Server)
  └── 数据库级(Database)
      └── 表级(Table)
          └── 列级(Column)

每个层级都可以单独设置字符集。如果某一层没有显式指定,就继承上一层的设置。

-- 查看服务器级字符集
SHOW VARIABLES LIKE 'character_set_server';

-- 创建数据库时指定字符集
CREATE DATABASE blog CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;

-- 创建表时指定字符集(覆盖数据库级)
CREATE TABLE articles (
    id INT PRIMARY KEY AUTO_INCREMENT,
    title VARCHAR(200),
    content TEXT
) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;

-- 创建列时指定字符集(覆盖表级)
CREATE TABLE mixed_content (
    id INT PRIMARY KEY AUTO_INCREMENT,
    chinese_name VARCHAR(100) CHARACTER SET utf8mb4,
    ascii_code VARCHAR(50) CHARACTER SET ascii
);

实际建议

不要搞得太复杂。在服务器级别统一设置为utf8mb4,然后所有数据库、表、列都继承这个设置。除非有非常特殊的理由,否则不要在不同层级用不同的字符集。

# my.cnf 配置文件
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_0900_ai_ci    # MySQL 8.0 默认且推荐
# 如果需要兼容 MySQL 5.7,使用 utf8mb4_unicode_ci

[mysql]
default-character-set = utf8mb4

三、utf8 vs utf8mb4——MySQL的一个”历史包袱”

这是MySQL中最容易踩的坑之一,值得用一整节来说清楚。

标准的UTF-8编码最多可以用4个字节来编码一个字符。但MySQL在很久以前(4.1版本,2004年)实现UTF-8时,做了一个”节省空间”的决定:MySQL的utf8字符集最多只支持3个字节

3字节的UTF-8能覆盖Unicode基本多文种平面(BMP)内的字符,包括常见的中文、日文、韩文。但有些字符需要4个字节,比如:

  • 表情符号(Emoji):😀 🎉 ❤️
  • 部分生僻汉字:𠮷(这个字不是”吉”)
  • 一些音乐符号:𝄞

如果你的表使用utf8字符集,试图存入一个Emoji,MySQL会报错或者截断数据。

为了解决这个问题,MySQL在5.5.3版本引入了utf8mb4——这才是真正完整的UTF-8编码,支持最多4字节。

-- 错误示范:使用utf8,存Emoji会失败
CREATE TABLE old_style (
    content VARCHAR(200)
) CHARACTER SET utf8;

INSERT INTO old_style (content) VALUES ('你好啊😀');
-- 可能报错:Incorrect string value: '\xF0\x9F\x98\x80' for column 'content'

-- 正确做法:使用utf8mb4
CREATE TABLE new_style (
    content VARCHAR(200)
) CHARACTER SET utf8mb4;

INSERT INTO new_style (content) VALUES ('你好啊😀');
-- 成功!

结论:在MySQL中,永远使用utf8mb4,忘掉utf8

MySQL 8.0已经将默认字符集从latin1改为utf8mb4,这是一个明智的决定。

⚠️ 常见误区 误区一:MySQL的utf8就是标准的UTF-8。 不是的!MySQL的utf8是阉割版的UTF-8(最多3字节),utf8mb4才是完整的UTF-8。这是MySQL的历史遗留问题,在其他数据库系统中通常不存在这个区别。

误区二:utf8mb4比utf8占更多空间。 只有在存储4字节字符时,utf8mb4才会比utf8多用1个字节。对于普通中英文字符,两者占用空间完全一样。所以用utf8mb4几乎没有额外代价。


四、排序规则(Collation)——字符比大小的规矩

字符集解决了”存什么字符”的问题,排序规则则解决了”字符之间怎么比较”的问题。

听起来简单?考虑这些情况:

  • 'a''A'应该被视为相同还是不同?
  • 'café''cafe'在排序时谁在前面?
  • 'ä'是排在'a'后面还是'z'后面?

不同语言、不同文化对这些问题的答案不同,所以需要排序规则来定义。

排序规则的命名规律

MySQL的排序规则命名有固定模式:字符集_语言_后缀

utf8mb4_unicode_ci
  │       │      │
  │       │      └── ci = Case Insensitive(大小写不敏感)
  │       └── unicode = 基于Unicode标准排序
  └── utf8mb4 = 字符集

常见后缀:

  • _ci:大小写不敏感(Case Insensitive)。'ABC' = 'abc'
  • _cs:大小写敏感(Case Sensitive)。'ABC''abc'
  • _bin:二进制比较。直接比较字符的编码值,完全精确
-- 演示排序规则对查询的影响
CREATE TABLE collation_demo (
    name_ci VARCHAR(50) COLLATE utf8mb4_unicode_ci,
    name_bin VARCHAR(50) COLLATE utf8mb4_bin
);

INSERT INTO collation_demo VALUES ('Apple', 'Apple');

-- ci模式:大小写不敏感,能查到
SELECT * FROM collation_demo WHERE name_ci = 'apple';
-- 返回1行

-- bin模式:精确匹配,查不到
SELECT * FROM collation_demo WHERE name_bin = 'apple';
-- 返回0行

怎么选排序规则?

对于 MySQL 8.0 用户,推荐使用默认的 utf8mb4_0900_ai_ci(基于 Unicode 9.0 标准,采用了更高效的排序算法实现,在复杂排序场景下性能显著优于 utf8mb4_unicode_ci)。需要兼容 MySQL 5.7 的场景使用 utf8mb4_unicode_ci

  • 大小写不敏感,符合大多数业务场景(用户搜索时通常不区分大小写)
  • 基于Unicode标准,国际化兼容好

注意 utf8mb4_0900_ai_ci 中的 ai 表示 accent-insensitive(重音不敏感),即 'café' = 'cafe'utf8mb4_unicode_ci 同样是 accent-insensitive 的(_ci 后缀不含 _as 时默认重音不敏感)。两者的主要区别在于:utf8mb4_0900_ai_ci 基于 Unicode 9.0 标准,排序算法更高效;utf8mb4_unicode_ci 基于 Unicode 4.0 标准。如果你的业务需要区分重音字符,应选择 utf8mb4_0900_as_ci

如果你的业务需要区分大小写(比如密码字段——虽然密码通常不应明文存储),可以对特定列使用_bin排序规则。

🤔 想一想 如果你在做一个用户名系统,要求”用户名大小写不敏感”(即注册了”Admin”后别人不能注册”admin”),你应该用哪种排序规则?


五、连接的字符集——最容易出乱码的环节

除了存储层面,MySQL还有一组和连接相关的字符集变量。这是乱码问题的”高发区”。

当你通过客户端向MySQL发送SQL语句时,涉及三个字符集变量:

变量含义
character_set_client客户端发来的SQL语句使用的字符集
character_set_connectionMySQL处理请求时使用的字符集
character_set_results返回给客户端的结果使用的字符集

一条SQL的”字符集旅程”:

客户端(你的终端/应用)
    ↓ 以 character_set_client 编码发出
MySQL服务端接收
    ↓ 转换为 character_set_connection 编码处理
和表中实际存储的字符集对比/转换
    ↓ 转换为 character_set_results 编码
返回给客户端

如果这三个环节的字符集不一致,转换过程中就可能出现乱码。

-- 查看连接相关的字符集设置
SHOW VARIABLES LIKE 'character_set%';

-- 一次性设置三个连接字符集(推荐)
SET NAMES utf8mb4;
-- 等价于同时设置:
-- SET character_set_client = utf8mb4;
-- SET character_set_connection = utf8mb4;
-- SET character_set_results = utf8mb4;

编程语言中如何设置

在实际开发中,连接字符集应该在建立数据库连接时指定:

# Python (pymysql)
import pymysql
conn = pymysql.connect(
    host='localhost',
    user='root',
    password='your_password',
    database='my_db',
    charset='utf8mb4'  # 关键:在连接时指定字符集
)
// Java (JDBC Connector/J 8.0.13+)
// characterEncoding 使用 UTF-8,Connector/J 8.0.13+ 会自动映射到 MySQL 的 utf8mb4
String url = "jdbc:mysql://localhost:3306/my_db"
    + "?characterEncoding=UTF-8";
// Go (go-sql-driver/mysql)
dsn := "root:password@tcp(localhost:3306)/my_db?charset=utf8mb4"

⚠️ 常见误区 误区:表的字符集设对了就不会乱码。 乱码往往不是存储环节的问题,而是连接环节的问题。想象快递:仓库里货物放得好好的(存储正确),但发货的时候贴错了标签(连接字符集不对),收件人拿到的就是”乱码”。确保SET NAMES utf8mb4或在连接串中指定charset=utf8mb4,是防止乱码的第一道防线。


六、乱码排查实战——系统化定位问题

遇到乱码不要慌,按以下步骤排查:

第一步:确认存储是否正确

-- 查看表和列的字符集
SHOW CREATE TABLE your_table;

-- 用HEX函数查看数据的实际二进制内容
SELECT name, HEX(name) FROM your_table LIMIT 5;

如果HEX值看起来正常(中文的UTF-8编码通常以E开头的3字节序列),说明存储没问题,问题在传输/显示环节。

第二步:检查连接字符集

SHOW VARIABLES LIKE 'character_set%';

确保character_set_clientcharacter_set_connectioncharacter_set_results都是utf8mb4

第三步:检查客户端编码

你的终端、IDE、应用程序的编码设置也需要是UTF-8。

# 检查终端编码
echo $LANG
# 应该类似:en_US.UTF-8 或 zh_CN.UTF-8

第四步:如果数据已经乱码入库

这是最麻烦的情况。数据以错误编码存入后,需要进行数据修复:

-- 查看某条疑似乱码数据的十六进制
SELECT HEX(name) FROM users WHERE id = 1;

-- 如果发现数据是用latin1编码存了utf8的内容(最常见的情况)
-- 可以尝试用CONVERT函数修复
UPDATE users SET name = CONVERT(CAST(CONVERT(name USING latin1) AS BINARY) USING utf8mb4)
WHERE id = 1;

但这种修复不一定总能成功。最好的策略是从一开始就把字符集设对,预防乱码。

🤔 想一想 如果你接手一个老项目,发现数据库用的是latin1字符集,里面已经存了大量中文数据。你该如何制定迁移方案?需要注意什么风险?


📝 掌握度自测

  1. UTF-8是一种字符集还是一种编码?它和Unicode是什么关系?
  2. MySQL中utf8utf8mb4的核心区别是什么?为什么推荐使用utf8mb4
  3. MySQL的字符集有哪四个层级?如果某一层没有显式设置字符集,会发生什么?
  4. SET NAMES utf8mb4这条命令等价于设置了哪三个变量?它在防止乱码中起什么作用?
  5. 排序规则(Collation)中,_ci_bin后缀分别代表什么含义?在一个用户名唯一性场景中,你会选择哪种?

💡 自我评估

  • 答对5题:对字符集有深入理解,不会再被乱码困扰
  • 答对3-4题:核心概念掌握了,建议动手做一下乱码复现实验
  • 答对0-2题:重新阅读本章,特别是”utf8 vs utf8mb4”和”连接的字符集”两节

购买课程解锁全部内容

让查询飞起来:MySQL 从索引到主从高可用

¥29.90