案例 utf8mb3 转 utf8mb4:767 字节索引上限、191/768 前缀与 row size 8126 的踩坑记录

2026-10-16 00:03:48

utf8mb3 转 utf8mb4:767 字节索引上限、191/768 前缀与 row size 8126 的踩坑记录

官方文档:MySQL 8.4 Reference Manual 12.9.8 Converting Between 3-Byte and 4-Byte Unicode Character Sets

为什么要转

utf8mb3 只支持 BMP(基本多文种平面)字符,每字符最多 3 字节;utf8mb4 额外支持 BMP 之外的补充字符,每字符最多 4 字节。转换的收益是能存补充字符(emoji 之类),代价是存储空间可能变大。

内容层面没问题,结构层面有问题

表内容层面:对 BMP 字符,utf8mb4 和 utf8mb3 存储完全相同(同码值、同编码、同长度);补充字符 utf8mb3 根本存不下,所以从 utf8mb3 转 utf8mb4 不存在「补充字符怎么转换」的问题。

表结构层面才是主要的不兼容点:

  • 对变长字符类型(VARCHAR 和 TEXT 系列),utf8mb4 列允许的最大字符数比 utf8mb3 少。
  • 对所有字符类型(CHAR、VARCHAR、TEXT),utf8mb4 列能建索引的最大字符数比 utf8mb3 少。

所以转表时可能需要改列定义或索引定义。

CREATE TABLE t1 (
  col1 CHAR(10) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci NOT NULL,
  col2 CHAR(10) CHARACTER SET utf8mb3 COLLATE utf8mb3_bin NOT NULL
) CHARACTER SET utf8mb3;

ALTER TABLE t1
  DEFAULT CHARACTER SET utf8mb4,
  MODIFY col1 CHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  MODIFY col2 CHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL;

关键在于:列或索引键的最大长度按「字节」算是不变的,单字符最大从 3 字节变成 4 字节,按「字符」算就变少了。转换前要检查两件事:

  • 所有 utf8mb3 列定义是否超过存储引擎最大长度;
  • 所有 utf8mb3 列上的索引是否超过最大长度(存储引擎增强时该上限会变)。

不满足就得缩小列或索引定义,或者继续用 utf8mb3。

这些改动一般只在列或索引特别长时才需要,否则用上面的 ALTER TABLE 就能顺利转换。

767 / 3072 字节上限与 255 / 191 / 1024 / 768 字符换算

InnoDB 在 COMPACT 或 REDUNDANT 行格式下索引最大长度 767 字节,于是 utf8mb3 列最多索引 255 字符、utf8mb4 列最多 191 字符:

767 / 3 = 255
767 / 4 = 191

若现有 utf8mb3 列索引超过 191 字符,转 utf8mb4 后必须索引更少的字符:

-- 合法定义
col1 VARCHAR(500) CHARACTER SET utf8mb3, INDEX (col1(255))

-- 转 utf8mb4 后
col1 VARCHAR(500) CHARACTER SET utf8mb4, INDEX (col1(191))

对使用 COMPRESSED 或 DYNAMIC 行格式的 InnoDB 表,允许超过 767 字节(最多 3072 字节)的索引键前缀。这些行格式下 utf8mb3 最多索引 1024 字符、utf8mb4 最多 768 字符:

3072 / 3 = 1024
3072 / 4 = 768

换算到具体列定义:VARCHAR(255) 在 utf8mb3 下是 765 字节,转 utf8mb4 变成 1020 字节;VARCHAR(768) 在 utf8mb3 下 2304 字节没问题,转 utf8mb4 后是 3072 字节正好到顶;VARCHAR(769+) 转换后索引会失败。

COMPACT / DYNAMIC 行格式与 innodb_large_prefix

  • COMPACT / REDUNDANT:最大索引长度 767 字节。
  • DYNAMIC / COMPRESSED:最大 3072 字节,更适合长文本和多字节字符集。
  • RDS MySQL 5.6 默认 innodb_large_prefix=OFF(767);5.7.7+ 默认 ON(3072);8.0 大索引已内置,不再有 innodb_large_prefix 参数,默认 3072。8.0 若仍报 767,通常是表的 ROW_FORMAT 还是旧的 COMPACT。

早期版本的参数组合要求:innodb_large_prefix=ON + innodb_file_format=Barracuda + ROW_FORMAT=DYNAMIC/COMPRESSED。到 5.7.7+,innodb_file_format 默认 Barracuda;5.7.9+ innodb_default_row_format 默认 DYNAMIC;innodb_large_prefix 默认 ON 且已弃用。

TINYTEXT / VARCHAR 长度与 row size 8126

TINYTEXT 列最多 255 字节,即最多 85 个 3 字节字符或 63 个 4 字节字符。若 utf8mb3 的 TINYTEXT 需要存 63 字符以上,转 utf8mb4 时必须同时把类型换成 TEXT 等更长类型。很长的 VARCHAR 列转 utf8mb4 时也可能需要换成更长的 TEXT 类型。

转换前诊断 row size(估算所有 VARCHAR/CHAR 的最大字节和,utf8mb4 按 4 字节计):

SELECT TABLE_SCHEMA, TABLE_NAME,
       SUM(CASE WHEN DATA_TYPE IN ('varchar','char') THEN CHARACTER_MAXIMUM_LENGTH*4 ELSE 0 END) AS estimated_row_bytes
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA NOT IN ('mysql','information_schema','performance_schema','sys')
GROUP BY TABLE_SCHEMA, TABLE_NAME
HAVING estimated_row_bytes > 8126
ORDER BY estimated_row_bytes DESC;

连接层 SET NAMES 与复制

SET NAMES 'utf8mb4' 会把连接字符集设为 4 字节字符集。只要服务器不发送 4 字节字符就没问题;否则预期每字符最多 3 字节的应用可能出问题。反过来,要发 4 字节字符的应用要确保服务器能识别。

复制场景:若源端使用支持补充字符的字符集,所有副本也必须能识别。源和副本表定义不同会导致意外结果——例如最大索引键长不同,源用 utf8mb3、副本用 utf8mb4 就有风险。复制中可能出现:

ERROR 13146: Column N cannot be converted from type 'varchar(X bytes)' to 'varchar(Y bytes) utf8mb4'

降级回 utf8mb3 / ucs2

降到老版本 MySQL 时,utf8mb3 和 ucs2 的数据本身没问题;服务器要足够新,能识别被转换字符集的定义。

对引用 utf8mb4 的对象定义,可以先 mysqldump 导出,编辑 dump 文件把 utf8mb4 改成 utf8,再在老服务器上重新导入——前提是数据里没有 4 字节字符。老服务器看到 dump 里的 utf8 就会建出 3 字节的 utf8 字符集对象。

mysqldump -u root -p db > dump.sql
sed -i 's/utf8mb4/utf8/g' dump.sql

常见报错与修复

ERROR 1071 (42000): Specified key was too long; max key length is 767 bytes / 3072 bytes
ERROR 1709 (HY000): Index column size too large. The maximum column size is 767 bytes
ERROR 1118 (42000): Row size too large (> 8126)
ERROR 13146:复制中 Column N cannot be converted from type 'varchar(X bytes)' to 'varchar(Y bytes) utf8mb4'

修复方式:

-- 把表改成 DYNAMIC 行格式(不影响现有功能)
ALTER TABLE your_table ROW_FORMAT=DYNAMIC;

-- 5.6/5.7 还要确保 innodb_large_prefix=ON,并让表 DYNAMIC/COMPRESSED;新建表加 ROW_FORMAT=DYNAMIC
CREATE TABLE your_new_table (...) ENGINE=InnoDB ROW_FORMAT=DYNAMIC;

-- 不改行格式时用前缀索引
CREATE INDEX idx_name ON your_table_name (long_column(100));

WordPress 的 wp_postmeta 是经典案例:meta_key 是 VARCHAR(255),utf8mb4 下索引必须写成前缀 meta_key(191),或者把表转成 ROW_FORMAT=DYNAMIC 摆脱前缀索引。

另外注意 InnoDB 的 SQL 校验时机:8.0.42 / 8.4.5 起会在 ALTER TABLE INPLACE 阶段就拒绝超长索引(Bug#116353);更早的 8.0.37–8.0.41、8.4.0–8.4.4 可能接受并持久化一个重启后打不开的非法索引。

参考:阿里云 RDS 支持文档 Specified key was too long; max key length is 767 bytes。

复制全文 生成海报 utf8mb4 MySQL 字符集 索引 InnoDB 运维 数据库

推荐文章

程序员茄子在线接单