嵌套查詢糟糕的優化
在上面我提到過,不考慮特殊的狀況,聯表查詢要比嵌套查詢更有效。儘管兩條查詢表達的是一樣的意思,儘管你的計劃是告訴服務器要作什麼,而後讓它決定怎麼作,但有時候你非得告訴它改怎麼作。不然優化器可能會作傻事。我最近就碰到這樣的狀況。這幾個表是三層分級關係:category, subcategory和item。有幾千條記錄在category表,幾百條記錄在subcategory表,以及幾百萬條在item表。你能夠忽略category表了,我只是交代一下背景,如下查詢語句都不涉及到它。這是建立表的語句:mysql
- create table subcategory (
- id int not null primary key,
- category int not null,
- index(category)
- ) engine=InnoDB;
-
- create table item(
- id int not null auto_increment primary key,
- subcategory int not null,
- index(subcategory)
- ) engine=InnoDB;
我又往表裏面填入一些樣本數據
sql
- insert into subcategory(id, category)
- select i, i/100 from number
- where i <= 300000;
-
- insert into item(subcategory)
- select id
- from (
- select id, rand() * 20 as num_rows from subcategory
- ) as x
- cross join number
- where i <= num_rows;
-
- create temporary table t as
- select subcategory from item
- group by subcategory
- having count(*) = 19
- limit 100;
-
- insert into item (subcategory)
- select subcategory
- from t
- cross join number
- where i < 2000;
再次說明,這些語句運行完須要一點時間,不適合放在產品環境中運行。思路是往item裏插入隨機行數的數據,這樣subcategory就有1到2018之間個item。這不是實際中的完整數據,但效果同樣。
我想找出某個category中item數大於2000的所有subcategory。首先,我找到一個subcategory item數大於2000的,而後把它的category用在接下來的查詢中。這是具體的查詢語句:
安全
- select c.id
- from subcategory as c
- inner join item as i on i.subcategory = c.id
- group by c.id
- having count(*) > 2000;
-
- select * from subcategory where id = ????
我拿到一個合適的值14,在如下的查詢中會用到它。這是用來查詢category 14 中全部item數大於2000的subcategory的語句:
服務器
- select c.id
- from subcategory as c
- inner join item as i on i.subcategory = c.id
- where c.category = 14
- group by c.id
- having count(*) > 2000;
在個人樣例數據裏,查詢的結果有10行記錄,並且只用10多秒就完成了。EXPLAIN顯示出很好地使用了索引;從數據的規模來看,至關不錯了。查詢計劃是在索引上遍歷並計算出目標記錄。目前爲止,很是好。
這回假設我要從subcategory取出所有的字段。我能夠把上面的查詢當成嵌套,而後用JOIN,或者SELECT MAX之類(既然分組集對應的值都是惟一的),但也寫成跟下面的同樣的,有木有?
性能
- select * from subcategory
- where id in (
- select c.id
- from subcategory as c
- inner join item as i on i.subcategory = c.id
- where c.category = 14
- group by c.id
- having count(*) > 2000
- );
跑完這條查詢估計要從破曉到夕陽沉入大地。我不知道它要跑多久,由於我沒打算讓它無休止地跑下去。你可能認爲,單從語句上理解,它會:a)計算出裏面的查詢,找出那10個值,b)繼續找出那10條記錄,而且在primary索引上去找會很是地快。錯,這是實際上的查詢計劃:
大數據
- *************************** 1. row ***************************
- id: 1
- select_type: PRIMARY
- table: subcategory
- type: ALL
- possible_keys: NULL
- key: NULL
- key_len: NULL
- ref: NULL
- rows: 300783
- Extra: Using where
- *************************** 2. row ***************************
- id: 2
- select_type: DEPENDENT SUBQUERY
- table: c
- type: ref
- possible_keys: PRIMARY,category
- key: category
- key_len: 4
- ref: const
- rows: 100
- Extra: Using where; Using index; Using temporary; Using filesort
- *************************** 3. row ***************************
- id: 2
- select_type: DEPENDENT SUBQUERY
- table: i
- type: ref
- possible_keys: subcategory
- key: subcategory
- key_len: 4
- ref: c.id
- rows: 28
- Extra: Using index
如何你不熟悉如何分析mysql的語句查詢計劃,請看大概意思:mysql計劃從外到內執行查詢,而不是從內到外。我會一個一個地介紹查詢的每一個部分。
外面的查詢簡單地變成了SELECT * FROM subcategory。雖然裏面的查詢對subcategory有個約束(WHERE category = 14),但出於某些緣由mysql沒有將它做用於外面的查詢。我不知道是神馬緣由。我只知道它掃描了整張表(這就是 type:ALL 表示的意思),而且沒有使用任何的索引。這是在10幾萬行記錄的表上掃描。
在外面的查詢,對每行都執行一次裏面的查詢,儘管沒有值被裏面的查詢使用到,由於裏面的查詢被「優化」成引用外面的查詢。照此分析,查詢計劃變成了嵌套循環。外面的查詢的每一次循環,都執行一次裏面的查詢。下面就是優化器重寫後的查詢計劃:
優化
- select * from subcategory as s
- where <in_optimizer>(
- s.id,<exists>(
- select c.id
- from subcategory as c
- join item as i
- where ((i.subcategory = c.id) and (c.category = 14))
- group by c.id
- having ((count(0) > 2000)
- and (<cache>(s.id) = <ref_null_helper>(c.id))))
- )
你能夠經過在EXPLAIN EXTENDED 後面帶上SHOW WARNINGS 獲得優化後的查詢。請留意在HAVING子句中指向的外部域。
我舉這個例子並不是有意抨擊mysql的優化策略。衆所皆知mysql在有些狀況下還不能很好地優化嵌套查詢,這個問題已經被普遍報告過。我想指出的是,開發者有必要檢查查詢語句確保它們不是被糟糕地優化。大多數狀況下,安全起見若非是非必要,避免使用嵌套——尤爲是WHERE...IN() 和 WHERE...NOT IN語句。
我本身的原則是「有疑問,EXPLAIN看看」。若是面對的是一個大數據表,我會天然而然地產生疑問。
spa
如何強制裏面的查詢先執行
上一節中的語句撞板只由於mysql把它當成相關的語句從外到裏地執行,而不是當成不相關語句從裏到外執行。讓mysql先執行裏面的查詢也是有辦法的,當成臨時表來實現,從而避免巨大的性能開銷。
mysql從臨時表來實現嵌套查詢(某種程度上被訛傳的衍生表)。這意味着mysql先執行裏面的查詢,而且把結果儲存在臨時表中,而後在其餘的表裏用到它。這就是我寫這個查詢時所期待的執行方式。查詢語句修改以下:.net
- select * from subcategory
- where id in (
- select id from (
- select c.id
- from subcategory as c
- inner join item as i on i.subcategory = c.id
- where c.category = 14
- group by c.id
- having count(*) > 2000
- ) as x
- );
我所作的就是把嵌套包着原來的嵌套查詢。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