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