MySQL 擴(kuò)展字段長度報(bào)錯(cuò) Specified key was too long
引言
本文主要分析一套 MySQL 分庫分表擴(kuò)展字段長度時(shí)其中一個(gè)實(shí)例報(bào)錯(cuò)索引超長的案例,其中失敗實(shí)例的版本是 5.7.21,而成功實(shí)例的版本都是 5.7.24。因此懷疑與版本有關(guān),最終通過測試與分析判斷是一個(gè) bug,官方文檔顯示在 5.7.23 中修復(fù)。
現(xiàn)象
首先介紹三個(gè)案例,都是字段長度擴(kuò)展時(shí)報(bào)錯(cuò)索引超長。
案例 1
時(shí)間:2023-09-08 21:31:02
數(shù)據(jù)庫版本:5.6.39
SQL
ALTER TABLE sign_bill_return_image_audit_result
MODIFY COLUMN image_name VARCHAR(250) COMMENT '圖片名稱';
日志顯示 pt-osc 執(zhí)行期間報(bào)錯(cuò)索引長度超過 767。
EXECUTE START AT 2023-09-08 21:31:02
Error altering new table `station_manager`.`_sign_bill_return_image_audit_result_new`: DBD::mysql::db do failed: Specified key was too long; max key length is 767 bytes [for Statement "ALTER TABLE `station_manager`.`_sign_bill_return_image_audit_result_new` MODIFY COLUMN image_name VARCHAR(250) COMMENT '圖片名稱';"] at /usr/bin/pt-online-schema-change line 9194.
EXECUTE FAIL AT 2023-09-08 21:31:03
查看表結(jié)構(gòu),顯示字符集為 utf8mb4,索引類型為單列唯一索引,image_name 字段長度從 50 擴(kuò)展到 250。
mysql> show create table station_manager.sign_bill_return_image_audit_result \G
*************************** 1. row ***************************
Table: sign_bill_return_image_audit_result
Create Table: CREATE TABLE `sign_bill_return_image_audit_result` (
`image_name` varchar(50) NOT NULL DEFAULT '' COMMENT '圖片名稱',
UNIQUE KEY `idx_img_name` (`image_name`) USING BTREE,
) ENGINE=InnoDB AUTO_INCREMENT=27756774 DEFAULT CHARSET=utf8mb4 COMMENT='簽單返還圖片審核結(jié)果表'
1 row in set (0.00 sec)
查看參數(shù),顯示未開啟 innodb_large_prefix。
mysql> show variables like 'innodb_large_prefix';
+---------------------+-------+
| Variable_name | Value |
+---------------------+-------+
| innodb_large_prefix | OFF |
+---------------------+-------+
1 row in set (0.00 sec)
案例 2
時(shí)間:2024-08-19 14:18:31
現(xiàn)象:上游字段擴(kuò)展,因此下游修改,但是執(zhí)行報(bào)錯(cuò)聯(lián)合索引超長
數(shù)據(jù)庫版本:5.7.33
SQL
alter table worker_board_quota_counting
modify column `business_id` varchar(1456) NOT NULL COMMENT '業(yè)務(wù)id';
日志顯示 pt-osc 執(zhí)行期間報(bào)錯(cuò)索引長度超過 3072。
Error altering new table `dms_offline`.`_worker_board_quota_counting_new`: DBD::mysql::db do failed: Specified key was too long; max key length is 3072 bytes [for Statement "ALTER TABLE `dms_offline`.`_worker_board_quota_counting_new` modify column `business_id` varchar(1456) NOT NULL COMMENT '業(yè)務(wù)id';"] at /usr/bin/pt-online-schema-change line 9194.
查看表結(jié)構(gòu),顯示字符集為 utf8mb4,索引類型為聯(lián)合唯一索引,business_id 字段長度從 456 擴(kuò)展到 1456。
mysql> show create table dms_offline.worker_board_quota_counting \G
*************************** 1. row ***************************
Table: worker_board_quota_counting
Create Table: CREATE TABLE `worker_board_quota_counting` (
`business_id` varchar(456) NOT NULL COMMENT '業(yè)務(wù)id',
UNIQUE KEY `idx_source_businessid` (`source`,`business_id`),
) ENGINE=InnoDB AUTO_INCREMENT=19747573 DEFAULT CHARSET=utf8mb4 COMMENT='人員看板計(jì)提表'
1 row in set (0.00 sec)
查看參數(shù),顯示已開啟 innodb_large_prefix。
mysql> select @@innodb_large_prefix;
+-----------------------+
| @@innodb_large_prefix |
+-----------------------+
| 1 |
+-----------------------+
1 row in set (0.00 sec)
案例 3
時(shí)間:2024-01-22 23:59:12
工單類型:分庫分表
數(shù)據(jù)庫版本:5.7.21 報(bào)錯(cuò),5.7.24 不報(bào)錯(cuò)
SQL
ALTER TABLE mst_sku
modify `upc_code` varchar(1000) DEFAULT NULL COMMENT '69碼';
日志顯示 pt-osc 執(zhí)行期間報(bào)錯(cuò)索引長度超過 767。
Error altering new table `wms3`.`__mst_sku_new`: DBD::mysql::db do failed: Index column size too large. The maximum column size is 767 bytes. [for Statement "ALTER TABLE `wms3`.`__mst_sku_new` MODIFY COLUMN `upc_code` varchar(1000) DEFAULT NULL COMMENT '69碼';"] at /usr/bin/pt-online-schema-change line 9194.
查看表結(jié)構(gòu),顯示字符集為 utf8,索引類型為單列非唯一索引,upc_code 字段長度從 64 擴(kuò)展到 1000,注意其中行格式為 COMPACT。
mysql> show create table `wms3`.`mst_sku` \G
*************************** 1. row ***************************
Table: mst_sku
Create Table: CREATE TABLE `mst_sku` (
`upc_code` varchar(64) DEFAULT NULL COMMENT '69碼',
KEY `idx_sku_upccode` (`upc_code`),
) ENGINE=InnoDB AUTO_INCREMENT=12952734 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='商品信息表表'
1 row in set (0.00 sec)
查看參數(shù),顯示已開啟 innodb_large_prefix。
mysql> select @@innodb_large_prefix;
+-----------------------+
| @@innodb_large_prefix |
+-----------------------+
| 1 |
+-----------------------+
1 row in set (0.00 sec)
由于分庫分表工單中只有一個(gè)實(shí)例報(bào)錯(cuò),因此查看每個(gè)實(shí)例的數(shù)據(jù)庫版本與執(zhí)行結(jié)果。
ysql> select a.instance_version, t.execute_status from inception_job as t
inner join assets_instance as a on t.mysql_ip=a.instance_ip
where t.xbp_id =9334073;
+------------------+----------------+
| instance_version | execute_status |
+------------------+----------------+
| MySQL5.7.24 | 4 |
| MySQL5.7.24 | 4 |
| MySQL5.7.24 | 4 |
| MySQL5.7.24 | 4 |
| MySQL5.7.24 | 4 |
| MySQL5.7.24 | 4 |
| MySQL5.7.24 | 4 |
| MySQL5.7.24 | 4 |
| MySQL5.7.21 | 3 |
+------------------+----------------+
9 rows in set (0.00 sec)
其中:
- execute_status = 4 表示成功,3 表示失??;
- 顯示有一個(gè)實(shí)例失敗,版本是 5.7.21,其他實(shí)例都是 5.7.24,都執(zhí)行成功,這一點(diǎn)很反常。
因此盡管上面三個(gè)案例都是報(bào)錯(cuò)索引超長,但是其中第三個(gè)案例中 5.7.21 報(bào)錯(cuò)的現(xiàn)象比較反常,因此進(jìn)行分析。
分析
索引最大長度
其中對于 InnoDB 存儲引擎,單列索引的最大長度是 767 字節(jié),聯(lián)合索引的最大長度是 3072 字節(jié)。
不同版本的索引最大長度也不同。
其中:
- 5.5 中引入 innodb_large_prefix 參數(shù),5.5 與 5.6 中該參數(shù)默認(rèn)關(guān)閉,5.7 中默認(rèn)開啟。其中:
參數(shù)關(guān)閉時(shí)單列索引的最大長度為 767 字節(jié);
參數(shù)開啟時(shí)單列索引的最大長度為 3072 字節(jié)。
- 8.0 中移除 innodb_large_prefix 參數(shù)。
innodb_large_prefix
參考官方文檔,innodb_large_prefix 參數(shù)用于控制行格式 DYNAMIC or COMPRESSED 中的索引最大長度。
When this option is enabled, index key prefixes longer than 767 bytes (up to 3072 bytes) are allowed for InnoDB tables that use DYNAMIC or COMPRESSED row format.
同時(shí)滿足以下三個(gè)條件時(shí)允許創(chuàng)建 large index(索引最大長度為 3072 字節(jié)):
- ROW_FORMAT = DYNAMIC or COMPRESSED
- innodb_file_format = Barracuda
- innodb_large_prefix = 1
其中有一個(gè)條件不滿足時(shí)索引最大長度為 767,且超長數(shù)據(jù)將被截?cái)唷?/p>
innodb_large_prefix is enabled by default in MySQL 5.7. This change coincides with the default value change for。innodb_file_format, which is set to Barracuda by default in MySQL 5.7. Together, these default value changes allow larger index key prefixes to be created when using DYNAMIC or COMPRESSED row format. If either option is set to a non-default value, index key prefixes larger than 767 bytes are silently truncated.
innodb_large_prefix is deprecated; expect it to be removed in a future release. innodb_large_prefix was introduced to disable large index key prefixes for compatibility with earlier versions of InnoDB that do not support large index key prefixes.
因此對于案例 3,第一個(gè)條件不滿足,原因是行格式指定為 COMPACT,因此索引最大長度為 766 字節(jié),那么超長時(shí)會報(bào)錯(cuò)嗎?
測試
5.7.24
測試環(huán)境 5.7.24 執(zhí)行報(bào)錯(cuò),與官方文檔描述一致,因此報(bào)錯(cuò)是正?,F(xiàn)象。
mysql> create table _mst_sku_new (
`upc_code` varchar(64) DEFAULT NULL COMMENT '69碼',
KEY `idx_sku_upccode` (`upc_code`)
) ENGINE=InnoDB CHARSET=utf8 ROW_FORMAT=COMPACT;
Query OK, 0 rows affected (0.02 sec)
mysql> alter table `_mst_sku_new` MODIFY COLUMN `upc_code` varchar(1000) DEFAULT NULL;
ERROR 1071 (42000): Specified key was too long; max key length is 767 bytes
mysql> select version();
+------------+
| version() |
+------------+
| 5.7.24-log |
+------------+
1 row in set (0.00 sec)
測試環(huán)境 5.7.33 執(zhí)行成功,但是有警告,原因是 sql_mode 為空,表明 sql_mode 的優(yōu)先級高于 ROW_FORMAT。
圖片
線上環(huán)境 5.7.24 執(zhí)行成功,原因是 sql_mode = NO_ENGINE_SUBSTITUTION,因此將報(bào)錯(cuò)降級為警告。
mysql> set session sql_mode='STRICT_TRANS_TABLES';
Query OK, 0 rows affected, 1 warning (0.00 sec)
mysql> alter table `_mst_sku_new` MODIFY COLUMN `upc_code` varchar(1000) DEFAULT NULL;
ERROR 1071 (42000): Specified key was too long; max key length is 767 bytes
mysql> set session sql_mode='NO_ENGINE_SUBSTITUTION';
Query OK, 0 rows affected (0.00 sec)
mysql> alter table `_mst_sku_new` MODIFY COLUMN `upc_code` varchar(1000) DEFAULT NULL;
Query OK, 0 rows affected, 1 warning (0.05 sec)
Records: 0 Duplicates: 0 Warnings: 1
線上環(huán)境 5.7.21 執(zhí)行失敗,可是同樣 sql_mode = NO_ENGINE_SUBSTITUTION,原因是什么呢?
5.7.21
線上環(huán)境 5.7.21 與 5.7.24 配置相同但是報(bào)錯(cuò)。
mysql> create table _mst_sku_new (
`upc_code` varchar(64) DEFAULT NULL COMMENT '69碼',
KEY `idx_sku_upccode` (`upc_code`)
) ENGINE=InnoDB CHARSET=utf8 ROW_FORMAT=COMPACT;
Query OK, 0 rows affected (0.01 sec)
mysql> select @@sql_mode;
+------------------------+
| @@sql_mode |
+------------------------+
| NO_ENGINE_SUBSTITUTION |
+------------------------+
1 row in set (0.00 sec)
mysql> alter table `_mst_sku_new` MODIFY COLUMN `upc_code` varchar(1000) DEFAULT NULL;
ERROR 1709 (HY000): Index column size too large. The maximum column size is 767 bytes.
mysql> alter table _mst_sku_new ROW_FORMAT=dynamic;
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> alter table `_mst_sku_new` MODIFY COLUMN `upc_code` varchar(1000) DEFAULT NULL;
Query OK, 0 rows affected (0.03 sec)
Records: 0 Duplicates: 0 Warnings: 0
其中:
- 報(bào)錯(cuò)不同,Index column size too large. The maximum column size is 767 bytes;
- ROW_FORMAT = COMPACT 報(bào)錯(cuò),理論上不報(bào)錯(cuò),不合理,原因是 sql_mode = NO_ENGINE_SUBSTITUTION;
- ROW_FORMAT = DYNAMIC 不報(bào)錯(cuò),理論上不報(bào)錯(cuò),合理;
因此懷疑 5.7.21 中存在 bug,比如判斷是否支持 create larger index 時(shí)沒有判斷 sql_mode。
5.7.23
查看 release notes,顯示 5.7.23 中修復(fù)了一個(gè) bug,bug 的現(xiàn)象是對于 COMPACT 或 REDUNDANT:
- 嚴(yán)格模式下不報(bào)錯(cuò)
- 非嚴(yán)格模式下不告警
For attempts to increase the length of a VARCHAR column of an InnoDB table using ALTER TABLE with the INPLACE algorithm, the attempt failed if the column was indexed.
If an index size exceeded the InnoDB limit of 767 bytes for COMPACT or REDUNDANT row format, CREATE TABLE and ALTER TABLE did not report an error (in strict SQL mode) or a warning (in nonstrict mode). (Bug #26848813)
對應(yīng) commit 為 MySQL Commit 913071c,下面表格中展示修復(fù)后的行格式與索引長度,以及嚴(yán)格模式與非嚴(yán)格模式下返回報(bào)錯(cuò)還是告警,其中 IL 表示 Index Limit。
Row Format | INDEX LIMIT | STRICT MODE (>IL) | NON-STRICT MODE (>IL) |
Compact/Redundant (Non Unique Index) | 767 bytes | Error | Index truncation (767) and warning |
Compact/Redundant (Unique/Primary Index) | 767 bytes | Error | Error |
Dynamic/Compressed (Non Unique Index) | 3072 bytes | Error | Index truncation (3072) and warning |
Dynamic/Compressed (Unique/Primary Index) | 3072 bytes | Error | Error |
其中當(dāng)索引超長時(shí),返回報(bào)錯(cuò)還是告警由索引類型與 sql_mode 共同決定:
- 唯一索引,對于嚴(yán)格模式與非嚴(yán)格模式,均返回報(bào)錯(cuò);
- 非唯一索引,對于嚴(yán)格模式,返回報(bào)錯(cuò),對于非嚴(yán)格模式,返回警告,并將索引值截?cái)酁榍熬Y索引。
因此,判斷該現(xiàn)象對應(yīng)該 bug,表現(xiàn)為 5.7.21 非嚴(yán)格模式中,非唯一索引超長后返回報(bào)錯(cuò),而不是警告。
debug
debug 數(shù)據(jù)庫版本為 5.7.33,測試索引超長返回警告的堆棧見下圖。
其中有以下兩個(gè)函數(shù):
- mysql_prepare_create_table
- push_warning_printf
commit 中顯示修改 ha_innobase::max_supported_key_part_length 函數(shù)。
因此給以上三個(gè)函數(shù)設(shè)置斷點(diǎn)。
測試顯示行記錄為 COMPACT 時(shí),返回索引最大長度為 767。
圖片
在判斷索引長度超長(1000 * 3 = 3000 > 767)后,判斷返回報(bào)錯(cuò)還是警告。
圖片
其中:
- 如果是唯一索引,返回報(bào)錯(cuò);
- 如果是非唯一索引,繼續(xù)判斷 sq_mode,如果是嚴(yán)格模式,返回報(bào)錯(cuò),否則返回警告,并且將索引長度自動截?cái)鄬?shí)現(xiàn)字節(jié)對齊。變量 key_part_length 從 767 改為 765 字節(jié),對應(yīng) utf8 字符集 255 字符。
相關(guān)代碼如下所示。
// 如果不是唯一索引,也就是二級非唯一索引,根據(jù) sql_mode 判斷是否返回報(bào)錯(cuò)
if (key->type == KEYTYPE_MULTIPLE)
{
/* not a critical problem */
// 警告
push_warning_printf(thd, Sql_condition::SL_WARNING,
ER_TOO_LONG_KEY, ER(ER_TOO_LONG_KEY),
key_part_length);
/* Align key length to multibyte char boundary */
// 索引長度自動截?cái)?,比?767 // 3 = 255
// 將 key_part_length 減少到最接近的整數(shù)倍數(shù),使得它不超過當(dāng)前字符集中最多的多字節(jié)字符長度
key_part_length-= key_part_length % sql_field->charset->mbmaxlen;
/*
If SQL_MODE is STRICT, then report error, else report warning
and continue execution.
*/
// 對于嚴(yán)格模式,將警告升級為錯(cuò)誤
if (thd->is_error())
DBUG_RETURN(true);
}
其中枚舉類型變量 keytype 的定義如下所示,沒有區(qū)分單列索引與聯(lián)合索引,因此判斷 KEYTYPE_MULTIPLE 表示非唯一索引。
enum keytype {
KEYTYPE_PRIMARY,
KEYTYPE_UNIQUE,
KEYTYPE_MULTIPLE,
KEYTYPE_FULLTEXT,
KEYTYPE_SPATIAL,
KEYTYPE_FOREIGN
};
而在 5.7.21 中,返回的索引最大長度等于 3072,大于當(dāng)前字段的長度 3000,因此判斷結(jié)果是索引不超長。
圖片
而在創(chuàng)建索引的時(shí)候還會二次檢查判斷索引長度是否超長。
/* Even though we've defined max_supported_key_part_length, we
still do our own checking using field_lengths to be absolutely
sure we don't create too long indexes. */
error = convert_error_code_to_mysql(
row_create_index_for_mysql(index, trx, field_lengths, handler),
flags, NULL);
其中:
- create_index 函數(shù)中調(diào)用 row_create_index_for_mysql 函數(shù)創(chuàng)建索引;
- row_create_index_for_mysql 函數(shù)中檢查索引的長度與行格式對應(yīng)的索引最大長度,其中通過宏 DICT_MAX_FIELD_LEN_BY_FORMAT 獲取索引長度;
/* Column or prefix length exceeds maximum column length */
if (len > (ulint) DICT_MAX_FIELD_LEN_BY_FORMAT(table)) {
err = DB_TOO_BIG_INDEX_COL;
dict_mem_index_free(index);
goto error_handling;
}
}
- DICT_MAX_FIELD_LEN_BY_FORMAT 宏中根據(jù)行格式返回索引最大長度,COMPACT 對應(yīng) 767;
/** Find out maximum indexed column length by its table format.
For ROW_FORMAT=REDUNDANT and ROW_FORMAT=COMPACT, the maximum
field length is REC_ANTELOPE_MAX_INDEX_COL_LEN - 1 (767). For
Barracuda row formats COMPRESSED and DYNAMIC, the length could
be REC_VERSION_56_MAX_INDEX_COL_LEN (3072) bytes */
#define DICT_MAX_FIELD_LEN_BY_FORMAT(table) \
((dict_table_get_format(table) < UNIV_FORMAT_B) \
? (REC_ANTELOPE_MAX_INDEX_COL_LEN - 1) \
: REC_VERSION_56_MAX_INDEX_COL_LEN)
- 由于 3000 > 767,因此判斷索引超長,最終返回報(bào)錯(cuò);
- 但是為什么非嚴(yán)格模式下沒有將報(bào)錯(cuò)降級為警告的原因暫時(shí)沒查到。
處理
時(shí)間:2024-11-24 02:00:27
10個(gè)月以后,這套該分庫分表給其他字段擴(kuò)展長度時(shí)再次觸發(fā)該問題,因此決定進(jìn)行處理,具體是將數(shù)據(jù)庫從 5.7.21 升級到 5.7.24。
而在升級后發(fā)現(xiàn)兩個(gè)現(xiàn)象:
- 索引中字符長度自動調(diào)整為 255,正?,F(xiàn)象;
- 不小心又踩坑了,先升級的主庫,執(zhí)行 DDL 后導(dǎo)致從庫復(fù)制中斷,異常現(xiàn)象。
如下所示,對比執(zhí)行失敗與執(zhí)行成功時(shí)的索引長度。
# 失敗后
KEY `idx_dispatch_no` (`dispatch_no`)
# 成功后
KEY `idx_dispatch_no` (`dispatch_no`(255)),
官方文檔顯示,從 5.7.17 版本開始:
- 對于非唯一索引,如果是非嚴(yán)格模式,索引超長后返回警告,并自動截?cái)嗟街С值乃饕畲箝L度;
- 對于唯一索引,索引超長后直接報(bào)錯(cuò),不會發(fā)生截?cái)啵蚴墙財(cái)嗪罂赡軐?dǎo)致唯一性約束失效。
As of MySQL 5.7.17, if a specified index prefix exceeds the maximum column data type size, CREATE INDEX handles the index as follows:
For a nonunique index, either an error occurs (if strict SQL mode is enabled), or the index length is reduced to lie within the maximum column data type size and a warning is produced (if strict SQL mode is not enabled).
For a unique index, an error occurs regardless of SQL mode because reducing the index length might enable insertion of nonunique entries that do not meet the specified uniqueness requirement.
如下所示,進(jìn)行測試。
其中:
- 嚴(yán)格模式,非唯一索引,索引超長后報(bào)錯(cuò);
- 非嚴(yán)格模式,非唯一索引,索引超長后警告,并自動截?cái)啵?/li>
- 非嚴(yán)格模式,唯一索引,索引超長后報(bào)錯(cuò)。
主庫升級后使用 pt-osc 執(zhí)行 DDL 導(dǎo)致從庫復(fù)制中斷,原因是從庫未升級。
重試時(shí)發(fā)生異常,日志顯示執(zhí)行暫停。
2024-11-25T11:35:07 Copying approximately 764 rows...
Replica MSS-2hbqmzhk2m is stopped. Waiting.
Killed
查看復(fù)制,顯示復(fù)制中斷,原因是從庫執(zhí)行 DDL 報(bào)錯(cuò),pt-osc 延遲檢測期間發(fā)現(xiàn)復(fù)制中斷后執(zhí)行暫停。
mysql> SHOW SLAVE STATUS \G
*************************** 1. row ***************************
Last_SQL_Errno: 1709
Last_SQL_Error: Coordinator stopped because there were error(s) in the worker(s). The most recent failure being: Worker 1 failed executing transaction '965c7418-175f-11ee-b6d3-fa163eae0649:12102' at master log mysql-bin.146487, end_log_pos 8137214. See error log and/or performance_schema.replication_applier_status_by_worker table for more details about this failure or others, if any.
Last_SQL_Error_Timestamp: 241125 11:35:07
Executed_Gtid_Set: 965c7418-175f-11ee-b6d3-fa163eae0649:1-12101
Auto_Position: 1
1 row in set (0.00 sec)
mysql> select * from performance_schema.replication_applier_status_by_worker limit 1 \G
*************************** 1. row ***************************
CHANNEL_NAME:
WORKER_ID: 1
THREAD_ID: NULL
SERVICE_STATE: OFF
LAST_SEEN_TRANSACTION: 965c7418-175f-11ee-b6d3-fa163eae0649:12102
LAST_ERROR_NUMBER: 1709
LAST_ERROR_MESSAGE: Worker 1 failed executing transaction '965c7418-175f-11ee-b6d3-fa163eae0649:12102' at master log mysql-bin.146487, end_log_pos 8137214; Error 'Index column size too large. The maximum column size is 767 bytes.' on query. Default database: 'wms3'. Query: 'ALTER TABLE `wms3`.`_task_group_new` MODIFY COLUMN dispatch_no varchar(500) NULL COMMENT '派車單號''
LAST_ERROR_TIMESTAMP: 2024-11-25 11:35:07
1 row in set (0.01 sec)
知識點(diǎn)
ROW_FORMAT
innodb_default_row_format 參數(shù)用于控制默認(rèn)行格式,取值與版本有關(guān):
- 5.0.3 版本之前,僅支持一種行格式 REDUNDANT;
- 5.0.3 - 5.7.8,默認(rèn)行格式為 COMPACT;
- 從 5.7.9 版本開始,默認(rèn)行格式為 DYNAMIC,包括 8.0。
行格式 COMPACT 與 DYNAMIC 的主要區(qū)別是行溢出(一個(gè)列中存儲的數(shù)據(jù)大于等于8098個(gè)字節(jié))數(shù)據(jù)的保存方式不同,其中:
- COMPACT,在記錄的真實(shí)數(shù)據(jù)處存儲字段真實(shí)數(shù)據(jù)的前 768 個(gè)字節(jié),剩余數(shù)據(jù)保存在其他頁中,并在真實(shí)數(shù)據(jù)中保存溢出頁地址;
- DYNAMIC,把所有的字節(jié)都存儲到其他頁面中,只在記錄的真實(shí)數(shù)據(jù)處存儲其他頁面的地址。
圖片
因此在數(shù)據(jù)庫升級過程中也需要關(guān)注行格式。
故障分析 | ERROR 1709: Index column size too large 引發(fā)的思考 文章中分享了一個(gè)案例,現(xiàn)象是數(shù)據(jù)庫重啟后有張表無法訪問,SELECT、DML 和 DDL 執(zhí)行均報(bào)錯(cuò) ERROR 1709 (HY000): Index column size too large. The maximum column size is 767 bytes.。
復(fù)現(xiàn)流程如下所示:
- 數(shù)據(jù)庫從 5.6.21 原地升級到 8.0.21,升級之前創(chuàng)建的一個(gè)表未指定行格式,因此使用默認(rèn)行格式 COMPACT;
- 升級后添加字段并創(chuàng)建索引,索引超長但是沒有報(bào)錯(cuò),也沒有警告;
- 數(shù)據(jù)庫重啟前,表可以正常訪問;
- 數(shù)據(jù)庫重啟后,表無法訪問,報(bào)錯(cuò)索引超長。
最終定位到也是一個(gè) bug,具體表現(xiàn)為非顯式定義的 redundant 行格式表允許創(chuàng)建的索引列大小超 767 bytes,并在 8.0.22 版本中修復(fù)。
因此建議在數(shù)據(jù)庫升級前檢查隱式創(chuàng)建行格式為 compact/redundant 的表,并顯式指定。
相關(guān)案例
下面引申一個(gè)話題,SQL 工單中遇到過 goinception 語法校驗(yàn)通過,但是執(zhí)行時(shí)報(bào)錯(cuò)行超長的現(xiàn)象,因此分別測試 goinception 是否可以識別字段超長與行超長。
已知:
- 對于VARCHAR(M)類型的列最多可以占用65535個(gè)字節(jié)。其中的M代表該類型最多存儲的字符數(shù)量;
- MySQL對一條記錄占用的最大存儲空間是有限制的,除了BLOB或者TEXT類型的列之外,其他所有的列(不包括隱藏列和記錄頭信息)占用的字節(jié)長度加起來不能超過65535個(gè)字節(jié)。
字段超長
SQL
create table,155355 * 3 > 65535,因此字段長度超長。
create table ttt(
id int primary key auto_increment comment 'id',
a varchar(155355) default '' comment 'a'
) comment 'ttt';
goinception 返回報(bào)錯(cuò)字段超長,建議使用大字段替換 varchar。
Column length too big for column 'a' (max = 21845); use BLOB or TEXT instead. Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. This includes storage overhead, check the manual. You have to change some columns to TEXT or BLOBs.
add column;
alter table t1 add column aa varchar(155355) default '' comment 'a';
goinception;
Column length too big for column 'aa' (max = 21845); use BLOB or TEXT instead.
modify column;
alter table t1 modify column a varchar(155355) default '' comment 'a';
goinception;
Column length too big for column 'a' (max = 21845); use BLOB or TEXT instead.
因此,測試顯示 goinception 可以驗(yàn)證字段超長,包括建表與改表時(shí),那么是否可以驗(yàn)證行超長?
行超長
create table,15535 * 3 * 2 = 93210 > 65535,因此雖然單個(gè)字段不超長,但是行超長。
create table ttt(
id int primary key auto_increment comment 'id',
a varchar(15535) default '' comment 'a',
b varchar(15535) default '' comment 'b'
) comment 'ttt';
goinception 返回校驗(yàn)通過,當(dāng)然實(shí)際執(zhí)行會失敗。
{
"id": 1,
"stage": "CHECKED",
"errlevel": 0,
"stagestatus": "Audit Completed",
"errormessage": "",
"sql": "USE `cctest`",
"affected_rows": 0,
"sequence": "0_0_00000000",
"backup_dbname": "",
"execute_time": "0",
"sqlsha1": "",
"backup_time": "0",
"actual_affected_rows": ""
}
因此結(jié)論是 goinception 可以發(fā)現(xiàn)單字段超長,但是無法發(fā)現(xiàn)多字段導(dǎo)致的行超長。
因此,SQL 工單中自定義行超長校驗(yàn),調(diào)用接口返回報(bào)錯(cuò)。
{
"code": 16,
"message": "SQLCheckMaxRowSizeError",
"error": "Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. This includes storage overhead, check the manual. You have to change some columns to TEXT or BLOBs. 庫:cctest,表:ttt,行大小為:93214,超過最大行大小65535字節(jié),請修改字段長度或類型"
}
原因是代碼中自行實(shí)現(xiàn)行超長檢測,并自定義異常類。
class SQLCheckMaxRowSizeError(BaseError):
def __init__(self, db_name="", table="", row_size=""):
BaseError.__init__(
self, code=SQL_Check_MAX_ROW_SIZE_Error, message="SQLCheckMaxRowSizeError",
error="Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. "
"This includes storage overhead, check the manual. You have to change some columns to TEXT or BLOBs." " 庫:%s,表:%s,行大小為:%s,超過最大行大小65535字節(jié),請修改字段長度或類型" % (db_name, table, row_size)
)
結(jié)論
MySQL 5.5 中引入 innodb_large_prefix 參數(shù),5.5 與 5.6 中該參數(shù)默認(rèn)關(guān)閉,5.7 中默認(rèn)開啟。
innodb_large_prefix 參數(shù)用于控制行格式 DYNAMIC or COMPRESSED 中的索引最大長度。
5.7 中同時(shí)滿足以下三個(gè)條件時(shí)索引最大長度為 3072 字節(jié):
- ROW_FORMAT = DYNAMIC or COMPRESSED
- innodb_file_format = Barracuda
- innodb_large_prefix = 1
其中有一個(gè)條件不滿足時(shí)索引最大長度等于 767 字節(jié)。因此對于行格式 COMPACT,索引最大長度為 767 字節(jié)。
當(dāng)索引超長時(shí),返回報(bào)錯(cuò)還是告警由索引類型與 sql_mode 共同決定:
- 唯一索引,對于嚴(yán)格模式與非嚴(yán)格模式,均返回報(bào)錯(cuò),注意不允許截?cái)啵駝t可能導(dǎo)致索引失效;
- 非唯一索引,對于嚴(yán)格模式,返回報(bào)錯(cuò),對于非嚴(yán)格模式,返回警告,并將索引值截?cái)酁榍熬Y索引。
而本文中 5.7.21 版本中的現(xiàn)象與上述描述不符,非嚴(yán)格模式中,非唯一索引超長后返回報(bào)錯(cuò),而不是警告。因此判斷該現(xiàn)象是 bug。
分析代碼后發(fā)現(xiàn),有兩次索引長度檢查,但是索引最大長度的判斷條件不一致:
- 第一次,索引最大長度由 innodb_large_prefix 決定,參數(shù)開啟時(shí)返回 3072;
- 第二次,索引最大長度由行格式?jīng)Q定,COMPACT 對應(yīng) 767。
因此在 5.7.21 中當(dāng) COMPACT 開啟 innodb_large_prefix 時(shí),將導(dǎo)致第一次檢查通過,第二次檢查報(bào)錯(cuò),但是具體為什么沒有將報(bào)錯(cuò)降級為警告的原因暫未查到。
而在 8.0 中移除了 innodb_large_prefix 參數(shù),索引最大長度統(tǒng)一由行格式?jīng)Q定,這樣也就避免了該問題。
回過頭來分析最初的三個(gè)案例,其中:
- 案例 1,5.6.39,隱式 COMPACT,單列唯一索引報(bào)錯(cuò)超長 767,報(bào)錯(cuò)正常;
- 案例 2,5.7.33,隱式 COMPACT,聯(lián)合唯一索引報(bào)錯(cuò)超長 3072,報(bào)錯(cuò)正常;
- 案例 3,5.7.21,顯式 COMPACT,單列非唯一索引報(bào)錯(cuò)超長 767,報(bào)錯(cuò)不正常,正常應(yīng)該是警告。
注意都是非嚴(yán)格模式。