程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 數據庫知識 >> MYSQL數據庫 >> MySQL綜合教程 >> MySQL字符集編碼

MySQL字符集編碼

編輯:MySQL綜合教程

MySQL字符集編碼


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),當然一個字符集也可能對應多個校對規則,但是兩個不同的字符集不能對應同一個規則.由於我平時都用的默認校對規則,所以就忽略不談了,後續有新的發現我會補上.

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

  • 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 '測試文件',按照前面的說法,因為character_set_filesystem為binary,因此'\xe6\x96\x87\xe4\xbb\xb61'會按照GBK編碼進行解碼在進行編碼,這樣最終還是'\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字符集編碼層次:服務器-數據庫-表-字段.

    簡單來說,服務器編碼就是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源碼,具體的原理還是請大神們指教.

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

    • 1.character_set_client是客戶端發送過來的sql語句的編碼,因為服務端本身並不知道客戶端的sql語句的編碼是什麼,所以是以這個變量作為客戶端sql語句的初始編碼.而服務端接收到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字符集編碼的轉換過程.好了,重點來了,現在我們在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('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.總結

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

        我總結了這些地方,時間也很倉促,可能也有理解不到位的地方,還請大家指出.當然,最後要致謝凱哥,是凱哥最初的博客讓我去研究了下mysql的編碼,後續有新的認識我會再繼續更新該文章.

        6.參考資料

        • mysql character set
        • 深入mysql字符集設置
        • MySQL-Sending-Non-ASCII-Text-Big5
        • Viewing-Character-Set-Variables

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved