背景

有两张表,都是主键递增,类似于主表和明细表:

  • statistics_apply:统计申请表,主键applyId,7万多条记录
  • statistics_apply_progress:统计申请进度表(申请统计的状态变更记录表),主键progressId,字段applyId保存的是上表的主键,30多万条记录

现在我们需要通过多个applyId查询对应的最新的progress记录。

当前数据库版本:5.6.16-log

原SQL

SELECT p2.APPLY_ID, p2.TASK_MESSAGE
FROM statistics_apply_progress AS p2
WHERE p2.progress_id IN (
SELECT max( p1.PROGRESS_ID ) AS PROGRESS_ID
FROM statistics_apply_progress AS p1
WHERE p1.APPLY_ID IN (
39574,49304,57423,8830,20416,29298,41672,52163,
62564,10850,20985,30982,46701,57364,3414,16753,
21808,46315,33520,47612,50974,61741,16210,19503,
28713,38700,48318,56743,20868,20870,38094,20872,
20805,8165,20525,29426,12345,75321,95325,12784 )
GROUP BY p1.APPLY_ID
)
ORDER BY p2.APPLY_ID;

最初的想法是通过对progress表的applyId字段做筛选然后通过applyId分组,由于主键都是自增的,选取分组后最大的progressId,然后把查询出的所有progressIds当作子查询的结果再在progress表用IN来筛选,最后拿到我们需要的applyId对应的最新的progress记录中的task_messsage字段信息。

开始觉得这能有啥问题,太顺理成章了,在测试环境数据量太小也看不出什么端倪,于是提交、上线~

发现问题

发布到生产之后,一开始没太处理这块,过段时间后在SkyWalking上看到有个查询接口之前一直稳定在200ms内返回,最近都上升到800ms了,加上其他接口页面响应都迟钝了一小会。

于是把该接口的所有SQL都过了一遍,看下它们的执行计划,才看到这个看上去老实但却不按套路出牌的SQL:



看到type那一栏了吗?那居然是all,天呐,全表扫描就不用看了,赶快优化吧。

但是觉得这个太反人类了,progress的progressId和applyId都是有索引的,分组,排序也都是走索引列,但这结果和想象差距太远了。

此外,子查询的select_typeSUBQUERY,又不是DEPENDENT SUBQUERY,都不需要依赖外部传入的参数去查询,为什么还全表扫呢?

解决问题

造成全表扫描的原因有哪些

查找官方文档,看到Avoiding Full Table Scans说以下情况经常会有全表扫描:

  • The table is so small that it is faster to perform a table scan than to bother with a key lookup. This is common for tables with fewer than 10 rows and a short row length.(不符合当前查询场景)
  • There are no usable restrictions in the ON or WHERE clause for indexed columns.(不符合当前查询场景)
  • You are comparing indexed columns with constant values and MySQL has calculated (based on the index tree) that the constants cover too large a part of the table and that a table scan would be faster. See Section 8.2.1.1, “WHERE Clause Optimization”.(不符合当前查询场景)
  • You are using a key with low cardinality (many rows match the key value) through another column. In this case, MySQL assumes that by using the key it probably will do many key lookups and that a table scan would be faster.(不符合当前查询场景)

到这我就很尴尬了,都不是我这种场景啊。那我们继续看看子查询有哪些限制。

子查询的限制

官方文档:SubQuery Restriction

  • 不能在子查询中查询了一张表还在外部更新它:

    In general, you cannot modify a table and select from the same table in a subquery. For example, this limitation applies to statements of the following forms:

    DELETE FROM t WHERE ... (SELECT ... FROM t ...);

    UPDATE t ... WHERE col = (SELECT ... FROM t ...);

    {INSERT|REPLACE} INTO t (SELECT ... FROM t ...);

    但我的是查询语句,并不满足描述中关于modify a table的说明,这应该不是我问题的本质。

  • 行比较操作部分支持

    我们使用的是很普通的IN操作,不属于此类。

  • 子查询不能是相关子查询

    很明显我这里的查询不存在内外依赖关系。关于这个相关子查询我要另写文章整理。

  • 子查询不能用limit

    我也没有使用

  • 最后一个限制和我的场景相去甚远我就不描述了

说实在的,这个问题优化起来还是很方便的,现在是要解释很难。最后Google了很久,在mysql bugs里面发现了和我使用场景一模一样的描述,太搞人了!

Design problem

官方回复问题MAX() causes a full table scan when used in a sub-query

这么重要的原话我得贴出来:

This is expected behavior. If you explain both EXPLAINs you shall see that query node as a nested query and standalone query have 100 % identical execution plan.

Your problem is that highest level query gets a full table scan. That is actually a cause of the design problem that was introduced when nested queries were developed, which affects a large number of queries that use nested queries. There are plans to solve this problem, but scheduling is yet unknown.

The only thing that you can try is to have an index on the column xxx and to avoid nested query by using prepared statements or stored function that will create a string with values in brackets returned by your inner query as standalone one.

看到了吧,有这个问题干嘛不在SubQuery Restriction中说明一下,让我一顿好找!

后面我把max语句删除掉之后重新看了下执行计划:

SELECT p2.APPLY_ID, p2.TASK_MESSAGE
FROM statistics_apply_progress AS p2
WHERE p2.progress_id IN (
SELECT PROGRESS_ID
FROM statistics_apply_progress AS p1
WHERE p1.APPLY_ID IN (
39574,49304,57423,8830,20416,29298,41672,52163,
62564,10850,20985,30982,46701,57364,3414,16753,
21808,46315,33520,47612,50974,61741,16210,19503,
28713,38700,48318,56743,20868,20870,38094,20872,
20805,8165,20525,29426,12345,75321,95325,12784 )
);



这样的语句看起来很傻,只是为了验证子查询来看下。

找到问题之后我前后在mysql5.6.47mysql5.7.28中都试了一下,仍然存在。 大家以后注意一下吧。

修改SQL

既然找到问题了,就转换思路了,同样在MySQL官方文档里能找到Rewriting Subqueries as Joins

文中有这么一段话

A LEFT [OUTER] JOIN can be faster than an equivalent subquery because the server might be able to optimize it better

那怎么就试试吧,修改后的SQL如下:

SELECT p2.APPLY_ID, p2.TASK_MESSAGE
FROM (
SELECT max( p1.PROGRESS_ID ) AS PROGRESS_ID
FROM statistics_apply_progress as p1
WHERE p1.APPLY_ID IN (
39574,49304,57423,8830,20416,29298,41672,52163,
62564,10850,20985,30982,46701,57364,3414,16753,
21808,46315,33520,47612,50974,61741,16210,19503,
28713,38700,48318,56743,20868,20870,38094,20872,
20805,8165,20525,29426,12345,75321,95325,12784 )
GROUP BY p1.APPLY_ID
) AS p3
LEFT JOIN statistics_apply_progress as p2 ON p3.PROGRESS_ID = p2.PROGRESS_ID
ORDER BY p2.APPLY_ID;

查看一下执行计划:



已经没有full scan了,第一行的PRIMARY/ALL表示查询中若包含任何复杂的子部分,最外层的select被标记为PRIMARY。主要还是看2、3两行,都走索引,结果查不到哪去了。

性能对比

不同机器配置不同查询时间也不同

  • 修改前的查询时间固定在800ms左右
  • 修改前的查询时间固定在2ms左右

但是知道原因后这么比较已经没多大意义了,全表扫描和走索引,哈哈哈,就这么吧,结束。

慢SQL优化:where id in (select max(id)...) 改为join后性能提升400倍的更多相关文章

  1. 优化临时表使用,SQL语句性能提升100倍

    [问题现象] 线上mysql数据库爆出一个慢查询,DBA观察发现,查询时服务器IO飙升,IO占用率达到100%, 执行时间长达7s左右.SQL语句如下:SELECT DISTINCT g.*, cp. ...

  2. 转--优化临时表使用,SQL语句性能提升100倍

    转自:http://www.51testing.com/html/01/n-867201-2.html [问题现象] 线上mysql数据库爆出一个慢查询,DBA观察发现,查询时服务器IO飙升,IO占用 ...

  3. 简单聊聊TiDB中sql优化的一个规则---左连接消除(Left Out Join Elimination)

    我们看看 TiDB 一段代码的实现 --- 左外连接(Left Out Join)的消除; select 的优化一般是这样的过程: 在逻辑执行计划的优化阶段, 会有很多关系代数的规则, 需要将逻辑执行 ...

  4. 有史以来性价比最高最让人感动的一次数据库&SQL优化(DB & SQL TUNING)——半小时性能提升千倍

    昨天,一个客户现场人员急急忙忙打电话找我,说需要帮忙调优系统,因为经常给他们干活,所以,也就没多说什么,先了解情况,据他们说,就是他们的系统最近才出现了明显的反应迟钝问题,他们的那个系统我很了解,软硬 ...

  5. 一次 Spark SQL 性能提升10倍的经历(转载)

    1. 遇到了啥问题 是酱紫的,简单来说:并发执行 spark job 的时候,并发的提速很不明显. 嗯,且听我慢慢道来,啰嗦点说,类似于我们内部有一个系统给分析师用,他们写一些 sql,在我们的 sp ...

  6. 云 MongoDB 优化让 LBS 服务性能提升十倍

    欢迎大家前往腾讯云技术社区,获取更多腾讯海量技术实践干货哦~ 随着国内服务共享化的热潮普及,共享单车,共享雨伞,共享充电宝等各种服务如雨后春笋,随之而来的LBS服务定位问题成为了后端服务的一个挑战.M ...

  7. mariadb使用with子句重写SQL性能提升5倍

    几个月前,我们有个产品的开发反馈了个问题,说有个组织结构的查询很慢,几千行的复杂关联需要1秒钟,表示太慢了,原语句如下: SELECT org.org_id, org.dimension, org.o ...

  8. (转)SQL 优化原则

    一.问题的提出 在应用系统开发初期,由于开发数据库数据比较少,对于查询SQL语句,复杂视图的的编写等体会不出SQL语句各种写法的性能优劣,但是如果将应用 系统提交实际应用后,随着数据库中数据的增加,系 ...

  9. SQL优化(zhuan)

    转自:http://www.jfox.info/SQL-you-hua 数据库的优化问题 一.问题的提出 在应用系统开发初期,由于开发数据库数据比较少,对于查询SQL语句,复杂视图的的编写等体会不出S ...

随机推荐

  1. 打爆你的 CPU

    打爆你的 CPU Intro 今天来尝试写一段代码,把 CPU 打满,让所有处理器的 CPU 使用率达到 100% 如何提高 CPU 使用率 想要提高 CPU 的使用率就是要让 CPU 一直在工作,单 ...

  2. image restoration(IR) task

    一般的,image restoration(IR)任务旨在从观察的退化变量$y$(退化模型,如式子1)中,恢复潜在的干净图像$x$ $y \text{} =\text{}\textbf{H}x\tex ...

  3. IntelliJ IDEA 使用指南:集成GIT客户端

    一.安装GIT客户端 首先需要在本地安装好GIT的客户端. GIT客户端官网下载地址:https://www.git-scm.com/download/ 安装说明 Linux系统安装 使用yum指令 ...

  4. day79:luffy:注册之对手机号的验证&实现基本的注册功能逻辑&点击获取验证码&redis

    目录 1.前端和后端对于手机号的验证 2.实现基本的注册功能-不包括验证码 3.点击获取验证码 4.解决登录不上Xadmin的bug 5.redis register.vue页面 <templa ...

  5. 01 . Go之Gin+Vue开发一个线上外卖应用

    项目介绍 我们将开始使用Gin框架开发一个api项目,我们起名为:云餐厅.如同饿了么,美团外卖等生活服务类应用一样,云餐厅是一个线上的外卖应用,应用的用户可以在线浏览商家,商品并下单. 该项目分为客户 ...

  6. 4G DTU和4G工业路由器有哪些区别?

    DTU的英文全称是Data Transfer unit,是一种专门用来将将IP数据转换为串口数据或者是将串口数据转换为IP数据并且通过无线通信网络将数据进行传送的无线终端设备.DTU也可以实现无线网络 ...

  7. DES 实现

    原理 加密 置换: IP逆置换: 迭代: PC-1置换: PC-2置换: 子秘钥的生成: 加密函数f: 解密 代码 // C语言实现 #include<stdio.h> #include& ...

  8. 【Postman】使用Postman实现接口数据关联

    首先下载安装Postman直接打开官网,点击下载按钮即可完成下载https://www.getpostman.com/downloads/ 栗子业务场景:用户登录医生账户,查询自己的处方列表数据:用户 ...

  9. mysql触发

    create procedure agex(in addage1 int,in addage2 int)begindeclare curl_stu_id int; declare curl_stu_s ...

  10. 文科妹子都会用 GitHub,你这个工科生还等什么

    在某乎上刷到一条关于 GitHub 的留言,如下: 点赞人数还不少,这说明还真有不少工科生不会用 GitHub,你看大小写都没有区分(手动狗头).所以我就想写篇文章科普下,"新手如何使用 G ...