使用_tidb_rowid进行小范围查询时执行效率异常

【 TiDB 使用环境】生产环境
【 TiDB 版本】5.0.2
【描述】
最近使用批量更新工具清理一个表的数据时遇到一种异常的情况,因此发出来一下。
工具之前有介绍过: 使用tidb-toolkit批量删除/更新数据
原始SQL为:

delete from db.t where  dt < '2023-06-01';

生成的批量删除SQL执行日志为:

[2023-08-23 16:57:11,381] [INFO] [tk_chunk_update.py:215]: chunk 131491 Done [split_time=0:00:00.014420] [duration=0:00:41.683770] [rows=1000] [sql=DELETE FROM db.t WHERE  dt < '2023-06-01' and (`t`.`_tidb_rowid` >= 252168339 and `t`.`_tidb_rowid` < 252169339)]
[2023-08-23 16:57:11,390] [INFO] [tk_chunk_update.py:215]: chunk 131498 Done [split_time=0:00:00.013967] [duration=0:00:40.087776] [rows=1000] [sql=DELETE FROM db.t WHERE  dt < '2023-06-01' and (`t`.`_tidb_rowid` >= 252175339 and `t`.`_tidb_rowid` < 252176339)]
[2023-08-23 16:57:11,391] [INFO] [tk_chunk_update.py:302]: write savepoint 252178339, complete percent: 73.18%

这里贴一下拆分出来的SQL以及表结构:
DELETE FROM db.t WHERE dt < '2023-06-01' and (t._tidb_rowid>= 252168339 andt._tidb_rowid < 252169339)

CREATE TABLE `t` (
  `book_id` int(11) NOT NULL COMMENT '主键',
  `dt` date NOT NULL,
  `entity_id` bigint(20) NOT NULL,
  `content_type` int(11) NOT NULL DEFAULT '0',
  `book_name` varchar(255) DEFAULT NULL,
  `inter_name` varchar(255) DEFAULT NULL,
  `author` varchar(255) DEFAULT NULL,
  `translator` varchar(255) DEFAULT NULL,
  `contract_id` bigint(10) DEFAULT NULL,
  `word_count` bigint(20) DEFAULT NULL,
  `book_type` int(4) DEFAULT NULL,
  `file_type` int(4) DEFAULT NULL,
  `desc_brief` varchar(255) DEFAULT NULL,
  `book_url` varchar(255) DEFAULT NULL,
  `volume_count` bigint(10) DEFAULT NULL,
  `chapter_count` bigint(10) DEFAULT NULL,
  `created_time` datetime DEFAULT NULL,
  `updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
  PRIMARY KEY (`dt`,`book_id`) /*T![clustered_index] NONCLUSTERED */,
  UNIQUE KEY `ix_uniq_entityId` (`dt`,`entity_id`),
  KEY `ix_contractId` (`contract_id`)
)

【问题为】:
最初这个SQL的执行效率还是很好的,执行计划显示直接通过TableRangeScan实现了rowid的快速扫描。
image

但是后来突然发现执行效率变慢了,执行计划变为了:


很迷惑的去使用了唯一索引,因此单个SQL的执行耗时从几十ms变为了40多秒。
尝试加上/*+ use_index(t) */的注释后执行计划可以恢复到最初的表范围扫描(我期望的方式)。
计划为批量更新工具添加hint,当rowid为主键时就 use_index(t,PRIMARY),为隐式的_tidb_rowid时就添加use_index(t),应该可以增强稳定性。

1 个赞

大量的删除之后,GC 之前这些数据还存在,就会引发以下2个场景:

  • 采样信息不准确,影响执行计划
  • 大量数据的删除,会有额外的扫描跳跃的动作,会有小量的损耗

解决方式:

  • 采用时间分区的方式,通过 truncate 来释放分区的数据,减少 Delete 操作,会更有效
  • 大量删除之后,尽量快速的执行 GC,释放掉版本数据
  • 手动执行数据收集,增加该表的健康度 或者 通过 hint 来解决这个问题也可
1 个赞

DELETE FROM db.t WHERE dt < '2023-06-01' and ( t. _tidb_rowid>= 252168339 and t. _tidb_rowid < 252169339);
这种sql执行如果符合dt < '2023-06-01’条件的数据量很少,就会走dt条件的索引,如果很多就会走tablerangescan,推测一开始符合条件的数据量很多,所以走了tablerangescan,后面数据量变少之后,开始走dt条件的索引ix_uniq_entityId。
我觉得你可以看一下这个表的统计信息,是不是已经过期,走上了pseudo了

1 个赞

回复楼上两位好心人 :heart:
统计信息现在看正常,没有analyze历史,我们每天会有定时任务手动更新统计信息。此外我这边gc间隔较短,并发只有25个,每个并发一次删1000条数据,不容易产生堆积。
数据量方面也应该不到足以放弃tableRangeScan转为走唯一键的程度,因为要删除的数据实际还很多,相比1000条的rowid范围算很小了。
但目前确实也只看作是统计信息有变动导致执行计划有变,我觉得优化器没有对这方面做专门的加强。
当前改进是先加了hint,看看以后还会不会出问题了,针对本次效果还是很好的:

此话题已在最后回复的 60 天后被自动关闭。不再允许新回复。