根據http://www.cnblogs.com/cchust/p/4601536.html進行驗證測試
問題背景
在mysql上面執行一條普通的insert語句,結果報錯:
Incorrect string value: '\x91;offl...' for column 'c' at row 1
重現:
1)連接MySQL字符集是UTF8
mysql --default-character-set=utf8 test
2)表結構
CREATE TABLE `abc` ( `id` int(11) DEFAULT NULL, `c` varchar(100) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=gbk
3)SQL
jinyizhou@localhost : test 05:58:25>insert into abc values(1,'我們'); Query OK, 1 row affected (0.01 sec) jinyizhou@localhost : test 05:58:32>insert into abc values(2,concat('cardName:校園網',char(59),'offlineCardType:campus')); Query OK, 1 row affected, 1 warning (0.00 sec) #報錯 jinyizhou@localhost : test 05:58:51>show warnings; +---------+------+----------------------------------------------------------------+ | Level | Code | Message | +---------+------+----------------------------------------------------------------+ | Warning | 1366 | Incorrect string value: '\x91;offl...' for column 'c' at row 1 | +---------+------+----------------------------------------------------------------+ 1 row in set (0.00 sec) jinyizhou@localhost : test 05:58:55>select * from abc; +------+-----------------------+ | id | c | +------+-----------------------+ | 1 | 我們 | | 2 | cardName:鏍″洯緗 | +------+-----------------------+ 2 rows in set (0.01 sec)
分析:
上面看到第2個插入亂碼了,八九不離十是字符集的問題了,看warnings就知道。這里有疑問,為什么第一條插入正常,第二條就報錯?2個插入的字符集都是一樣的(客戶端字符集),要是字符集出錯,第一個也會報錯。即使表的字符集是gbk,連接字符集是utf8,大家都知道utf8字符集范圍要大於gbk,難道是”校內網“這3個中文超出了gbk范圍?測試下:
jinyizhou@localhost : test 05:59:02>insert into abc values(1,'校園網'); Query OK, 1 row affected (0.00 sec) jinyizhou@localhost : test 06:04:12>select * from abc; +------+-----------------------+ | id | c | +------+-----------------------+ | 1 | 我們 | | 2 | cardName:鏍″洯緗 | | 1 | 校園網 | +------+-----------------------+ 3 rows in set (0.00 sec)
插入成功,上面說明和中文沒有關系,其實MySQL內部會進行轉換,所以問題不在這里。既然問題在第2個insert,那直接查看下是否亂碼:
jinyizhou@localhost : test 06:04:14>select concat('cardName:校園網',char(59),'offlineCardType:campus'); +----------------------------------------------------------------+ | concat('cardName:校園網',char(59),'offlineCardType:campus') | +----------------------------------------------------------------+ | cardName:校園網;offlineCardType:campus | +----------------------------------------------------------------+
顯示沒有問題,沒有亂碼,但是寫入卻出現亂碼。這里注意到了一個函數:char(56),我們把這個函數轉成字符串看看:
jinyizhou@localhost : test 06:10:09>insert into abc values(2,concat('cardName:校園網',';','offlineCardType:campus')); Query OK, 1 row affected (0.00 sec) jinyizhou@localhost : test 06:14:49>select * from abc; +------+-------------------------------------------+ | id | c | +------+-------------------------------------------+ | 1 | 我們 | | 2 | cardName:鏍″洯緗 | | 1 | 校園網 | | 2 | cardName:校園網;offlineCardType:campus | +------+-------------------------------------------+
到此問題找到了,原來是char()函數的問題。那就在手冊里看看char函數的相關知識:
CHAR() returns a binary string. To produce a string in a given character set, use the optional USING clause:
#char()返回的是一個二進制字符串,可選擇使用USING語句產生一個給出的字符集中的字符串:
mysql> SELECT CHARSET(CHAR(0x65)), CHARSET(CHAR(0x65 USING utf8)); +---------------------+--------------------------------+ | CHARSET(CHAR(0x65)) | CHARSET(CHAR(0x65 USING utf8)) | +---------------------+--------------------------------+ | binary | utf8 | +---------------------+--------------------------------+
按照上面的提醒,使用using utf8 給出一個字符串,即把二進制轉換成了字符串。指定字符集后再次執行:
jinyizhou@localhost : test 06:14:52>insert into abc values(2,concat('cardName:校園網',char(59 using utf8),'offlineCardType:campus')); Query OK, 1 row affected (0.00 sec) jinyizhou@localhost : test 06:22:24>select * from abc; +------+-------------------------------------------+ | id | c | +------+-------------------------------------------+ | 1 | 我們 | | 2 | cardName:鏍″洯緗 | | 1 | 校園網 | | 2 | cardName:校園網;offlineCardType:campus | | 2 | cardName:校園網;offlineCardType:campus | +------+-------------------------------------------+
看到成功插入,問題現在很明顯了,就是出在char()函數上面?再推敲下:
jinyizhou@localhost : test 09:38:17>insert into abc values(2,char(59)); Query OK, 1 row affected (0.00 sec) jinyizhou@localhost : test 09:38:50>select * from abc; +------+-------------------------------------------+ | id | c | +------+-------------------------------------------+ | 1 | 我們 | | 2 | cardName:鏍″洯緗 | | 1 | 校園網 | | 2 | cardName:校園網;offlineCardType:campus | | 2 | cardName:校園網;offlineCardType:campus | | 2 | ; | +------+-------------------------------------------+
問題要是出在char()上面的話,上面的應該報錯,那為什么沒問題呢,再看看和concat一起用:
jinyizhou@localhost : test 09:38:53>insert into abc values(2,concat('我',char(59))); Query OK, 1 row affected, 1 warning (0.00 sec) jinyizhou@localhost : test 09:39:38>select * from abc; +------+-------------------------------------------+ | id | c | +------+-------------------------------------------+ | 1 | 我們 | | 2 | cardName:鏍″洯緗 | | 1 | 校園網 | | 2 | cardName:校園網;offlineCardType:campus | | 2 | cardName:校園網;offlineCardType:campus | | 2 | ; | | 2 | 鎴 | +------+-------------------------------------------+
這里就看出來問題所在了,最終問題定位在concat+char上面。
上面已經知道char()在官方的說明了,繼續在手冊里看看concat函數的相關知識:
If the arguments include any binary strings, the result is a binary string. A numeric argument is converted to its equivalent string form. to avoid that and produce a nonbinary string, you can use an explicit type cast, as in this example: SELECT CONCAT(CAST(int_col AS CHAR), char_col); 如果所有參數均為非二進制字符串,則結果為非二進制字符串。 如果自變量中含有任一二進制字符串,則結果為一個二進制字符串。一個數字參數被轉化為與之相等的二進制字符串格式;若要避免這種情況,可使用顯式類型 cast, 例如: SELECT CONCAT(CAST(int_col AS CHAR), char_col)
用charset查看類型:
jinyizhou@localhost : test 10:58:21>select charset(concat('cardName:校園網',char(56),'offlineCardType:campus')); +------------------------------------------------------------------------+ | charset(concat('cardName:校園網',char(56),'offlineCardType:campus')) | +------------------------------------------------------------------------+ | binary | +------------------------------------------------------------------------+ 1 row in set (0.00 sec)
按照上面的提醒,使用cast給出一個字符串,即把二進制轉換成了字符串:
jinyizhou@localhost : test 10:18:43>insert into abc values(2,concat('cardName:校園網',cast(char(59) as char),'offlineCardType:campus')); Query OK, 1 row affected (0.00 sec) jinyizhou@localhost : test 10:19:13>select * from abc; +------+-------------------------------------------+ | id | c | +------+-------------------------------------------+ | 1 | 我們 | | 2 | cardName:鏍″洯緗 | | 1 | 校園網 | | 2 | cardName:校園網;offlineCardType:campus | | 2 | cardName:校園網;offlineCardType:campus | | 2 | ; | | 2 | 鎴 | | 2 | cardName:校園網;offlineCardType:campus | +------+-------------------------------------------+
看到成功插入。說到這里大家都知道原因了:
char()函數返回的是一個二進制,concat()函數里只要有一個參數是二進制,則結果就是一個二進制。由於把一個二進制的值轉成十六進制再寫入到表,所以在插入的時候就會出現亂碼,為什么不報錯呢?因為轉換出來的編碼在字符集里找不到,雖然不報錯,但會變成亂碼。亂碼的原因是:char()函數返回的是一個binary類型字符串,在進行concat時,會導致'cardName:校園網'字符串到binary的轉換,整個結果返回是一個binary。UTF8字符集的三個漢字“校園網”占了9個字節。由於目標表字符集是GBK,因此在入庫時,還會發生一次binary到GBK的轉碼,此時由於utf8和gbk字符集的范圍不一樣導致轉換出錯。要是表的字符集是utf8,則不會報錯。
解決:
1)直接把char函數用其轉換好的結果表示:如用";"來代替char(56)。
2)直接轉化成字符串:char(59 using utf8) 來替換char(56)。
3)直接轉化成字符串:cast(char(59) as char) 來代替char(56)。
4)直接把表的字符集改成utf8。
總結:
MySQL亂碼的問題有很多情況,大致的原理可以看 http://www.cnblogs.com/zhoujinyi/p/4618887.html。總的一句話就是保證客戶端,服務端和表的字符集一致,盡量避免不一致引發的問題。
參考:
http://www.cnblogs.com/cchust/p/4601536.html
http://www.cnblogs.com/zhoujinyi/p/4618887.html