天天看點

PgSQL · 特性分析 · Plan Hint

plan hint在rds for pg裡面預設是沒有打開的,可以load指令啟用:

但注意這隻在會話級别有效,重新連接配接後将失效。如果想要每次連接配接都自動啟用hint,可以使用下面的指令(注意必須以rds的根使用者執行,否則會遇到權限錯誤)。這樣下次連接配接時,hint就預設啟用了。

為了便于說明,我們使用下面的shell腳本來建立2張表:

然後在t1上進行查詢,不使用和使用hint的查詢計劃分别如下:

可以看出,利用hint後,我們成功強制使用了表掃描。hint一般以sql注釋的形式,出現在sql的前面,并以/+開頭,以/結尾。注意/*和+之間不能有空格。

插件主要的代碼集中在pg_hint_plan.c裡面。從其中<code>pg_init</code>函數的代碼可以看出,它利用了<code>planner_hook</code>(優化器的函數鈎子,實際上是全局變量,存放函數位址,可以被插件更改,換成插件自定義函數的位址),用<code>pg_hint_plan_planner</code>取代了原來的優化器邏輯。這樣pg在處理一個sql時,将調用<code>pg_hint_plan_planner</code>來做優化。而<code>pg_hint_plan_planner</code>會調用<code>get_hints_from_comment</code>,來讀取hint,并調用<code>create_hintstate</code>進行文法分析。這裡要說明的是,<code>create_hintstate</code>遇到一張表上的多個同類型hint(包括重複的hint),隻保留最後一個,前面的會忽略。

另外,還有兩個函數鈎子被利用:<code>get_relation_info_hook</code> 和 <code>join_search_hook</code>。這兩個鈎子分别被修改指向了<code>pg_hint_plan_get_relation_info</code>和<code>pg_hint_plan_join_search</code>。前者是在優化器處理基本表(非視圖、非函數的表)擷取表資訊時被調用,調用棧如下:

這個<code>pg_hint_plan_get_relation_info</code>做了什麼呢?仔細看會驚訝的發現,它是用來删除索引的!對,它在優化器擷取表的基本資訊後被調用,然後其從基本資訊删除了那些在hint中未使用的索引。例如,t1上有兩個索引t1_i_a和t1_i_b,如果指定了indexscan(t1 t_i_b)這個hint,那麼t1_i_a的索引資訊在這裡被删除,這樣在後續的優化中,就永遠不會考慮t1_i_a這個索引了!

再看<code>pg_hint_plan_join_search</code>,其被調用的位置如下:

可見,它是在為一個sql語句生成連接配接結果時被調用,其輸入為待連接配接的表,輸出為連接配接後生成的表及其最優的查詢計劃。它主要做了兩件事:

調用<code>rebuild_scan_path</code>重新生成基本表的通路路徑。為什麼要重新生成呢?因為在基本表的通路計劃生成階段,掃描類的hint并未實際起作用(隻是對索引做過删除處理)。例如,即使指定了indexscan(t1 t1_i_a),但外部的guc變量<code>enable_indexscan</code>被設定為了off,在這裡也隻會看到一個表掃描(seqscan)的查詢計劃。是以這裡需要重新設定好guc變量(例如如果遇到indexscan hint,需要把guc變量enable_indexscan重置為on),再做一遍通路計劃。由于基本表一般數量較少,通路計劃也隻需再生成一次,是以此步開銷是可接受的;

調用<code>pg_hint_plan_standard_join_search</code>生成連接配接的計劃。這裡是應用連接配接方法和連接配接順序hint的地方。要想改變連接配接方法或順序,需要進一步修改優化器的整個邏輯,但優化器沒那麼多的預定義鈎子可用了,采用函數鈎子的方法不可行。于是,插件便“自備”了優化器的主流程代碼(其實是從同版本的pg裡面拷貝出來的),見插件代碼中的core.c和make_join_rel.c兩個檔案。裡面很多地方是被插件修改過的。其中核心的是修改對<code>add_paths_to_joinrel</code>的調用,使優化器實際調用<code>add_paths_to_joinrel_wrapper</code>。這個函數是用于為輸入的兩張表(可能是連接配接生成的中間表),生成一個連接配接計劃。可以看到<code>add_paths_to_joinrel_wrapper</code>會先去查找有沒有對應的hint,如果有就直接利用,并舍棄掉不符合hint的連接配接方法和順序(這是連接配接順序hint其作用的地方)。

可以看到,此插件的實作并不複雜,它巧妙利用了優化器優化流程中的關鍵點,來應用hint,達到固定查詢計劃的目的。

從核心實作可以看出,指定hint後會帶來如下開銷:基本表的通路路徑要生成兩次;每次連接配接兩個中間表時,要檢查是否有對應的hint;很多地方需要反複更新guc變量來影響計劃生成。當然,由于直接指定了表的連接配接方法、順序等,減少了生成的中間計劃,這一點又節省了很多開銷。是以,對使用hint後的編譯時間是否比原來長,不能一概而論。下面我們對hint造成的編譯開銷做一下粗略測試。測試用例如下:

這裡我們使用了8張表,每張表都隻有a、b兩個int字段。用兩個do語句,每個都執行同一sql語句10000次。一個do語句是不帶hint的,另一個帶了較複雜的hint。測試結果,不帶hint的執行耗時17秒左右,帶hint的14秒左右。即帶hint的反而編譯時間更短(注意這裡隻執行了explain,為真正執行sql語句)。