记住,永远不要在 MySQL 中使用 “utf8” 编码

翻译 Ellison ⋅ 于 2周前 ⋅ 最后回复由 Ali 3天前 ⋅ 605 阅读 ⋅ 原文地址

站点的翻译文章创建时,您将第一时间收到通知。

这是一篇社区协同翻译的文章,已完成翻译,更多信息请点击 协同翻译介绍

file

今天发现一个 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 编码是-每个人都使用,包括你自己-每个字符需要四个字节。

miss201 翻译于 2周前

MySQL 的开发人员并没有修复这个 bug,但是他们在 2010 年发布了一个解决方案:一种叫做「utf8mb4」的新编码。

当然,他们并没有对外宣传这件事 (大概是因为这个 bug 太尴尬了),现在网上仍然有很多教程让我们使用「utf8」编码,但都是错的。

简而言之:

  • MySQL 的「utf8mb4」才是真正意义上的「UTF-8」。
  • MySQL 的「utf8」指的是「一种专有的编码」,这种编码有很多 Unicode 字符不能编码。

我在此做一个明确的声明:所有还在使用「utf8」编码的 MySQL 和 MariaDB 的用户实际上应当使用「utf8mb4」编码。而不应该继续使用「utf8」。

He110 翻译于 2周前

查看其他 1 个版本

什么是编码?什么是 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 服务器。
Honvid 翻译于 2周前

编码是一个已经被解决的问题,几乎网上所有的程序都使用了 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」编码不能和其他程序兼容,当需要保存「?」时会出错。

He110 翻译于 2周前

查看其他 1 个版本

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 个字节序列」。

He110 翻译于 2周前

是谁发起了这个 commit?为什么发布这个 commit?我并不知晓。MySQL 的代码仓库似乎在采用 Git 作为版本控制后丢失了一些曾经的贡献者的名字(MySQL 过去的使用的是 BitKeeper 版本控制,像 Linux 内核一样)。MySQL 官方也没有在 2003 年 9 月的正式版发布时的邮件清单中解释这个变更。

但是我可以猜想。

我们说回 2002 年,MySQL 告诉用户,如果用户需要保证每条记录在表中有相同的字节长度的话,有一套 加速优化方案 可以使用。这套方案要求用户在定义字符字段的时候使用「CHAR」类型。一个「CHAR」类型的字段不管存储的数据内容是什么,存储的字符数量始终相同。如果存储的字符比字段规定的要少,会使用空格在结尾填充,直到数量匹配为止;如果存储的字符比字段规定的要多时,会截断多出来的字符。

He110 翻译于 2周前

当 MySQL 开发人员第一次尝试 UTF-8 时,就是还在使用每个字符 6 个字节这种方案时,他们似乎有些犹豫:一个 CHAR(1) 的列会使用 6 个字节长度;一个 CHAR(2) 的列会使用 12 个字节长度等等。

明确的说:他们最初的没有正式发布的做法是正确的,这种解决方案是有据可查且广泛适用的,任何一个只要是理解 UTF-8 编码的人都会认同这个方案。

但是很明显,MySQL 开发人员(或者是发行商)考虑到可能有一些用户会做这两件事情:

  • 使用 CHAR 类型字段。(CHAR 字段现在基本没人使用了。但是在当时,MySQL 中使用 CHAR 字段会有更优的速度,但是在 2005 年之后却并非如此)
  • CHAR 字段列的编码设置为「utf8」。
He110 翻译于 2周前

我的猜测是 MySQL 开发人员为了方便那些既想优化空间和时间性能,又没能成功优化空间和时间性能的用户,废弃了原有的「utf8」编码。

然而没有人能得到自己想要的。想要优化时间和空间性能的用户仍旧在错误的使用「utf8」CHAR 字段,但是这些字段往往会很大,导致 MySQL 的速度比不使用 CHAR 的时候还要慢。而那些想要使用真正的 UTF-8 的用户错误的使用了「utf8」,导致他们不能存储「?」这类字符。

但是,一旦 MySQL 发布这个无用的字符编码,就再也不能修复它了:因为这会令所有的用户都要重新构建每一个数据库。所以 MySQL 最终在 2010 年发布了真正的 UTF-8,不过使用了另一个名字「utf8mb4」。

He110 翻译于 2周前

为什么这么糟糕

我这个星期很心情很糟糕,由于我被「utf8」这个名字给愚弄了,令我找 bug 很艰难,并且我并不是唯一一个被愚弄的,几乎我在网上找的所有文章都在吹捧「utf8」是 UTF-8。

命名为「utf8」本就是一个错误,这是一套专有的字符编码。这套编码导致了很多新的问题,并且这套编码并不能解决它所说的能解决的问题。

这是虚假宣传。

He110 翻译于 2周前

我的课程总结

  • 数据库系统有微妙的错误和异常,你可以通过数据库的选择避免许多错误。
  • 如果你需要一个数据库, 不要使用 MySQL 或者 MariaDB。 使用 PostgreSQL
  • 如果你需要使用 MySQL或 MariaDB,千万不要使用「UTF8」。当你想要UTF-8时,总是使用「UTF8Mb4」。现在去 转换你的数据库吧避免发生头痛的事。
AspireHe 翻译于 2周前

原文地址:https://medium.com/@adamhooper/in-mysql-...

译文地址:https://laravel-china.org/topics/13864/r...


本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

本帖已被设为精华帖!
回复数量: 9
  • whatever_none
    2周前

    没有评论?言之凿凿.抛砖引玉吧.

  • 北冥
    2周前

    我就遇到这个问题了,utf8存一个字符报错,折腾了很长时间,最后改utf8mb4解决的

  • Ali 终生学习者... I want to learn English every day.
    2周前

    微信存储用户发来的信息的时候,表情就会报错。。现在的做法是没有体会编码格式,而是检测到有表情直接替换为空了。

  • 史沟飞
    2周前

    我有个问题,如何将utf8的数据库改为utf8mb4?我试过直接修改mysql的字符集好像原来的旧数据并不会生效?

  • Soldoros
    2周前

    为什么 MariaDB 也不行

  • Clarencep
    2周前

    使用utf8mb4也会有很多坑…… 数据库连接的编码一定也要改

    @Ali
    utf8直接存表情肯定会遇到一些问题,直接替换为空不太好吧,可以编码后再存储:

        function nickname_encode($s){
            $chars = preg_split('//u', $s, null, PREG_SPLIT_NO_EMPTY);
            foreach ($chars as &$c) {
                if (strlen($c) > 3 || $c === "%"){
                    $c = urlencode($c);
                }
            }
    
            return implode($chars);
        }

    解码直接用urldecode就行了。

  • Ali 终生学习者... I want to learn English every day.
    2周前

    @Clarencep 领导要求不存,说用不到。只有听领导的喽

  • Clarencep
    2周前

    @Ali ? 现在好多人都喜欢用emoji表情的…… 看来你们领导是不喜欢用emoji表情的

  • Ali 终生学习者... I want to learn English every day.
    2周前

    @Clarencep 我们领导说。就为了记录一下用户问的那种问题比较多。而表情不重要。?

暂无评论~~
您需要登陆以后才能留下评论!

Composer 中国全量镜像

Top 100 扩展包

Lumen 中文文档

Laravel 速查表

Laravel 中文文档

Laravel 项目开发规范

Laravel 开发环境部署

Elasticsearch-PHP 中文文档

Lumen 中文文档

GraphQL PHP 中文文档

社区文档撰写指南

TDD 构建 Laravel 论坛笔记

PHP PSR 标准规范

PHP 设计模式全集

Dingo API 中文文档