根本原因是SUBSTRING按字符计数而非字节,UTF-8/utf8mb4中中文占3–4字节,跨字节截断导致非法编码;需用CONVERT(... USING binary)转为字节串再截取,并注意转回时的合法性。

SUBSTRING 按字节截取中文会乱码,根本原因是什么
MySQL 的 SUBSTRING(或 SUBSTR)默认按「字符」计数,但底层存储和截取行为受字符集和排序规则影响。如果你用的是 utf8mb4 字符集,一个中文字符占 3–4 字节;而 SUBSTRING(str, pos, len) 的 len 参数是「字符个数」,不是字节数。强行按字节理解会导致截断在某个中文字符中间,出现乱码(如 æ 或 )。
关键点:SQL 标准里没有「按字节截取字符串」的内置函数,SUBSTRING 从不接受字节长度参数。
想按字节截取中文,必须先转成二进制再操作
MySQL 提供了 CONVERT(... USING binary) 和 SUBSTRING 配合的方式,把字符串临时转为字节序列,再按字节位置切:
- 先用
CONVERT(str USING binary)得到字节串 - 再对这个二进制串用
SUBSTRING,此时len就是字节数 - 最后如果需要显示,得转回字符集(但注意:若字节截断在 UTF-8 中间,转回会失败或变成
?)
SELECT CONVERT(SUBSTRING(CONVERT('你好世界' USING binary), 1, 6) USING utf8mb4);上面这句取前 6 字节 —— ‘你好’共 6 字节(每个 3 字节),结果是 你好;但如果取 SUBSTRING(..., 1, 5),就卡在第二个‘好’字的第 2 字节上,转回时会丢弃非法 UTF-8 序列,结果可能是 你? 或空。
更安全的做法:用 LENGTH() 判断字节长度,避免跨字符截断
直接按字节截容易出错,实际中更推荐:
- 用
LENGTH(str)获取原始字节数(不是CHAR_LENGTH()) - 结合
CHAR_LENGTH(str)算出平均字节/字符比,辅助估算 - 如果业务真要「最多 N 字节且不破坏字符」,得用存储过程或应用层处理(SQL 不擅长 UTF-8 边界检测)
常见错误场景:
- 把
SUBSTRING(col, 1, 10)当作“取前 10 字节”,结果中文被砍半 - 在
utf8mb4下用CHAR_LENGTH()判断后直接截,仍可能因 emoji(4 字节)或生僻字出问题 - 忘记
CONVERT(... USING binary)后没转回,结果返回二进制 blob,客户端报错
PostgreSQL 和 SQL Server 怎么办
- PostgreSQL 没有原生字节 SUBSTRING,但可用
substring(convert_to(str, 'UTF8'), 1, n) + convert_from(..., 'UTF8') 组合,原理类似 MySQL 的 binary 转换
- SQL Server 的
SUBSTRING 默认按字符,但可以用 CAST/CONVERT 转 VARBINARY 后操作,同样面临 UTF-16 编码下中文占 2 字节、emoji 占 4 字节的问题
substring(convert_to(str, 'UTF8'), 1, n) + convert_from(..., 'UTF8') 组合,原理类似 MySQL 的 binary 转换SUBSTRING 默认按字符,但可以用 CAST/CONVERT 转 VARBINARY 后操作,同样面临 UTF-16 编码下中文占 2 字节、emoji 占 4 字节的问题真正难的不是语法,是搞清你字段的字符集、客户端连接的编码、以及目标字节数是否落在合法 Unicode 码点边界上。多数时候,与其硬截字节,不如在应用层用 Python 的 text.encode('utf8')[:n].decode('utf8', 'ignore') 更可控。

















