最近遇见一个 MySQL 的慢查问题,于是排查了下,这里把相关的过程做个总结。

定位原因

我首先查看了 MySQL 的慢查询日志,发现有这样一条 query 耗时非常长(大概在 1 秒多),而且扫描的行数很大(10 多万条数据,差不多是全表了):

SELECT * FROM tgdemand_demand t1
WHERE
(
t1.id IN
(
SELECT t2.demand_id
FROM tgdemand_job t2
WHERE (t2.state = 'working' AND t2.wangwang = 'abc')
)
AND
NOT (t1.state = 'needConfirm')
)
ORDER BY t1.create_date DESC

这个查询不是很复杂,首先执行一个子查询,取到任务的状态(state)是 ‘working’ 并且任务的关联人 (wangwang)是’abc’的所有需求 id(这个设计师进行中的任务对应的需求 id),然后再到主表 tgdemand_demand中带入刚才的 id 集合,查询出需求状态(state)不是 ‘needConfirm’ 的所有需求,最后进行一个排序。

按道理子查询筛选出 id 后到主表过滤是直接使用到主键,应该是很快的啊。而且,我检查了子查询的 tgdemand_job 表的索引,where 中用到的查询条件都已经增加了索引。怎么会这样呢?

于是,我对这个 query 执行了一个 explain(输出 sql 语句的执行计划),看看 MySQL 的执行计划是怎样的。输出如下:

我们看到,第一行是 t1 表,type 是 ALL(全表扫描),rows(影响行数)是157089,没有用到任何索引;第二行是 t2 表,用到了索引。和我之前理解的执行顺序完全不一样!

为什么 MySQL 不是先执行子查询,而是对 t1 表进行了全表扫描呢?我们仔细看第二行的 select_type,发现它的值是 DEPENDENT_SUBQUERY,意思是这个子查询的查询方式依赖外层的查询。这是什么意思?

实际上,MySQL 对于这种子查询会进行改写,上面的 SQL 会被改写成下面的形式:

SELECT * FROM tgdemand_demand t1 WHERE EXISTS (
SELECT * FROM tgdemand_job t2 WHERE t1.id = t2.demand_id AND (t2.state = 'working' AND t2.wangwang = 'abc')
) AND NOT (t1.state = 'needConfirm')
ORDER BY t1.create_date DESC;

这表示,SQL 会去扫描 tgdemand_demand 表的所有数据,每条数据再传入到子查询中与表 tgdemand_job 进行关联,执行子查询,子查询根本不会先执行,而且子查询会执行 157089 次(外层表的记录数量)。还好我们的子查询加了必要的索引,不然结果会更加惨不忍睹。

这个结果真是太坑爹,而且十分违反直觉。对于慢查询,千万不要想当然,还是多多 explain,看看数据库实际上是怎么去执行的。

问题修复

既然子查询会被改写,那最简单的解决方案就是不用子查询,将内层获取需求 id 的 SQL 单独拿出来执行,取到结果后再执行一条 SQL 去获取实际的数据。大概像这样(下面的语句是不合法的,只是示意):

ids = SELECT t2.demand_id
FROM tgdemand_job t2
WHERE (t2.state = 'working' AND t2.wangwang = 'abc'); SELECT * FROM tgdemand_demand t1
WHERE
(
t1.id IN ids
AND
NOT (t1.state = 'needConfirm')
)
ORDER BY t1.create_date DESC;

说干咱就干,我找到了下面的代码(是 python 语言写的):

demand_ids = Job.objects.filter(wangwang=user['wangwang'], state='working').values_list("demand_id", flat=True)

demands = Demand.objects.filter(id__in=demand_ids).exclude(state__in=['needConfirm']).order_by('-create_date')

咦!这不是和我想得是一样的嘛?先查出需求 id(代码第一行),然后用 id 集合再去执行实际的查询(代码第二行)。为什么经过 ORM 框架的处理后产出的 SQL 就不一样了呢?

带着这个问题我搜索了一番。原来 Django 自带的 ORM 框架生成的 QuerySet 是懒执行的(lazy evaluated),我们可以将这种 QuerySet 到处传,直到需要时才会实际的执行 SQL。

比如,我们代码里面的 Job.objects.filter(wangwang=user['wangwang'], state='working').values_list("demand_id", flat=True)这个 QuerySet 实际上并没有执行,就被作为参数传递给了id__in,当Demand.objects.filter(id__in=demand_ids).exclude(state__in=['needConfirm']).order_by('-create_date')这个 QuerySet 执行时,刚才未执行的 QuerySet 才开始作为 SQL 执行,于是生成了最开始的 SQL 语句。

既然如此,我们的目的要让 QuerySet 提前执行,获得结果集。根据文档,对 QuerySet 进行循环、slice、取 len、list 转换的时候被执行。于是我将代码更改为了下面的样子:

demand_ids = list(Job.objects.filter(wangwang=user['wangwang'], state='working').values_list("demand_id", flat=True))

demands = Demand.objects.filter(id__in=demand_ids).exclude(state__in=['needConfirm']).order_by('-create_date')

终于,页面打开速度恢复正常了。

实际上,我们也可以对 SQL 进行改写来解决问题:

select * from tgdemand_demand t1, (select t.demand_id from tgdemand_job t where t.state = 'working' and t.wangwang = 'abc') t2
where t1.id=t2.demand_id and not (t1.state = 'needConfirm')
order by t1.create_date DESC

思路是去掉子查询,换用 2 个表进行 join 的方式来取得数据。这里就不展开了。

感想

框架可以提高生产率的前提是对背后的原理足够了解,不然应用很可能就会在某个时间暴露出一些隐蔽的要命问题(这些问题在小规模阶段可能根本都发现不了……)。保证应用的健壮真是个大学问,还有很多东西值得我们去探索。

性能分析 | MySQL 的慢查分析实例的更多相关文章

  1. MySQL 的性能(上篇)—— SQL 执行时间分析

    简介 文中内容均为阅读前辈的文章所整理而来,参考文章已在最后全指明 本文分为上下两篇: 上篇:MySQL 的 SQL 执行时间分析 下篇:MySQL 性能优化 后端开发必然会接触到数据库,数据层的优劣 ...

  2. MySQL 的性能(上篇)—— SQL 执行分析

    简介 文中内容均为阅读前辈的文章所整理而来,参考文章已在最后全指明 本文分为上下两篇: 上篇:MySQL 的 SQL 执行分析 下篇:MySQL 性能优化 后端开发必然会接触到数据库,数据层的优劣会影 ...

  3. 【mysql】MySQL知识整理-死锁分析-性能优化等

    [[TOC]] 常用操作指令 show databases:显示所有的数据库: use dbName: 使用指定数据库 show tables: 显示所有的数据表: desc tableName: 查 ...

  4. mysql show profiles使用分析sql性能

    mysql show profiles使用分析sql性能 Show profiles是5.0.37之后添加的,要想使用此功能,要确保版本在5.0.37之后. 查看一下我的数据库版本 mysql> ...

  5. MySQL性能分析, mysql explain执行计划详解

    MySQL性能分析 MySQL性能分析及explain用法的知识是本文我们主要要介绍的内容,接下来就让我们通过一些实际的例子来介绍这一过程,希望能够对您有所帮助. 1.使用explain语句去查看分析 ...

  6. [转载]mysql慢日志文件分析处理

    原文地址:mysql慢日志文件分析处理作者:maxyicha mysql有一个功能就是可以log下来运行的比较慢的sql语句,默认是没有这个log的,为了开启这个功能,要修改my.cnf或者在mysq ...

  7. 从运维角度来分析mysql数据库优化的一些关键点【转】

    概述 一个成熟的数据库架构并不是一开始设计就具备高可用.高伸缩等特性的,它是随着用户量的增加,基础架构才逐渐完善. 1.数据库表设计 项目立项后,开发部根据产品部需求开发项目,开发工程师工作其中一部分 ...

  8. 分析MySQL中哪些情况下数据库索引会失效

    要想分析MySQL查询语句中的相关信息,如是全表查询还是部分查询,就要用到explain. 一.explain 用法:explain +查询语句. id:查询语句的序列号,上面图片中只有一个selec ...

  9. 重新学习MySQL数据库5:根据MySQL索引原理进行分析与优化

    重新学习MySQL数据库5:根据MySQL索引原理进行分析与优化 一:Mysql原理与慢查询 MySQL凭借着出色的性能.低廉的成本.丰富的资源,已经成为绝大多数互联网公司的首选关系型数据库.虽然性能 ...

随机推荐

  1. paypal支付 NVP支付 paypal 手续费 GetTransactionDetails

    主要内容: 本文章主要讲解的是NVP的对接,以最简单的接口案例,讲解一下对接NVP的方案. 先提供下paypal 官方文档的主要功能对接说明,如下 1.请求API 服务器端点 描述 https://a ...

  2. sklearn--决策树和基于决策树的集成模型

    一.决策树 决策树一般以选择属性的方式不同分为id3(信息增益),c4.5(信息增益率),CART(基尼系数),只能进行线性的分割,是一种贪婪的算法,其中sklearn中的决策树分为回归树和分类树两种 ...

  3. 启动Tomcat报错:A child container failed during start

    在之前的工作和学习当中,有碰到过很多未知的,在当时看来十分令人疑惑的问题.当时的解决手段简单粗暴,就是直接百度,搜到对应的解决方案,直接抄过来试试,不行再换,直到问题消失,程序可以正常跑了,立马就不管 ...

  4. h5 特效

    地址:http://www.cnblogs.com/sun927/p/5842852.html 几个别人总结的css3炫酷效果,有需要直接拿来用即可,包括以下几个效果: 1.悬浮时放大 2.悬浮时转一 ...

  5. 使用php实现javascript的escape和unescape函数

    javascript有编码函数escape()和对应的解码函数unescape(),而php中只有个urlencode和urldecode,这个编码和解码函数对encodeURI和encodeURIC ...

  6. React 入门与实战-课时7 虚拟DOM的本质和目的

    DOM树的概念: 一个网页呈现的过程: 1.浏览器请求服务器获取页面HTML代码 2.浏览器先在内存中,解析DOM结构,并在浏览器内存中,渲染出一颗DOM树: 3.浏览器把DOM树,呈现到页面上: R ...

  7. k8s-应用部署

    该demo主要作为一个dubbo项目通过maven自动化docker打包插件发布到镜像仓库样例工程.该wiki后面同时会提供k8s部署zk,mysql,应用包的整个过程.该项目大体功能:zk作为注册中 ...

  8. 一个列表实现__iter__和__next__方法的例子

    x = ['厉智','陈培昌','程劲','徐晓冬'].__iter__() #这非得这么写不可,否则无法调用下面的__next__()方法,切记! print(x.__next__()) print ...

  9. mySql数据重复数据去重

    1.问题来源:数据中由于并发问题,数据存在多次调用接口,插入了重复数据,需要根据多条件删除重复数据: 2.参考博客文章地址:https://www.cnblogs.com/jiangxiaobo/p/ ...

  10. [jenkins] 启动错误 Failed to start LSB: Jenkins Automation Server.

    解决办法见https://blog.csdn.net/qq_34208844/article/details/87865672