筆者最近遇到一則典型的因為sql中存在派生表造成的性能案例,通過改寫SQL改善了的性能,但當時并沒有弄清楚這其中的原因,派生表究竟是什么原因會導致性能上的副作用。 開始之前,先看一下MySQL 5.7.20下面的奇葩的現(xiàn)象,感受一下MySQL對派生表的支持有多弱。 同樣的表結構,在sqlserver里面,按照預期的走了索引的seek
什么是派生表 關于派生表的定義,不贅述了,以下截圖來自于愛可生公司的公眾號中,說的非常清晰,連接地址為:https://mp.weixin.qq.com/s/CxagKla3Z6Q6RJ-x5kuUAA,侵刪,謝謝。
測試場景 假設是在MySQL的關系數(shù)據中,試想有這個一個查詢:一個訂單表以及對應的物流信息表,關系為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的比例往兩張表中寫入測試數(shù)據,也就是說一條訂單存在10條物流信息,其訂單的物流信息的創(chuàng)建時間隨機分布在一定的時間范圍。測試數(shù)據在百萬級就夠了。 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 這是典型的一條數(shù)據示例(訂單和其物流信息
派生表的性能問題 這里插一句:很多人包括面試都會問,SQL優(yōu)化有哪些技巧? 對于這個SQL,我個人傾向于先通過派生表對子表做一個清晰的排序實現(xiàn),然后父查詢進行過濾(篩選最新的一條數(shù)據), 可以看到,派生表內部是一個全表掃描,也就是說跟t2做做一個全表掃描,然后對每個訂單的物流信息排序,然后再根據外層的查詢進行訂單號的篩選(where a.c1 = 99999) 這里涉及到一個derived_merge相關的實現(xiàn), 基于此重新改寫了一下SQL,如下,主表和子表先join起來,同時對子表進行排序,然后再外層篩選最新的一條信息(t.sort_num = 1), 其實這個執(zhí)行計劃,才是上面提到的“預期的”執(zhí)行計劃,篩選條件同時應用到了兩張表中,進過篩選之后再做邏輯上的排序計算。 其實這里就可以不回歸到本文一開始提到的派生表的限制了,這個截圖來自于這里:https://blog.csdn.net/sun_ashe/article/details/89522394,侵刪。 可以認為,任何一個走向continue的分支的情況,都是無法使用derived_merge的。
其實本文中的示例SQL繼續(xù)簡化一下,就非常明顯了,這里不去join任何表,僅對t2表做一個分析查詢,然后刻意基于派生表實現(xiàn)篩選,其執(zhí)行計劃并不是理想中的索引查找 上文中的查詢,與join的參與并無關系,其實就派生表中有用戶變量造成的,這里看到執(zhí)行計劃走的是一個全表掃描 如果不使用派生表的方式,其執(zhí)行計劃就是索引查找
MySQL 8.0的分析函數(shù)
總結 以上通過一個簡單的案例,來說了了derived_merge的限制,可能這些在其他數(shù)據庫上不是問題的問題,在MySQL上都是問題,實際上MySQL優(yōu)化器還是需要提升的。
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;
|
|