背景

由于业务变迁,合规要求,我们需要删除大量非本公司的数据,涉及到上百张表,几个T的数据清洗。我们的做法是先从基础数据出发,将要删除的数据id收集到一张表,然后再由上往下删除子表,多线程并发处理。

我们使用的是阿里的polardb,完全兼容mysql协议,5.7版本,RC隔离级别。删除过程一直很顺利,突然有一天报了大量:“Lock wait timeout exceeded; try restarting transaction”。从日志上看是获取锁失败了,马上想到出现死锁了,但我们使用RC,这个隔离级别下会出现不可重复读和幻读,但没有间隙锁等,并发效率比较高,在我们实际应用过程中,也很少遇到加锁失败的问题。

单从日志看我们确实先入为主了,以为是死锁问题,但sql比较简单,表数据量在千万级别,其中task_id和uid均有索引,如下:

delete from t_table_1 where task_id in (select id from t_table_2 where uid = #{uid})

拿到报错的参数,查询要删除的数据也不多,联系dba同学确认没有死锁日志,但出现大量慢sql,那为什么这条sql会是慢sql呢?

问题复现

表结构简化如下:

CREATE TABLE `t_table_1` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`task_id` bigint(20) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_task_id` (`task_id`)
) ENGINE=InnoDB; CREATE TABLE `t_table_2` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`uid` bigint(20) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_uid` (`uid`)
) ENGINE=InnoDB;

开始我们拿sql到数据库查询平台查库执行计划,无奈这个平台有bug,delete语句无法查看,所以我们改成select,“应该”是一样。这个“应该”加了双引号,导致我们走了一点弯路。

EXPLAIN SELECT * from t_table_1 where task_id in (select id from t_table_2 where uid = 1)

explain后可以看到是走了索引的

到这里可以总结:

1.没有死锁,这点比较肯定,因为没有日志,也符合我们的理解。

2.有慢sql,这点比较奇怪,通过explain select语句是走索引的,但数据库慢日志记录到,全表扫描,不会错。

那是select和delete的执行计划不同吗?正常来说应该是一样的,delete无非就是先查,加锁,再删。

拿到本地环境执行再次查看执行计划,发现确实不同,select的是一样的,但delete的变成全表扫描了。

首先这就符合问题现象了,虽然没有死锁,但每个delete语句都全表扫描,相当于全表加锁,后面的请求就只能等待释放锁,等到超时就出现“Lock wait timeout exceeded”。

那为什么delete会不走索引呢,接下来我们分析一下。

分析

select * from t_table_1 where task_id in (select id from t_table_2 where uid = #{uid})

回到这条简单sql,包含子查询,按照我们的理解,mysql应该是先执行子查询:select id from t_table_2 where uid = #{uid},然后再执行外部查询:select * from t_table_1 where task_id in(),但这不一定,例如我关了这个参数:

set optimizer_switch='semijoin=off';

这里我们先不用管这个参数的作用,下面会说到。

关闭后上面的sql就变成先扫描外部的t_table_1,然后再逐行去匹配子查询了,假设t_table_1的数据量非常大,那全表扫描时间就会很长,我们可以通过optimizer_trace证明一下。

optimizer_trace是mysql一个跟踪功能,可以跟踪优化器做的各种决策,包括sql改写,成本计算,索引选择详细过程,并将跟踪结果记录到INFORMATION_SCHEMA.OPTIMIZER_TRACE表中。

set session optimizer_trace="enabled=on";
set OPTIMIZER_TRACE_MAX_MEM_SIZE=10000000; -- 防止内容过多被截断
SELECT * from t_table_1 where task_id in (select id from t_table_2 where uid = 1)
SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE;

输出结果比较长,这里我只挑选主要信息

"steps": [
{
"expanded_query": "/* select#2 */ select `t_table_2`.`id` from `t_table_2` where (`t_table_2`.`uid` = 1)"
},
{
"transformation": {
"select#": 2,
"from": "IN (SELECT)",
"to": "semijoin",
"chosen": false
}
},
{
"transformation": {
"select#": 2,
"from": "IN (SELECT)",
"to": "EXISTS (CORRELATED SELECT)",
"chosen": true,
"evaluating_constant_where_conditions": [
]
}
}
] "expanded_query": "/* select#1 */ select `t_table_1`.`id` AS `id`,`t_table_1`.`task_id` AS `task_id` from `t_table_1` where <in_optimizer>(`t_table_1`.`task_id`,<exists>(/* select#2 */ select `t_table_2`.`id` from `t_table_2` where ((`t_table_2`.`uid` = 1) and (<cache>(`t_table_1`.`task_id`) = `t_table_2`.`id`)))) limit 0,1000"

sql简写一下就是

select * from t_table_1 t1 where exists (select t2.id from t_table_2 t2 where t2.uid = 1 and t1.task_id = t2.id)

可以看到in可以改成semijoin或exists,最终优化器选择了exists,因为我们关闭了semijoin开关。

按照这条sql逻辑查询,将会遍历t_table_1表的每一行,然后代入子查询看是否匹配,当t_table_1表的行数很多时,耗时将会很长。

通过explain观察执行计划可以看到t_table_1进行了全表扫描。

备注:想查看优化器改下后生成的sql,也可以通过show extended + show warnings:

explain extended SELECT * from t_table_1 where task_id in (select id from t_table_2 where uid = 1);
show warnings;

接着我们打开上面的参数开关,再次optimizer_trace跟踪一下

set optimizer_switch='semijoin=on';

得到如下:

"steps": [
{
"expanded_query": "/* select#2 */ select `t_table_2`.`id` from `t_table_2` where (`t_table_2`.`uid` = 1)"
},
{
"transformation": {
"select#": 2,
"from": "IN (SELECT)",
"to": "semijoin",
"chosen": true
}
}
] "expanded_query": "/* select#1 */ select `t_table_1`.`id` AS `id`,`t_table_1`.`task_id` AS `task_id` from `t_table_1` semi join (`t_table_2`) where (1 and (`t_table_2`.`uid` = 1) and (`t_table_1`.`task_id` = `t_table_2`.`id`)) limit 0,1000"

sql简写一下就是

select * from t_table_1 semi join t_table_2 where (`t_table_2`.`uid` = 1 and `t_table_1`.`task_id` = `t_table_2`.`id`)"

可以看到优化器这次选择将in转换成semijoin了,观察执行计划可以看到走了索引。

那如果换成delete呢?同样保持开关打开,跟踪如下:

"steps": [
{
"expanded_query": "/* select#2 */ select `t_table_2`.`id` from `t_table_2` where (`t_table_2`.`uid` = 1)"
},
{
"transformation": {
"select#": 2,
"from": "IN (SELECT)",
"to": "semijoin",
"chosen": false
}
},
{
"transformation": {
"select#": 2,
"from": "IN (SELECT)",
"to": "EXISTS (CORRELATED SELECT)",
"chosen": true,
"evaluating_constant_where_conditions": [
]
}
}
]

可以看到和关闭semijoin一样,对于delete优化器也是选择了exists,我们表是千万级别,全表扫描加锁,其它操作语句自然都会超时获取不到锁而失败。

semijoin

semijoin翻译过来是半连接,是mysql针对in/exists子查询进行优化的一种技术,参见文档

可以使用SHOW VARIABLES LIKE 'optimizer_switch';查看semijoin是否开启。

上面使用IN-TO-EXISTS改写后,外层表变成驱动表,效率很差,那如果使用inner join呢,使用条件过滤后,用小表驱动大表,但join查询结果是会重复的,和子查询语义不一定相同。如:

SELECT class.class_num, class.class_name
FROM class
INNER JOIN roster
WHERE class.class_num = roster.class_num;

这样会查询出多条相同class_num的记录,如果子查询,那么查询出来的class_num是不一样的,也就是去重。当然也可以加上distinct,但这样效率比较低。

SELECT class_num, class_name
FROM class
WHERE class_num IN
(SELECT class_num FROM roster);

semijoin有以下几种策略,以下是官方的解释:

Duplicate Weedout: Run the semijoin as if it was a join and remove duplicate records using a temporary table.

FirstMatch: When scanning the inner tables for row combinations and there are multiple instances of a given value group, choose one rather than returning them all. This "shortcuts" scanning and eliminates production of unnecessary rows.

LooseScan: Scan a subquery table using an index that enables a single value to be chosen from each subquery's value group.

Materialize the subquery into an indexed temporary table that is used to perform a join, where the index is used to remove duplicates. The index might also be used later for lookups when joining the temporary table with the outer tables; if not, the table is scanned. For more information about materialization, see Section 8.2.2.2, “Optimizing Subqueries with Materialization”.

以Duplicate Weedout为例,mysql会先将roster的记录以class_num为主键添加到一张临时表,达到去重的目的。接着扫描临时表,每行去匹配外层表,满足条件则放到结果集,最终返回。

具体使用哪种策略是优化器根据具体情况分析得出的,可以从explain的extra字段看到。

那么为什么delete没有使用semijoin优化呢?

这其实是mysql的一个bug,bug地址,描述场景和我们的一样。

文中还提到这个问题在mysql 8.0.21被修复,地址

大致就是解释了一下之前版本没有支持的原因,提到主要是因为单表没有可以JOIN的对象,没法进行一系列的优化,所以单表的UPDATE/DELETE是无法用semijoin优化的。

这个优化还有一些限制,例如不能使用order by和limit,我们还是应该尽量避免使用子查询。

在我们的场景通过将子查询改写为join即可走索引,现在也明白为什么老司机们都说尽量用join代替了子查询了吧。

这样delete居然不走索引的更多相关文章

  1. MySql Delete不走索引问题

    如果delete语句带有查询,写法不对会导致不走索引. 简单粗暴的办法:拆两条sql,一条查询,一条delete ======================= [不走索引的写法] DELETE FR ...

  2. not in 和 <> 不走索引

    首先我们要知道的一点就是CBO的代码oracle是不会对我们公开的,起码现在是.所以本文中的结论不一定适用所有的版本.在应用本文的结论之前最好先试一下. ok 下面就是本文的结论,当你在where语句 ...

  3. 诊断一句SQL不走索引的原因

    from http://www.itpub.net/thread-1852897-1-1.html 有论坛朋友在上面的帖子里问SQL为什么不走索引,正好这两天我也刚刚在看SQL优化,于是试着回答了一下 ...

  4. Select * 一定不走索引是否正确?

    Select * 一定不走索引是否正确? 走索引指的是:SQL语句的执行计划用到了1.聚集索引查找  2.索引查找  ,并且查询语句中需要有where子句 根据where子句的过滤条件,去聚集索引或非 ...

  5. Update关联查询不走索引,效率低下

    优化一个sql,就是有A,B两个表,要利用b表的字段更新a表对应的字段.形如 Sql代码 update A set A.a=(select B.b from B where A.id=B.id); 原 ...

  6. 【摘】Oracle执行计划不走索引的原因总结

    感谢原博主 http://soft.chinabyte.com/database/364/12471864.shtml 在Oracle数据库操作中,为什么有时一个表的某个字段明明有索引,当观察一些语的 ...

  7. oracle查询不走索引的一些情况(索引失效)

    Oracle建立索引的目的是为了避免全表扫描,提高查询的效率. 但是有些情况下发现即使建立了索引,但是写出来的查询还是很慢,然后会发现是索引失效导致的,所以需要了解一下那些情况会导致索引失效,即查询不 ...

  8. mysql 索引 大于等于 走不走索引 最左前缀

    你可以认为联合索引是闯关游戏的设计 例如你这个联合索引是state/city/zipCode 那么state就是第一关 city是第二关, zipCode就是第三关 你必须匹配了第一关,才能匹配第二关 ...

  9. oracle order by 字段不能为空 为空速度慢 不走索引

    oracle order by 字段不能为空 为空速度慢 不走索引

  10. mysql 索引优化,索引建立原则和不走索引的原因

    第一:选择唯一性索引 唯一性索引的值是唯一的,可以更快捷的通过该索引来确定某条记录. 2.索引的列为where 后面经常作为条件的字段建立索引 如果某个字段经常作为查询条件,而且又有较少的重复列或者是 ...

随机推荐

  1. 分拣平台API安全治理实战 | 京东物流技术团队

    导读 本文主要基于京东物流的分拣业务平台在生产环境遇到的一些安全类问题,进行定位并采取合适的解决方案进行安全治理,引出对行业内不同业务领域.不同类型系统的安全治理方案的探究,最后笔者也基于自己在金融领 ...

  2. WPF开发必备

    类库 1.XamlFlair The goal of the XamlFlair library is to ease the implementation of common animations ...

  3. Solution -「洛谷 P7395」「CoE-I 2021C」弹珠游戏

    Description Link. 游戏在 \(4\times4\) 的菱形棋盘上进行: 两名玩家轮流放置弹珠,可以在横向.纵向.\(45\) 度斜线.\(135\) 度斜线方向未放置弹珠的位置连续放 ...

  4. Note -「网络流 flows」

    基本没有严谨证明. Part. 1 概念 Part. 1-1 流网络 流网络是一个有向图(不考虑反向边),我们把这个图记为 \(G=(V,E)\). 其中有两个特殊的点 \(s,t\),分别成为源点和 ...

  5. python实现简单的爬虫功能

    前言Python是一种广泛应用于爬虫的高级编程语言,它提供了许多强大的库和框架,可以轻松地创建自己的爬虫程序.在本文中,我们将介绍如何使用Python实现简单的爬虫功能,并提供相关的代码实例. 如何实 ...

  6. oracle 问题:ORA-28040:没有匹配的验证协议

    Oracle11g客户端连接Oracle12C服务器端,需配置项 前置条件:已安装Oracle11g客户端,配置好环境变量,用PL/SQL Developer登录数据库 出现问题:登录数据库时,提示& ...

  7. 12 款最棒 Vue 开源 UI 库

    文章目录 Element Plus - 经典中的经典,全面支持 Vue 3 TDesign Vue - 鹅厂优质 UI 组件,配套工具完满,设计工整,文档清晰 ArcoDesign Vue- 字节优质 ...

  8. 分布式与微服务——Iaas,Paas和Saas、单体应用和缺点、微服务概念、传统 分布式 SOA 架构与微服务架构的区别、微服务实战、什么是RPC、CAP定理和BASE理论、唯一ID生成、实现分布式

    文章目录 1-什么是Iaas,Paas和Saas 一 IaaS基础设施服务 二 paas平台即服务 三saas软件即服务 四 总结 2-单体应用和缺点 一 单体应用 二 单体应用的缺陷 3-微服务概念 ...

  9. 【Dotnet 工具箱】基于 .NET 6 和 Angular 构建项目任务管理平台

    1.Reha 时间管理大师 Rhea 是一个基于 C# 和 .NET 6 开发的在线任务管理平台,类似于 禅道.Jira.Redmine, 滴答清单等. 支持多视图多维度统一管理任务.多级结构,工作区 ...

  10. 【RocketMQ】数据的清理机制

    Broker在启动的时候会注册定时任务,定时清理过期的数据,默认是每10s执行一次,分别清理CommitLog文件和ConsumeQueue文件: public class DefaultMessag ...