1. utf8 與 utf8mb4 異同
先看 官方手冊 https://dev.mysql.com/doc/refman/5.6/en/charset-unicode-utf8mb4.html 的說明:
1 |
The character set named utf8 uses a maximum of three bytes per character and contains only BMP characters. The utf8mb4 character set uses a maximum of four bytes per character supports supplementary characters: |
MySQL在 5.5.3 之后增加了 utf8mb4
字符編碼,mb4即 most bytes 4。簡單說 utf8mb4 是 utf8 的超集並完全兼容utf8,能夠用四個字節存儲更多的字符。
但拋開數據庫,標准的 UTF-8 字符集編碼是可以用 1~4 個字節去編碼21位字符,這幾乎包含了是世界上所有能看見的語言了。然而在MySQL里實現的utf8最長使用3個字節,也就是只支持到了 Unicode 中的 基本多文本平面(U+0000至U+FFFF),包含了控制符、拉丁文,中、日、韓等絕大多數國際字符,但並不是所有,最常見的就算現在手機端常用的表情字符 emoji和一些不常用的漢字,如 “墅” ,這些需要四個字節才能編碼出來。
注:QQ里面的內置的表情不算,它是通過特殊映射到的一個gif圖片。一般輸入法自帶的就是。
也就是當你的數據庫里要求能夠存入這些表情或寬字符時,可以把字段定義為 utf8mb4,同時要注意連接字符集也要設置為utf8mb4,否則在 嚴格模式 下會出現 Incorrect string value: /xF0/xA1/x8B/xBE/xE5/xA2… for column 'name'
這樣的錯誤,非嚴格模式下此后的數據會被截斷。
提示:另外一種能夠存儲emoji的方式是,不關心數據庫表字符集,只要連接字符集使用 latin1,但相信我,你絕對不想這個干,一是這種字符集混用管理極不規范,二是存儲空間被放大(讀者可以想下為什么)。
2. utf8mb4_unicode_ci 與 utf8mb4_general_ci 如何選擇
字符除了需要存儲,還需要排序或比較大小,涉及到與編碼字符集對應的 排序字符集(collation)。ut8mb4對應的排序字符集常用的有 utf8mb4_unicode_ci
、utf8mb4_general_ci
,到底采用哪個在 stackoverflow 上有個討論,What’s the difference between utf8_general_ci and utf8_unicode_ci
主要從排序准確性和性能兩方面看:
- 准確性
utf8mb4_unicode_ci
是基於標准的Unicode來排序和比較,能夠在各種語言之間精確排序utf8mb4_general_ci
沒有實現Unicode排序規則,在遇到某些特殊語言或字符是,排序結果可能不是所期望的。
但是在絕大多數情況下,這種特殊字符的順序一定要那么精確嗎。比如Unicode把ß
、Œ
當成ss
和OE
來看;而general會把它們當成s
、e
,再如ÀÁÅåāă
各自都與A
相等。 - 性能
utf8mb4_general_ci
在比較和排序的時候更快utf8mb4_unicode_ci
在特殊情況下,Unicode排序規則為了能夠處理特殊字符的情況,實現了略微復雜的排序算法。
但是在絕大多數情況下,不會發生此類復雜比較。general理論上比Unicode可能快些,但相比現在的CPU來說,它遠遠不足以成為考慮性能的因素,索引涉及、SQL設計才是。 我個人推薦是utf8mb4_unicode_ci
,將來 8.0 里也極有可能使用變為默認的規則。相比選擇哪一種collation,使用者應該更關心字符集與排序規則在db里要統一就好。
這也從另一個角度告訴我們,不要可能產生亂碼的字段作為主鍵或唯一索引。我遇到過一例,以 url 來作為唯一索引,但是它記錄的有可能是亂碼,導致后來想把它們修復就特別麻煩。
3. 怎么從utf8轉換為utf8mb4
3.1 “偽”轉換
如果你的表定義和連接字符集都是utf8,那么直接在你的表上執行
1 |
ALTER TABLE tbl_name CONVERT TO CHARACTER SET utf8mb4; |
則能夠該表上所有的列的character類型變成 utf8mb4,表定義的默認字符集也會修改。連接的時候需要使用set names utf8mb4
便可以插入四字節字符。(如果依然使用 utf8 連接,只要不出現四字節字符則完全沒問題)。
上面的 convert 有兩個問題,一是它不能ONLINE,也就是執行之后全表禁止修改,有關這方面的討論見 mysql 5.6 原生Online DDL解析;二是,它可能會自動該表字段類型定義,如 VARCHAR 被轉成 MEDIUMTEXT,可以通過 MODIFY 指定類型為原類型。
另外 ALTER TABLE tbl_name DEFAULT CHARACTER SET utf8mb4
這樣的語句就不要隨便執行了,特別是當表原本不是utf8時,除非表是空的或者你確認表里只有拉丁字符,否則正常和亂的就混在一起了。
最重要的是,你連接時使用的latin1字符集寫入了歷史數據,表定義是latin1或utf8,不要期望通過 ALTER ... CONVERT ...
能夠讓你達到用utf8讀取歷史中文數據的目的,沒卵用,老老實實做邏輯dump。所以我才叫它“偽”轉換
3.2 character-set-server
一旦你決定使用utf8mb4,強烈建議你要修改服務端 character-set-server=utf8mb4
,不同的語言對它的處理方法不一樣,c++, php, python可以設置character-set,但java驅動依賴於 character-set-server 選項,后面有介紹。
同時還要謹慎一些特殊選項,如 遇到騰訊雲CDB連接字符集設置一個坑。個人不建議設置全局 init_connect
。
4. key 768 long 錯誤
字符集從utf8轉到utf8mb4之后,最容易引起的就是索引鍵超長的問題。
對於表行格式是 COMPACT
或 REDUNDANT
,InnoDB有單個索引最大字節數 768 的限制,而字段定義的是能存儲的字符數,比如 VARCHAR(200)
代表能夠存200個漢字,索引定義是字符集類型最大長度算的,即 utf8 maxbytes=3, utf8mb4 maxbytes=4,算下來utf8和utf8mb4兩種情況的索引長度分別為600 bytes和800bytes,后者超過了768,導致出錯:Error 1071: Specified key was too long; max key length is 767 bytes
。
COMPRESSED
和DYNAMIC
格式不受限制,但也依然不建議索引太長,太浪費空間和cpu搜索資源。
如果已有定義超過這個長度的,可加上前綴索引,如果暫不能加上前綴索引(像唯一索引),可把該字段的字符集改回utf8或latin1。
但是,( 敲黑板啦,很重要 ),要防止出現 Illegal mix of collations (utf8_general_ci,IMPLICIT) and (utf8mb4_general_ci,COERCIBLE) for operation '='
錯誤:連接字符集使用utf8mb4,但 SELECT/UPDATE where條件有utf8類型的列,且條件右邊存在不屬於utf8字符,就會觸發該異常。表示踩過這個坑。
再多加一個友好提示:EXPLAIN 結果里面的 key_len 指的搜索索引長度,單位是bytes,而且是以字符集支持的單字符最大字節數算的,這也是為什么 INDEX_LENGTH 膨脹厲害的一個原因。
5. C/C++ 內存空間分配問題
這是我們這邊的開發遇到的一個棘手的問題。C或C++連接MySQL使用的是linux系統上的 libmysqlclient 動態庫,程序獲取到數據之后根據自定義的一個網絡協議,按照mysql字段定義的固定字節數來傳輸數據。從utf8轉utf8mb4之后,c++里面針對character單字符內存空間分配,從3個增加到4個,引起異常。
這個問題其實是想說明,使用utf8mb4之后,官方建議盡量用 varchar 代替 char,這樣可以減少固定存儲空間浪費(關於char與varchar的選擇,可參考 這里)。但開發設計表時 varchar 的大小不能隨意加大,它雖然是變長的,但客戶端在定義變量來獲取數據時,是以定義的為准,而非實際長度。按需分配,避免程序使用過多的內存。
6. java驅動使用
Java語言里面所實現的UTF-8編碼就是支持4字節的,所以不需要配置 mb4
這樣的字眼,但如果從MySQL讀寫emoji,MySQL驅動版本要在 5.1.13 及以上版本,數據庫連接依然是 characterEncoding=UTF-8
。
但還沒完,遇到一個大坑。官方手冊 里還有這么一段話:
1 |
Connector/J did not support utf8mb4 for servers 5.5.2 and newer. |
意思是,java驅動會自動檢測服務端 character_set_server
的配置,如果為utf8mb4,驅動在建立連接的時候設置 SET NAMES utf8mb4
。然而其他語言沒有依賴於這樣的特性。
7. 主從復制報錯
這個問題沒有遇到,只是看官方文檔有提到,曾經也看到過類似的技術文章。
大概就是從庫的版本比主庫的版本低,導致有些字符集不支持;或者人工修改了從庫上的表或字段的字符集定義,都有可能引起異常。
8. join 查詢問題
這個問題是之前在姜承堯老師公眾號看到的一篇文章 MySQL表字段字符集不同導致的索引失效問題,自己也驗證了一下,的確會有問題:
1 |
CREATE TABLE t1 ( |
對應上面1,2 的截圖:
其中 2 的warnings 有convert:
- (convert(t1.f_id using utf8mb4) = ‘421036’)
官網能找到這一點解釋的還是開頭那個地址:
1 |
Similarly, the following comparison in the WHERE clause works according to the collation of utf8mb4_col: |
只是索引失效發生在utf8mb4列 在條件左邊。(關於MySQL的隱式類型轉換,見這里)。
9. 參考
- https://dev.mysql.com/doc/refman/8.0/en/charset-unicode-conversion.html
- http://forums.mysql.com/read.php?103,187048,188748#msg-188748
- Why are we using utf8mb4_general_ci and not utf8mb4_unicode_ci?
- How to support full Unicode in MySQL databases
- 10分鍾學會理解和解決MySQL亂碼問題
原文鏈接地址:http://seanlook.com/2016/10/23/mysql-utf8mb4/