優化mysql嵌套查詢和聯表查詢

嵌套查詢糟糕的優化

在上面我提到過,不考慮特殊的狀況,聯表查詢要比嵌套查詢更有效。儘管兩條查詢表達的是一樣的意思,儘管你的計劃是告訴服務器要作什麼,而後讓它決定怎麼作,但有時候你非得告訴它改怎麼作。不然優化器可能會作傻事。我最近就碰到這樣的狀況。這幾個表是三層分級關係:category, subcategory和item。有幾千條記錄在category表,幾百條記錄在subcategory表,以及幾百萬條在item表。你能夠忽略category表了,我只是交代一下背景,如下查詢語句都不涉及到它。這是建立表的語句:mysql

[sql]  view plain copy
 
  1. create table subcategory (  
  2.     id int not null primary key,  
  3.     category int not null,  
  4.     index(category)  
  5. ) engine=InnoDB;  
  6.   
  7. create table item(  
  8.     id int not null auto_increment primary key,  
  9.     subcategory int not null,  
  10.     index(subcategory)  
  11. ) engine=InnoDB;  


我又往表裏面填入一些樣本數據

sql

[sql]  view plain copy
 
  1. insert into subcategory(id, category)  
  2.     select i, i/100 from number  
  3.     where i <= 300000;  
  4.   
  5. insert into item(subcategory)  
  6.     select id  
  7.     from (  
  8.         select id, rand() * 20 as num_rows from subcategory  
  9.     ) as x  
  10.         cross join number  
  11.     where i <= num_rows;  
  12.   
  13. create temporary table t as  
  14.     select subcategory from item  
  15.     group by subcategory  
  16.     having count(*) = 19  
  17.     limit 100;  
  18.   
  19. insert into item (subcategory)  
  20.     select subcategory  
  21.     from t  
  22.         cross join number  
  23.     where i < 2000;  


再次說明,這些語句運行完須要一點時間,不適合放在產品環境中運行。思路是往item裏插入隨機行數的數據,這樣subcategory就有1到2018之間個item。這不是實際中的完整數據,但效果同樣。

我想找出某個category中item數大於2000的所有subcategory。首先,我找到一個subcategory item數大於2000的,而後把它的category用在接下來的查詢中。這是具體的查詢語句:

安全

[sql]  view plain copy
 
  1. select c.id  
  2. from subcategory as c  
  3.     inner join item as i on i.subcategory = c.id  
  4. group by c.id  
  5. having count(*) > 2000;  
  6.   
  7. -- choose one of the results, then  
  8. select * from subcategory where id = ????  
  9. -- result: category = 14  


我拿到一個合適的值14,在如下的查詢中會用到它。這是用來查詢category 14 中全部item數大於2000的subcategory的語句:

服務器

[sql]  view plain copy
 
  1. select c.id  
  2. from subcategory as c  
  3.     inner join item as i on i.subcategory = c.id  
  4. where c.category = 14  
  5. group by c.id  
  6. having count(*) > 2000;  


在個人樣例數據裏,查詢的結果有10行記錄,並且只用10多秒就完成了。EXPLAIN顯示出很好地使用了索引;從數據的規模來看,至關不錯了。查詢計劃是在索引上遍歷並計算出目標記錄。目前爲止,很是好。

這回假設我要從subcategory取出所有的字段。我能夠把上面的查詢當成嵌套,而後用JOIN,或者SELECT MAX之類(既然分組集對應的值都是惟一的),但也寫成跟下面的同樣的,有木有?

性能

[sql]  view plain copy
 
  1. select * from subcategory  
  2. where id in (  
  3.     select c.id  
  4.     from subcategory as c  
  5.         inner join item as i on i.subcategory = c.id  
  6.     where c.category = 14  
  7.     group by c.id  
  8.     having count(*) > 2000  
  9. );  


跑完這條查詢估計要從破曉到夕陽沉入大地。我不知道它要跑多久,由於我沒打算讓它無休止地跑下去。你可能認爲,單從語句上理解,它會:a)計算出裏面的查詢,找出那10個值,b)繼續找出那10條記錄,而且在primary索引上去找會很是地快。錯,這是實際上的查詢計劃:

大數據

[sql]  view plain copy
 
  1. *************************** 1. row ***************************  
  2.            id: 1  
  3.   select_type: PRIMARY  
  4.         table: subcategory  
  5.          type: ALL  
  6. possible_keys: NULL  
  7.           key: NULL  
  8.       key_len: NULL  
  9.           ref: NULL  
  10.          rows: 300783  
  11.         Extra: Using where  
  12. *************************** 2. row ***************************  
  13.            id: 2  
  14.   select_type: DEPENDENT SUBQUERY  
  15.         table: c  
  16.          type: ref  
  17. possible_keys: PRIMARY,category  
  18.           key: category  
  19.       key_len: 4  
  20.           ref: const  
  21.          rows: 100  
  22.         Extra: Using where; Using index; Using temporary; Using filesort  
  23. *************************** 3. row ***************************  
  24.            id: 2  
  25.   select_type: DEPENDENT SUBQUERY  
  26.         table: i  
  27.          type: ref  
  28. possible_keys: subcategory  
  29.           key: subcategory  
  30.       key_len: 4  
  31.           ref: c.id  
  32.          rows: 28  
  33.         Extra: Using index  


如何你不熟悉如何分析mysql的語句查詢計劃,請看大概意思:mysql計劃從外到內執行查詢,而不是從內到外。我會一個一個地介紹查詢的每一個部分。

外面的查詢簡單地變成了SELECT * FROM subcategory。雖然裏面的查詢對subcategory有個約束(WHERE category = 14),但出於某些緣由mysql沒有將它做用於外面的查詢。我不知道是神馬緣由。我只知道它掃描了整張表(這就是 type:ALL 表示的意思),而且沒有使用任何的索引。這是在10幾萬行記錄的表上掃描。

在外面的查詢,對每行都執行一次裏面的查詢,儘管沒有值被裏面的查詢使用到,由於裏面的查詢被「優化」成引用外面的查詢。照此分析,查詢計劃變成了嵌套循環。外面的查詢的每一次循環,都執行一次裏面的查詢。下面就是優化器重寫後的查詢計劃:

優化

[sql]  view plain copy
 
  1. select * from subcategory as s  
  2. where <in_optimizer>(  
  3.    s.id,<exists>(  
  4.    select c.id  
  5.    from subcategory as c  
  6.       join item as i  
  7.    where ((i.subcategory = c.id) and (c.category = 14))  
  8.    group by c.id  
  9.    having ((count(0) > 2000)  
  10.       and (<cache>(s.id) = <ref_null_helper>(c.id))))  
  11. )  


你能夠經過在EXPLAIN EXTENDED 後面帶上SHOW WARNINGS 獲得優化後的查詢。請留意在HAVING子句中指向的外部域。

我舉這個例子並不是有意抨擊mysql的優化策略。衆所皆知mysql在有些狀況下還不能很好地優化嵌套查詢,這個問題已經被普遍報告過。我想指出的是,開發者有必要檢查查詢語句確保它們不是被糟糕地優化。大多數狀況下,安全起見若非是非必要,避免使用嵌套——尤爲是WHERE...IN() 和 WHERE...NOT IN語句。

我本身的原則是「有疑問,EXPLAIN看看」。若是面對的是一個大數據表,我會天然而然地產生疑問。

spa

如何強制裏面的查詢先執行

上一節中的語句撞板只由於mysql把它當成相關的語句從外到裏地執行,而不是當成不相關語句從裏到外執行。讓mysql先執行裏面的查詢也是有辦法的,當成臨時表來實現,從而避免巨大的性能開銷。

mysql從臨時表來實現嵌套查詢(某種程度上被訛傳的衍生表)。這意味着mysql先執行裏面的查詢,而且把結果儲存在臨時表中,而後在其餘的表裏用到它。這就是我寫這個查詢時所期待的執行方式。查詢語句修改以下:.net

[sql]  view plain copy
 
  1. select * from subcategory  
  2. where id in (  
  3.     select id from (  
  4.         select c.id  
  5.         from subcategory as c  
  6.             inner join item as i on i.subcategory = c.id  
  7.         where c.category = 14  
  8.         group by c.id  
  9.         having count(*) > 2000  
  10.     ) as x  
  11. );  


我所作的就是把嵌套包着原來的嵌套查詢。mysql會認爲最裏面是一個獨立的嵌套查詢先執行,而後如今只剩下包着外面的嵌套,它已經被裝進一個臨時表裏,只有少許記錄,所以要快不少。依此分析,這是至關笨的優化辦法;倒不如把它重寫成join方式。再說,省得被別人看到,當成多餘代碼清理掉。

有些狀況可使用這種優化方法,好比mysql拋出錯誤,嵌套查詢的表在其餘地方被修改(譯註:另外一篇文章MySQL SELECT同時UPDATE同一張表 )。不幸的是,對於臨時表只能在查詢語句中使用一次的狀況,這種方法就無能爲力了。


轉載請標明出處  http://blog.csdn.net/afeiqiang/article/details/8620038
節選自 http://www.xaprb.com/blog/2006/04/30/how-to-optimize-subqueries-and-joins-in-mysql/blog

相關文章
相關標籤/搜索