現象
在使用MySQL客戶端書寫SQL語句的時候,我們可以在字符串前邊加_charset_name的符號,其中的charset_name對應著某個具體的字符集,廢話不多說,先寫兩個例子看一下:
可以看到第一個查詢結果正常,第二個查詢出現了亂碼。為什么呢?下邊細細道來。
原因
我們知道MySQL是一個C/S架構的軟件,可以有很多客戶端連接到服務器進行交互。客戶端發送給服務器的請求以及服務器發送給客戶端的響應本質上都是一個二進制的字節串,每當我們從客戶端發送一個請求到服務器,服務器處理完成之后再把響應返回給客戶端的過程其實發生了很多字符集轉換過程。
- 首先請求會被MySQL客戶端編碼為字節序列之后通過網絡傳輸到服務器。對于MySQL自帶的客戶端來說,這個編碼過程使用的字符集和我們使用的操作系統的默認字符集是一樣的,類Unix系統的默認字符集就是utf8,windows系統的默認字符集就是gbk。
- 服務器收到字節序列請求之后,會認為該字節串是按照character_set_client系統變量編碼的,之后將其從character_set_client轉換到character_set_connection,之后進行更深入的處理。
- 最后再將響應發送到客戶端的時候,又會按照character_set_results進行編碼。
- 客戶端收到響應字節串之后,按照本客戶端規定的字符集進行解碼。對于MySQL自帶的客戶端來說,這個解碼過程使用的字符集和我們使用的操作系統的默認字符集是一樣的,類Unix系統的默認字符集就是utf8,Windows系統的默認字符集就是gbk。
總結一下這幾個涉及到的通信字符集系統變量:
系統變量
描述
現在我的系統中的這幾個系統變量的值都是utf8:
如果我們使用了_charset_name前綴,意味著禁止服務器將后續字節從character_set_client轉換到character_set_connection,而是默認使用_charset_name代表的字符集作為它后續字節的字符集。比方說:
我現在使用的是macOS操作系統,所以
- 客戶端發送請求時會將字符'我'按照utf8進行編碼,也就是:0xE68891。
- 服務器收到請求后發現有前綴_gbk,則不會將其后邊的字節0xE68891進行從character_set_client到character_set_connection的轉換,而是直接把0xE68891認為是某個字符串由gbk編碼后得到的字節序列。
- 然后再把上述0xE68891從gbk轉換為character_set_results,也就是utf8。0xE688在gbk中代表漢字'鎴',而0x91無法解碼(我們可以看到上述查詢結果中有1個warning)。我們緊接著上邊的查詢語句執行一下SHOW WARNINGS:
- 之后將漢字'鎴'再按照utf8進行編碼,得到的結果就是E98EB4,把它發送到客戶端。
- 客戶端收到之后再解碼到屏幕上,解碼也使用utf8字符集,所以就出現了鎴。
擴展
如果在我的機器上我執行SELECT LENGTH(_gbk '我')會得到什么結果呢(LENGTH函數用來統計某個字符串共占用多少字節)?有很多小伙伴不經思考,脫口而出:2!哈哈,我們看一下結果驗證一下:
WTH?竟然是3?其實再回想一下我們上邊所說的,因為'我'前邊加了_gbk,所以不會經歷從character_set_client到character_set_connection的轉換過程,而是直接把0xE68891當作是一個采用gbk編碼的字節串。這個字節串中有3個字節,當然結果就返回3了(雖然0x91這個字節在gbk字符集中是無效的,可以看到上邊查詢語句中也給出了Warning)。
思考
如果我現在不使用基于macOS操作系統的客戶端,而采用基于Windows操作系統的客戶端來發送請求,那么下邊的語句的返回結果將會是什么呢:
SELECT LENGTH(_utf8 '我');