前言
在 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,要么用前缀索引