笔者最近遇到一则典型的因为sql中存在派生表造成的性能案例,通过改写SQL改善了的性能,但当时并没有弄清楚这其中的原因,派生表究竟是什么原因会导致性能上的副作用。 开始之前,先看一下MySQL 5.7.20下面的奇葩的现象,感受一下MySQL对派生表的支持有多弱。 同样的表结构,在sqlserver里面,按照预期的走了索引的seek
什么是派生表 关于派生表的定义,不赘述了,以下截图来自于爱可生公司的公众号中,说的非常清晰,连接地址为:https://mp.weixin.qq.com/s/CxagKla3Z6Q6RJ-x5kuUAA,侵删,谢谢。
测试场景 假设是在MySQL的关系数据中,试想有这个一个查询:一个订单表以及对应的物流信息表,关系为1:N,查询订单和其最新的1条物流信息,这个查询该怎么写(假设问题存在而不论证其是否合理)? CREATE TABLE t1 ( id INT AUTO_INCREMENT PRIMARY key, c1 INT, c2 VARCHAR(50), create_date datetime ); CREATE TABLE t2 ( id INT AUTO_INCREMENT PRIMARY key, c1 INT, c2 VARCHAR(50), create_date datetime ); CREATE INDEX idx_c1 ON t1(c1); CREATE INDEX idx_c1 ON t2(c1); 按照1:10的比例往两张表中写入测试数据,也就是说一条订单存在10条物流信息,其订单的物流信息的创建时间随机分布在一定的时间范围。测试数据在百万级就够了。 CREATE DEFINER=`root`@`%` PROCEDURE `create_test_data`( IN `loop_count` INT ) BEGIN SET @p_loop = 0; while @p_loop<loop_count do SET @p_date = DATE_ADD(NOW(),INTERVAL -RAND()*100 DAY); INSERT INTO t1 (c1,c2,create_date) VALUES (@p_loop,UUID(),@p_date); INSERT INTO t2 (c1,c2,create_date) VALUES (@p_loop,UUID(),DATE_ADD(@p_date,INTERVAL RAND()*10000 MINUTE)); INSERT INTO t2 (c1,c2,create_date) VALUES (@p_loop,UUID(),DATE_ADD(@p_date,INTERVAL RAND()*10000 MINUTE)); INSERT INTO t2 (c1,c2,create_date) VALUES (@p_loop,UUID(),DATE_ADD(@p_date,INTERVAL RAND()*10000 MINUTE)); INSERT INTO t2 (c1,c2,create_date) VALUES (@p_loop,UUID(),DATE_ADD(@p_date,INTERVAL RAND()*10000 MINUTE)); INSERT INTO t2 (c1,c2,create_date) VALUES (@p_loop,UUID(),DATE_ADD(@p_date,INTERVAL RAND()*10000 MINUTE)); INSERT INTO t2 (c1,c2,create_date) VALUES (@p_loop,UUID(),DATE_ADD(@p_date,INTERVAL RAND()*10000 MINUTE)); INSERT INTO t2 (c1,c2,create_date) VALUES (@p_loop,UUID(),DATE_ADD(@p_date,INTERVAL RAND()*10000 MINUTE)); INSERT INTO t2 (c1,c2,create_date) VALUES (@p_loop,UUID(),DATE_ADD(@p_date,INTERVAL RAND()*10000 MINUTE)); INSERT INTO t2 (c1,c2,create_date) VALUES (@p_loop,UUID(),DATE_ADD(@p_date,INTERVAL RAND()*10000 MINUTE)); INSERT INTO t2 (c1,c2,create_date) VALUES (@p_loop,UUID(),DATE_ADD(@p_date,INTERVAL RAND()*10000 MINUTE)); SET @p_loop = @p_loop+1; END while; END 这是典型的一条数据示例(订单和其物流信息
派生表的性能问题 这里插一句:很多人包括面试都会问,SQL优化有哪些技巧? 对于这个SQL,我个人倾向于先通过派生表对子表做一个清晰的排序实现,然后父查询进行过滤(筛选最新的一条数据), 可以看到,派生表内部是一个全表扫描,也就是说跟t2做做一个全表扫描,然后对每个订单的物流信息排序,然后再根据外层的查询进行订单号的筛选(where a.c1 = 99999) 这里涉及到一个derived_merge相关的实现, 基于此重新改写了一下SQL,如下,主表和子表先join起来,同时对子表进行排序,然后再外层筛选最新的一条信息(t.sort_num = 1), 其实这个执行计划,才是上面提到的“预期的”执行计划,筛选条件同时应用到了两张表中,进过筛选之后再做逻辑上的排序计算。 其实这里就可以不回归到本文一开始提到的派生表的限制了,这个截图来自于这里:https://blog.csdn.net/sun_ashe/article/details/89522394,侵删。 可以认为,任何一个走向continue的分支的情况,都是无法使用derived_merge的。
其实本文中的示例SQL继续简化一下,就非常明显了,这里不去join任何表,仅对t2表做一个分析查询,然后刻意基于派生表实现筛选,其执行计划并不是理想中的索引查找 上文中的查询,与join的参与并无关系,其实就派生表中有用户变量造成的,这里看到执行计划走的是一个全表扫描 如果不使用派生表的方式,其执行计划就是索引查找
MySQL 8.0的分析函数
总结 以上通过一个简单的案例,来说了了derived_merge的限制,可能这些在其他数据库上不是问题的问题,在MySQL上都是问题,实际上MySQL优化器还是需要提升的。
demo的sql SET @sort_num=0; SET @group_category=NULL; SELECT a.c1,a.c2 AS order_info,a.create_date AS order_date,t.c2 AS express_log,t.create_date AS express_log_date FROM t1 a INNER JOIN ( SELECT IF(@group_category=b.c1, @sort_num:=@sort_num+1, @sort_num:=1) sort_num, IF(@group_category=b.c1, @group_category, @group_category:=b.c1) group_category, b.* FROM t2 b ORDER BY b.c1 DESC , b.create_date DESC )t ON t.c1 = a.c1 WHERE a.c1 = 99999 AND t.sort_num = 1; SET @sort_num=0; SET @group_category=NULL; SELECT * FROM ( SELECT IF(@group_category=b.c1, @sort_num:=@sort_num+1, @sort_num:=1) sort_num, IF(@group_category=b.c1, @group_category, @group_category:=b.c1) group_category, a.c1,a.c2 AS order_info, a.create_date AS order_date, b.c2 AS express_log, b.create_date AS express_log_date FROM t1 a inner join t2 b ON a.c1 = b.c1 WHERE a.c1 = 99999 ORDER BY b.c1 DESC , b.create_date DESC )t WHERE t.sort_num = 1; SELECT * FROM ( SELECT row_number()over(PARTITION BY a.c1 ORDER BY b.create_date desc) as sort_num, a.c1, a.c2 AS order_info, a.create_date AS order_date, b.c2 AS express_log, b.create_date AS express_log_date FROM t1 a inner join t2 b ON a.c1 = b.c1 WHERE b.c1 = 99999 )t WHERE t.sort_num = 1;
|
|