SQL慢查詢優化之聯表查詢
一、前提基礎
1、關聯查詢:
MySQL 表關聯的演算法是 Nest Loop Join,是通過驅動表的結果集作為迴圈基礎資料,然後一條一條地通過該結果集中的資料作為過濾條件到下一個表中查詢資料,然後合併結果。2、驅動表定義:
1)制定了聯接條件時,滿足查詢條件記錄行數少的為驅動表;2)未指定聯接條件時,行數少的表為驅動表。(劃重點!!!)。
對驅動表可以直接排序,對非驅動表(的欄位排序)需要對迴圈查詢的合併結果(臨時表)進行排序(劃重點!!!)。
3、Explain結果詳情:
Explain的結果中,第一行出現的表就是驅動表(劃重點!!!)
在上面的Explain結果中,id如果相同,可以認為是一組,從上往下順序執行;在所有組中,id值越大,優先順序越高,越先執行。
建議:如果你搞不清楚該讓誰做驅動表、誰 join 誰,請讓 MySQL 執行時自行判斷
既然“未指定聯接條件時,行數少的表為[驅動表]”了,
而且你也對自己寫出的複雜的 Nested Loop Join 不太有把握,
就別指定誰 left/right join 誰了,
請交給 MySQL優化器 執行時決定吧。
如果對自己特別有信心,可以參考:像火丁一樣優化。
優化的目標是儘可能減少JOIN中Nested Loop的迴圈次數,以此保證:
永遠用小結果集驅動大結果集(圈重點!!!)。
二、例項分析
1、SQL語句如下:
SELECT shipD.ID, shipD.SHIPMENT_NO, shipD.OUTBOUND_ID, shipD.OUTBOUND_NO, orderM.OUTBOUND_TYPE, orderM.OUTBOUNDSUB_TYPE, shipD.GOODS_VOLUMN, shipD.PACKING_UNIT, shipD.PACKING_QTY, shipD.PICKTASK_ID, shipD.TASK_TYPE, ... orderM.dest_warehouse_no, orderM.dest_distribute_no, orderM.dest_org_no FROM OB_INTERNAL_SHIP_M shipM JOIN OB_INTERNAL_SHIP_D shipD ON shipD.shipment_no = shipM.shipment_no AND shipM.ORG_NO = shipD.ORG_NO AND shipM.warehouse_no = shipD.WAREHOUSE_NO JOIN OB_INTERNAL_ORDER_M orderM ON shipD.outbound_no = orderM.outbound_no WHERE shipM.BILL_NO = 'CD117090620' AND shipM.HAND_NO = '43017090603370789' AND shipD.ORG_NO = '4' AND shipD.DISTRIBUTE_NO = '4' AND shipD.WAREHOUSE_NO = '30'
Explain結果:
解決思路:ob_internal_ship_m: KEY `idx_update_time` (`UPDATE_TIME`), KEY ‘idx_SHIPMENT_NO’ (‘SHIPMENT_NO’) ob_internal_ship_d: KEY `idx_update_time` (`UPDATE_TIME`), KEY `idx_unique_key` (`UNIQUE_KEY`) KEY ‘idx_SHIPMENT_NO’ (‘SHIPMENT_NO’), KEY ‘idx_BOX_NO’(‘BOX_NO’) ob_internal_order_m: UNIQUE KEY `OB_INTERNAL_ORDER_m_index` (`OUTBOUND_NO`), KEY `idx_update_time` (`UPDATE_TIME`), KEY’idx_outbound_no’(‘OUTBOUND_NO’), KEY’idx_WAVE_NO’(‘WAVE_NO’), KEY’idx_OPRATE_STATUS’(‘OPRATE_STATUS’)
聯查時on後面的條件不滿足時可能會出現全表查詢,where查詢條件也沒有shipment_no,確定shipment_no是否滿足查詢,若不滿足,考慮將現有where條件中的bill_no, hand_no加上索引。
最終方案:
業務程式碼中有兩處使用該sql語句,其中一個無法獲取shipment_no,所以採用新增索引的方式,將shipment_m表新增KEY `idx_bill_no` (`BILL_NO`), KEY `idx_hand_no` (`HAND_NO`)。
2、SQL語句如下:
SELECT DISTINCT
(waveM.wave_no)
FROM
OB_INTERNAL_WAVE_M waveM
LEFT JOIN OB_INTERNAL_ORDER_M orderM ON waveM.wave_no = orderM.wave_no
WHERE
waveM.YN = N
AND waveM.ORG_NO = 'S'
AND waveM.DISTRIBUTE_NO = 'S'
AND waveM.WAREHOUSE_NO = 'S'
AND waveM.OUTBOUND_TYPE = 'S'
AND waveM.WAVE_TYPE = 'S'
AND waveM.WAVE_STATUS = N
AND orderM.outboundsub_type IN ('S', 'S', 'S')
ORDER BY
waveM.CREATE_TIME DESC
Explain結果:
表中現有索引:
ob_internal_order_m: UNIQUE KEY `OB_INTERNAL_ORDER_m_index` (`OUTBOUND_NO`), KEY `idx_update_time` (`UPDATE_TIME`), KEY’idx_outbound_no’(‘OUTBOUND_NO’), KEY’idx_WAVE_NO’(‘WAVE_NO’), KEY’idx_OPRATE_STATUS’(‘OPRATE_STATUS’)
ob_internal_wave_m: KEY `idx_update_time` (`UPDATE_TIME`), KEY `idx_wave_no` (`WAVE_NO`), KEY `idx_wave_type` (`WAVE_TYPE`), KEY `idx_source` (`SOURCE`), KEY ’WAVE_STATUS’(‘WAVE_STATUS’)
解決思路:確定業務邏輯是否需要對SQL排序,去掉distinct去重,在業務層做去重。
最終方案:
業務中不需要排序,去掉order by,去掉sql中的distinct,在業務層做去重。(如業務中需要排序,考慮將wave_m表中的update_time或者主鍵id作為order by條件)
PS:去掉distinct可解決Using temporary臨時表問題,去掉order by或使用update_time或id可解決Using filesort問題。
三、歸納總結
不要太過於相信生產環境的SQL的執行速度,多觀察執行效率慢的SQL語句,不要只看執行的瞬時時間。使用Explain,如出現以下常見的情況,請注意優化:- type出現ALL或者index的;
- possible_keys出現過多(待選)索引;
- key為NULL的(沒走索引);
- rows過多,或者幾乎是全表的記錄數的;
- Extra中出現Using temporary的,(Using filesort需視情況而定);
type為ALL或index,all最壞情況全表查,index和全表掃描一樣。只是掃描表的時候按照索引次序進行而不是行。主要優點就是避免了排序, 但是開銷仍然非常大。如在Extra列看到Using index,說明正在使用覆蓋索引,只掃描索引的資料,它比按索引次序全表掃描的開銷要小很多。
Using temporary,用臨時表儲存中間結果,常用於GROUP BY 和 ORDER BY操作中,一般看到它說明查詢需要優化了,就算避免不了臨時表的使用也要儘量避免硬碟臨時表的使用。
Using filesort,MySQL有兩種方式可以生成有序的結果,通過排序操作或者使用索引,當Extra中出現了Using filesort 說明MySQL使用了後者,但注意雖然叫filesort但並不是說明就是用了檔案來進行排序,只要可能排序都是在記憶體裡完成的。大部分情況下利用索引排序更快,所以一般這時也要考慮優化查詢了。使用檔案完成排序操作,這是可能是ordery by,group by語句的結果,這可能是一個CPU密集型的過程,可以通過選擇合適的索引來改進效能,用索引來為查詢結果排序。
日常SQL優化的大體思路:
使用Explain,確定是否出現如上需要優化的情況;
理清該SQL對應的業務邏輯(如:為什麼這麼寫,改動對現有業務有無影響,在業務層改動是否可行等);現有條件能否滿足優化需求(如:已存在索引等);
以上方法無法滿足現有優化需求時,最後在考慮是否對現有表結構或SQL語句進行改動(如:新增索引,join是否去掉等)。
總之,SQL優化是一個複雜的過程,需要慢工出細活,其中的道理需要我們慢慢積累,逐步進階。
參考資料: