原文地址:https://segmentfault.com/p/1210000010686697/read
一、 問題是這樣來的
BG內部要進行數據庫的容災演習,需要模擬線上實際的環境進行測試,這就需要copy一份線上的數據庫到測試庫中,其實也就是重建一個線上數據庫。要完成這個任務其實有N種不同的做法,但是我當時一想:“好久沒有測試一下數據備份是否正常了。”就決定利用備份數據進行重建測試庫,於是乎就導出了一份XXXX.sql文件,直接往測試庫里面導入,由於線上庫的實際數據量還不小,所以當我按完“Enter”后就決定下班回家,估計等我到家了就差不多完成了。
結果竟然出問題了,屏幕上出現:
ERROR 1071 (42000): Specified key was too long; max key length is 767 bytes
二、 苦逼的探索
對於報錯信息“ERROR 1071 (42000): Specified key was too long; max key length is 767 bytes”,其實意思就是“索引字段長度太長,超過了767bytes”。
mysql的varchar主鍵只支持不超過767個字節或者768/2=384個雙字節 或者767/3=255個三字節的字段 而GBK是雙字節的,UTF8是三字節的。
那么問題來了,為什么線上業務庫中正常的數據直接移植到另一個庫,竟然會報設置表中字段不合法的錯!
(1)字符集設置不同?
第一個想法是查看兩個庫的字符集設置是不是不一樣,果然線上庫的字符集character_set_database=lantin1,而測試庫的字符集character_set_database= utf8。這時候,我很天真的以為可能是因為建表的時候沒有指定字符集,所以在導入測試庫的時候默認使用utf-8字符集,導致Specified key was too long。所以就更改了測試庫中默認的字符集,但是導入數據的時候依然還是報同樣的錯誤。而且我在建表的語句中發現了,其實原來就已經指定了字符集,都是UTF8(CHARSET=UTF8),所以字符集設置不同並不是問題的症結所在。
(2)更改索引字段長度?
定位到出錯的建表語句,果然是使用UTF8字符集,而且長度是256(如下圖所示)。
為了驗證索引字段的字符集設置為utf8時,varchar(256)確實是超長了,做了如下的測試:
那么問題簡單了,更改tagvarchar(256)為tagvarchar(255),然后再進行導入數據,竟然成功了。但是依然后兩個困惑在我腦海里,
① 為什么線上庫可以設置tagvarchar(256)?
② 要是tag字段剛好有256個字節的數據,那么轉存的時候,數據不就丟失了?
(3)存儲引擎惹的禍?
當我去檢查剛剛那張被我手動修改過的表的狀態時,意想不到的發現了一個問題,為什么建表的語句明明寫的是“ENGINE=MYISAM”,但是導入后的表變成了“ENGINE=InnoDB”。(如下圖所示)
再仔細一回想,剛剛竟然只記得索引字段長度不能超過768,但是忘了innodb和myisam的區別了。
① innodb存儲引擎,多列索引的長度限制如下:
每個列的長度不能大於767 bytes;所有組成索引列的長度和不能大於3072 bytes
② myisam存儲引擎,多列索引長度限制如下:
每個列的長度不能大於1000 bytes,所有組成索引列的長度和不能大於1000 bytes
原來是兩張表的存儲引擎不同,這樣就解釋了剛剛的兩個疑問,但是又一個疑問就出現了,為什么建表語句中明明寫的是MYISAM表,怎么導入之后就變成了INNODB。(經過對比,發現所有的MYISAM表都被轉換成了INNODB表)
這個問題暫且先擱置,緊急的是要怎么規避。先分別查了下兩個數據庫的默認存儲引擎,果然發現在線上庫中default_storage_engine和storage_engine都是MYISAM,而測試庫卻均是INNODB。(如下圖所示)
通過修改默認的存儲引擎:
1)在my.cnf中的 [mysqld] 下加入default-storage-engine=INNODB ,保存(需重啟)。
2)set global default_storage_engine="InnoDB"
通過這種方法終於保證了不修改源數據文件的情況下,能正確的重建線上數據至測試庫,並且核對了數據和存儲引擎,均與線上庫一致。
(4)InnoDB存儲優化選項?
問題雖然是解決了,但是難道InnoDB中單個索引字段的長度真的只能小於767?
又經過一番探索,發現在InnoDB中,可以啟用啟用innodb_large_prefix參數,來使得單個索引字段的長度突破767。
注意:
①啟用innodb_large_prefix參數能夠取消對於索引中每列長度的限制(但是無法取消對於索引總長度的限制)
②啟用innodb_large_prefix必須同時指定innodb_file_format=barracuda,innodb_file_per_table=true,並且建表的時候指定表的row_format為dynamic或者compressed(mysql 5.6中row_format默認值為compact)
具體的操作如下:
① 查看innodb_large_prefix,innodb_file_format參數
mysql> show variables like 'innodb_large_prefix';
+-------------------------+---------------+
| Variable_name | Value |
+--------------------------+--------------+
| innodb_large_prefix | OFF |
+--------------------------+--------------+
② 查看mysql> show variables like 'innodb_file_format';
+-------------------------+---------------+
| Variable_name | Value |
+-------------------------+---------------+
| innodb_file_format | Antelope |
+-------------------------+---------------+
③ 建索引測試(innodb_large_prefix,innodb_file_format都為默認值的情況下)
mysql> create table test (id varchar(256),key (id));
mysql>create table test (id varchar(255),name varchar(255),name1 varchar(255),name2 varchar(255),name3 varchar(5),key (id,name,name1,name2,name3));
##索引列大於767 bytes時報錯,組成索引列總長度大於3072 bytes時報錯
④ 修改innodb_large_prefix,innodb_file_format參數
mysql> set global innodb_large_prefix=1;
mysql> set global innodb_file_format=BARRACUDA;
⑤ 對row_format為dynamic格式表創建索引測試
mysql>create table test(id varchar(256),key (id)) row_format=dynamic;
mysql>create table test(id varchar(255),name varchar(255),name1 varchar(255),name2 varchar(255),name3 varchar(5),key (id,name,name1,name2,name3)) row_format=dynamic;
##innodb_large_prefix=1並且innodb_file_format=BARRACUDA時,對於row_format為dynamic的表可以指定索引列長度大於767 bytes。但是索引列總長度的不能大於3072 bytes的限制仍然存在。
三、 有待深思的細節
相信大家一定還記得上面我們跳過的一個問題:為什么建表語句中明明寫的是MYISAM表,怎么導入之后就變成了INNODB。於是乎我做個2組簡單的測試:
① 設置數據庫default_storage_engine="InnoDB",將含有InnoDB和MYISAM的庫導入。
② 設置數據庫default_storage_engine=”MYISAM",將含有InnoDB和MYISAM的庫導入。
實驗結果是,default_storage_engine設置成InnoDB時,不論建表語句指定的是InnoDB或MYISAM,最終生成的表均為InnoDB;設置成MYISAM時,最終生成的表與其建表語句中設置的一致。