SQL優化三板斧:精簡之道、驅動為王、集合為本
作者介紹
黃浩,現任職於中國惠普,從業十年,始終專注於SQL。在華為做專案的兩年多,做過大大小小的SQL多達1500個。閒暇之餘,喜歡將部分案例寫成部落格發表在華為內部資料庫官方社群,反響強烈,已連續四個月蟬聯該社群最佳博主。目前已開設專欄“優哉悠齋”,成為首個受邀社群“專家訪談”的外協人員。
公元2016年8月1日晚上,朋友圈流行著這樣一個段子:特想摸清檯風“妮妲”的威力有多大,一專業人士說:只須一句話就能讓你深刻理解。遂追問,答曰:“就連華為都通知放假了?”感謝“妮妲”,讓深圳這座高速運轉的城市在星期二這天暫停了;感謝華為,讓我這個來深10年,為生活奔波勞頓的人也能倚在窗前,眼觀疾風驟雨之變,心遊驚濤駭浪之中。
妮妲走了,SQL來了
8月3日,一同事轉來一個SQL,我開啟檔案,發現整個程式碼多達347行。
在DB中執行,時耗達到了4分多鐘,再往下鑽取,如同蝸牛一般,根本鑽不動,14分鐘過去了,還只鑽取到了800行。
由此該SQL的效能表現為“兩慢”:首條返回慢、下鑽提取慢。大多數情況,我們只會遇其一,要麼快速返回出現效能瓶頸,要麼全部提取出現效能瓶頸。這回好了,都齊全了。透過窗戶,望著被“妮妲”肆意狂虐後葉顫枝亂的樹木,心裡不禁在想:伺服器也被“妮妲”肆虐了?
此時,颱風“妮妲”瘋狂過後的溫馨涼意,也沒能讓我心如止水,畢竟這個優化任務看起來有些棘手。
人生若只如初見
因為來者不善,而時間寬限,我也計劃打持久戰。在展開分析前,我對SQL中的表物件和資料量做了初步統計。如下:
人生若只如初見,初見往往是美妙的,讓人心曠神怡的。而與該SQL的初次交流,畫面卻是暗潮湧動殺機四伏:
- 動輒千萬上億的資料量,近40次物件訪問,還不包括VIEW中的表物件。
- 從SQL程式碼上看,出現了聚合函式,因此可以斷定是批量資料處理。
以上兩點,按經驗,能2分鐘跑出來就不錯了,現在是要求2~3S,看起來是一個不可完成的任務。
第一板斧:大刀闊斧
在初步分析中,ORDER_RELEASE和ORDER_RELEASE_REFNUM兩個表是最搶眼的,資料量分別是千萬級和億級,訪問次數更是驚人的達到了10次以上。好奇心我決定以這兩個表為切入口,探究下是如何被訪問的?
藉助於NOTEPAD++編輯神器,很快定位到了這兩個表的訪問情況:
初步一看:
這兩個表的訪問基本上都是在子查詢中,而且都是成對出現
仔細對比了子查詢後,發現這些子查詢可分A、B兩類
A類子查詢共有5個的程式碼都是完全一樣的,如下:
4、B類子查詢共有3個的程式碼都是完全一樣的,如下
深入子查詢內部,無論是A類子查詢還是B類子查詢,ORDER_RELEASER和ORDER_RELEASE_REFNUMO_REF的關聯方式都是一樣的,關聯欄位是ORDER_RELEASE_GID。此時,結合兩個表的命名,按多年的經驗,我猜想:
ORDER_RELEASE_GID為ORDER_RELEASE表的主鍵欄位
ORDER_RELEASE_REFNUM與ORDER_RELEASE表存在主外來鍵約束,欄位就是ORDER_RELEASE_GID
為了驗證我的假設,我VIEW了ORDER_RELEASE_REFNUM的表結構,如下:
果真如此。那麼問題來了,即便如此,我們又能做什麼呢?答案很簡單,這兩類子查詢中,ORDER_RELEASE表可以被“砍掉”。等價的SQL如下:
A類:
B類:
再看看這個子查詢的資料量:
只有8千多條,相對於千萬上億,已經是非常少的資料量了。
結合上述分析結果,我對SQL做了如下調整:
將A、B類子查詢用兩個with子查詢代替,這樣就能減少大表的訪問次數;
在A、B類子查詢中,將ORDER_RELEASE表“砍掉”,減少表關聯帶來的IO開銷;
由於子查詢的資料量非常小,將之前的IN子查詢改寫為INNERJOIN,這樣就可以形成小結果集驅動大表的效果。
調整後的程式碼如下:
對於這次的優化,我並沒有抱什麼希望,因為這僅僅是常規性的精簡,還沒有深入到程式碼內部。或者說,這還僅僅是規範性改寫。
果真,執行仍然需要耗時4分多鐘,但是,這次的精簡併不是沒有任何收益。因為當往下鑽取時,速度非常快,鑽取完6625條記錄不到10S。
第二板斧:披荊斬棘
第二天一上班,就開始接著昨天的節奏繼續優化。
SQL的精簡併沒有為快速返回帶來任何收益,我決定看下執行計劃,嘗試著從執行計劃中得到更多的資訊。果真,F5後看到的執行計劃中,一個VIEW的COST猶如“鶴立雞群”,特別的扎眼:
從執行計劃看,Oracle對這個檢視做了傳統的處理,沒有合併,也沒有謂詞推入。所以檢視中的表基本上都是table access full。此時,突然想起在當時統計表物件的時候,記得只有一個檢視,而在昨天在精簡B類子查詢的時候,也出現過一個檢視。那這兩個檢視應該是同一個了。而昨天B類子查詢的速度是非常快的。
我趕緊將執行計劃定位到了B類子查詢,如下:
原來如此,在B類子查詢中,該檢視被merge了。
受此啟發,我也計劃將主查詢中的VIEW通過HINT進行MERGE,但是HINT似乎並不生效,始終都無法改變現有的執行計劃。無奈之際,只有深入SQL,實地窺探這個VIEW到底“何德何能”,會讓ORACLE優化器如此“死心塌地”的“維持原判”。
從上圖中可以看出,該檢視與A類子查詢進行了關聯,而事實上,B類子查詢就是該檢視與A類子查詢關聯的結果呀。怎麼在這裡又要臨時關聯呢?難道昨天做精簡的時候還存在漏網之魚?
再看程式碼:
原來這裡需要獲取該檢視的兩個欄位,而在B類子查詢中,我們只獲取了SHIPMENT_GID一個欄位。那是否可以直接在B類子查詢中加一個欄位呢?
我們再來看看B類子查詢的程式碼邏輯:
在這裡,我們獲取了SHIPMENT_GID欄位,並對該欄位通過DISTINCT去除了重複值。這樣做的目的在於,在後面呼叫該子查詢時,以該子查詢為驅動表,驅動關聯其他表物件。因為子查詢的結果集很小,而被關聯的表物件都是千萬上億級別的。
很顯然,如果我們在B類子查詢中增加ORDER_RELEASE_GID欄位,就會影響到SHIPMENT_GID的唯一性,這樣,在後續的關聯查詢中,就不能直接用B類子查詢驅動關聯。這會直接破壞掉已經建立好的驅動關係。
既然增加欄位之路行不通,那就嘗試著再增加一個WITH子查詢,程式碼如下:
與此同時,對訪問該檢視的程式碼也進行了適應性的修改,修改後的指令碼如下:
再次執行,耗時2:28,雖然與秒級的效能要求相距甚遠,但是至少效能提升了近50%,其意義並在於提升的效果,而在於證明了優化方向是正確的,即在大表林立群狼環視虎視眈眈的環境中,要快速準確的定位出驅動表,需要明確將驅動表資料準備好。
第三板斧:神工鬼斧
效能尚未達標,優化仍需繼續。
先看看執行計劃:
從COST列,並沒有看到成本特別高的操作。所以,我放棄了繼續在執行計劃上做文章,轉而深入分析SQL程式碼邏輯。
經過一番抽絲剝繭起承轉合後,SQL的整體程式碼邏輯也呼之欲出,發現頂層的邏輯設計非常簡單明瞭,就是三個子查詢的結果集內連線,如下圖所示:
接下來,我做了一件被人“鄙視”的小兒科的事,就是分別執行了這三個子查詢。原本想著總會有一個慢的,我就重點優化慢的那個子查詢。而結果卻出人意表,三個子查詢都是在2S左右就能完成執行,而且資料量都在1萬以內。那為何三個子查詢關聯在一起,效能會如此受影響呢?要知道,如果是三個1萬以內的表關聯,即便是無任何索引,那也是秒出呀。
那麼問題出在哪裡呢?沒的說,肯定是執行計劃並沒有按我們預想的去執行這個SQL。此時,我也沒有心思去仔細分析執行計劃,而是直接祭出了第三板斧通過with子查詢的方式將ORDER_REL、SHP、REL三個子查詢封裝成結果集,改寫後的SQL如下:
再看執行計劃:
看起來與我們預期的效果一致了,而關鍵還是要看執行的效率。
3.5S,再往下鑽取,也不到10s皇天不負有心人,終於可以畫“句號”了。此時,已經是第三天上午,距離拿到原始SQL將近2天的時間了。颱風“妮妲”早已銷聲匿跡,來也匆匆去也匆匆。你方唱罷我登場,立秋前的燒烤模式再次以勝利者的姿態,歇斯底里的“蒸烤”著這片大地。而躲在空調房的人類,也在盡情的透支著地球賜予的有限資源,最終會如同這個SQL一樣,終有一天會引發災難;而再去治理,再去挽救,需要花費更多的資源與精力。
後記
從4分鐘到3.5S,從鑽取卡頓到一瀉千里,整整經歷了近2天時間,耗時之長在以往的優化案例中實屬少見。事實上,當一開始拿到這個SQL時,尤其是在瞭解到這個SQL及背後的資料環境時,我心裡面是直打鼓的。可以說,是硬著頭皮拿下了這個SQL,現在回想起來仍然後怕。然而,除了後怕,更多的是該案例優化過程中所體現出的SQL(優化)精髓:精簡之道、驅動為王、集合為本。
精簡之道
大道至簡、簡單即高效、複雜的事情簡單化等等這些我們喜聞樂見的生活常識,同樣適用於SQL(優化)。記得SQL優化大師曾說過:不要讓ORACLE做多餘的事。而對於ORACLE而言,多餘的事情是什麼呢?多餘的表關聯、重複的表訪問、冗餘的關聯(過濾)條件、不必要的DISTINCT\ORDERBY\GROUPBY、曲折的訪問路徑。雖然ORACLE優化器引擎也在努力識別並消除這些“多餘的事”(可參見部落格,然而,在面對複雜的SQL時,ORACLE也往往束手無策。因此,SQL優化的首要之事就是精簡SQL。
驅動為王
有這樣一句話:一頭獅子領著一群羊,要勝過一頭羊領著一群獅子。這就道出了“領頭”的重要性,在ORACLE優化器中,就是“驅動表”。驅動表的意義有如木楔子,只有薄如紙片銳如刀刃的楔子,才能輕而易舉的插入堅硬木樁中。如果給你一個圓頭的木頭,任憑你力氣再大,也不能插入。這就要求驅動表的資料量要足夠的少。儘管ORACLE優化器也在努力尋找合適的“領頭”,而有的時候,ORACLE優化器會被腰裡別了杆槍的老鼠給騙了。比如本案例中的A類子查詢,起初是通過IN子查詢進行過濾的,這就存在很大的效能風險。關於驅動表的優化案例有很多,後續會專題分享。
集合為本
集合操作是二維關係資料庫引擎在資料處理時的根本,單表是一個集合,多表關聯後的結果也是一個集合,檢視、子查詢的返回結果還是一個集合,整個SQL執行完後的結果仍然是一個集合。
因此,一個高效的SQL一定有一個合理的集合運算結構。根據業務需求,結合程式碼邏輯,有的時候需要將程式碼片通過子查詢封裝;而有的時候又需要將子查詢合併到主查詢中;有的時候需要將大集合根據業務邏輯切片成多個小的集合;有的時候又需要將若干個小的集合預先合併成大集合。總之,在進行SQL(優化)時,一定要有集合的概念,用集合的思維指導SQL(優化)。
文章出處:DBAplu社群(訂閱號ID:dbaplus)