引言
本文主要分析一套 MySQL 分库分表扩展字段长度时其中一个实例报错索引超长的案例,其中失败实例的版本是 5.7.21,而成功实例的版本都是 5.7.24。因此怀疑与版本有关,最终通过测试与分析判断是一个 bug,官方文档显示在 5.7.23 中修复。
现象
首先介绍三个案例,都是字段长度扩展时报错索引超长。
案例 1
时间:2023-09-08 21:31:02
数据库版本:5.6.39
SQL
ALTER TABLE sign_bill_return_image_audit_result
MODIFY COLUMN image_name VARCHAR(250) COMMENT '图片名称';
日志显示 pt-osc 执行期间报错索引长度超过 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
查看表结构,显示字符集为 utf8mb4,索引类型为单列唯一索引,image_name 字段长度从 50 扩展到 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='签单返还图片审核结果表'
1 row in set (0.00 sec)
查看参数,显示未开启 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
时间:2024-08-19 14:18:31
现象:上游字段扩展,因此下游修改,但是执行报错联合索引超长
数据库版本:5.7.33
SQL
alter table worker_board_quota_counting
modify column `business_id` varchar(1456) NOT NULL COMMENT '业务id';
日志显示 pt-osc 执行期间报错索引长度超过 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 '业务id';"] at /usr/bin/pt-online-schema-change line 9194.
查看表结构,显示字符集为 utf8mb4,索引类型为联合唯一索引,business_id 字段长度从 456 扩展到 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 '业务id',
UNIQUE KEY `idx_source_businessid` (`source`,`business_id`),
) ENGINE=InnoDB AUTO_INCREMENT=19747573 DEFAULT CHARSET=utf8mb4 COMMENT='人员看板计提表'
1 row in set (0.00 sec)
查看参数,显示已开启 innodb_large_prefix。
mysql> select @@innodb_large_prefix;
+-----------------------+
| @@innodb_large_prefix |
+-----------------------+
| 1 |
+-----------------------+
1 row in set (0.00 sec)
案例 3
时间:2024-01-22 23:59:12
工单类型:分库分表
数据库版本:5.7.21 报错,5.7.24 不报错
SQL
ALTER TABLE mst_sku
modify `upc_code` varchar(1000) DEFAULT NULL COMMENT '69码';
日志显示 pt-osc 执行期间报错索引长度超过 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.
查看表结构,显示字符集为 utf8,索引类型为单列非唯一索引,upc_code 字段长度从 64 扩展到 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)
查看参数,显示已开启 innodb_large_prefix。
mysql> select @@innodb_large_prefix;
+-----------------------+
| @@innodb_large_prefix |
+-----------------------+
| 1 |
+-----------------------+
1 row in set (0.00 sec)
由于分库分表工单中只有一个实例报错,因此查看每个实例的数据库版本与执行结果。
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 表示失败;
- 显示有一个实例失败,版本是 5.7.21,其他实例都是 5.7.24,都执行成功,这一点很反常。
因此尽管上面三个案例都是报错索引超长,但是其中第三个案例中 5.7.21 报错的现象比较反常,因此进行分析。
分析
索引最大长度
其中对于 InnoDB 存储引擎,单列索引的最大长度是 767 字节,联合索引的最大长度是 3072 字节。
不同版本的索引最大长度也不同。
其中:
- 5.5 中引入 innodb_large_prefix 参数,5.5 与 5.6 中该参数默认关闭,5.7 中默认开启。其中:
参数关闭时单列索引的最大长度为 767 字节;
参数开启时单列索引的最大长度为 3072 字节。
- 8.0 中移除 innodb_large_prefix 参数。
innodb_large_prefix
参考官方文档,innodb_large_prefix 参数用于控制行格式 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.
同时满足以下三个条件时允许创建 large index(索引最大长度为 3072 字节):
- ROW_FORMAT = DYNAMIC or COMPRESSED
- innodb_file_format = Barracuda
- innodb_large_prefix = 1
其中有一个条件不满足时索引最大长度为 767,且超长数据将被截断。
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,第一个条件不满足,原因是行格式指定为 COMPACT,因此索引最大长度为 766 字节,那么超长时会报错吗?
测试
5.7.24
测试环境 5.7.24 执行报错,与官方文档描述一致,因此报错是正常现象。
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)
测试环境 5.7.33 执行成功,但是有警告,原因是 sql_mode 为空,表明 sql_mode 的优先级高于 ROW_FORMAT。
图片
线上环境 5.7.24 执行成功,原因是 sql_mode = NO_ENGINE_SUBSTITUTION,因此将报错降级为警告。
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
线上环境 5.7.21 执行失败,可是同样 sql_mode = NO_ENGINE_SUBSTITUTION,原因是什么呢?
5.7.21
线上环境 5.7.21 与 5.7.24 配置相同但是报错。
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
其中:
- 报错不同,Index column size too large. The maximum column size is 767 bytes;
- ROW_FORMAT = COMPACT 报错,理论上不报错,不合理,原因是 sql_mode = NO_ENGINE_SUBSTITUTION;
- ROW_FORMAT = DYNAMIC 不报错,理论上不报错,合理;
因此怀疑 5.7.21 中存在 bug,比如判断是否支持 create larger index 时没有判断 sql_mode。
5.7.23
查看 release notes,显示 5.7.23 中修复了一个 bug,bug 的现象是对于 COMPACT 或 REDUNDANT:
- 严格模式下不报错
- 非严格模式下不告警
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)
对应 commit 为 MySQL Commit 913071c,下面表格中展示修复后的行格式与索引长度,以及严格模式与非严格模式下返回报错还是告警,其中 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 |
其中当索引超长时,返回报错还是告警由索引类型与 sql_mode 共同决定:
- 唯一索引,对于严格模式与非严格模式,均返回报错;
- 非唯一索引,对于严格模式,返回报错,对于非严格模式,返回警告,并将索引值截断为前缀索引。
因此,判断该现象对应该 bug,表现为 5.7.21 非严格模式中,非唯一索引超长后返回报错,而不是警告。
debug
debug 数据库版本为 5.7.33,测试索引超长返回警告的堆栈见下图。
其中有以下两个函数:
- mysql_prepare_create_table
- push_warning_printf
commit 中显示修改 ha_innobase::max_supported_key_part_length 函数。
因此给以上三个函数设置断点。
测试显示行记录为 COMPACT 时,返回索引最大长度为 767。
图片
在判断索引长度超长(1000 * 3 = 3000 > 767)后,判断返回报错还是警告。
图片
其中:
- 如果是唯一索引,返回报错;
- 如果是非唯一索引,继续判断 sq_mode,如果是严格模式,返回报错,否则返回警告,并且将索引长度自动截断实现字节对齐。变量 key_part_length 从 767 改为 765 字节,对应 utf8 字符集 255 字符。
相关代码如下所示。
// 如果不是唯一索引,也就是二级非唯一索引,根据 sql_mode 判断是否返回报错
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 */
// 索引长度自动截断,比如 767 // 3 = 255
// 将 key_part_length 减少到最接近的整数倍数,使得它不超过当前字符集中最多的多字节字符长度
key_part_length-= key_part_length % sql_field->charset->mbmaxlen;
/*
If SQL_MODE is STRICT, then report error, else report warning
and continue execution.
*/
// 对于严格模式,将警告升级为错误
if (thd->is_error())
DBUG_RETURN(true);
}
其中枚举类型变量 keytype 的定义如下所示,没有区分单列索引与联合索引,因此判断 KEYTYPE_MULTIPLE 表示非唯一索引。
enum keytype {
KEYTYPE_PRIMARY,
KEYTYPE_UNIQUE,
KEYTYPE_MULTIPLE,
KEYTYPE_FULLTEXT,
KEYTYPE_SPATIAL,
KEYTYPE_FOREIGN
};
而在 5.7.21 中,返回的索引最大长度等于 3072,大于当前字段的长度 3000,因此判断结果是索引不超长。
图片
而在创建索引的时候还会二次检查判断索引长度是否超长。
/* 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 函数中调用 row_create_index_for_mysql 函数创建索引;
- row_create_index_for_mysql 函数中检查索引的长度与行格式对应的索引最大长度,其中通过宏 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 宏中根据行格式返回索引最大长度,COMPACT 对应 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,因此判断索引超长,最终返回报错;
- 但是为什么非严格模式下没有将报错降级为警告的原因暂时没查到。
处理
时间:2024-11-24 02:00:27
10个月以后,这套该分库分表给其他字段扩展长度时再次触发该问题,因此决定进行处理,具体是将数据库从 5.7.21 升级到 5.7.24。
而在升级后发现两个现象:
- 索引中字符长度自动调整为 255,正常现象;
- 不小心又踩坑了,先升级的主库,执行 DDL 后导致从库复制中断,异常现象。
如下所示,对比执行失败与执行成功时的索引长度。
# 失败后
KEY `idx_dispatch_no` (`dispatch_no`)
# 成功后
KEY `idx_dispatch_no` (`dispatch_no`(255)),
官方文档显示,从 5.7.17 版本开始:
- 对于非唯一索引,如果是非严格模式,索引超长后返回警告,并自动截断到支持的索引最大长度;
- 对于唯一索引,索引超长后直接报错,不会发生截断,原因是截断后可能导致唯一性约束失效。
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.
如下所示,进行测试。
其中:
- 严格模式,非唯一索引,索引超长后报错;
- 非严格模式,非唯一索引,索引超长后警告,并自动截断;
- 非严格模式,唯一索引,索引超长后报错。
主库升级后使用 pt-osc 执行 DDL 导致从库复制中断,原因是从库未升级。
重试时发生异常,日志显示执行暂停。
2024-11-25T11:35:07 Copying approximately 764 rows...
Replica MSS-2hbqmzhk2m is stopped. Waiting.
Killed
查看复制,显示复制中断,原因是从库执行 DDL 报错,pt-osc 延迟检测期间发现复制中断后执行暂停。
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)
知识点
ROW_FORMAT
innodb_default_row_format 参数用于控制默认行格式,取值与版本有关:
- 5.0.3 版本之前,仅支持一种行格式 REDUNDANT;
- 5.0.3 - 5.7.8,默认行格式为 COMPACT;
- 从 5.7.9 版本开始,默认行格式为 DYNAMIC,包括 8.0。
行格式 COMPACT 与 DYNAMIC 的主要区别是行溢出(一个列中存储的数据大于等于8098个字节)数据的保存方式不同,其中:
- COMPACT,在记录的真实数据处存储字段真实数据的前 768 个字节,剩余数据保存在其他页中,并在真实数据中保存溢出页地址;
- DYNAMIC,把所有的字节都存储到其他页面中,只在记录的真实数据处存储其他页面的地址。
图片
因此在数据库升级过程中也需要关注行格式。
故障分析 | ERROR 1709: Index column size too large 引发的思考 文章中分享了一个案例,现象是数据库重启后有张表无法访问,SELECT、DML 和 DDL 执行均报错 ERROR 1709 (HY000): Index column size too large. The maximum column size is 767 bytes.。
复现流程如下所示:
- 数据库从 5.6.21 原地升级到 8.0.21,升级之前创建的一个表未指定行格式,因此使用默认行格式 COMPACT;
- 升级后添加字段并创建索引,索引超长但是没有报错,也没有警告;
- 数据库重启前,表可以正常访问;
- 数据库重启后,表无法访问,报错索引超长。
最终定位到也是一个 bug,具体表现为非显式定义的 redundant 行格式表允许创建的索引列大小超 767 bytes,并在 8.0.22 版本中修复。
因此建议在数据库升级前检查隐式创建行格式为 compact/redundant 的表,并显式指定。
相关案例
下面引申一个话题,SQL 工单中遇到过 goinception 语法校验通过,但是执行时报错行超长的现象,因此分别测试 goinception 是否可以识别字段超长与行超长。
已知:
- 对于VARCHAR(M)类型的列最多可以占用65535个字节。其中的M代表该类型最多存储的字符数量;
- MySQL对一条记录占用的最大存储空间是有限制的,除了BLOB或者TEXT类型的列之外,其他所有的列(不包括隐藏列和记录头信息)占用的字节长度加起来不能超过65535个字节。
字段超长
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 返回报错字段超长,建议使用大字段替换 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 可以验证字段超长,包括建表与改表时,那么是否可以验证行超长?
行超长
create table,15535 * 3 * 2 = 93210 > 65535,因此虽然单个字段不超长,但是行超长。
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 返回校验通过,当然实际执行会失败。
{
"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": ""
}
因此结论是 goinception 可以发现单字段超长,但是无法发现多字段导致的行超长。
因此,SQL 工单中自定义行超长校验,调用接口返回报错。
{
"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字节,请修改字段长度或类型"
}
原因是代码中自行实现行超长检测,并自定义异常类。
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字节,请修改字段长度或类型" % (db_name, table, row_size)
)
结论
MySQL 5.5 中引入 innodb_large_prefix 参数,5.5 与 5.6 中该参数默认关闭,5.7 中默认开启。
innodb_large_prefix 参数用于控制行格式 DYNAMIC or COMPRESSED 中的索引最大长度。
5.7 中同时满足以下三个条件时索引最大长度为 3072 字节:
- ROW_FORMAT = DYNAMIC or COMPRESSED
- innodb_file_format = Barracuda
- innodb_large_prefix = 1
其中有一个条件不满足时索引最大长度等于 767 字节。因此对于行格式 COMPACT,索引最大长度为 767 字节。
当索引超长时,返回报错还是告警由索引类型与 sql_mode 共同决定:
- 唯一索引,对于严格模式与非严格模式,均返回报错,注意不允许截断,否则可能导致索引失效;
- 非唯一索引,对于严格模式,返回报错,对于非严格模式,返回警告,并将索引值截断为前缀索引。
而本文中 5.7.21 版本中的现象与上述描述不符,非严格模式中,非唯一索引超长后返回报错,而不是警告。因此判断该现象是 bug。
分析代码后发现,有两次索引长度检查,但是索引最大长度的判断条件不一致:
- 第一次,索引最大长度由 innodb_large_prefix 决定,参数开启时返回 3072;
- 第二次,索引最大长度由行格式决定,COMPACT 对应 767。
因此在 5.7.21 中当 COMPACT 开启 innodb_large_prefix 时,将导致第一次检查通过,第二次检查报错,但是具体为什么没有将报错降级为警告的原因暂未查到。
而在 8.0 中移除了 innodb_large_prefix 参数,索引最大长度统一由行格式决定,这样也就避免了该问题。
回过头来分析最初的三个案例,其中:
- 案例 1,5.6.39,隐式 COMPACT,单列唯一索引报错超长 767,报错正常;
- 案例 2,5.7.33,隐式 COMPACT,联合唯一索引报错超长 3072,报错正常;
- 案例 3,5.7.21,显式 COMPACT,单列非唯一索引报错超长 767,报错不正常,正常应该是警告。
注意都是非严格模式。