MySQL字符集編碼解析

前段時間公司內部博客上凱哥分享了一篇關于mysql字符集編碼的文章,之前我對mysql字符集一塊基本沒有深究過,看到凱哥文章后有些地方有點疑惑,遂自己去看了mysql的官方文檔,并參考了凱哥的文章,總結了這篇博文.本文主要是對mysql常見的字符集問題進行整理,如有錯誤,請大家指正.

1.MySQL字符集編碼簡介

談到字符集,總會跟編碼扯上關系,有關字符集和編碼的理論知識請參見我之前的文章.MySQL內部是支持多種字符集的,這里就不再嚴格區分字符集和編碼的概念了.同時,MySQL中不同層次有不同的字符集編碼格式,主要有四個層次:服務器,數據庫,表和列.字符集編碼不僅影響數據存儲,還影響客戶端程序和數據庫之間的交互.在mysql中輸入命令show session variables like '%character%'可以看到如下一些字符集:

+--------------------------+--------------------------------------------------------+
| Variable_name            | Value                                                  |
+--------------------------+--------------------------------------------------------+
| character_set_client     | utf8                                                   |
| character_set_connection | utf8                                                   |
| character_set_database   | latin1                                                 |
| character_set_filesystem | binary                                                 |
| character_set_results    | utf8                                                   |
| character_set_server     | latin1                                                 |
| character_set_system     | utf8                                                   |
| character_sets_dir       | /usr/local/mysql-5.6.15-osx10.7-x86_64/share/charsets/

mysql中的字符集都對應著一個默認的校對規則(COLLATION),當然一個字符集也可能對應多個校對規則,但是兩個不同的字符集不能對應同一個規則。校對規則不指定就是使用默認的,比如utf8字符集對應的默認校對規則就是utf8_general_ci。校對規則后綴如_cs,_ci,_bin分別表示是大小寫相關/大小寫無關/以字符串編碼的二進制值來比較大小。如果比較的兩個字符集不同,則mysql在比較前會先將其轉換到同一個字符集再比較,如果兩個字符集不兼容,則會報錯Illegal mix of collations

需要注意的是,校對規則可能會影響查詢。比如數據表的一個字段本身設置的校對規則為utf8_general_ci,且在title字段有索引,而你查詢的時候使用了SELECT xx FROM test ORDER BY title COLLATE utf8_bin來用另外校對規則進行排序,則此時就用不了索引,轉而使用filesort。在實際項目中,一般不去顯示指定的校對規則。

下面來看看上面命令列出的字符集相關變量的含義

  • character_set_client:服務器解析客戶端sql語句的字符集.(The character set for statements that arrive from the client. The session value of this variable is set using the character set requested by the client when the client connects to the server).
  • character_set_connection:字符串字面值(literal strings)的字符集.
  • character_set_results:服務器返回給客戶端的查詢結果或者錯誤提示的字符集編碼.(The character set used for returning query results such as result sets or error messages to the client)
  • character_set_system:這是mysql服務器用來存儲元數據的編碼,通常就是utf8,不要去修改它.
  • character_sets_dir:這是mysql字符集編碼存儲目錄.
  • character_set_filesystem:這是文件系統字符集編碼,主要用于解析用于文件名的字符串字面值,如LOAD DATA INFILE和SELECT ...INTO OUTFILE等語句以及LOAD_FILE()函數.在打開文件之前,文件名會從character_set_client轉換為character_set_filesystem指定的編碼.默認值為binary,也就是說不會進行轉換.例如我們設置的character_set_client=GBK,而character_set_filesystem為默認值的話,則采用SELECT...INTO OUTFILE "文件名",文件名為GBK編碼.反之,如果我們設置了character_set_filesystem=UTF8,則導出的文件名為UTF8編碼.
    例如:我的終端編碼是UTF8,系統默認語言和編碼為zh_CN.UTF8.我有一個數據庫名為test,test中有個表名為t1,編碼為latin1,另外,我在mysql客戶端執行了SET NAMES GBK,如果我不修改character_set_filesystem的值,執行SELECT * FROM t1 INTO OUTFILE '文件1', 可以發現對應的目錄下面生成了一個名為"文件1"的文件,那文件名編碼是什么呢?其實這里有幾個地方需要注意,首先,我們的sql語句里面的"文件1"原生編碼就是終端編碼UTF8,也就是'\xe6\x96\x87\xe4\xbb\xb61',而導出數據的語句SELECT * FROM t1 INTO OUTFILE '文件1',按照前面的說法,因為character_set_filesystem為binary,因此'\xe6\x96\x87\xe4\xbb\xb61'不會轉換編碼,這樣最終還是'\xe6\x96\x87\xe4\xbb\xb61',這樣在zh_CN.UTF8的系統中文件名不會亂碼.而如果我們設置了character_set_filesystem=UTF8,則原生的'\xe6\x96\x87\xe4\xbb\xb61'會先按照GBK解碼,然后用UTF8編碼,最后的結果是"\xe9\x8f\x82\xe5\x9b\xa6\xe6\xac\xa21",這樣文件名就會亂碼了.所以這個變量也最好不要修改,用默認值就OK.
  • character_set_server:服務器默認字符集編碼,如果創建數據庫的時候沒有指定編碼,則采用character_set_server指定編碼.
  • character_set_database:默認數據庫的字符集編碼.如果沒有默認數據庫,則該變量值與character_set_server相同.其實這個值代表的就是你當前數據庫的編碼而已,比如使用"use test",而test數據庫的編碼為latin1的話,這個值就是latin1.而你切換的時候"use test2",則character_set_database的值就是數據庫test2的編碼.LOAD DATA INFILE的時候,數據庫總是將文件中的字符按照character_set_database解析,在5.0之后的版本中,可以在LOAD的時候用character set指定字符集。

2.MySQL字符集編碼層次

第一部分主要是歸納了MySQL文檔中關于字符集編碼的說明.這部分主要說明下MySQL字符集編碼層次:服務器-數據庫-表-字段.

簡單來說,服務器編碼就是character_set_server來指定的.當我們創建數據庫的時候可以指定編碼,如果沒有指定,采用的就是character_set_server指定的編碼.例如:我們使用"create database t1 character set gbk",這里我們指定了數據庫t1的編碼為gbk,所以不會采用character_set_server指定的編碼.而如果我們使用"create database t2",則通過"show create database t2"可以看到t2的編碼為character_set_server定的編碼.

同理,mysql表也可以有自己獨立的編碼,在創建表的時候可以指定,如果沒有指定,則默認采用數據庫的編碼.比如我們再之前的數據庫t1創建表t11,create table t11(i int) character set utf8,則表t11的編碼為utf8,如果不指定編碼則編碼為數據庫t1的編碼gbk.

此外,mysql表中的字段也可以有自己的編碼,如果不指定字段編碼,則字段編碼與表的編碼一致.

3.MySQL連接字符集

前面談到的編碼內容基本都不會產生亂碼問題,mysql中容易產生亂碼的地方在character_set_client, character_set_connection, character_set_results這三個變量的設定.可以簡單的通過set names utf8或者charset utf8命令來一次設置這三個參數.

剛剛接觸這幾個變量的時候我完全沒有看懂,后來查找了不少資料,姑且算是理解了一點,如有錯誤,請大家指正。

從文檔中的解釋來看,mysql連接字符集轉換主要包括下面三個步驟:

  • 1.character_set_client是客戶端發送過來的sql語句的編碼,因為服務端本身并不知道客戶端的sql語句的編碼是什么,所以是以這個變量作為客戶端sql語句的初始編碼.而服務端接收到sql語句后,則會將sql語句從character_set_client轉換為character_set_connection指定的編碼(注意,對于字面值字符串,如果前面有introducer標記如_latin1或_utf8,則不會進行這一步轉換).轉換完成,才會真正執行sql語句.

  • 2.進行內部操作前將sql語句中的數據從character_set_connection轉換為數據表中相應字段的編碼.

  • 3.將操作結果從內部字符集編碼轉換為character_set_results編碼.

更加詳細的轉換過程如下:

Client program sends SQL statement
   |
   | Encoding: A, defined as "character_set_client"
   v
MySQL server - Convertion from encoding A to encoding B
   |
   | Encoding: B, defined as "character_set_connection"
   v
MySQL server - Execution to store data
MySQL server - Conversion from encoding B to encoding C
   |
   | Encoding: C, defined by text column encoding 
   v
MySQL server - Storage
...
MySQL server - Storage
   |
   | Encoding: C, defined by text column encoding
   v
MySQL server - Execution to fetch data
MySQL server - Convertion from encoding C to encoding D
   |  
   | Encoding: D, defined as "character_set_results"
   v
Client program receives result set

接下來就實例分析下mysql可能亂碼的情況以及我認為的原因,不對之處請指出.

4.MySQL亂碼實例分析

4.1 問題實例

這個測試例子跟凱哥的一樣,我們創建一個測試的數據庫db1,數據庫編碼為latin1,注意當前我的機器的終端編碼為zh_CN.UTF-8,數據庫的編碼設定如第1部分所示,然后在db1中創建一個表test,sql語句如下:

CREATE TABLE `test` (
  `gbk` varchar(2) CHARACTER SET gbk DEFAULT NULL,
  `utf8` varchar(2) CHARACTER SET utf8 DEFAULT NULL,
  `latin_utf8` varchar(6) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

注意到我們的表的編碼是latin1,而表中三個字段的編碼各不相同,分別為gbk編碼,utf8編碼以及latin1編碼.之所以這樣創建正是為了驗證mysql字符集編碼的轉換過程.好了,重點來了,現在我們在mysql客戶端執行:

mysql> insert into test values("中文", "中文", "中文");
Query OK, 1 row affected, 1 warning (0.00 sec)

安裝了mysql的筒子可以測試下,在mysql沒有開啟strict模式的時候,這個插入語句會報一個警告,內容如下:

mysql> show warnings;
+---------+------+-------------------------------------------------------------------------------------+
| Level   | Code | Message                                                                             |
+---------+------+-------------------------------------------------------------------------------------+
| Warning | 1366 | Incorrect string value: '\\xE4\\xB8\\xAD\\xE6\\x96\\x87' for column 'latin_utf8' at row 1 |
+---------+------+-------------------------------------------------------------------------------------+

我們可以先select看看test表中的內容:

mysql> select * from test;
+--------+--------+------------+
| gbk    | utf8   | latin_utf8 |
+--------+--------+------------+
| 中文   | 中文   | ??         |
+--------+--------+------------+

我們還可以查看下test表中實際存儲的內容:

mysql> select hex(gbk), hex(utf8), hex(latin_utf8) from test;
+----------+--------------+-----------------+
| hex(gbk) | hex(utf8)    | hex(latin_utf8) |
+----------+--------------+-----------------+
| D6D0CEC4 | E4B8ADE69687 | 3F3F            |
+----------+--------------+-----------------+

可以發現直接select查看的時候latin_utf8字段亂碼了,而通過hex函數查看發現原來latin_utf8字段存儲的內容有問題. 出現這個問題的原因就是編碼轉換過程出了錯,按照之前的原理來分析下整個編碼轉換過程:

  • 首先我們mysql客戶端發送插入語句insert into test values("中文", "中文", "中文");,注意到"中文"的編碼是跟我們的環境相關的,我這里是zh_CN.UTF-8,因此"中文"字節表示為\\xE4\\xB8\\xAD\\xE6\\x96\\x87.

  • 服務器端接收到該語句會當作utf8編碼,因為character_set_client=utf8,接下來是會進行第一步轉換,即將語句從character_set_client轉成character_set_connection的編碼,由于我們這里這2個編碼相同,實際就不會轉換(此外,如果插入的數據前面有_latin1或者_utf8等introducer標記,也不會轉換,因為introducer標記已經指明了字面值字符的編碼).

  • 接下來,數據要存儲到數據庫了,這個時候實際要插入的三個字段的編碼都是原始編碼\\xE4\\xB8\\xAD\\xE6\\x96\\x87,這個時候發生第二次編碼轉換,即由character_set_connection編碼轉換為數據表字段指定的編碼.那么接下來,我們可以看到,由本身的UTF8編碼與字段utf8相同,不需要進行轉換.接下來看gbk字段,它的編碼是gbk,這時會將原始編碼s="\\xE4\\xB8\\xAD\\xE6\\x96\\x87"按照utf8編碼轉換為GBK編碼,即執行s.decode('utf8').encode('gbk'),所以存儲的是D6D0CEC4,也沒有問題. 最后,看latin_utf8字段,同樣需要轉換編碼,由于latin1表示不了utf8編碼的范圍,所以s.decode('utf8').encode('latin1')這個轉換過程會出錯,導致的結果就是latin_utf8字段存儲的是??,即3F3F.

  • 最后就是select語句返回的結果分析,這是第三個需要轉換編碼的地方,即將字段從字段編碼轉換為character_set_results指定的編碼.這也是我們上面為什么gbk字段和utf8字段都能正常顯示中文的原因,因為在返回結果的時候,gbk字段會經過'\xD6\xD0\xCE\xC4'.decode('gbk').encode('utf8')返回,這樣我們在utf8編碼的mysql客戶端能夠正常顯示gbk字段.同理,由于utf8字段本身與character_set_results,所以不會發生編碼轉換,原樣返回\\xE4\\xB8\\xAD\\xE6\\x96\\x87,因此也是能正常顯示的.而latin_utf8字段本身存儲的就是3F3F,再經過編碼轉換,雖然utf8編碼能夠兼容latin1,但是本身的編碼是3F3F,所以最終結果就是"??".

4.2 解決方案

這一小節就來說說4.1中的問題,根據上面的分析,為了表test中的latin_utf8字段能夠正常的插入內容,我們不重新設置character_set_client和character_set_connection的情況下,那么有個好的方法就是加入introducer,關于introducer可以參見mysql官方文檔.那么我們的插入語句改為

mysql> insert into test values("中文", "中文", _latin1"中文");
Query OK, 1 row affected (0.02 sec)

由于指定了latin_utf8字段的introducer為_latin1,這樣在第一次由character_set_client轉換為character_set_connection的時候會忽略latin_utf8的轉換,所以還是保持原來的utf8字符,接下來將其存入到latin1字段中,亦不會有問題,因為編碼相同,不需要轉換,所以latin_utf8字段實際存儲的還是\\xE4\\xB8\\xAD\\xE6\\x96\\x87.這點可以通過下面的命令來驗證:

mysql> select hex(gbk), hex(utf8), hex(latin_utf8) from test;
+----------+--------------+-----------------+
| hex(gbk) | hex(utf8)    | hex(latin_utf8) |
+----------+--------------+-----------------+
| D6D0CEC4 | E4B8ADE69687 | 3F3F            |
| D6D0CEC4 | E4B8ADE69687 | E4B8ADE69687    |
+----------+--------------+-----------------+

那么我們如果直接select查詢,還會出錯么呢?答案是會的,因為如前所說,查詢的時候會將字段編碼轉換為character_set_results編碼的,顯然gbk和utf8字段都沒有問題,但是對于latin_utf8字段,其值會通過s.decode('latin1').encode('utf8'),從而導致在查詢的時候會亂碼。

mysql> select * from test;
+--------+--------+----------------+
| gbk    | utf8   | latin_utf8     |
+--------+--------+----------------+
| 中文   | 中文   | ??             |
| 中文   | 中文   | ??-?–?         |
+--------+--------+----------------+
2 rows in set (0.01 sec)

那么解決的方法也比較簡單,就是中select語句中的字段前面加上binary標識,表示該字段查詢結果不需要經過character_set_results的轉換.如下:

mysql> select gbk, utf8, binary latin_utf8 from test;
+--------+--------+-------------------+
| gbk    | utf8   | binary latin_utf8 |
+--------+--------+-------------------+
| 中文   | 中文   | ??                |
| 中文   | 中文   | 中文              |
+--------+--------+-------------------+
2 rows in set (0.00 sec)

5.總結

mysql編碼系統復雜,依照原理和測試的結果來看,character_set_client一定要與傳入的數據編碼一致,不然就會容易出現亂碼問題,character_set_connection可以與character_set_client不同,但是個人建議一樣最好,免得出現其他問題.此外,如果對結果編碼有要求,就設置下character_set_results編碼,當然我個人覺得這三個編碼一致是最省事的.此外,數據表字段編碼如果用latin1編碼,對于like搜索會有一些問題。最好是utf8編碼省時省力,如果用gbk一定要注意寬字節注入問題。

UPDATED
之前有個疑問是為什么有了character_set_client了還要加上character_set_connection,多出來的這次轉換的意義在哪里。看官方文檔描述:

也就是說,character_set_connection的應用情況基本就是不帶introducer的字符串字面值,或者更確切的說這是mysql代碼內部所用編碼(不是數據存儲編碼,數據存儲編碼由表的定義指定)。在前面的試驗中,如果將character_set_connection設置為GBK,則執行 SELECT length('中文')返回為4,而如果設置character_set_connection為UTF8,則執行結果為6。這里的中文編碼就是依據的character_set_connection。

此外,比如字符串比較SELECT '中' > '哈’(中的GBK編碼為D6D0,UTF8編碼為E4B8AD,哈的GBK編碼為B9FE,UTF8編碼為E59388),在兩者都是字面值的字符串的情況下,比較的時候字符集是以character_set_connection為準的,當該值為GBK時,我們發現結果為1;而如果設置character_set_connection為UTF8時,則結果為0。而如果查詢語句是跟列的值相比的, SELECT * from test WHERE gbk>'哈',則此時會將字面值字符串'哈'轉換為該列對應的編碼GBK進行比較。

我總結了這些地方,時間也很倉促,可能也有理解不到位的地方,還請大家指出。

6.參考資料

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容

  • 轉自: http://www.laruence.com/2008/01/05/12.html 略有修改 基本概念 ...
    布丁芝麻糊糊閱讀 993評論 1 1
  • From: 博客園 Johney最近,在項目組使用的mysql數據庫中,插入數據出現亂碼,關于這個問題做了下總結...
    zheng7閱讀 970評論 1 2
  • 基本概念 字符(Character)是指人類語言中最小的表義符號。例如'A'、'B'等; 給定一系列字符,對每個字...
    Leo_Yi閱讀 382評論 0 0
  • MySQL字符集 1、基本概念 字符(Character): 是指人類語言中最小的表義符號。例如'A'、'B...
    Jesper2357閱讀 1,279評論 0 0
  • 文/林覺明 意映卿卿如晤:吾今以此書與汝永別矣!吾作此書時,尚是世中一人;汝看此書時,吾已成為陰間一鬼。吾作此書,...
    陶斯音閱讀 212評論 0 0