故障分析 | MySQL 派生表优化
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
up_pro_accept
开头的子表数据,最终组成 t 表(派生表)
。扫描
t 表(派生表)
相关的所有子表,可以看到这里每张子表走的都是全表扫描
,有些表较大,有 100 多 w,检索较慢。
a 表(bm_id)
与 t表(派生表)
进行关联查询,得到最后的结果。t 表 (派生表)
作为被驱动表
大约164W 行
左右,与a
表做关联查询时走的是全表扫描(ALL)
,a 表(bm_id)
作为驱动表
大约1.3W 行
左右,也就是说,表关联需要全表扫描t 表(派生表) 1.3W
次,而每次都需要扫描164W 行
数据,显然 SQL 的绝大部分时间其实都花在这一步上。
既然这个 SQL 优化涉及到了派生表,那么我们先看下何谓派生表,派生表有什么特性?
Derivedtable(派生表)
实际上是一种特殊的 subquery(子查询)
,它位于 SQL 语句中 FROM 子句
里面,可以看做是一个单独的表。Derived table(派生表)
进行 Materialize(物化)
,生成一个 临时表
用于保存 Derived table(派生表)
的结果,然后利用 临时表
来协助完成其他父查询的操作,比如 JOIN 等操作。Derived table(派生表)
做了一个新特性,该特性允许将符合条件的 Derived table(派生表)
中的子表与父查询的表合并进行直接 JOIN,类似于 Oracle 中的 子查询展开
,由优化器参数 optimizer_switch='derived_merge=ON'
来控制,默认为 打开
。derived_merge
特性存在很多限制,当派生子查询存在以下操作时,该特性无法生效。DISTINCT
、 GROUP BY
、 UNION/UNION ALL
、 HAVING
、 关联子查询
、 LIMIT/OFFSET
以及 聚合操作
等。举个简单例子:
select * from (select * from up_pro_accept_v3_bdc) a
where a.rowguid = '185c44aa-c23f-4e6f-bcd2-a38df16e2cc3'
union all
时全表扫描的问题。我们先解决问题 1,这个问题比较简单。
select TaskName from up_pro_accept_v3_xxx
类似这样,且外部关联字段也是 taskname
,所以我们只要在对应表上建立 taskname 的索引即可。t 表(派生表)
在关联时还是走的全表扫描,并没有用到 derived_merge
特性,所以 SQL 还是非常非常慢(上万 s)。接着我们来解决问题 2,这里主要解决派生表无法走索引的问题。
derived_merge
特性,将外部条件推入进子查询,但是这个特性的限制条件也很多,就比如我们这个 SQL,因为子查询里包括了 union all
,那么该 SQL 是无法利用到 derived_merge
特性的,因此无法直接走索引过滤。驱动表 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
) t
GROUP BY t.name
被驱动表 type 为 ref
),然后 union all
汇聚数据,形成派生表,最后扫描派生表进行分组排序。这里分组排序时只需要 全表扫描一次派生表
就可以得到结果,效率比之前快太多了!
13s
左右,速度快很多!bm_id 表
关联查询,由于我们可以在临时表上创建索引,就不会出现原始 SQL 那种全表扫描的问题啦。CREATE TABLE `tmp_up` (
`taskname` varchar(500) DEFAULT NULL,
KEY `idx_taskname` (`taskname`));
insert into tmp_up
select taskname from up_pro_accept_v3_bdc
union 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
,其实就不会产生重复数据,我们也测试下,结果确实如所想,内联是没问题的~解决方式总结有 2 个:
内联
替代 左联
,然后使用上述的改写 SQL,优点是 比较方便且查询速度较快
,但是 结果集会变化
。临时表
代替 子查询
,缺点是 比较繁琐,需要多个步骤实现
,优点是 速度也较快
且 结果集不会变化
。附录:http://mysql.taobao.org/monthly/2017/03/05/https://blog.csdn.net/sun_ashe/article/details/89522394https://imysql.com/node/103https://dev.mysql.com/doc/refman/5.7/en/derived-table-optimization.html
社区近期动态