MySQL 笔记:utf8 与 utf8mb4 的坑,emoji 与排序规则

· Mysql笔记 MySQL连接 MySQL utf8mb4 字符集 · 1 阅读 · 约 6 分钟

前言

在 utf8 字段的列里存 emoji,会得到这句老朋友:

ERROR 1366 (HY000): Incorrect string value: '\xF0\x9F\x98\x80' for column 'content' at row 1

原因不是字符集配置错了,而是 MySQL 里的 utf8 根本不是完整 UTF-8。

utf8 其实是 utf8mb3

MySQL 的 utf8 是 utf8mb3 的别名:每个字符最多 3 字节,只能编码基本多文种平面(BMP)里的字符。emoji、部分生僻汉字、扩展 B 区之后的字符是 4 字节,utf8mb3 存不下。utf8mb4 才是完整的 UTF-8 实现,最多 4 字节。

好消息是 utf8mb4 向下兼容:BMP 内的中文在两种字符集下字节序列一致,原来能存的字符换过去照样能存。查当前实例的实际情况:

SHOW VARIABLES LIKE 'character_set%';
SHOW VARIABLES LIKE 'collation%';

我本机 5.7 的结果里 character_set_server 还是 utf8(也就是 utf8mb3),而客户端连接已经是 utf8mb4——这种"服务器默认窄、连接宽"的组合很常见,坑多半就出在中间的表或列上。

五层字符集,就近覆盖

优先级从高到低:列 > 表 > 库 > 服务器,外加独立的连接层(character_set_client、character_set_connection、character_set_results)。

CREATE DATABASE blog DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;

CREATE TABLE post (
  id      INT UNSIGNED NOT NULL AUTO_INCREMENT,
  title   VARCHAR(200) NOT NULL DEFAULT '',
  content TEXT NOT NULL,
  PRIMARY KEY (id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;

-- 建完表才想起来:整表转换,会重写数据,大表谨慎
ALTER TABLE post CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

SET NAMES utf8mb4 是在连接里同时把 client、connection、results 三个变量设成 utf8mb4,等价于建连后立刻执行的一条语句;PHP 侧更稳妥的做法是在连接配置里就带上:

$pdo = new PDO('mysql:host=127.0.0.1;dbname=blog;charset=utf8mb4', 'root', 'password');
$mysqli->set_charset('utf8mb4');

MySQL 配置文件里对应 [client] default-character-set 和 [mysqld] character-set-server / collation-server 两处,改完要重启。

排序规则的选择

  • utf8mb4_general_ci:5.7 里 utf8mb4 的默认排序规则,简单快速,个别语言的排序不够准确
  • utf8mb4_unicode_ci:基于 Unicode 排序规则,多语言更准,性能略低
  • *_bin:按字节比较,区分大小写和重音,适合唯一性校验类的列
  • utf8mb4_0900_ai_ci 是 8.0 才有的,5.7 里写了会报错

ci 是 case-insensitive。中文按拼音排序是另一个话题,常见取巧写法是借 GBK 的排序规则:

SELECT name FROM student ORDER BY CONVERT(name USING gbk) COLLATE gbk_chinese_ci;

要求列本身允许转换,且数据里没有 GBK 覆盖不到的字符,只能算实用技巧。

索引长度这条硬线

InnoDB 单个索引键的前缀长度上限是 767 字节(REDUNDANT、COMPACT 行格式),DYNAMIC 行格式配合 innodb_large_prefix = ON 才放宽到 3072 字节。utf8mb4 一列最多 4 字节每字符,于是:

  • VARCHAR(255) 建索引:255 × 4 = 1020 字节,COMPACT 行格式下直接报 Specified key was too long
  • 老库里流传的 VARCHAR(191):191 × 4 = 764 字节,刚好卡在 767 以内
  • MySQL 5.7.9 起 innodb_default_row_format 默认 dynamic,同时本机 innodb_large_prefix 是 ON,所以 1020 字节没问题;5.6 及更早的环境就要认真核对

如果确实要索引很长的文本,用前缀索引,同时注意前缀的区分度:

ALTER TABLE post ADD INDEX idx_title (title(64));

导出导入也要带字符集

mysqldump 按连接字符集导出,跨版本恢复或者目标库更窄时会悄悄丢字符,显式指定更稳:

mysqldump --default-character-set=utf8mb4 -uroot -p blog_php post > post.sql

用 mysql < post.sql 导入时客户端字符集也要对齐,否则中文和 emoji 在导入这一步就变成问号了。判断是否真的存进去很简单:

SELECT HEX(content) FROM post WHERE id = 1;

😀 在 utf8mb4 里的字节是 F09F9880,看到 3F(问号)就说明链路上某一环还停留在 utf8mb3。

小结

  • 新库新表一律 utf8mb4,utf8 只是 utf8mb3 的别名
  • 报错先分清是列、表还是连接字符集窄,用两个 SHOW VARIABLES 定位
  • 排序规则里 general_ci 快、unicode_ci 准、_bin 严格,5.7 没有 0900 系列
  • 长文本索引要么考虑行格式与 large_prefix,要么用前缀索引