SQL優化之基於SQL特徵的改寫

前言

今天老K繼續與你們分享第九期。算法

週末老K宅在家觀戰了兩局精彩的「人狗」大戰。老K既算不上科技迷,也算不上圍棋迷,不過對此很有感觸:阿爾法狗不過是經過左右互博的方式不斷學習圍棋,然而依賴其最優的學習算法(學習方法)卻能再短短的數月以內達到人類圍棋水平的最頂端;而李世石在倒是依賴其已有的經驗結合人類特有的靈感下出「神之一手」,人類終究仍是能夠打敗擁有超強計算能力的阿爾法狗。這些不由讓老K想起了本身在工做過程當中的最有藝術性的部分---「SQL tunning」,一方面要不斷學習積累運用不一樣的優化方法,同時在必要時多一分想象力和靈感,這樣面對不一樣的SQL問題,咱們才能下出本身的「神之一手」。sql

好了,今天老K與你們分享的案例是SQL調優的案例,但老K更但願你們能從中體會到SQL tunning過程當中的優化方法和思惟方式,真正作到它山之石,能夠攻玉。同時,你們若是以爲老K的方法還不錯,不妨輕輕的轉發一下,分享給身邊更多的ORACLE技術愛好者。數據庫

今天分析的問題是客戶DBA給過來的一條SQL語句,已經困擾其一段時間了,但願老K一塊兒來分析解決。解決這個問題對老K來講並非特別難,不過在這個問題的分析過程當中,老K給出了幾種優化的方向,最終選擇了不管是對整個系統仍是對該條SQL均可謂最佳的一種方式,最後在測試環境執行效果很是不錯。ide

Part 1函數

擺問題、列信息post

對於SQL tunning,老K上手最早關注的是SQL文本、執行計劃和執行統計信息,固然也不要忘了關注一下系統/數據庫版本。性能

1.1 環境介紹學習

操做系統 AIX 6.1測試

數據庫 ORACLE 11.2.0.3 兩節點RAC優化

1.2 SQL文本

wpsEA2B.tmp

1.3 執行計劃

wpsEA3C.tmp

1.4 執行統計信息

wpsEA4C.tmp

信息都在這了,咱們要關注些什麼呢?老K的經驗是,先找特徵,再根據不一樣的特徵來進一步提取本身須要的信息。

Part 2

找特徵、補信息

2.1 SQL文本特徵

>> exists子句 (part1)和update set部分(part2)的sql代碼基本相同,以下圖;

>> part1部分中,標量子查詢的結果做爲set列的目標值,說明從業務邏輯上能保證該部查詢返回記錄數最多爲1;

wpsEA4D.tmp

2.2 執行計劃的特徵

>> 該執行計劃各過程均使用filter

>> 結合sql文本及predicate information能夠看到,對目標表TARGET_BIG_TABLE通過濾條件POST_DATE=:V1後,返回記錄數預估爲623K條。

wpsEA5E.tmp

2.3 補充信息收集之表統計信息

>> TARGET_BIG_TABLE大約2G大小,SOURCE_SMALL_TABLE大約3M 大小;

>> TARGET_BIG_TABLE表中記錄數約250W左右,統計信息估算POST_DATE過濾後返回623K條記錄,注意:這是預估值,實際值會隨着傳入的變量V1而變化。

>> SOURCE_SMALL_TABLE表中記錄數約12W左右,ad02_acct_no列的選擇度比較高;

2.4 補充信息收集之執行計劃解讀

注:TARGET_BIG_TABLE簡稱爲T表 SOURCE_SMALL_TABLE 簡稱爲S表

另注:解讀關鍵----理解執行計劃中的filter

>> 執行計劃分開成兩部分來看,其中ID2-7步表示對應SQL文本的part2部分,ID8-12步對應SQL文本的part1部分;

>> part2部分的過程:使用POST_DATE過濾T表,將過濾後的記錄迭代入EXISTS子查詢(T表的結果集此時做爲變量傳入子查詢),在子查詢執行的過程當中,若是前面的關聯條件符合,再次迭代入第二層子查詢(select max()部分)進行匹配;

>> part1部分的過程:針對ID2-7步過濾出的結果集,逐條update,而update的目標值,一樣是經過相似2-7步過程當中的逐步迭代查詢而來;

>> 在各步驟單表訪問方式均爲全表掃描;

>> 從執行計劃中能夠看到,在第3步對錶T表進行過濾以後結果集估算爲623K(rows列),其後對S表過濾後均爲1;

>> 由此能夠估算執行過程當中表訪問的狀況應爲:(老K建議在本分享中記住下面的公式,暫且稱之爲 「 訪問公式 」 吧)

過濾過程的表訪問=(T表全掃+ 623K 次 ×(S表全掃 +(0或者 1次)×(S表全掃)))

修改過程的表訪問=(須要修改的記錄數 ×(S表全掃 + (0或者 1次)×(S表全掃)))

總的訪問過程=過濾過程的表訪問次數 +修改過程的表訪問

注意:此處的(0或者 1次)×(S表全掃)表示的是第二層子查詢的狀況,若是在第一層子查詢過程當中關聯條件就不符合,則再也不須要迭代入第二層,即0次S表全掃,不然便是1次S表全掃;因此過濾過程對S表最少須要作623K次全掃,最多須要作1246K次全掃;修改過程同理。

2.5 執行統計信息特徵

>> SQL單次執行平均邏輯讀爲355,245,774(block數)

>> SQL單次執行平均時間約2000秒

>> SQL單次平均修改記錄數約爲0條

wpsEA6F.tmp

Part 3

思考吧DBA

好了,信息收集完成了,進入老K的既定思考軌道,其實對於任何一個SQL tunning的問題,老K都會提出下面的三個問題,這個也不用例外;

3.1 老K的例行思考

>> 這個執行計劃是否爲當前SQL語句下最優的執行計劃?(選擇優化目標)

>> 咱們想要的執行計劃是什麼樣的?(肯定優化目標)

>> 咱們怎麼來讓SQL跑出咱們想要的執行計劃?(實現優化目標)

若是能夠,正在閱讀此文的你,也許也能夠思考一下上面的三個問題,或者回憶一下當你面對SQL tunning的問題時你有沒有思考過這三個問題,亦或者你會思考/思考過什麼呢。

綜合前期的分析思考片刻以後,老K鄭重地給出了本身的答案:

3.2 老K的答案----不是最優的計劃

老K先查看過該SQL的歷史執行計劃,只有這一個,但這並不意味着着就是該SQL的最優執行計劃;

在執行計劃解讀部分,老K給出了這個執行計劃的「訪問公式」,從公式中能夠知道其實S表雖小,但其其實是整個執行計劃的關鍵,整個過程當中最多可能須要對S表進行1246K×2次訪問呢,那咱們可不能夠提升對S表的訪問效率呢?固然能夠,從執行計劃中的估算能夠知道對S表的訪問大約返回1-2條記錄(這裏老K還單獨驗證過),說明總體選擇度比較高,咱們只有建立合適的索引,就能夠就能夠大大將提升S表的訪問效率。

咱們簡單來估算一下使用索引的狀況下的執行效率是怎樣的。原來對S表全掃所需的邏輯讀數爲3M(表大小)÷8192=375次,使用索引後預估對S表一次訪問最多所需邏輯讀數爲:(2次索引塊訪問 + 2次數據塊訪問)=4次;因此說,使用索引的邏輯讀約爲使用全掃的的1%,估算建立索引後該語句單次執行平均邏輯讀約在350w左右。

那麼,新建索引,將S表的全掃都變爲索引掃描,這就是老K想要的執行計劃嗎?

顯然不是,這樣的執行計劃只是原執行計劃的一個升級版而已,其過程仍是一個迭代的過程,這樣執行的時間/消耗的時間基本都會隨着原計劃中第3步返回的數據量(還記得623K這個值嗎,就是它!它是可變的,可能隨着傳入的)變化而線性變化;因此這個執行計劃雖然較原執行計劃預計會有很是大的改善,但仍然不是老K想要的執行計劃。

3.3 老K的答案----想要的計劃

SQL文本告訴咱們,其實SQL作的就是使用exists方式將T表和S表進行關聯更新,老K想要的執行計劃應該是使用NL或者hash join的方式來鏈接兩表,而不是使用filter迭代的方式,這樣就能保證SQL執行過程當中只須要對T表和S表進行極少的一次或幾回掃描,從而下降SQL執行的邏輯讀。

3.4 老K的答案----如何生成漂亮的執行計劃

要回答這個問題,咱們首先要思考爲何SQL當前沒有跑出咱們想要的執行計劃,是由於統計信息不許?索引設計不合理?仍是列類型不匹配?

都不是!

咱們再次回到SQL語句自己,來看看SQL語句的特別之處。

wpsEA70.tmp

在這裏,咱們看到了問題的關鍵,正是由於最外層的T表與兩層子查詢均有關聯關係,致使ORACLE沒法自動改寫SQL,最終生成執行計劃時沒法使用T表與S表進行JOIN,只能生成使用filter方式的執行計劃。

因此,最終思考的結果已經出來:

>> 由於兩層子查詢的緣由致使ORACLE沒法使用JOIN的方式關聯T表和S表

>> 要想生成較好的執行計劃必須改寫語句

>> 改寫後的語句不該該存在相似的最外層表涉及第二層子查詢的狀況

其中最後一點,指出了咱們改寫的關鍵點。

Part 4

改寫吧DBA

依據老K的經驗,SQL語句的改寫一般要求改寫者對SQL涉及業務很是瞭解,經過業務特徵重構出合理的SQL語句,才能更好的作到既不改變SQL的業務邏輯,又有效提升SQL性能;不過針對這個SQL,咱們已經知道了致使其執行計劃不優的根本緣由,老K相信能夠在不考慮業務特性的狀況,利用數據庫的特性來進行有效的改寫。

4.1 改寫的花絮

基於SQL特性中,part1和part2基本相同的特性,老K先隨性的對SQL作了以下改寫(固然沒有針對前面提到的改寫關鍵點);

wpsEA80.tmp

這一改寫方式的幾個關鍵點:

>> 先把post_date字段的過濾條件直接提取出來,與原邏輯一致

>> 基於part1和part2基本相同,使用了nvl函數代替了原來的exists子句

>> 若是select部分能查到記錄(相似原來的exists子句成立),則用查詢出的結果更新chq_pay_name字段

>> 若是select部分不能查到記錄,則用原記錄自身進行更新(set chq_pay_name=chq_pay_name),更新先後該記錄的數據不變

以上幾點保證了改寫後的SQL與原SQL邏輯一致,不過有一點不同的很是值得注意,原SQL只修改極少的幾條記錄,新SQL卻修改了623K條記錄,只是其中絕大多數是冗餘的修改。

咱們再看改寫後的SQL執行計劃:

wpsEA81.tmp

與原SQL執行計劃相似,不過少了原執行計劃的part1部分。

新的執行計劃,老K又問了本身一句:

4.2 這樣改寫真的好嗎?

你們是否還記得原執行計劃解析過程當中老K給出的「訪問公式」:

總的訪問過程=過濾過程的表訪問次數 +修改過程的表訪問

那麼,在這個執行計劃下,由於去掉了冗餘的一部分,公式就變成了:

總的訪問過程=過濾過程的表訪問次數

實際上就能夠理解爲,SQL在修改數據的過程當中能夠重用過濾過程當中生成的數據;

不過針對這個語句,咱們從執行統計信息裏知道,每次語句執行最終修改的數據量都很是少,也就是說這樣改寫所減小的「修改過程的表訪問」對總體執行效率影響並不大。

這樣改寫會帶來什麼壞處嗎?

會!根本緣由就在於上面提到的新SQL實際修改的記錄數是623K條:

>> 持有行鎖範圍變大,可能大量致使其餘對該表進行DML操做的會話被阻塞

>> 若是修改列上有索引,索引維護的時間將大大增長,致使新SQL執行效率更低

綜上,針對這條SQL語句,這種改寫方式並不合適。

不過,若是原SQL在執行過程當中修改的數據量接近623K條,那麼這種改寫方式的收益就要高很是多,而其帶來的壞處也就不復存在了,這種改寫方式只是不適合這種業務環境下(每次只修改極少幾條記錄),然而卻有必定的廣泛性,因此老K也把這部分分享給你們,最重要的是解決問題過程當中的思路和方法。

4.3 繼續改寫

前面咱們已經分析出改寫的關鍵點:改寫後的語句不該該存在相似的最外層表涉及第二層子查詢的狀況;下面咱們就朝着這個目標去改寫咱們的SQL語句。

改寫前信息補充:

改寫思路在老K腦中醞釀好後,老K又補查了T表的信息,確認T表存在主鍵約束,主鍵列爲ACCT_NO和JRNL_NO;

4.4 增長冗餘

wpsEA92.tmp

>> 在exists子句中增長一個冗餘的T表,別名爲d

>> 增長d表和a表的關聯關係,其中jrnl_no列和acct_no列組合爲T表的主鍵,其餘冗餘列的關聯主要爲下一步繼續的改寫做鋪墊;

>> 整個SQL語句中沒有使用d表與其餘表進行關聯;

>> 因爲d表和a表使用的是主鍵進行關聯,因此能確保對a表的每條記錄,都能從d中找到且只能找到一條記錄符合語句中的關聯關係;

綜上,能夠知道上述增長冗餘徹底不改變SQL的邏輯關係。

4.5 關鍵角色轉變:

wpsEA93.tmp

基於第一步冗餘等價關係,將exists子句中的全部a與b、c的關聯關係替換爲d與b、c的關聯關係。

4.6 減小冗餘:

wpsEAA4.tmp

由於主鍵a、d的主鍵列值相等,便可保證a、d的其餘列值必然相等,因此a、d的關聯字段只須要保留主鍵字段便可(保留也是能夠的,去掉顯得更簡潔)

以上一步一步的改寫保證了邏輯的一致性,同時實現了最外層的T表再也不涉及第二層子查詢的關聯,咱們能夠推斷執行計劃應該與老K預期的相差不遠了:

wpsEAB4.tmp

>> 執行計劃中b、d、c表使用hash join進行關聯

>> join完成後經過一系列SORT/FILTER後造成結果集VW_SQ_2,其中這裏的filter部分爲結果集內部的比較(即同一條記錄的不一樣列的比較),效率很是高

>> 最後VW_SQ_2和外層的T表使用NL的方式進行join,關聯字段爲主鍵字段

執行計劃出來之後,咱們來估算一下這個SQL在執行過程當中的「訪問公式」:

總的訪問過程 = S表全掃 + T表全掃 + S表全掃 + VW_SQ_2記錄數 *(1個T表主鍵索引塊 + 1個T表數據塊)

4.7 別忘了」set「

原語句的part2部分修改的跟老K預期的差很少,原語句part1部分與part2部分一致,那麼咱們簡單的修改part1部分紅part2部分就能夠了嗎?顯然不是!一般,使用merge into語句能很方便的改寫update語句,這裏咱們更能利用原語句part1和part2一致的特性,改寫以下:

wpsEAB5.tmp

>> 將語句改寫爲merge into的方式;

>> Merge的源與上一步改寫的exists子句中的內容一致,只是把與a的關聯關係提取到merge語句的on 部分;

>> 這樣改寫後SQL執行過程當中也會鎖定須要修改的極少記錄。

這裏改寫後的執行計劃與前面的update語句相似,老K也就不單獨列出分析了。

Part 5

最後的總覽

最後咱們再來看看咱們改寫後的語句及其執行計劃:

語句以下:

wpsEAC6.tmp

最終的執行計劃:

wpsEAC7.tmp

最終測試效果:

wpsEAD7.tmp

在測試環境,改寫後的語句執行了兩次,每次平均修改7.5條記錄,耗時4s,邏輯讀3.4w;細心的讀者可能能從最終的執行計劃中看到,對T表的全表掃描也許能夠避免等,因爲篇幅緣由以及測試環境的緣由,老K沒有再在這裏深究,畢竟老K分享的是SQL tuning的方法,而如何避免全表掃描以及如何分析避免了全表掃描後對SQL執行效率提高的預估,相信讀者你必定已經學到了,不妨本身作一個估算。

寫在最後

讀到了最後,老K分享了什麼,咱們不妨來仔細回憶一番。

>> SQL分析過程當中如何經過執行計劃推算SQL執行的邏輯讀

>> 針對CASE中的SQL如何經過添加索引來改善其執行效率

>> 針對CASE中的SQL經過使用NVL的方式進行改寫,它在什麼場景下是合適的,什麼狀況下是不合適的。

>> 怎樣經過添加冗餘關聯來引導數據庫生成咱們想要的執行計劃

>> 怎樣使用merge語法來改寫update語句

最後,老K再一次強調,在SQLtunning的過程當中最重要的是優化的思路和對問題的思考方式,但願聰明的讀者已從此次分享中獲得啓示。

編外:老K後來經過與應用開發團隊溝通了解文中SQL的業務特徵後,再次結合其業務特徵改寫了SQL,執行效率再次獲得了極大的提高,可見,在SQLtunning的過程當中,瞭解業務確實是很是重要的一環。

相關文章
相關標籤/搜索