记住,永远不要在 MySQL 中使用 “utf8” 编码
今天发现一个 bug:我尝试着将一个 UTf-8
编码的字符串存储到 MariaDB 的 UTF-8
编码中,然后 Rails 报了一个奇怪的错误:
Incorrect string value: ‘\xF0\x9F\x98\x83 <…’ for column ‘summary’ at row 1
这是一个 UTF-8
的客户端服务端,在一个 UTF-8
编码的数据库里面。这个字符串 「?< …」也是合法的。
但是这里却有点问题: MySQL的 utf8 不是 UTF-8
。
这个 「utf8」编码只支持每个字符占三个字节。真正的 UTF-8
编码是-每个人都使用,包括你自己-每个字符需要四个字节。
MySQL 的开发人员并没有修复这个 bug,但是他们在 2010 年发布了一个解决方案:一种叫做「utf8mb4」的新编码。
当然,他们并没有对外宣传这件事 (大概是因为这个 bug 太尴尬了),现在网上仍然有很多教程让我们使用「utf8」编码,但都是错的。
简而言之:
- MySQL 的「utf8mb4」才是真正意义上的「UTF-8」。
- MySQL 的「utf8」指的是「一种专有的编码」,这种编码有很多 Unicode 字符不能编码。
我在此做一个明确的声明:所有还在使用「utf8」编码的 MySQL 和 MariaDB 的用户实际上应当使用「utf8mb4」编码。而不应该继续使用「utf8」。
什么是编码?什么是 UTF-8?
Joel on Software 博客中有 我最喜欢的介绍。我概括一下它。
计算机以 1 和 0 的形式存储文本。这个段落中的第一个字母被存储为「01000011」,而你的计算机输出「C」。计算机分为两个步骤选择输出「C」:
- 你的计算机读取「01000011」并确定这个是数字 67。这是因为 67 被编码为「01000011」。
- 你的计算机在 Unicode 字符集中查找了 67 号字符,发现 67 代表「C」。
当我键入「C」时,同样的事情发生在结尾。
- 我的计算机在 Unicode 字符集中将「C」映射为 67。
- 我的计算机将 67 编码,并将「01000011」发送给 Web 服务器。
编码是一个已经被解决的问题,几乎网上所有的程序都使用了 Unicode 编码,因为很多人都不建议使用其他编码。
编码需要考虑的东西更多,Unicode 能编码超过一百万个字符(「C」和「?」是其中的两个字符)。其中有一种简单的编码叫 UTF-32 编码,它将所有的字符都保存为 32 个 bit,这个编码方式很简单,因为计算机处理 32 个 bit 的数据如整数(年龄)时,这是一种很好的编码方式。但是这种编码方式很不实用:太浪费空间了。
UTF-8 保存空格的方式是,在 UTF-8 编码中,常见的字符如「C」存储 8 个 bit,比较生僻的字符如「?」需要 32 个 bit,而其他编码则需要 16 或 24 个 bit。一篇相关的博文提到,UTF-8 编码比 UTF-32 编码减少了四倍的空间消耗,所以 UTF-8 加载速度会更快。
你可能没有意识到这个问题,但是我们的电脑在私底下一直使用 UTF-8 编码。如果电脑不使用 UTF-8 编码,那当我输入「?」时,你将看到一个随机的很混乱的数据。
MySQL 的「utf8」编码不能和其他程序兼容,当需要保存「?」时会出错。
MySQL 的一点历史故事
为什么 MySQL 的开发者使用这种不合理的「utf8」?我们可以通过 MySQL 的 commit 日志猜测一下。
MySQL 从 version 4.1 开始支持 UTF-8,大约是在 2003 的时候,比现在的 UTF-8 标准,RFC 3629 还要早。
而以前使用的是 RFC 2279 这套 UTF-8 标准,这套标准中每个字符 6 个字节。MySQL 开发者在 first pre-pre-release version of MySQL 4.1,也就是 2002 年 3 月 28 号实现 RFC 2279 标准。
然后又发生了一件神奇的事情,MySQL 的源代码在 9 月的版本中有一个字节的调整:「UTF8 最大仅支持 3 个字节序列」。
是谁发起了这个 commit?为什么发布这个 commit?我并不知晓。MySQL 的代码仓库似乎在采用 Git 作为版本控制后丢失了一些曾经的贡献者的名字(MySQL 过去的使用的是 BitKeeper 版本控制,像 Linux 内核一样)。MySQL 官方也没有在 2003 年 9 月的正式版发布时的邮件清单中解释这个变更。
但是我可以猜想。
我们说回 2002 年,MySQL 告诉用户,如果用户需要保证每条记录在表中有相同的字节长度的话,有一套 加速优化方案 可以使用。这套方案要求用户在定义字符字段的时候使用「CHAR」类型。一个「CHAR」类型的字段不管存储的数据内容是什么,存储的字符数量始终相同。如果存储的字符比字段规定的要少,会使用空格在结尾填充,直到数量匹配为止;如果存储的字符比字段规定的要多时,会截断多出来的字符。
当 MySQL 开发人员第一次尝试 UTF-8 时,就是还在使用每个字符 6 个字节这种方案时,他们似乎有些犹豫:一个 CHAR(1)
的列会使用 6 个字节长度;一个 CHAR(2)
的列会使用 12 个字节长度等等。
明确的说:他们最初的没有正式发布的做法是正确的,这种解决方案是有据可查且广泛适用的,任何一个只要是理解 UTF-8 编码的人都会认同这个方案。
但是很明显,MySQL 开发人员(或者是发行商)考虑到可能有一些用户会做这两件事情:
- 使用
CHAR
类型字段。(CHAR
字段现在基本没人使用了。但是在当时,MySQL 中使用CHAR
字段会有更优的速度,但是在 2005 年之后却并非如此) - 将
CHAR
字段列的编码设置为「utf8」。
我的猜测是 MySQL 开发人员为了方便那些既想优化空间和时间性能,又没能成功优化空间和时间性能的用户,废弃了原有的「utf8」编码。
然而没有人能得到自己想要的。想要优化时间和空间性能的用户仍旧在错误的使用「utf8」CHAR
字段,但是这些字段往往会很大,导致 MySQL 的速度比不使用 CHAR
的时候还要慢。而那些想要使用真正的 UTF-8 的用户错误的使用了「utf8」,导致他们不能存储「?」这类字符。
但是,一旦 MySQL 发布这个无用的字符编码,就再也不能修复它了:因为这会令所有的用户都要重新构建每一个数据库。所以 MySQL 最终在 2010 年发布了真正的 UTF-8,不过使用了另一个名字「utf8mb4」。
为什么这么糟糕
我这个星期很心情很糟糕,由于我被「utf8」这个名字给愚弄了,令我找 bug 很艰难,并且我并不是唯一一个被愚弄的,几乎我在网上找的所有文章都在吹捧「utf8」是 UTF-8。
命名为「utf8」本就是一个错误,这是一套专有的字符编码。这套编码导致了很多新的问题,并且这套编码并不能解决它所说的能解决的问题。
这是虚假宣传。
我的课程总结
- 数据库系统有微妙的错误和异常,你可以通过数据库的选择避免许多错误。
- 如果你需要一个数据库, 不要使用 MySQL 或者 MariaDB。 使用 PostgreSQL。
- 如果你需要使用 MySQL或 MariaDB,千万不要使用「UTF8」。当你想要UTF-8时,总是使用「UTF8Mb4」。现在去 转换你的数据库吧避免发生头痛的事。
本作品采用《CC 协议》,转载必须注明作者和本文链接
没有评论?言之凿凿.抛砖引玉吧.
我就遇到这个问题了,utf8存一个字符报错,折腾了很长时间,最后改utf8mb4解决的
微信存储用户发来的信息的时候,表情就会报错。。现在的做法是没有体会编码格式,而是检测到有表情直接替换为空了。
我有个问题,如何将utf8的数据库改为utf8mb4?我试过直接修改mysql的字符集好像原来的旧数据并不会生效?
为什么 MariaDB 也不行
使用utf8mb4也会有很多坑…… 数据库连接的编码一定也要改
@Ali
utf8直接存表情肯定会遇到一些问题,直接替换为空不太好吧,可以编码后再存储:
解码直接用urldecode就行了。
@Clarencep 领导要求不存,说用不到。只有听领导的喽
@Ali ? 现在好多人都喜欢用emoji表情的…… 看来你们领导是不喜欢用emoji表情的
@Clarencep 我们领导说。就为了记录一下用户问的那种问题比较多。而表情不重要。?
我这到底选啥。。
我一直用的是utf8mb4,还没报过错。:laughing:
我就是一直被坑的其中之一
受教了,战略性mark
之前存表情就被坑过
前几年都是用utf8,这几年都是微信相关的开发,微信昵称.....,已经把习惯改成了utf8mb4了....
Mysql 的 UTF8 是当年支持双字节编码的方式,因此最大每个字只有3字节。像GBK、Big5、Shift-JIS等。后来,Unicod为了统一世界编码,使用了4字节的编码方式。因此原本的UTF8是无法支持的,需要使用最大6字节的方式来支持。为此,mysql也增加了新的编码格式utf8mb4