1. 先从"乱码"说起:字符集到底是什么
如果你的MySQL表里存了一堆问号,或者明明存的"张三",查询出来却变成"寮犱笁",又或者一个JOIN查询直接报Illegal mix of collations的错误——不用怀疑,基本都是字符集(character set)和校对规则(collate)没配对。
这个坑我踩过不止一次,而且每次的场景都不一样。有一次是在接手某个老项目时,数据库里的数据看起来正常,但新写的查询一旦带上排序就出问题;还有一次是导入备份文件后,某个字段的中文变成了乱码。排查到最后,全都是字符集和排序规则不匹配惹的祸。
很多人会把字符集和校对规则混为一谈,其实它们是两个不同的概念。字符集决定的是"一个字用什么字节序列来表示",而校对规则决定的是"字符串之间怎么比较、怎么排序"。简单类比一下:字符集相当于给每个字分配了一个身份证号,而校对规则相当于规定"按身份证号码大小排,还是按拼音排,还是按笔画排"。两者紧密相关,但解决的问题完全不同。
对这个概念的理解深度,直接决定你能不能写好建表语句、能不能根治乱码问题,以及能不能在线上环境里快速定位故障。这篇文章我不打算只讲概念,而是会把我实际排查、配置、迁移过程中总结出来的经验全部摊开来讲,包括配置文件怎么写、SET NAMES到底干了什么、排序规则选错了会有什么后果,以及存量数据怎么安全迁移。看完你应该能做到:从建库到建表到连接层,一套配置不出错;遇到乱码和排序问题,能自己定位到具体环节。
2. utf8与utf8mb4:一个差点毁掉表情包的历史遗留
2.1 MySQL的utf8其实是"残疾版"utf8
先聊一个最容易踩的坑:MySQL里的utf8并不是完整的UTF-8编码,它最多只支持3个字节的字符。我们平时说的UTF-8编码,理论上一个字符最长可以是4个字节,那些超出基本多语言平面的字符——比如最常见的emoji表情,以及很多生僻汉字——需要4个字节才能表示。而MySQL的utf8(准确的说是utf8mb3)只实现了UTF-8的一个子集,遇到4字节字符就存不进去。
我记不清有多少次看到同事建表时写了DEFAULT CHARSET=utf8,然后过两天跑过来问:"为什么我的表情符号存进去就变成问号了?"其实就是因为这个。utf8字符集对大部分中文和英文字符是够用的,但一旦业务里面出现emoji、某些冷门的汉字(比如"𪛖"这种扩展区的字),就会被强制截断成?或者干脆写入失败。
后来MySQL官方也意识到这个问题很坑,但保留兼容性又不允许直接把utf8的定义改成4字节。所以MySQL 5.5.3之后引入了utf8mb4,才是完整的、支持4字节字符的UTF-8实现。注意mb4是"max bytes 4"的缩写,意思是最多用4个字节表示一个字符。
2.2 为什么我强烈推荐一律使用utf8mb4
现在的开发环境下,除非有极其特殊的历史原因,否则我建议所有新建的库表一律使用utf8mb4。理由非常简单:
- 它是完整的UTF-8,中文、英文、日文、韩文、emoji、生僻字都能存。
- 从MySQL 5.5.3到现在的8.0版本,
utf8mb4的成熟度已经非常高,不存在性能问题。 utf8(utf8mb3)在MySQL 8.0里已经被标记为废弃,官方明确建议迁移到utf8mb4,后续版本里可能就不再支持utf8这个别名了。
有人会担心utf8mb4因为一个字符最多4字节,所以存储占用会变大。这个担心只对了一半。对于英文字符和数字,它和latin1、utf8的存储是一模一样的,都是1字节;对于常见中文,和utf8一样占用3字节;只有遇到上面说的4字节字符时才会多个字节。所以绝大多数业务场景,从utf8切换到utf8mb4根本不会带来明显的空间增长。
真正需要注意的反而是索引长度的限制。因为utf8mb4每个字符最多占4字节,所以在定义索引的时候,能索引的字符数量会比utf8更少。比如在MySQL 5.7的innodb存储引擎下,一个索引键值的最大长度是767字节(如果开启了innodb_large_prefix可以到3072字节),用utf8的时候单个字段最多可以建255个字符的前缀索引,但用utf8mb4就只能建191个字符。这个问题在MySQL 5.6、5.7时代特别突出,经常会出现Specified key was too long; max key length is 767 bytes的报错。MySQL 8.0默认索引键值上限是3072字节,情况好很多,但如果你在维护老版本,还是要注意。
3. 核心实操:一套完整的字符集配置方案
3.1 从配置文件到建库建表,逐层抓好
字符集配置不只是建表语句里写一句CHARSET=utf8mb4就完事了。MySQL的字符集是有层级结构的,从上到下分别是:
- 服务器层(server)
- 数据库层(database)
- 表层级(table)
- 字段层级(column)
- 连接层(connection)
- 结果返回层(results)
每一层都有自己的字符集设置,而且遵循"就近原则":如果内层没有显式指定,就继承外层。整理成表格会更直观:
| 层级 | 系统变量或语句 | 默认来源 |
|---|---|---|
| 服务器 | character_set_server | 配置文件或编译参数 |
| 数据库 | CREATE DATABASE ... DEFAULT CHARACTER SET | 服务器层 |
| 表 | CREATE TABLE ... DEFAULT CHARSET | 数据库层 |
| 字段 | 字段类型后跟CHARACTER SET | 表层级 |
| 连接 | character_set_client/character_set_connection | 会话级变量 |
所以在配置一个新建项目时,我的习惯是三层都写清楚。
第一层,是MySQL配置文件my.cnf(或my.ini)中的服务器层设置。在[mysqld]段落里加上:
[mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_0900_ai_ci这里顺便提一句,MySQL 8.0默认的字符集已经是utf8mb4,默认排序规则是utf8mb4_0900_ai_ci。也就是说,装了8.0之后你什么都不做,默认就是正确的。但如果你的MySQL是5.7甚至更早的版本,或者是从老版本升级上来的,配置文件里可能还是latin1或者utf8,就需要显式改掉。
第二层,是建库的时候指定。千万不要图省事只写CREATE DATABASE xxx;,我建议至少写成:
CREATE DATABASE `my_app` DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_0900_ai_ci;这个问题我在接手老项目时见得特别多。数据库是很多年前建的,当时默认字符集是latin1,后来业务要存中文,就在某个字段上单独指定了utf8。结果就是同一个库里,有的表是latin1,有的表是utf8,JOIN的时候各种报错。建库的时候就把默认值定死,后面建表即使不写CHARSET,也会自动继承,省心太多。
第三层,是建表语句。虽然可以继承数据库层的设置,但我仍然建议在建表语句里显式声明:
CREATE TABLE `users` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `nickname` varchar(50) NOT NULL, `signature` varchar(500) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;别嫌啰嗦。显式声明的好处是,表结构本身就携带了完整信息,以后做mysqldump备份、数据迁移、在不同环境间复制表结构时,不会因为环境默认值不同而出现偏差。我在实际工作中就遇到过:开发环境的MySQL配置改了,测试环境的没改,结果同一份建表SQL在两个环境生成出来的表字符集不一样,线上排查了半天才发现是这个原因。
3.2 连接层:SET NAMES到底做了什么
刚才说的都是存储端的字符集,现在说连接层。很多人搞不清楚SET NAMES utf8mb4这条语句为什么这么重要,它其实是一次性设置了三个系统变量:
SET character_set_client = utf8mb4; SET character_set_connection = utf8mb4; SET character_set_results = utf8mb4;什么意思呢?一条SQL从应用程序发送到MySQL服务器,再返回结果,整个过程经历了好几次字符集转换:
- 客户端把SQL语句发送给服务器,服务器按照
character_set_client所指定的字符集来解释这段字节。 - 服务器解析SQL后,把字符串字面量从
character_set_client转换成character_set_connection,在SQL执行过程中统一使用这个连接字符集。 - 查询结果返回给客户端时,服务器会把结果数据从字段的存储字符集转换成
character_set_results指定的字符集,再发送给客户端。
所以如果配套设置对不上,出现乱码几乎是必然的。比如客户端发送的是UTF-8字节,但服务器以为你发的是latin1,中文就会被错误解析;再比如数据库里存的明明是UTF-8,但因为character_set_results被设置成了latin1,返回的结果就是乱码。
在命令行客户端里,最常用的做法是连接之后立刻执行:
SET NAMES utf8mb4;在Java、Go、PHP等语言的数据库驱动里,一般也都有配置项,比如MySQL JDBC驱动的characterEncoding=utf8配置(注意有些版本的驱动里写utf8就够,因为它会在内部处理4字节逻辑),或Go的charset=utf8mb4参数。用Python的PyMySQL时,则是在连接参数里指定charset="utf8mb4"。
注意:还有一个经常被人忽略的变量叫
character_set_database,它表示"当前默认数据库"的字符集。当你执行USE some_db之后,这个变量会被自动切换成该库的字符集。它主要影响的是在SQL里直接使用CAST或CONVERT但没指定字符集时的默认行为,平时不太需要手动改,但排查问题时要清楚它的存在。
我之前帮人排查过一个诡异的乱码:数据存进去是对的,查出来也是对的,但通过某个接口返回给前端之后就乱了。定位了很久,最后发现是那个接口用的数据库连接池配置里漏了字符集参数,导致实际character_set_results是服务器的默认值latin1。数据库本身一点问题没有,纯粹是连接层背了锅。这个教训让我之后养成了一个习惯:写数据库连接相关的代码时,永远显式指定字符集,不要把命运交给默认值。
4. 排序规则(Collation)深入解析:不只是排序
4.1 排序规则的核心作用
如果字符集决定了"这个字符长什么样",那么排序规则(collation)就决定了"这个字符在比较和排序时怎么算大小"。
这里我要用实例来解释,比干讲概念容易理解多了。假设有一张表存了一些用户名:
| 用户名 |
|---|
| Alice |
| bob |
| Charlie |
| 张三 |
| 李四 |
| 王五 |
执行SELECT * FROM users ORDER BY username;的结果会是什么?最终结果取决于这张表的默认排序规则。
- 如果排序规则是
utf8mb4_bin,那么会直接按照字符的Unicode编码二进制值来排。英文字母按ASCII码排,大写字母排在小写字母前面,汉字按Unicode码点排。 - 如果排序规则是
utf8mb4_general_ci或utf8mb4_0900_ai_ci,那么比较过程中会做"大小写不敏感"的处理,A和a被视为相等的字符,排序时按某种约定决定先后。
utf8mb4_general_ci中的_ci是 case insensitive(大小写不敏感)的意思。MySQL里有大量不同的排序规则,单是一个utf8mb4字符集下就有好几种可选择:
| 排序规则 | 特点 | 适用场景 |
|---|---|---|
utf8mb4_bin | 二进制逐字节比较,大小写敏感 | 需要精确匹配,如唯一标识 |
utf8mb4_general_ci | 大小写不敏感,比较速度快,但规则相对粗糙 | 老项目常用,通用场景 |
utf8mb4_unicode_ci | 基于Unicode排序算法,比general_ci更准确 | 多语言排序,需要更规范的结果 |
utf8mb4_0900_ai_ci | MySQL 8.0引入,基于Unicode 9.0,支持重音不敏感 | MySQL 8.0新项目的默认选择 |
utf8mb4_0900_as_cs | 重音敏感、大小写敏感 | 对排序精度要求高的场景 |
4.2 别把排序规则不当回事:几个真实翻车现场
很多人觉得排序规则无所谓,反正不影响数据存储。真的不是这样。我已经见过太多由于排序规则不统一而导致的故障了。
第一个翻车现场是JOIN报错。两个表字段的字符集一致,但排序规则不一致,比如一个字段用的是utf8mb4_general_ci,另一个用utf8mb4_unicode_ci,直接执行JOIN时MySQL会报错:
ERROR 1267 (HY000): Illegal mix of collations (utf8mb4_general_ci,IMPLICIT) and (utf8mb4_unicode_ci,IMPLICIT) for operation '='这个错误很常见,解决办法有三个:一是统一两个字段的排序规则;二是给其中一个字段加COLLATE强制指定;三是在比较操作里用CONVERT显式转换。但归根结底,统一表的默认排序规则才是正解,临时用COLLATE只是缓解症状。
第二个翻车现场是查询性能。如果索引字段和查询条件里的字符串排序规则不一致,MySQL可能无法使用索引。举个例子,表里字段定义的是utf8mb4_bin,但你查询时用了WHERE name = 'alice' COLLATE utf8mb4_0900_ai_ci,这个COLLATE会让MySQL在比较之前先做一次转换,函数或表达式套在索引列上的时候,索引就失效了,可能从索引扫描退化成全表扫描。
第三个翻车现场是DISTINCT和GROUP BY的语义变化。utf8mb4_bin下,'A'和'a'是两个不同的值;但在utf8mb4_general_ci下,它们是相同的。所以用DISTINCT去重时,同一个数据集在不同排序规则下返回的结果可能不同。这种问题最隐蔽,不容易被发现,但一旦被用户反馈"为什么这个字母大小写重复了",排查起来就很头疼。
4.3 排序规则选择经验:不要盲目追求最新
给新项目选排序规则,我的建议是:
- MySQL 8.0及以上,直接用默认的
utf8mb4_0900_ai_ci就好,不用折腾。 - MySQL 5.7及以下,用
utf8mb4_general_ci或者utf8mb4_unicode_ci。前者速度稍快一点,后者排序更准确。对绝大多数业务系统来说差别感知不到,选一个统一使用即可。 - 如果业务中需要区分大小写,比如用户名登录时大小写敏感,可以给对应的字段单独指定
utf8mb4_bin或utf8mb4_0900_as_cs。
有一个原则是最重要的:整个系统里,同一个字段的字符集和排序规则要全局统一。与其纠结选哪个"最优",不如保证"一致"。只要所有层级的排序规则都一致,绝大多数的排序规则类问题都不会出现。我在生产环境里遇到过最麻烦的一次故障,就是一个历史表用了latin1_swedish_ci,而新表用了utf8mb4_general_ci,每次JOIN都要显式COLLATE,代码里到处是补丁,后面花了大半天的代价才把所有表的字符集统一掉。
5. 乱码问题排查:三个环节逐一验证
5.1 乱码到底发生在哪个环节?
乱码是最常见的字符集问题,但我发现很多人在排查乱码时完全没有思路,只会百度"MySQL乱码怎么办",然后试各种命令,运气好就解决了,运气不好就折腾半天。其实乱码就三种可能,按数据流动的路径来排查就非常清楚。
第一站是存储环节。如果存进去的时候就已经是错误编码,那查出来必然是乱的。验证方法是直接查看数据库中存储的原始字节。执行:
SELECT HEX(nickname) FROM users WHERE id = 123;假设你存入的是"张三",如果结果是一串E5 BC A0 E4 B8 89,说明存储的是正确的UTF-8字节;如果是其他乱七八糟的值,就说明数据在写入之前已经被错误编码了,这种只能修复存量数据。
第二站是传输环节。如果存储正确,但读取时乱码,那问题一般出在character_set_connection或者客户端发送SQL时的编码。验证方法是查看当前会话的字符集变量:
SHOW VARIABLES LIKE 'character_set_client'; SHOW VARIABLES LIKE 'character_set_connection'; SHOW VARIABLES LIKE 'character_set_results';这三个变量必须和客户端实际使用的编码一致。在命令行里可以用SET NAMES utf8mb4修正,在应用程序里则要检查连接参数。
第三站是展示环节。有时候数据在MySQL里完全正确,浏览器或者终端显示出来却是乱码,那就得检查页面本身的编码声明是不是UTF-8,HTTP响应的Content-Type头有没有带对charset。这个问题已经不属于MySQL了,所以在排查时如果前两个环节都正常,一定要跳出数据库的思维,去看看终端日志、API网关或前端渲染的编码。
我之前处理过一个工单,对方坚持说"数据库乱码了",要我帮忙恢复数据。远程看了半天,发现数据库里的数据字节是100%正确的,最后发现是他们用来查看数据的那个老运维工具的显示编码默认是GBK,换成UTF-8显示后一切正常。数据库纯粹是背锅的。
5.2 排序规则不一致导致的查询报错
除了乱码之外,Illegal mix of collations是另一个高频问题。这个报错的意思是,MySQL在执行比较操作时,发现两边的字符串用了不同的排序规则,而且无法自动判断用哪一个来比较。
常见触发场景有几个:
- 两个表的字段排序规则不一样,JOIN时比较出错。
- WHERE条件里字符串和表字段的排序规则不一样。
- UNION把不同排序规则的字段合并在一起。
- INSERT时把不同排序规则的字符串赋值给某个字段(这个比较少见但也会报错)。
验证排序规则是否一致,可以执行:
SHOW FULL COLUMNS FROM users;看输出里的Collation列就能知道每个字段用了什么排序规则。也可以直接看表结构:
SHOW CREATE TABLE users\G;临时解决报错的办法是在比较时显式指定排序规则:
SELECT * FROM users u JOIN orders o ON u.id = o.user_id WHERE u.nickname = '张三' COLLATE utf8mb4_0900_ai_ci;但是请注意,这只适合临时救火。根治的方法一定是让所有相关的字段用相同的排序规则。如果你在一个已经运行很久的系统中遇到了这个问题,我建议把整理所有表的字符集和排序规则差异列成一个表格,排优先级逐表修改,而不是一直靠COLLATE补丁包着走。
5.3 排序规则对索引的隐藏影响
排序规则和索引的关系,我不只一次在面试和实际工作中提到。MySQL的索引本质上是一棵B+树,树上的节点顺序是按照索引列定义的排序规则来排列的。同一个字段,如果排序规则不同,索引树的顺序就不同,这直接影响查询能否走索引。
举个例子,字段email定义为VARCHAR(100),没有指定排序规则,默认继承表的设置。如果表是utf8mb4_bin,那么索引就是区分大小写的,WHERE email = 'Alice@example.com'和WHERE email = 'alice@example.com'是两个完全不同的条件,走索引查出来的结果也不同。如果表是utf8mb4_0900_ai_ci,这两个条件等效,索引树的顺序也不区分大小写。
这里有一个非常容易被忽略的问题:当你在查询中使用COLLATE去改变某列的排序规则时,MySQL往往无法直接使用该列上的索引,因为它需要先做表达式转换,转换的结果并没有对应的索引可用。这在生产环境的慢查询日志里非常常见。
我之前优化过一个慢SQL,查询条件很简单:
SELECT * FROM users WHERE email = LOWER(?) COLLATE utf8mb4_bin;这个SQL在10万行的表上跑了整整1.2秒。排除掉那条多余的COLLATE之后,时间直接降到了几毫秒。原因是原来的排序规则和字段的索引排序规则不一致,MySQL每次都要先把email字段的值转换一遍再做比较,索引就废了。这种问题在代码审查阶段很难发现,但如果对排序规则和索引的关系有清晰的认识,看到COLLATE出现在查询条件里就会立刻产生警觉。
6. 存量系统迁移实战:从utf8到utf8mb4
6.1 先摸清现状,再动手改造
如果你的系统已经在跑,而且用的还是老旧的utf8(utf8mb3),又或者某些表是latin1有些是utf8,那就涉及到存量数据的迁移。我经手过好几次这类迁移,总结出一套比较稳妥的操作路径。
第一步,全面摸清现状。生成一个清单,列出所有数据库、所有表的字符集和排序规则。
SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_COLLATION FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'your_db';这里我非常建议把所有结果导出成表格,看清楚了再动手。不要凭感觉,因为老系统里往往存在大量"历史遗迹"——可能80%的表是utf8,但偏偏有一张几年前创建的表是latin1,这种不一致往往就是未来故障的种子。
第二步,规划迁移方案。如果表里有外键关系,改造前一定要梳理清楚。因为当两个有关联的字段排序规则不一致时,ALTER TABLE改字段可能会因为外键约束而失败。我建议先改维表、再改事实表,并且每个表修改之前都做好结构备份和行数核对。
第三步,执行迁移。对于单表,可以用ALTER TABLE直接转换:
ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;这条语句会做两件事:把表的默认字符集改掉,同时把表中所有字符串列都转为目标字符集。注意,CONVERT TO和ALTER TABLE MODIFY COLUMN是有区别的。CONVERT TO会重写所有字符串列的数据,把旧的字节流转换成新的编码;而MODIFY COLUMN只改列定义,不一定会自动转换已有数据的存储格式。对于从utf8升级到utf8mb4的场景,CONVERT TO是正确的选择,因为它会触发数据的物理重写。
不过这里要提醒一个关键点:utf8里的数据要转成utf8mb4基本是无损的,因为utf8mb4是utf8的超集。但如果是从latin1转utf8mb4,有可能出现latin1里无法映射到UTF-8的字节,导致转换失败或产生问号。这类数据需要提前用HEX()方式检查原始字节,做一次数据治理,不能盲目执行转换。
6.2 迁移过程中的两个隐藏坑
第一个坑是临时表的问题。如果在存储过程或者触发器里引用了临时表,字符集的继承方式有时取决于character_set_connection,而不是目标表的字符集。迁移后如果某些存储过程开始报排序规则混用的错误,先检查临时表定义里有没有显式指定COLLATE。
第二个坑是索引长度。之前提过,utf8mb4每个字符最多占4字节,如果你原来的utf8表上有一个较长的VARCHAR字段并且加了索引,转换成utf8mb4后可能会发生"索引键过长"的报错。MySQL 5.7里默认是3072字节(需要开启innodb_large_prefix并且使用DYNAMIC行格式),所以一个VARCHAR(500)的字段如果用utf8mb4,最多占2000字节,还能接受;但如果又一个VARCHAR(1000)的字段建了索引,2000字节对于默认767字节(老版本)或3072字节(新版本)都可能超限。这种情况下你需要在迁移前先调整索引设计,缩短字段长度,或者改成前缀索引。
迁移完成之后,务必做一次完整性校验。我一般会做两件事:一是对比迁移前后的行数,二是抽样用HEX()函数检查几个关键字段的字节内容。迁移不是跑完SQL就完了,你得确认数据没有被静默改写。尤其是那些包含生僻字和特殊符号的字段,肉眼可能看不出问题,但对字节内容进行抽样对比就比较可靠。
6.3 会话级排查指令速查表
为了方便日常排查,我整理了一个常用指令表,都是我平时实际会用到的。
| 排查目标 | 指令 | 用途 |
|---|---|---|
| 查看服务器默认字符集 | SHOW VARIABLES LIKE 'character_set_server'; | 确认默认配置 |
| 查看当前会话三个关键变量 | SHOW VARIABLES LIKE 'character_set_client';等 | 排查连接层 |
| 查看某库的默认字符集 | SHOW CREATE DATABASE your_db\G; | 确认库定义 |
| 查看某表的实际字符集 | SHOW CREATE TABLE your_table\G; | 确认表定义 |
| 查看所有列的字符集和排序规则 | SHOW FULL COLUMNS FROM your_table; | 精确定位到字段 |
| 查看某字段原始字节 | SELECT HEX(col) FROM your_table WHERE id=...; | 验证存储编码 |
| 临时改变连接层字符集 | SET NAMES utf8mb4; | 快速验证连接层问题 |
| 查看所有排序规则 | SHOW COLLATION WHERE Charset = 'utf8mb4'; | 选择合适的排序规则 |
这套指令我已经保存了快十年,每次排查相关问题都是先对着它跑一遍,基本能覆盖90%的字符集故障场景。
7. 我踩过几次坑之后的体会
做数据库相关的运维和开发这么多年,字符集和排序规则给我的感觉是:它不像SQL语法那样直观,也不像索引优化那样有明确的"最佳实践",它更像是一层底层的土壤——土壤对了,上面种什么都省心;土壤不对,后面各种怪问题层出不穷。
我个人最后悔的一件事,是在一个项目最初建库的时候偷了懒,没有在建库语句里显式指定CHARACTER SET和COLLATE,当时想着"反正MySQL有默认值,用默认的不就行了"。结果那个项目的数据库跑在了一台从老版本升级上来的服务器上,默认字符集是latin1,某位同事建表时又手动指定了utf8,乱象从第一天就埋下了。后来线上出现暖昧的乱码问题,排查了整整一个下午。如果当时多花两分钟把库表定义写完整,后面起码省出十个小时的排查时间。
所以如果你现在正在设计一个新项目的数据库,或者即将接手一个老系统需要做规范化改造,我的建议很简单:
- 所有新建库表,一律显式指定
utf8mb4和配套的排序规则。 - 所有应用层的数据库连接,显式设置字符集参数。
- 定期巡检线上库表的字符集和排序规则,把不一致的情况消除在萌芽状态。
字符集这个东西,平时不出事的时候你根本感觉不到它的存在,一旦出事就是那种"看着不难但就是搞不定"的疑难杂症。与其到时候花半天排查,不如在建库的第一天就把它定下来。这是我做数据库相关工作以来最想对同行说的一句话。