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

定位原因

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

SELECT * FROM tgdemand_demand t1
WHERE(t1.id IN(SELECT t2.demand_idFROM tgdemand_job t2WHERE (t2.state = 'working' AND t2.wangwang = 'abc'))ANDNOT (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 idsANDNOT (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 的方式来取得数据。这里就不展开了。

感想

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

参考资料

  • http://www.cnblogs.com/zhengyun_ustc/p/slowquery3.html

  • http://dev.mysql.com/doc/refman/5.5/en/explain-output.html

  • https://docs.djangoproject.com/en/1.9/ref/models/querysets/

记一次 MySQL 的慢查优化相关推荐

  1. mysql group by 性能_记一次Mysql group by的优化

    直接上干货: 数据量:50万 mysql版本:5.6.26 表结构: CREATE TABLE bet_order_product ( bet_order_product_id varchar(40) ...

  2. 记一次mysql性能优化过程

    2019独角兽企业重金招聘Python工程师标准>>> 转发自:记一次mysql性能优化过程 由于配置是运行过那么长时间,很稳定,基本上不考虑,所以本次主要是sql的优化,并且集中在 ...

  3. MySQL海量数据存储与优化(上)

    MySQL起源和分支 MySQL 是最流行的关系型数据库软件之一,由于其体积小.速度快.开源免费.简单易用.维护成本 低等,在集群架构中易于扩展.高可用,因此深受开发者和企业的欢迎. Oracle和M ...

  4. 分布式存储系统——《MySQL海量数据存储与优化》

    文章目录 前言介绍 必备基础 主要内容 MySQL起源和分支 MySQL应用架构演变 MySQL架构原理 MySQL架构体系 MySQL运行机制 MySQL存储引擎 InnoDB和MyISAM对比 I ...

  5. 一起进阶学习JAVA:MySQL海量数据存储与优化

    一起进阶学习JAVA:MySQL海量数据存储与优化 第一部分 分库分表实战及中间件 1.1 背景介绍 1.1.1 背景描述 刚开始我们的系统只用了 单机数据库 随着用户的不断增多,考虑到系统的高可用和 ...

  6. mysql数据库优化课程---16、mysql慢查询和优化表空间

    mysql数据库优化课程---16.mysql慢查询和优化表空间 一.总结 一句话总结: a.慢查询的话找到存储慢查询的那个日志文件 b.优化表空间的话可以用optimize table sales; ...

  7. MySQL索引分析和优化(转)

    MySQL索引分析和优化(转) 索引用来快速地寻找那些具有特定值的记录,所有MySQL索引都以B-树的形式保存.如果没有索引,执行查询时MySQL必须从第一个记录开始扫描整个表的所有记 录,直至找到符 ...

  8. MySQL—增删改查,分组,连表,limit,union,alter,排序,去重

    MySQL增删改查 在表格的增删改查中,查的内容是最多的,包括group by ,join,limit,union,alter,排序都是服务于查的 #sql语句数据行操作补充#增加:#insert i ...

  9. 学习笔记之-MySql高级之sql优化

    一 Mysql简介 概述 MySQL是一个关系型数据库管理系统,由瑞典MySQL AB公司开发,目前属于Oracle公司. M/SQL是一种关联数据库管理系统,将数据保存在不同的表中,而不是将所有数据 ...

最新文章

  1. 【Python】轻松识别文字,这款Python OCR库支持超过80种语言
  2. 【计算机网络】应用层 : 总结 ( 网络应用模型 C/S P2P | 域名解析 DNS | 文件传输协议 FTP | 电子邮件 | 万维网 与 HTTP ) ★★★
  3. C++异常处理(try和catch)
  4. 使用 010 Editor 分析二进制文件格式
  5. 神经网络基础_反向传播_证明
  6. Mozilla发布最大公共语音数据集Common Voice
  7. 【litrpa专题】首个rpa程序,使用litrpa采集百度地图地铁数据
  8. java 读取图片给 matlab_如何将MATLAB图像处理程序转换为java?
  9. 基于Ganos百行代码实现亿级矢量空间数据在线可视化
  10. Cent OS 6/7 中通过yum安装软件时提示cannot find a valid baseurl...的解决方法
  11. kettle 参数传递_kettle 存储过程 参数传递参数
  12. 陶瓷封装产品的6大优点
  13. Cobalt Strike Malleable C2
  14. 纯php 给pdf加水印,如何使用PHP为现有PDF文件添加水印?
  15. stm32配置wifi
  16. 请你讲讲wait方法的底层原理
  17. Auto CAD标注表面粗糙度的方法
  18. PPT导入视频裁剪后,如何裁剪后的视频另存为保存下来?
  19. 利用bat批处理做启动mongodb脚本
  20. 【公钥密码】ECC椭圆密码体制 (实现Elgamal加密方法)

热门文章

  1. python字典、列表、元祖使用场景_python学习第七讲,python中的数据类型,列表,元祖,字典,之元祖使用与介绍...
  2. python与vb可以互换吗_vb能配合python写程序么?
  3. python导入matplotlib出错_解决导入matplotlib的RuntimeError: Python is not installed as a framework....
  4. dateformat java 并发_java.text.DateFormat 多线程并发问题
  5. python3安装setuptools步骤_setuptools、pip的安装
  6. 【硬件基础】有源蜂鸣器与无源蜂鸣器
  7. nodejs回调函数理解
  8. 线程、线程匿名内部类、解决线程不安全的方式
  9. 实验四-常用图像增强方法
  10. iOS 多级下拉菜单