標籤:order 經曆 各類 鏈表 資源管理 分享圖片 odi context 資源分派
2.可最佳化語句的執行
可最佳化語句的共同特點是它們被查詢編譯器處理後都會產生査詢計劃樹,這一類語句由執行器(Executor)處理。該模組對外提供了三個介面: ExecutorStart、ExecutorRun 和 ExecutorEnd,其輸入是包含査詢計劃樹的資料結構QueryDesc,輸出則是相關執行資訊或結果資料。如果希望執行某個計劃樹,僅需構造包含此計劃樹的QueryDesc,並依次調用ExecutorStart、ExecutorRun、ExecutorEnd 3個過程即能完成相應的處理過程。從我之前的文章跟我一起讀postgresql源碼(六)——Executor(查詢執行模組之——查詢執行策略)中可以看到,執行器的三個介面函數都是在Portal的相關函數中調用的,分別負責執行器的初始化、執行和清理工作,Portal在處理時也使用了同樣的方式,這樣可以把資源分派回收工作與執行過程獨立開,能夠簡化執行過程,更是一種很好的資源管理方式。
執行器對於査詢計劃樹的處理,最終被轉換為針對計劃樹上每一個節點的處理。每種節點表示一種物理代數(Physical Algebra)操作,PostgreSQL會依次對其進行初始化、處理和清理。節點的處理被設計為demand-driven模式,父節點使用子節點提供的資料作為輸入,並向其上層節點返回處理結果。實際執行時,從根節點開始處理,每個節點的執行過程會根據需求自動調用子節點的執行過程來擷取輸入資料(一般為元組),從而層層遞迴執行,實現整個計劃樹的遍曆執行過程。初始化和清理也採用相同的設計模式,這種設計模式使得節點處理的代碼結構簡潔統一、語義明確,且實現方式簡單有效。
接下來將會對執行器部分的各種原理、實現做進一步的介紹。
2.1處理模式
PostgreSQL中的計劃節點被定義為有0~2個輸入和一個輸出,這是為了在實現中能夠對應二叉樹結構。所以你知道了:所有的計劃節點都被組織為二叉樹結構。
每一個計劃節點對應於樹中的一個節點,下層節點的輸出作為上層節點輸入。資料(元組)從底層節點向上層節點流動,直至根節點,而根節點的輸出即為整個査詢的結果。
例如有這麼一個查詢:
select a.q,b.w,c.e from a join b join c order by a.q limit 1;
那麼計劃節點可能如下所示(其實說實話你explain一條查詢就能看到各個節點直接的關係):
limit ^ | sort ^ | join ^^ / join scan ^^ / scan scan
在PostgreSQL的實現中,上層函數通過ExecInitNode、ExecProcNode、ExecEndNode二個介面函數來統一對節點進行初始化、執行和清理,這三個函數會根據所處理節點的實際類型調用相應的初始化、執行、清理函數(可見我這這篇文章的末尾)。
由此可見,査詢計劃樹上的節點就構成了物理元組到執行結果的管道,因此査詢計劃樹的執行過程可以看成是拉動元組穿過管道的過程。PostgreSQL採用了一次一元組的執行模式,每個節點被執行一次僅向上層節點返回一條元組。因此,對於整個査詢計劃樹的執行也是一次一元組的模式。這種模式有很多的優點:
2.2計劃節點的資料結構
從前面的介紹我們已經看到査詢計劃樹是由各種計劃節點構成,那麼在PostgreSQL中是如何儲存和表示各類節點的呢?給出了Hash類型節點的資料結構表示。
所有計劃節點節點的資料結構都以一個Plan類型的欄位開頭,這有點像類的繼承:把Plan看成一個父類,其他計劃節點節點都是它的直接或者間接子類。濃濃的物件導向的味道。
如所示,Join節點是Plan的子類,從Plan中繼承了左右子樹指標(lefttree, rightlree)、節點類型(type)、選擇運算式(qual)、投影鏈表(targedisO等公用欄位,並有自己的擴充欄位連線類型(jointype)和串連條件(joinqual); Hashjoin節點則是Join節點的子類,有自己的擴充欄位hashclauses。
PostgreSQL系統中將所有的計劃節點按功能分為四類:
控制節點(control node)掃描節點(scan node)串連節點(join node)物化節點(materialization node)
並分別為掃描、串連節點類型定義了公用父類Scan、Join。Hash串連屬於串連節點,因此Hash串連繼承於Join節點。串連節點類型的公用父類定義了串連的類型以及串連的條件。作為Hash串連節點,需要使用Hash函數,所以Hashjoin節點擴充定義了hashclauses欄位來儲存相關資訊,其中包括需要做Hash的屬性以及使用的Hash函數等。
Plan的眾多子類節點通過lefttree和righttree欄位構成了整個査詢計劃樹,其根節點指標被儲存在PlannedStmt類型的資料結構中,其中包含了語句的類型(commandType)、査詢計劃樹根節點(planTrce)、査詢涉及的範圍表(rtable)、結果關係表(resultRelation )PlannedStmt 結構則被放在QueryDesc中,QueryDesc結構的基本定義如所示。
作為執行器的輸入,QueryDesc中包含査詢計劃樹(plannedstmt欄位)、功能語句相關執行計畫(utilitystmt欄位)、執行器全域狀態(estate欄位)以及計劃節點執行狀態(planstate欄位)等。從可以看出,執行器全域狀態estate中儲存了査詢涉及的範圍表(es_range_table)、Estate所在的記憶體上下文(es_query_cxt,也是執行過程中一直保持的記憶體上下文)、用於在節點間傳遞元組的全域元組表(es_TupleTable)和每擷取一個元組就會回收的記憶體上下文(es_per_tuple_exprContext) 。
執行器初始化時,ExecutorStart會根據査詢計劃樹構造執行器全域狀態(estate)以及計劃節點執行狀態(planstate)。在査詢計劃樹的執行過程中,執行器將使用planstate來記錄計劃節點執行狀態和資料,並使用全域狀態記錄中的es_tupleTable欄位在節點間傳遞結果元組。執行器的清理函數ExecutorEnd將回收執行器全域狀態和計劃節點執行狀態。
給出了PostgreSQL中用於計劃節點執行狀態記錄的資料結構與計劃節點之間的對應關係。PostgreSQL為每種計劃節點定義了一種狀態節點。所有的狀態節點均繼承於PlanState節點,其中包含輔助計劃節點指標(Plan)、執行器全域狀態結構指標(state)、投影運算相關資訊(targetlist)、選擇運算相關條件(qual),以及左右子狀態節點指標(lefttree、righttree)。
狀態節點之間通過lefttree和righttree指標組織成和査詢計劃樹結構類似的狀態節點樹,同時,每個狀態節點都儲存了指向其對應的計劃節點的指標(PlanState類型中的Plan欄位)。(計劃節點和節點執行狀態)中展示了串連節點狀態的公用父類JoinStale,它繼承於PlanState,擴充了連線類型(jointype)和串連條件(joinqual)屬性。而HashJoinState繼承於JoinState並擴充了更多的屬性,包括Hash函數相關內容(hashclauses、hj_HashTable、hj_OuterHashKeys、hj_InnerHashKeys、hj_HashOperators)、左子節點返回元組指標(hj_OuterTupleSlot)、右子節點返回元組指標(hj_HashTupleSlot)等。
至此,執行器執行過程中涉及的主要各種資料結構已經介紹完畢。執行器的輸入是QueryDesc,它包含了儲存査詢計劃樹根節點指標的PlannedStmt結構。執行器執行時,首先構造全域狀態記錄Estate結構,並為每個計劃節點(Plan)構造對應的狀態節點(PlanState),然後在執行中使用相關結構儲存執行狀態,執行完畢後釋放相關的資料結構。
2.3執行器的運行
在PostgreSQL中提供了三個介面函數用於調用執行器,分別為ExecutorStart、ExecutorRun和ExecutorEnd。當需要使用執行器來處理査詢計劃時,僅需依次調用三個函數即可完成執行器的整個執
行過程。
執行器運行時的函數調用關係如所示,ExecutorStart通過調用standard_ExecutorStart對執行器進行必要的初始化,主要工作包括構造EState結構和査詢計劃樹的初始化(即構造對應的PlanState樹,由InitPlan函數完成)。ExecutorRun的功能由standard_ExecutorRun實現,在執行過程中會調用ExecutePlan完成査詢計劃的執行。ExecutorEnd由standard_ExecutorEnd函數完成,通過調用ExecEndPlan處理執行狀態樹根節點釋放已指派的資源,最後釋放執行器全域狀態EState完成整個執行過程。
執行器中對査詢計劃樹的初始化都是從其根節點開始,並遞迴地對其子節點進行初始化。計劃節點的初始化過程一般都會經曆如所示的幾個基本步驟,該過程在完成計劃節點的初始化之後會輸出與該計劃節點對應的PlanSute結構指標,計劃節點的PlanState結構也會按照査詢計劃樹的結構組織成計劃節點執行狀態樹。對計劃節點初始化的主要工作是根據計劃節點中定義的相關資訊,構造對應的PlanStale結構並對相關欄位賦值。
計劃節點的初始化由函數ExecInitNode完成,該函數以要初始化的計劃節點為輸入,並返回該計劃節點所對應的PlanState結構指標。在ExecInitNode中,通過判斷計劃節點的類型來調用相應的處理過程,每一種計劃節點都有專門的初始化函數,且都以“ExecInit節點類型”的形式命名。例如,NestLoop節點的初始化函數為ExecInitNestLoop。在計劃節點的初始化過程中,如果該節點還有下層的子節點,則會遞迴地調用子節點的初始化函數來對子節點進行初始化。ExecInitNode函數會根據計劃節點的類型(T_NestLoop)調用該類型節點的初始化函數(ExecInitNestLoop)。由於NestLoop節點有兩個子節點,因此ExecInitNestLoop會先調用ExecInitNode對其左子節點進行初始化,並將其返回的PlanState結構指標存放在為NestLoop構造的NestLoopState結構的lefttree欄位中;然後以同樣的方式初始化右子節點,將返回的PlanState結構指標存放於NestLoopState的righttree欄位中。同樣,如果左右子節點還有下層節點,初始化過程將以完全相同的方式遞迴下去,直到到達査詢計劃樹的葉子節點。而在初始化過程中構造的樹也會層層返回給上層節點,並被連結在上層節點的PlanState結構中,最終構造出完整的PlanState樹。
査詢計劃的實際執行由函數ExecutePlan完成,該函數的主體部分是一個大的迴圈,每一次迴圈都通過ExecProcNode函數從計劃節點狀態樹中擷取一個元組,然後對該元組進行相應的處理(增刪查改),然後返回處理的結果。當ExecProcNode從計劃節點狀態樹中再也取不到有效元組時結束迴圈過程。
ExecProoNode的執行過程也和ExecInitNode類似:從計劃節點狀態樹的根節點擷取資料,上層節點為了能夠完成自己的處理將會遞迴調用ExecProcNode從下層節點擷取輸入資料(一般為元組),然後根據輸入資料進行上層節點對應的處理,最後進行選擇條件的運算和投影運算,並向更上層的節點返回結果元組的指標。同ExecInitNode 一樣,ExecProcNode 也是一個選擇函數,它會根據要處理的節點的類型調用對應的處理函數。例如,對於NestLoop類型的節點,其處理函數為ExecNestLoop。ExecNestLoop函數同樣會對NestLoop類型的兩個子節點調用ExecProcNode以擷取輸入資料。如果其子節點還有下層節點,則以同樣的方式遞迴調用ExecProcNode進行處理,直到到達葉子節點。每一個節點被ExecProcNode處理之後都會返回一個結果元組,這些結果元組作為上層節點的輸入被處理形成上層節點的結果元組,最終根節點將返回結果元組。
每當通過ExecProcNode從計劃節點狀態樹中獲得一個結果元組後,ExecutePlan函數將根據整個語句的操作類型調用相應的函數進行最後的處理。對於不掃描表的簡單查詢(例如select 1),調用的是Result節點,通過ExecResult函數直接輸出“査詢”結果。對於需要掃描表的查詢(例如select xx from tablexx這種),系統在掃描完節點後直接返回結果,而對於增刪改查詢,情況特殊,有一個專門的ModifyTable節點來處理它:主要調用了ExecInsert、ExecDelete、ExecUpdate這三個函數進行處理。對於插入語句,則首先需要調用ExecConstraints對即將插入的元組進行約束檢査,如果滿足要求,ExecInsert會調用函數heap_insert將元組儲存到儲存系統。對於刪除和更新,則分別由 ExecDelete 和 ExecUpdate 調用 heap_delete 和 heap_update 完成。
對於其他的特殊情況,也有特殊的節點去處理,這在Postgresql裡面稱為控制節點。讀者們可以查看ExecProcNode函數擷取詳情。(這部分代碼9.5.4和8.x版本差異較大)
當執行器處理完所有能夠獲得的元組之後,由執行器清理函數ExecutorEnd負責善後工作。該函數調用ExecEndPlan對計劃節點執行狀態樹進行清理。對計劃節點執行狀態樹的清理和執行狀態樹的初始化、執行相類似:從根節點開始遞迴調用ExecEndNode對每一個計劃節點的執行狀態節點進行清理。同樣,ExecEndNode只是一個選擇函數,針對不同類型的節點有相應的淸理函數。例如,NestLoop節點的清理函數是ExecEndNestLoop。如所示,清理過程的任務主要是回收初始化過程中分配的資源、投影和選擇結構的記憶體、結果元組儲存空間等,計劃節點執行狀態樹清理完之後,ExecutorEnd還將調用FreeExecutorState清理執行器全域狀態。
註:本文中的圖文大量參考了彭煜瑋老師《Postgresql資料庫核心分析》一書,在此鳴謝。
後面開始講講查詢執行所涉及到的各種計劃節點吧。
跟我一起讀postgresql源碼(八)——Executor(查詢執行模組之——可最佳化語句的執行)