MySQL的utf8和utf8mb4到底有啥区别?

摘要:从一次"用户昵称存不进去"的诡异bug出发,揭秘MySQL的utf8字符集陷阱。深度剖析utf8只支持3字节字符、utf8mb4才是真正的UTF-8、以及varchar(255)背后的秘密。通过真实的emoji表情存储失败案例、字符集迁移的完整方案、以及排序规则的性能对比,让你彻底搞懂"为什么MySQL的utf8不是UTF-8"、"如何安全地升级到utf8mb4"等核心问题。


💥 翻车现场

周三下午,测试同学在钉钉群里@了哈吉米。

测试同学:@哈吉米 用户反馈昵称保存失败,你看看是啥问题?
哈吉米:啥昵称?发我看看
测试同学:这个 → "小明😀"

哈吉米试着在数据库里插入:

INSERT INTO user (username, nickname) 
VALUES ('xiaoming', '小明😀');

ERROR 1366 (HY000): Incorrect string value: '\xF0\x9F\x98\x80' for column 'nickname'

哈吉米:"???为啥emoji表情存不进去?"

查看表结构:

SHOW CREATE TABLE user\G

CREATE TABLE `user` (
  `id` bigint PRIMARY KEY,
  `username` varchar(50) NOT NULL,
  `nickname` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci,
  ...
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

哈吉米:"字符集是utf8啊,没问题啊?"

下午5点,南北绿豆和阿西噶阿西来了。

南北绿豆:"MySQL的utf8不是真正的UTF-8!"
哈吉米:"???"
阿西噶阿西:"这是MySQL的历史遗留问题,来,我给你讲讲。"


🤔 MySQL的utf8不是UTF-8?

UTF-8的真实定义

标准UTF-8

  • 可变长度编码:1-4字节
  • 支持全世界所有字符(包括emoji表情)

编码规则

字符范围字节数编码格式示例
ASCII(U+0000 ~ U+007F)1字节0xxxxxxxA0x41
中文等(U+0080 ~ U+07FF)2字节110xxxxx 10xxxxxx0xE4B8AD
中文等(U+0800 ~ U+FFFF)3字节1110xxxx 10xxxxxx 10xxxxxx0xE69687
emoji等(U+10000 ~ U+10FFFF)4字节11110xxx 10xxxxxx 10xxxxxx 10xxxxxx😀0xF09F9880

MySQL的utf8只支持3字节

历史原因

  • MySQL在2003年实现utf8字符集时,UTF-8标准还不完善
  • 当时为了性能,只实现了1-3字节的字符
  • 后来UTF-8标准扩展到4字节(支持emoji),但MySQL的utf8没更新

结果

MySQL的utf8 ≠ 标准UTF-8

MySQL的utf8:
- 只支持1-3字节字符
- 不支持emoji表情(需要4字节)
- 不支持部分生僻汉字(需要4字节)

南北绿豆:"所以MySQL的utf8是个'阉割版'的UTF-8!"


utf8mb4才是真正的UTF-8

MySQL 5.5.3引入utf8mb4

  • utf8mb4 = utf8 most bytes 4
  • 支持1-4字节字符
  • 完全兼容标准UTF-8

对比

字符集支持字节数支持emoji是否标准UTF-8
utf81-3字节
utf8mb41-4字节

哈吉米:"所以应该用utf8mb4?"

阿西噶阿西:"对!MySQL 8.0默认就是utf8mb4了。"


📊 实战测试:哪些字符存不进去?

测试1:emoji表情

-- utf8字符集
CREATE TABLE test_utf8 (
  id INT PRIMARY KEY,
  content VARCHAR(100) CHARACTER SET utf8
);

INSERT INTO test_utf8 VALUES (1, '😀');
-- ERROR 1366 (HY000): Incorrect string value: '\xF0\x9F\x98\x80'

-- utf8mb4字符集
CREATE TABLE test_utf8mb4 (
  id INT PRIMARY KEY,
  content VARCHAR(100) CHARACTER SET utf8mb4
);

INSERT INTO test_utf8mb4 VALUES (1, '😀');
-- Query OK, 1 row affected

测试2:生僻汉字

-- 𠮷(吉的异体字,4字节)
INSERT INTO test_utf8 VALUES (2, '𠮷野家');
-- ERROR 1366 (HY000): Incorrect string value

INSERT INTO test_utf8mb4 VALUES (2, '𠮷野家');
-- Query OK, 1 row affected

测试3:特殊符号

-- 常见的可以存
INSERT INTO test_utf8 VALUES (3, '♥♦♣♠');  -- ✅ 3字节,可以存

-- 新的符号不行
INSERT INTO test_utf8 VALUES (4, '🀄🎮🎯');  -- ❌ 4字节,不能存
INSERT INTO test_utf8mb4 VALUES (4, '🀄🎮🎯');  -- ✅

🔄 如何从utf8迁移到utf8mb4?

哈吉米:"我的表都是utf8,怎么改成utf8mb4?"

南北绿豆:"有个安全的迁移流程。"

步骤1:备份数据

# 全量备份
mysqldump -uroot -p your_db > backup_20241007.sql

步骤2:修改数据库默认字符集

-- 查看当前字符集
SHOW VARIABLES LIKE 'character%';

+----------------------------+----------------------------------+
| Variable_name              | Value                            |
+----------------------------+----------------------------------+
| character_set_database     | utf8                             |
| character_set_server       | utf8                             |
+----------------------------+----------------------------------+

-- 修改数据库默认字符集
ALTER DATABASE your_db CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;

步骤3:修改表的字符集

-- 方法1:只修改表的默认字符集(新增列生效)
ALTER TABLE user DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- 方法2:修改表和所有列的字符集(推荐)
ALTER TABLE user CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

区别

方法影响范围已存在的列新增的列
DEFAULT只改表的默认设置不变utf8mb4
CONVERT TO改表和所有列改为utf8mb4utf8mb4

阿西噶阿西:"推荐用CONVERT TO,一次性改完。"


步骤4:修改配置文件(永久生效)

# my.cnf 或 my.ini
[client]
default-character-set = utf8mb4

[mysql]
default-character-set = utf8mb4

[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci

重启MySQL

systemctl restart mysqld

步骤5:验证

-- 查看数据库字符集
SHOW CREATE DATABASE your_db\G

-- 查看表字符集
SHOW CREATE TABLE user\G

-- 查看列字符集
SHOW FULL COLUMNS FROM user;

+----------+--------------+--------------------+
| Field    | Type         | Collation          |
+----------+--------------+--------------------+
| nickname | varchar(50)  | utf8mb4_unicode_ci |
+----------+--------------+--------------------+

步骤6:测试emoji

-- 插入emoji
INSERT INTO user (username, nickname) VALUES ('test', '小明😀');

-- 查询
SELECT * FROM user WHERE username = 'test';

+----------+----------+
| username | nickname |
+----------+----------+
| test     | 小明😀   |
+----------+----------+

哈吉米:"终于可以存emoji了!"


⚠️ 迁移注意事项

注意1:varchar(255)的秘密

为什么很多字段都是varchar(255)?

原因:InnoDB的索引长度限制。

InnoDB索引键的最大长度:767字节(MySQL 5.6-)

utf8字符集:
- 每个字符最多3字节
- varchar(255) * 3 = 765字节 < 767字节 ✅

utf8mb4字符集:
- 每个字符最多4字节
- varchar(255) * 4 = 1020字节 > 767字节 ❌

解决方案

-- 方案1:缩短字段长度
ALTER TABLE user MODIFY COLUMN nickname varchar(191);  -- 191 * 4 = 764字节

-- 方案2:只索引前缀
CREATE INDEX idx_nickname ON user(nickname(100));  -- 只索引前100个字符

-- 方案3:升级MySQL 5.7+(支持更大的索引)
-- innodb_large_prefix = ON
-- 最大索引长度:3072字节

南北绿豆:"所以varchar(255)在utf8mb4下要特别注意索引长度!"


注意2:迁移时间

-- 大表迁移很慢
ALTER TABLE big_table CONVERT TO CHARACTER SET utf8mb4;
-- 100万行数据,可能要几分钟

-- 建议:
-- 1. 低峰期执行
-- 2. 分批迁移(先迁小表,再迁大表)
-- 3. 或者用pt-online-schema-change工具

注意3:存储空间

utf8 → utf8mb4:
- 如果数据都是英文/中文,存储空间不变(按实际字节存)
- 如果有emoji,存储空间增加(4字节 vs 无法存储)

varchar(50)的实际占用:
- 存储"abc":3字节(utf8和utf8mb4一样)
- 存储"中文":6字节(utf8和utf8mb4一样)
- 存储"😀":4字节(utf8mb4),utf8无法存储

阿西噶阿西:"所以迁移到utf8mb4,存储空间基本不会增加。"


🎨 排序规则(Collation)的选择

哈吉米:"utf8mb4_unicode_ci 和 utf8mb4_general_ci 有啥区别?"

常见的排序规则

排序规则特点性能准确性
utf8mb4_general_ci快速排序(不区分大小写)⭐⭐⭐⭐⭐⭐⭐⭐
utf8mb4_unicode_ci标准排序(不区分大小写)⭐⭐⭐⭐⭐⭐⭐⭐⭐
utf8mb4_bin二进制排序(区分大小写)⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
utf8mb4_0900_ai_ciMySQL 8.0默认(最准确)⭐⭐⭐⭐⭐⭐⭐⭐⭐

ci = case insensitive(不区分大小写)
bin = binary(二进制,区分大小写)


general_ci vs unicode_ci

测试

-- general_ci(简单排序)
CREATE TABLE test_general (
  name VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci
);

INSERT INTO test_general VALUES ('ß'), ('ss');
SELECT * FROM test_general ORDER BY name;

+------+
| name |
+------+
| ss   |
| ß    |
+------+
-- general_ci认为 ß 和 ss 是不同的

-- unicode_ci(标准排序)
CREATE TABLE test_unicode (
  name VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
);

INSERT INTO test_unicode VALUES ('ß'), ('ss');
SELECT * FROM test_unicode ORDER BY name;

+------+
| name |
+------+
| ß    |
| ss   |
+------+
-- unicode_ci认为 ß 和 ss 等价(德语规则)

对比

特性general_ciunicode_ci
性能快(简单比较)稍慢(复杂规则)
准确性英语准确多语言准确
适用场景纯英文/中文多语言混合

推荐

  • 英文/中文项目:utf8mb4_general_ci(性能好)
  • 多语言项目:utf8mb4_unicode_ci(准确)
  • MySQL 8.0:utf8mb4_0900_ai_ci(默认,最好)

bin(区分大小写)

CREATE TABLE test_bin (
  name VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin
);

INSERT INTO test_bin VALUES ('ABC'), ('abc'), ('Abc');
SELECT * FROM test_bin WHERE name = 'abc';

+------+
| name |
+------+
| abc  |  -- 只匹配小写的abc
+------+

-- 如果是utf8mb4_general_ci:
SELECT * FROM test_general WHERE name = 'abc';
+------+
| name |
+------+
| ABC  |
| abc  |  -- 不区分大小写,都匹配
| Abc  |
+------+

适用场景

  • 密码字段(区分大小写)
  • 敏感数据

🎓 面试标准答案

题目1:MySQL的utf8和utf8mb4有什么区别?

答案

特性utf8utf8mb4
支持字节数1-3字节1-4字节
是否标准UTF-8❌ 阉割版✅ 完整版
支持emoji
支持生僻汉字❌(部分)
MySQL版本所有版本5.5.3+

推荐:新项目直接用utf8mb4,老项目尽快迁移。


题目2:为什么很多字段是varchar(255)?

答案

因为InnoDB索引长度限制:

  • MySQL 5.6-:最大767字节
  • utf8:255 * 3 = 765字节 < 767字节 ✅
  • utf8mb4:255 * 4 = 1020字节 > 767字节 ❌

所以utf8mb4下,如果要建索引:

  • 方案1:缩短到varchar(191)
  • 方案2:只索引前缀
  • 方案3:升级MySQL 5.7+(支持3072字节)

题目3:如何安全地从utf8升级到utf8mb4?

答案

6步流程:

  1. 备份数据
  2. 修改数据库默认字符集
  3. 修改表字符集(用CONVERT TO)
  4. 修改配置文件(永久生效)
  5. 检查varchar(255)字段(可能需要调整)
  6. 测试emoji存储

注意:大表迁移时间长,建议低峰期执行。


🎉 结束语

晚上6点,哈吉米终于把所有表都迁移到了utf8mb4。

哈吉米:"终于可以存emoji了!原来MySQL的utf8是个坑。"

南北绿豆:"对,这是历史遗留问题,MySQL 8.0已经默认用utf8mb4了。"

阿西噶阿西:"记住:新项目直接用utf8mb4,老项目尽快迁移。"

哈吉米:"还有varchar(255)的秘密,涨姿势了!"

南北绿豆:"对,utf8mb4下要注意索引长度限制。"


记忆口诀

utf8只三字节,emoji生僻存不了
utf8mb4全支持,标准UTF-8才是好
varchar(255)有讲究,索引长度要算好
迁移之前先备份,低峰执行别出错


希望这篇文章能帮你彻底搞懂MySQL字符集的坑!下次遇到emoji存不进去,你就知道怎么解决了!💪