今天老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文本
1.3 執行計劃
1.4 執行統計信息
信息都在這了,咱們要關注些什麼呢?老K的經驗是,先找特徵,再根據不一樣的特徵來進一步提取本身須要的信息。
Part 2
找特徵、補信息
2.1 SQL文本特徵
>> exists子句 (part1)和update set部分(part2)的sql代碼基本相同,以下圖;
>> part1部分中,標量子查詢的結果做爲set列的目標值,說明從業務邏輯上能保證該部查詢返回記錄數最多爲1;
2.2 執行計劃的特徵
>> 該執行計劃各過程均使用filter
>> 結合sql文本及predicate information能夠看到,對目標表TARGET_BIG_TABLE通過濾條件POST_DATE=:V1後,返回記錄數預估爲623K條。
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條
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語句的特別之處。
在這裏,咱們看到了問題的關鍵,正是由於最外層的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作了以下改寫(固然沒有針對前面提到的改寫關鍵點);
這一改寫方式的幾個關鍵點:
>> 先把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執行計劃:
與原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 增長冗餘
>> 在exists子句中增長一個冗餘的T表,別名爲d
>> 增長d表和a表的關聯關係,其中jrnl_no列和acct_no列組合爲T表的主鍵,其餘冗餘列的關聯主要爲下一步繼續的改寫做鋪墊;
>> 整個SQL語句中沒有使用d表與其餘表進行關聯;
>> 因爲d表和a表使用的是主鍵進行關聯,因此能確保對a表的每條記錄,都能從d中找到且只能找到一條記錄符合語句中的關聯關係;
綜上,能夠知道上述增長冗餘徹底不改變SQL的邏輯關係。
4.5 關鍵角色轉變:
基於第一步冗餘等價關係,將exists子句中的全部a與b、c的關聯關係替換爲d與b、c的關聯關係。
4.6 減小冗餘:
由於主鍵a、d的主鍵列值相等,便可保證a、d的其餘列值必然相等,因此a、d的關聯字段只須要保留主鍵字段便可(保留也是能夠的,去掉顯得更簡潔)
以上一步一步的改寫保證了邏輯的一致性,同時實現了最外層的T表再也不涉及第二層子查詢的關聯,咱們能夠推斷執行計劃應該與老K預期的相差不遠了:
>> 執行計劃中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一致的特性,改寫以下:
>> 將語句改寫爲merge into的方式;
>> Merge的源與上一步改寫的exists子句中的內容一致,只是把與a的關聯關係提取到merge語句的on 部分;
>> 這樣改寫後SQL執行過程當中也會鎖定須要修改的極少記錄。
這裏改寫後的執行計劃與前面的update語句相似,老K也就不單獨列出分析了。
Part 5
最後的總覽
最後咱們再來看看咱們改寫後的語句及其執行計劃:
語句以下:
最終的執行計劃:
最終測試效果:
在測試環境,改寫後的語句執行了兩次,每次平均修改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的過程當中,瞭解業務確實是很是重要的一環。