記住,永遠不要在 MySQL 中使用 “utf8” 編碼

Richard101發表於2018-07-02

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 編碼是-每個人都使用,包括你自己-每個字元需要四個位元組。

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 協議》,轉載必須註明作者和本文連結

烏合之眾...

相關文章