故障分析 | MySQL 派生表优化
moboyou 2025-06-04 00:15 11 浏览
作者:xuty
一、问题 SQL
原 SQL 如下:
select name,count(name) from bm_id a left JOIN (select TaskName from up_pro_accept_v3_bdc union all select TaskName from up_pro_accept_v3_hsjs union all select TaskName from up_pro_accept_v3_hszjj union all select TaskName from up_pro_accept_v3_hzl union all select TaskName from up_pro_accept_v3_kjyw union all select TaskName from up_pro_accept_v3_kpzzzxwx union all select TaskName from up_pro_accept_v3_qdzc union all select TaskName from up_pro_accept_v3_rsj union all select TaskName from up_pro_accept_v3_sjba union all select TaskName from up_pro_accept_v3_spk union all select TaskName from up_pro_accept_v3_test union all select TaskName from up_pro_accept_v3_wygl union all select TaskName from up_pro_accept_v3_yms union all select TaskName from up_pro_accept_v3_zjj union all select TaskName from up_pro_accept_v3w) t on a.zxi = t.TaskName group by name
这是一个统计类的 SQL,直接执行跑了好几个小时都没有结束,所以暂时不知道实际耗时,因为实在是太久了~
二、执行计划
老步骤,我们先看下执行计划,如下图:
这里 SQL 执行主要分为 2 个步骤:
1. 顺序扫描每个 up_pro_accept 开头的子表数据,最终组成 t 表(派生表)。
- 扫描 t 表(派生表) 相关的所有子表,可以看到这里每张子表走的都是 全表扫描,有些表较大,有 100 多 w,检索较慢。
2. a 表(bm_id) 与 t表(派生表) 进行关联查询,得到最后的结果。
- t 表 (派生表) 作为 被驱动表 大约 164W 行 左右,与 a 表做关联查询时走的是 全表扫描(ALL), a 表(bm_id) 作为 驱动表 大约 1.3W 行 左右,也就是说,表关联需要全表扫描 t 表(派生表) 1.3W 次,而每次都需要扫描 164W 行 数据,显然 SQL 的绝大部分时间其实都花在这一步上。
那么其实 SQL 优化也分为了 2 步,首先是多张子表的全表扫描,是否可以用索引扫描替换,加快数据检索。
而后是主要的环节,这个派生表作为被驱动表时,是否可以走索引?如果不能走索引,有没有其他方式减少 SQL 开销?
三、派生表
既然这个 SQL 优化涉及到了派生表,那么我们先看下何谓派生表,派生表有什么特性?
Derivedtable(派生表) 实际上是一种特殊的 subquery(子查询),它位于 SQL 语句中 FROM 子句 里面,可以看做是一个单独的表。
MySQL 5.7 之前的处理都是对 Derived table(派生表) 进行 Materialize(物化),生成一个 临时表 用于保存 Derived table(派生表) 的结果,然后利用 临时表 来协助完成其他父查询的操作,比如 JOIN 等操作。
MySQL 5.7 中对 Derived table(派生表) 做了一个新特性,该特性允许将符合条件的 Derived table(派生表) 中的子表与父查询的表合并进行直接 JOIN,类似于 Oracle 中的 子查询展开,由优化器参数 optimizer_switch='derived_merge=ON' 来控制,默认为 打开。
但是 derived_merge 特性存在很多限制,当派生子查询存在以下操作时,该特性无法生效。DISTINCT、 GROUP BY、 UNION/UNION ALL 、 HAVING、 关联子查询、 LIMIT/OFFSET 以及 聚合操作 等。
举个简单例子:
其中 a 表就是一个派生表
1. 如果走 derived_merge 特性,那么可以走主键索引,速度非常快。
2. 如果关闭 derived_merge 特性,那么就会走全表扫描,速度非常慢。
select * from (select * from up_pro_accept_v3_bdc) awhere a.rowguid = '185c44aa-c23f-4e6f-bcd2-a38df16e2cc3'
四、SQL 优化
简单介绍了下派生表,下面我们开始尝试优化这个 SQL,步骤分 2 步:
1. 解决多张派生子表 union all 时全表扫描的问题。
2. 解决派生表在关联过程中无法使用索引的问题。
我们先解决问题 1,这个问题比较简单。
因为所有派生子表的查询都是 select TaskName from up_pro_accept_v3_xxx 类似这样,且外部关联字段也是 taskname,所以我们只要在对应表上建立 taskname 的索引即可。
建好索引后,我们再看下执行计划,所有的派生子表都走了 index 扫描,那么问题 1 基本解决了,但是由于 t 表(派生表) 在关联时还是走的全表扫描,并没有用到 derived_merge 特性,所以 SQL 还是非常非常慢(上万 s)。
接着我们来解决问题 2,这里主要解决派生表无法走索引的问题。
从之前介绍派生表的内容来看,想要派生表走索引,就需要用到 derived_merge 特性,将外部条件推入进子查询,但是这个特性的限制条件也很多,就比如我们这个 SQL,因为子查询里包括了 union all,那么该 SQL 是无法利用到 derived_merge 特性的,因此无法直接走索引过滤。
既然无法在原有 SQL 的基础上优化,那么我们只能考虑改写 SQL,通过 SQL 改写来达到优化的目的。
这里 SQL 其实是因为 驱动表 bm_id 最终是和派生表作表关联,导致无法利用索引,我们可以尝试将 驱动表 bm_id 也放到子查询中,只要前后语义是一致的,那么改写就没问题。这样就可以在子查询里就走完表关联,剩下的就是外部的分组排序,我们尝试下。
/* 改写后 SQL */SELECT NAME ,count(NAME)FROM ( SELECT NAME FROM bm_id LEFT JOIN up_pro_accept_v3_bdc bdc ON bm_id.zxi = bdc.TaskName UNION ALL SELECT NAME FROM bm_id LEFT JOIN up_pro_accept_v3_hsjs hsjs ON bm_id.zxi = hsjs.TaskName UNION ALL SELECT NAME FROM bm_id LEFT JOIN up_pro_accept_v3_hszjj hszjj ON bm_id.zxi = hszjj.TaskName UNION ALL SELECT NAME FROM bm_id LEFT JOIN up_pro_accept_v3_hzl hzl ON bm_id.zxi = hzl.TaskName UNION ALL SELECT NAME FROM bm_id LEFT JOIN up_pro_accept_v3_kjyw kjyw ON bm_id.zxi = kjyw.TaskName UNION ALL SELECT NAME FROM bm_id LEFT JOIN up_pro_accept_v3_kpzzzxwx kp ON bm_id.zxi = kp.TaskName UNION ALL SELECT NAME FROM bm_id LEFT JOIN up_pro_accept_v3_qdzc qdzc ON bm_id.zxi = qdzc.TaskName UNION ALL SELECT NAME FROM bm_id LEFT JOIN up_pro_accept_v3_rsj rsj ON bm_id.zxi = rsj.TaskName UNION ALL SELECT NAME FROM bm_id LEFT JOIN up_pro_accept_v3_sjba sjba ON bm_id.zxi = sjba.TaskName UNION ALL SELECT NAME FROM bm_id LEFT JOIN up_pro_accept_v3_spk spk ON bm_id.zxi = spk.TaskName UNION ALL SELECT NAME FROM bm_id LEFT JOIN up_pro_accept_v3_test test ON bm_id.zxi = test.TaskName UNION ALL SELECT NAME FROM bm_id LEFT JOIN up_pro_accept_v3_wygl wygl ON bm_id.zxi = wygl.TaskName UNION ALL SELECT NAME FROM bm_id LEFT JOIN up_pro_accept_v3_yms yms ON bm_id.zxi = yms.TaskName UNION ALL SELECT NAME FROM bm_id LEFT JOIN up_pro_accept_v3_zjj zjj ON bm_id.zxi = zjj.TaskName UNION ALL SELECT NAME FROM bm_id LEFT JOIN up_pro_accept_v3w v3w ON bm_id.zxi = v3w.TaskName ) tGROUP BY t.name
再来看下改写后的 SQL 执行计划,发现确实如我们预想的,在子查询中可以通过索引来进行表关联( 被驱动表 type 为 ref),然后 union all 汇聚数据,形成派生表,最后扫描派生表进行分组排序。
这里分组排序时只需要 全表扫描一次派生表 就可以得到结果,效率比之前快太多了!
改写后的 SQL 运行耗时为 13s 左右,速度快很多!
五、测试验证
为了严谨性,我们需要验证改写后的 SQL 结果集是否与原始 SQL 一致,也就是证明下这样改写 SQL 是否会产生语义上的变化,如果为了优化 SQL,连结果集都不准了,那就没意义了~
这里因为原始 SQL 执行太久,没法直接得到结果集对比,那么我们只能通过手动创建临时表来记录子查询结果集,然后再与 bm_id 表 关联查询,由于我们可以在临时表上创建索引,就不会出现原始 SQL 那种全表扫描的问题啦。
具体操作如下:
1. 创建临时表(带索引)
CREATE TABLE `tmp_up` ( `taskname` varchar(500) DEFAULT NULL, KEY `idx_taskname` (`taskname`));
2. 将子查询结果插入至临时表
insert into tmp_upselect taskname from up_pro_accept_v3_bdcunion all select taskname up_pro_accept_v3_hsjs......
3. 使用临时表代替子查询
select name,count(name) from bm_id a left JOIN (select TaskName from tmp_up )t on a.zxi = t.TaskName group by name
4. 对比下查询结果是否一致
惊讶的发现改写 SQL 的结果集会多出来很多?这里可以确认走临时表的结果集是肯定没问题的,那么问题肯定出在改写 SQL 上!
回头再仔细想一下,结合小测试,发现这样改写 SQL 确实会改变语义,问题主要是出在 LEFT JOIN,原本 bm_id 只做了 一次表关联,而改写 SQL 后,要做 多次表关联,导致最后的结果集会多出来一部分因为 LEFT JOIN 而产生的重复数据。
如果是 INNER JOIN,其实就不会产生重复数据,我们也测试下,结果确实如所想,内联是没问题的~
六、个人总结
这次 SQL 优化案例个人感觉是比较有难度的,很多点自己一开始也没有想到。就比如 SQL 改写,一开始以为是没有语义上的区别,直到做了测试才知道,所以啊,很多时候不能盲目自信啊。
针对这个 SQL 来说,想要直接通过改写 SQL 优化还是比较难的,当然这里说的是不改变语义的情况下,我暂时没有想到好的改写方式,也许是火候还不够。
解决方式总结有 2 个:
1. 用 内联 替代 左联,然后使用上述的改写 SQL,优点是 比较方便且查询速度较快,但是 结果集会变化。
2. 通过 临时表 代替 子查询,缺点是 比较繁琐,需要多个步骤实现,优点是 速度也较快 且 结果集不会变化。
附录:
http://mysql.taobao.org/monthly/2017/03/05/
https://blog.csdn.net/sun_ashe/article/details/89522394
https://imysql.com/node/103
https://dev.mysql.com/doc/refman/5.7/en/derived-table-optimization.html
相关推荐
- jQuery EasyUI使用教程:创建展开行详细编辑表单的CRUD应用
-
当切换datagrid视图到"detailview"时,用户可以展开一行来显示该行下面的任何详细信息。此功能允许用户为放置在行详细信息面板中的编辑表单提供恰当的布局。在本教程中,我们使用DataGri...
- 前端入门——html 表单控件使用(html表单组件)
-
上篇介绍了表单的使用,表单有很多控件,比如输入框,密码框、文本域,按钮等。按类型可分如下:输入类控件菜单类控件输入类组件——input此类控件有很多种类型,使用<inputtype=...
- [北大青鸟广州新嘉华]HTML5 表单属性有哪些?(1)
-
在编写HTML5页面时,我们很多时候都需要用到表单属性,那么HTML5作为一个新晋IT界红人,HTML5表单属性有哪些呢?今天先来分享一下其中的<form>/<input>...
- JavaScript FormData 对象(js file对象)
-
下面的代码创建了一个空的FormData对象:varformData=newFormData();//CurrentlyemptyFormData.append()FormData...
- 「layui」表单验证:验证注册(表单验证是什么)
-
注册界面手动验证获取短信验证码代码原文<!DOCTYPEhtml><htmllang="zh"><head>&...
- php使用file_get_contents(‘php://input‘)和$_POST的区别
-
为什么和第三方平台对接接口的时候,在接收http请求数据包时,一般都是用file_get_contents("php://input"),而不是用$_POST呢?file_get_co...
- 专为Vue打造的开源表单验证框架,Github star7k+——VeeValidate
-
介绍vee-validate是Vue.js的基于模板的验证框架,可以验证输入并显示错误。基于模板,只需为每个输入值更改时指定应使用哪种验证器。系统会在支持40多种语言环境的情况下自动生成错误。现成的规...
- 如何通过FORScan修改福特汽车系统模块内置数据
-
如何在Windows电脑或平板电脑上使用FORScan进行各种调整或编程MOD。FORScan与多个蓝牙或Wi-FiOBD适配器兼容。我个人建议您使用vlinkerMC蓝牙或vlinerMCW...
- PHP如何上传文件(php中实现文件上传需要用到哪几个函数)
-
文件上传是网站开发中常见的功能之一,它可以使用户轻松上传图片、音频、视频等文件。在PHP中,实现文件上传也非常简单。下面为大家介绍具体的步骤,让你的网站功能更加强大。步骤一:创建文件上传表单首先,我们...
- PHP入门读书笔记(十六):WEB页面使用PHP
-
Web表单主要用来在网页中发送数据到服务器,经过程序处理中,将用户所需要的信息再传递给客户端的浏览器上。这样就形成了一个浏览者和网站之间的一个互动。一、表单的提交方式<formname=’NA...
- 前端入门——html 表单(前端的表单是怎么实现的)
-
前言前面已经学习相关html大部分知识,基本上可以制作出简单的页面,但是这些页面都是静态的,一个网站如果要实现用户的互动交流,这时表单就起到关键的作用,表单的用途很多,它主要用来收集用户的相关信息,是...
- HTML表单4(form的action、method属性)——零基础自学网页制作
-
表单的工作过程表单的信息发送与处理过程可以简单的进行图示,如下图。以注册会员为例,用户在自己的电脑上打开相应的注册表单页面填写信息,完成填写后点击提交按钮,也就是图中1所示过程。这时浏览器会将这些信息...
- 为你的WordPress widget建立表单(wordpress divi)
-
通过之前的三部分教程我们已经创建了一个自己的WordPresswidget。今天我们将给大家介绍如何为你的widget创建表单,以至于WordPress可以及时的更新widget设置。为widget...
- 如何使用PHP编写一个简单的留言板?
-
留言板是一个常见的Web应用程序,允许用户在网站上发布和查看留言。在本文中,我们将使用PHP编写一个简单的留言板,介绍构建过程中的关键步骤和技巧。一、准备工作在开始编写留言板之前,我们需要准备好以下工...
- 3分钟拥有一个属于自己的博客网站「腾讯云篇」
-
一、前言想要搭建一个让全世界的人都可以访问的网站,我们最少需要准备三样东西:①服务器腾讯云服务器首年低至40元/年,「链接」阿里云服务器新用户可以免费使用6个月,新人特惠_云产品推荐_云服务器-阿里云...
- 一周热门
- 最近发表
-
- jQuery EasyUI使用教程:创建展开行详细编辑表单的CRUD应用
- 前端入门——html 表单控件使用(html表单组件)
- [北大青鸟广州新嘉华]HTML5 表单属性有哪些?(1)
- JavaScript FormData 对象(js file对象)
- 「layui」表单验证:验证注册(表单验证是什么)
- php使用file_get_contents(‘php://input‘)和$_POST的区别
- 专为Vue打造的开源表单验证框架,Github star7k+——VeeValidate
- 如何通过FORScan修改福特汽车系统模块内置数据
- PHP如何上传文件(php中实现文件上传需要用到哪几个函数)
- PHP入门读书笔记(十六):WEB页面使用PHP
- 标签列表
-
- 外键约束 oracle (36)
- oracle的row number (32)
- 唯一索引 oracle (34)
- oracle in 表变量 (28)
- oracle导出dmp导出 (28)
- oracle两个表 (20)
- oracle 数据库 字符集 (20)
- oracle安装补丁 (19)
- matlab化简多项式 (20)
- 多线程的创建方式 (29)
- 多线程 python (30)
- java多线程并发处理 (32)
- 宏程序代码一览表 (35)
- c++需要学多久 (25)
- css class选择器用法 (25)
- css样式引入 (30)
- html5和css3新特性 (19)
- css教程文字移动 (33)
- php简单源码 (36)
- php个人中心源码 (25)
- 网站管理平台php源码 (19)
- php小说爬取源码 (23)
- github好玩的php项目 (18)
- 云电脑app源码 (22)
- js创建txt文件 (18)