MySQL字符集編碼總結
之前內部博客上凱哥分享了一篇關於mysql字符集的文章,之前我對mysql字符集一塊基本沒有深究過,看到凱哥文章后有些地方有點疑惑,遂自己去看了mysql的官方文檔,並參考了凱哥的文章,總結了這篇博文.本文主要是對mysql常見的字符集問題進行整理,如有錯誤,請大家指正.
1.MySQL字符集編碼簡單介紹
談到字符集,總會跟編碼扯上關系,有關字符集和編碼的理論知識請參見我之前的文章.MySQL內部是支持多種字符集的,這里就不再嚴格區分字符集和編碼的概念了.同一時候,MySQL中不同層次有不同的字符集編碼格式,主要有四個層次:server,數據庫,表和列.字符集編碼不僅影響數據存儲,還影響client程序和數據庫之間的交互.在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),當然一個字符集也可能相應多個校對規則,可是兩個不同的字符集不能相應同一個規則.因為我平時都用的默認校對規則,所以就忽略不談了,興許有新的發現我會補上.
以下來看看上面命令列出的字符集相關變量的含義
- character_set_client:server解析客戶端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:server返回給客戶端的查詢結果或者錯誤提示的字符集編碼.(The character set used for returning query results such as result sets or error messages to the client)
- character_set_system:這是mysqlserver用來存儲元數據的編碼,通常就是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,另外,我在mysqlclient運行了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的編碼.
2.MySQL字符集編碼層次
第一部分主要是歸納了MySQL文檔中關於字符集編碼的說明.這部分主要說明下MySQL字符集編碼層次:server-數據庫-表-字段.
簡單來說,服務器編碼就是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中easy產生亂碼的地方在character_set_client, character_set_connection, character_set_results這三個變量的設定.能夠簡單的通過set names utf8或者charset utf8命令來一次設置這三個參數.
剛剛接觸這幾個變量的時候我全然沒有看懂,后來查找了不少資料,姑且算是理解了一點,當然也可能是錯的,由於沒有看過mysql源代碼,詳細的原理還是請大神們不吝賜教.
從文檔中的解釋來看,mysql連接字符集轉換主要包含以下三個步驟:
- 1.character_set_client是client發送過來的sql語句的編碼,由於服務端本身並不知道client的sql語句的編碼是什么,所以是以這個變量作為clientsql語句的初始編碼.而服務端接收到sql語句后,則會將sql語句轉換為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字符集編碼的轉換過程.好了,重點來了,如今我們在mysqlclient運行:
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.
- server端接收到該語句會當作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編碼的mysqlclient可以正常顯示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('gbk'),從而導致在查詢的時候會亂碼.
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.番外篇
在charset utf8情況下,顯示正常是
mysql> charset utf8;
Charset changed
mysql> select "中文";
+--------+
| 中文 |
+--------+
| 中文 |
+--------+
由於我們終端本身是UTF8編碼,這個也非常好解釋,由於三個編碼都同樣,所以值本身不會發生轉換,有一點要注意的是,對於column名稱,是會轉成character_set_results指定的編碼的,由於這里本身就是UTF8,所以顯示正常.mysql> charset gbk;
Charset changed
mysql> select "中文";
+--------+
| ??
?? |
+--------+
| 中文 |
+--------+
能夠發現值本身沒有變化,可是column名稱變成亂碼了,由於由utf8轉成gbk編碼導致的.
而假設charset latin1,則能夠發現結果也是正確的,由於UTF8編碼轉換成latin1是能夠正常顯示的.
mysql> charset latin1;
Charset changed
mysql> select "中文";
+--------+
| 中文 |
+--------+
| 中文 |
6.總結
mysql編碼系統復雜,按照原理和測試的結果來看,character_set_client一定要與傳入的數據編碼一致,不然就會easy出現亂碼問題,character_set_connection能夠與character_set_client不同,可是個人建議一樣最好,免得出現其它問題.此外,假設對結果編碼有要求,就設置下character_set_results編碼,當然我個人認為這三個編碼一致是最省事的.此外,數據表字段編碼假設用latin1編碼,對於like搜索會有一些問題,最好大家按照自己需求來設定合理的字段編碼了.
我總結了這些地方,時間也非常倉促,可能也有理解不到位的地方,還請大家指出.當然,最后要致謝凱哥,是凱哥最初的博客讓我去研究了下mysql的編碼,興許有新的認識我會再繼續更新該文章.