標籤:sql語句 基礎 資料 exe 狀態 sem hash join 迴圈 params
Join節點
JOIN節點有以下三種:
T_NestLoopState, T_MergeJoinState, T_HashJoinState,
連線類型節點對應於關係代數中的串連操作,PostgreSQL中定義了如下幾種連線類型(以T1 JOIN T2 為例):
1)Inner Join:內串連,將T1的所有元組與T2中所有滿足串連條件的元組進行串連操作。
2)Left Outer Join:左串連,在內串連的基礎上,對於那些找不到可串連T2元組的T1元組,用一個空值元組與之串連。
3)Right Outer Join:右串連,在內串連的基礎上,對於那些找不到可串連T1元組的T2元組,用一個空值元組與之串連。
4)Full Outer Join:全外串連,在內串連的基礎上,對於那些找不到可串連T2元組的T1元組,以及那些找不到可串連T1元組的T2元組,都要用一個空值元組與之串連。
5)Semi Join:類似IN操作,當T1的一個元組在T2中能夠找到一個滿足串連條件的元組時,返回該T1元組,但並不與匹配的T2元組串連。
6)Anti Join:類型NOT IN操作,當T1的一個元組在T2中未找到滿足串連條件的元組時,返回該T1元組與空元組的串連。
我們再看看postgres使用者手冊裡是怎麼說的:
條件串連:
T1 { [INNER] | { LEFT | RIGHT | FULL } [OUTER] } JOIN T2 ON boolean_expressionT1 { [INNER] | { LEFT | RIGHT | FULL } [OUTER] } JOIN T2 USING ( join column list )T1 NATURAL { [INNER] | { LEFT | RIGHT | FULL } [OUTER] } JOIN T2
這樣看起來,只有頭四種串連方式(INNER, LEFT JOIN, RIGHT JOIN, FULL JOIN)在SQL語句中顯示使用了,後兩種其實是作為postgres內部使用的,例如Semi Join,我之前說過對於SubqueryScan節點,有可能把ANY和EXIST子句轉換為半串連。半串連就是Semi Join。
而對於你所指定的串連方式,PostgreSQL內部會見機行事,使用不同的串連操作。
這裡,postgres實現了三種串連操作,分別是:嵌套迴圈串連(Nest Loop)、歸併串連(Merge Join)和Hash串連(Hash Join)。
其中歸併串連演算法可以實現上述六種串連,而嵌套迴圈串連和Hash串連只能實現 Inner Join、Left Outer Join、Semi Join 和 AntiJoin四種串連。
6-52所示,串連節點有公用父類Join, Join繼承了 Plan的所有屬性,並擴充定義了 jointype用以儲存串連的類型,joinqual用於儲存串連的條件。
typedef struct Join{ Plan plan; JoinType jointype; List *joinqual; /* JOIN quals (in addition to plan.qual) */} Join;
對應的執行狀態節點JoinState中定義了jointype儲存連線類型,joinqual儲存串連條件初始化後的狀態鏈表。
typedef struct JoinState{ PlanState ps; JoinType jointype; List *joinqual; /* JOIN quals (in addition to ps.qual) */} JoinState;
1.NestLoop節點
NestLoop節點實現了嵌套迴圈串連方法,能夠進行Inner Join、Left Outer Join、Semi Join和Anti Join四種串連方式。
舉例如下:
postgres=# explain select a.*,b.* from test_dm a join test_dm2 b on a.id > b.id; QUERY PLAN-------------------------------------------------------------------------------- Nested Loop (cost=0.00..150000503303.17 rows=3333339000000 width=137) Join Filter: (a.id > b.id) -> Seq Scan on test_dm2 b (cost=0.00..223457.17 rows=10000017 width=69) -> Materialize (cost=0.00..27346.00 rows=1000000 width=68) -> Seq Scan on test_dm a (cost=0.00..22346.00 rows=1000000 width=68)(5 行)
typedef struct NestLoop{ Join join; List *nestParams; /* list of NestLoopParam nodes */} NestLoop;
NestLoop節點在Join節點的基礎上擴充了nestParams欄位,這個欄位nestParams是一些執行器參數的列表,這些參數的用處是將外部子計劃的當前行執行值傳遞到內部子計劃中。目前主要的傳遞形式是Var型,這個資料結構的定義在:
src/include/nodes/primnodes.hVar - expression node representing a variable (ie, a table column)
下面是狀態節點NestLoopState的定義。
typedef struct NestLoopState{ JoinState js; /* its first field is NodeTag */ bool nl_NeedNewOuter; //true if need new outer tuple on next call bool nl_MatchedOuter; //true if found a join match for current outer tuple TupleTableSlot *nl_NullInnerTupleSlot; //prepared null tuple for left outer joins} NestLoopState;
NestLoop節點的初始化過程(ExecEndNestLoop函數)中初始化NestLoopState節點,構造運算式上下文這些自不必說,還會對節點中串連條件(joinqual欄位)進行處理,轉化為對應的狀態節點JoinState中的joinqual鏈表。並且對於LEFT JOIN和ANTI JOIN會初始化一個nl_NullInnerTupleSlot。why?
因為對於T1 JOIN T2,當T1的一個元組在T2中未找到滿足串連條件的元組時,這兩種串連方式會返回該T1元組與空元組的串連,這個空元組就是由nl_NullInnerTupleSlot實現。
最後還將進行如下兩個操作:
1)將nl_NeedNewOuter標記為true,表示需要擷取左子節點元組。
2)將nl_MatchedOuter標記為false,表示沒有找到與當前左子節點元組匹配的右子節點元組。
初始化就是這些。
接下來是NESTLOOP的執行過程(ExecNestLoop函數)。
迴圈嵌套串連的基本思想如下(以表R(左關係)與表S(右關係)串連為例):
FOR each tuple s in S DO FOR each tuple r in R DO IF r and s join to make a tuple t THEN output t;
為了迭代實現此方法,NestLoopState中定義了欄位nl_NeedNewOuter和nl_MatchedOuter。當元組處於內層迴圈時,nl_NeedNewOuter為false,內層迴圈結束時nl_NeedNewOuter設定為true。為了能夠處理Left Outer Join和Anti Join,需要知道內層迴圈是否找到了滿足串連條件的內層元組,此資訊由nl_MatchedOuter記錄,當內層迴圈找到合格元組時將其標記為true。
NestLoop執行過程主要是由ExecNestLoop函數來做。該函數主要是一個如上面提到的一個大迴圈。
該迴圈執行如下操作:
<1>如果nl_NeedNewOuter為true,則從左子節點擷取元組,若擷取的元組為NULL則返回空元組並結束執行過程。如果nLNeedNewOuter為false,則繼續進行步驟2。
<2>從右子節點擷取元組,若為NULL表明內層掃描完成,設定nl_NeedNewOuter為true,跳過步驟3繼續迴圈。
<3>判斷右子節點元組是否與當前左子節點元組符合串連條件,若符合則返回串連結果。
以上過程能夠完成Inner Join的遞迴執行過程。但是為了支援其他幾種串連則還需要如下兩個特殊的處理:
1)當找到符合串連條件的元組後將nl_MatchedOuter標記為true。內層掃描完畢時,通過判斷nl_MatchedOuter即可知道是否已經找到滿足串連條件的元組,在處理Left Outer Join和Anti Join時需要進行與空元組(nl_NullInnerTupleSlot)的串連,然後將nLMatchedOuter設定為false。
2)當找到滿足匹配條件的元組後,對於Semi JOIN和Anti JOIN方法需要設定nl_NeedNewOuter為true。區別在於Anti Join需要不滿足串連條件才能返回,所以要跳過返回串連結果繼續執行迴圈。
NestLoop節點的清理過程(ExecEndNestLoop函數)沒有特殊處理,只需遞迴調用左右子節點的清理過程。
2.MergeJoin 節點
Mergejoin實現了對排序關係的歸併串連演算法,歸併串連的輸人都是已經排好序的。PostgreSQL中Mergejoin演算法實現的虛擬碼如下:
Join { get initial outer and inner tuples INITIALIZE do forever { while (outer != inner) { SKIP_TEST if (outer < inner) advance outer SKIPOUTER_ADVANCE else advance inner SKIPINNER_ADVANCE } mark inner position SKIP_TEST do forever { while (outer == inner) { join tuples JOINTUPLES advance inner position NEXTINNER } advance outer position NEXTOUTER if (outer == mark) TESTOUTER restore inner position to mark TESTOUTER else break // return to top of outer loop } } }
演算法首先初始化左右子節點,然後執行以下操作(其中對於大小的比較都是指對串連屬性值的比較):
1)掃描到第一個匹配的位置,如果左子節點(outer)較大,從右子節點(inner)中擷取元組;如果右子節點較大,從左子節點中擷取元組。
2)標記右子節點當前的位置。
3)迴圈執行左子節點==右子節點判斷,若符合則串連元組,並擷取下一條右子節點元組,否則退出迴圈執行步驟4。
4)擷取下一條左子節點元組。
5)如果左子節點==標記處的右子節點(說明該條左子節點與上一條相等),需要將右子節點掃描位置回退到掃描位置,並返冋步驟3;否則跳轉到步驟1。
為了說明歸併排序的串連演算法,我們以Inner Join為例給出部分執行過程,兩個current分別指向輸人的當前元組,mark用於標記掃描的位置。
1)首先找到左右序列第一個匹配位置,中current(outer)=0小於Current(inner),因此outer的current向後移動。
2),當找到匹配項後,則進行串連,使用mark標記當前inner的掃描位置,並將inner的current向後移動。
3)接著判斷current(outer) = 1小於current(inner) =2,則將outer的current向後移動,並判斷outer是否與mark相同(這是為了發現outer的current與前一個相同的情況)。
4)顯示current(outer) =2不等於mark(inner) = 1,則繼續掃描過程。
5)判斷兩個current是否相同,發現Currem(outer)=2等於current(inner)=2,則進行串連,同樣標記inner的當前位置,並將inner的cuirent向後移動,如所示。其中的current(inner) = 2仍滿足串連條件,因此串連完成後inner的current繼續向後移動。
6)如所示,current(outer)=2 小於current(inner)=5,則將 outer的current指標向後移動。
7)此時判斷current(outer)和mark(inner)相等,則將inner的current指向mark的位置,重新擷取inner的元組進行匹配,如所示。
8)不斷重複這樣的匹配模式,直到inner或outer中的一方被掃描完畢,則表示串連完成。
MergeJoin節點的定義如下:
typedef struct MergeJoin{ Join join; List *mergeclauses; /* mergeclauses as expression trees */ /* these are arrays, but have the same length as the mergeclauses list: */ Oid *mergeFamilies; /* per-clause OIDs of btree opfamilies */ Oid *mergeCollations; /* per-clause OIDs of collations */ int *mergeStrategies; /* per-clause ordering (ASC or DESC) */ bool *mergeNullsFirst; /* per-clause nulls ordering */} MergeJoin;
該節點在join的基礎上擴充定義了幾個mergexxx欄位。其中mergeclauses儲存用於計算左右子節點元組是否匹配的運算式鏈表,mergeFamilies、mergeCollations、mergeStrategies、mergeNullsFirst均與運算式鏈表對應,表明其中每一個操作符的操作符類、執行的策略(ASC或DEC)以及空值排序策略。
在初始化過程中,會使用Mergejoin構造MergeJoinState結構:
typedef struct MergeJoinState{ JoinState js; /* its first field is NodeTag */ int mj_NumClauses; MergeJoinClause mj_Clauses; /* array of length mj_NumClauses */ int mj_JoinState; bool mj_ExtraMarks; bool mj_ConstFalseJoin; bool mj_FillOuter; bool mj_FillInner; bool mj_MatchedOuter; bool mj_MatchedInner; TupleTableSlot *mj_OuterTupleSlot; TupleTableSlot *mj_InnerTupleSlot; TupleTableSlot *mj_MarkedTupleSlot; TupleTableSlot *mj_NullOuterTupleSlot; TupleTableSlot *mj_NullInnerTupleSlot; ExprContext *mj_OuterEContext; ExprContext *mj_InnerEContext;} MergeJoinState;
通過對於連線類型的判斷來設定如下幾個變數的值:
1)mj_FillOuter:為true表示不能忽略沒有匹配項的左子節點元組,需要與空元組進行串連,在 LEFT JOIN、ANTI JOIN 和 FULL JOIN時為true。
2)mj_FillInner:為true表示不能忽略沒有匹配項的右子節點元組,需要與空元組進行串連,在 RIGHT JOIN、FULL JOIN 時為 true。
3)mj_InnerTupleSlot:為右子節點元組產生的空元組,在mj_FillOuter為真時構造。
4)mj_OuterTupleSlot:為左子節點元組產生的空元組,在mj_FillInner為真時構造。
除此之外,需要將標記當前左(右)子節點元組是否找到能夠串連的元組的變數mj_MatchedOuter(mj_MatchedInner)設定為 false,將儲存左(右)子節點元組的欄位mj_NullOuterTupleSlot(mj_InnerTupleSlot)設定為 NULL,並為mj_MarkedTupleSlot分配儲存空間。
還剩一個hashjoin,我看了半天看不太懂,下篇再說吧~
跟我一起讀postgresql源碼(十三)——Executor(查詢執行模組之——Join節點(上))