PG歸併排序演算法詳解
- 2020 年 3 月 15 日
- 筆記
前言
-
歸併排序演算法是連接演算法中比較複雜的演算法,相比嵌套循環與Hash匹配而言。本節會通過實例來說明該演算法在PG中的具體實現。
-
在PG中,通過狀態機來實現——歸併-連接。當然這裡的完整流程是排序——歸併——連接,由於排序通過Sort操作來完成,這裡就不贅述。
-
這裡的狀態機一共有11中狀態,在這11中狀態的轉換過程中,會根據外表(inner)或內表(inner)讀取到數據的狀態分為——
MJEVAL_MATCHABLE、MJEVAL_NONMATCHABLE、MJEVAL_ENDOFJOIN。可以簡單理解為讀取到非空數據,讀取到數據為空以及所有數據讀取完成。 - 前置條件,內外表均已排序完成且都是升序策略。
-----------初始狀態----------------- EXEC_MJ_INITIALIZE_OUTER EXEC_MJ_INITIALIZE_INNER -----------中間狀態----------------- EXEC_MJ_NEXTOUTER EXEC_MJ_TESTOUTER EXEC_MJ_NEXTINNER EXEC_MJ_SKIP_TEST EXEC_MJ_SKIPOUTER_ADVANCE EXEC_MJ_SKIPINNER_ADVANCE -------------結束狀態----------------- EXEC_MJ_JOINTUPLES EXEC_MJ_ENDOUTER EXEC_MJ_ENDINNER
舉例
- 接下來,以一個實例來說明狀態機轉換的具體流程。這裡假設有兩張表進行全外連接——outer/inner。數據如下所示:
outer inner 5 5 5 5 6 8 6 8 7 12 8 14
- 簡單提及一下後面說明過程中會涉及到的變數
- 1 (current) outer tuple掃描到的一條外表數據,eg: 5/6/7
- 2 (current) inner tuple掃描到的一條內表數據,eg: 8/12/14
- 3 marked tuple 上一次與outer tuple匹配到的inner tuple,記錄為marked tuple(描述的是inner tuple)
-
4 offset tuple 在方法ExecMarkPos執行時,會將當前inner tuple的位置做標記,以便後面inner tuple的重新掃描。通過方法ExecSortRestrPos實現標記。通過方法ExecRestrPos來實現將(current) inner tuple恢復為offset tuple。
-
1 第一輪,初始狀態為EXEC_MJ_INITIALIZE_OUTER,外表游標下移獲取到outer tuple——5(1)。通過MJEVAL_MATCHABLE切換到狀態EXEC_MJ_INITIALIZE_INNER。
在狀態EXEC_MJ_INITIALIZE_INNER下,內表游標下移獲取到——5(1)。通過MJEVAL_MATCHABLE切換到EXEC_MJ_SKIP_TEST。
來到狀態EXEC_MJ_SKIP_TEST,這裡執行方法MJCompare,也就是對outer tuple與inner tuple進行了比較,5(1) == 5(1)。標記offset為current,同時將marked tuple置為當前tuple。然後切換到狀態EXEC_MJ_JOINTUPLES。
在狀態EXEC_MJ_JOINTUPLES下,首先更改狀態為EXEC_MJ_NEXTINNER,然後將匹配到的數據返回。
到這裡為止,完成了外表/內表的數據匹配,一輪匹配就結束了。
- 匹配結果 5(1) == 5(1)
outer inner outer tuple - 5 5 - marked tuple - offset tuple - inner tuple 5 5 6 8 6 8 7 12 8 14
-
2 第二輪,由於上一輪在退出時將狀態置為EXEC_MJ_NEXTINNER,因此,這一輪從狀態EXEC_MJ_NEXTINNER出發。該狀態下,內表游標下移獲取數據——5(2),然後調用方法MJCompare比較outer tuple與inner tuple,5(1) == 5(2),切換到狀態EXEC_MJ_JOINTUPLES。同樣,切換狀態為EXEC_MJ_NEXTINNER,並且完成該輪掃描(後面不再贅述)。
- 匹配結果 5(1) == 5(2)
outer inner outer tuple - 5 5 - marked tuple - offset tuple 5 5 - inner tuple 6 8 6 8 7 12 8 14
-
3 第三輪,繼續來到狀態EXEC_MJ_NEXTINNER,此時內表游標下移獲取數據——8(1),與outer tuple相比——5(1) < 8(1),切換到狀態EXEC_MJ_NEXTOUTER。
在狀態EXEC_MJ_NEXTOUTER下,外表游標下移獲取到outer tuple——5(2),同時通過MJEVAL_MATCHABLE切換到EXEC_MJ_TESTOUTER。
在狀態EXEC_MJ_TESTOUTER下,這裡對於outer tuple與marked tuple進行了比較,也就是5(2) == 5(1),這裡調用方法ExecRestrPos重置了內表的游標,將inner tuple置為offset tuple,也就是說,下一次內表游標的下移後獲取的數據是——5(2)。然後切換狀態為EXEC_MJ_JOINTUPLES,同時結束了本輪的匹配。
- 匹配結果 5(2) == 5(1)
outer inner 5 5 - marked tuple - offset tuple - inner tuple outer tuple - 5 5 6 8 6 8 7 12 8 14
-
4 第四輪,其實與第二輪的描述基本一致,只是結果發生了變化
- 匹配結果 5(2) == 5(2)
outer inner 5 5 - marked tuple - offset tuple outer tuple - 5 5 - inner tuple 6 8 6 8 7 12 8 14
-
5 第五輪,在狀態EXEC_MJ_NEXTINNER下,內表游標下移獲取數據——8(1),需要注意的是,這裡將mj_MatchedInner標記為false。與outer tuple比較——5(2) < 8(1),切換到狀態EXEC_MJ_NEXTOUTER。
在狀態EXEC_MJ_NEXTOUTER下,外表游標下移獲取到outer tuple——6(1),同時通過MJEVAL_MATCHABLE切換到EXEC_MJ_TESTOUTER。
在狀態EXEC_MJ_TESTOUTER下,這裡對於outer tuple與marked tuple進行了比較——6(1) > 5(1),重新載入current inner tuple——8(1),然後切換到狀態EXEC_MJ_SKIP_TEST。
在狀態EXEC_MJ_SKIP_TEST下,調用方法MJCompare比較outer tuple與inner tuple——6(1) < 8(1)。切換到狀態EXEC_MJ_SKIPOUTER_ADVANCE。
來到狀態EXEC_MJ_SKIPOUTER_ADVANCE。由於在該輪開始,變數mj_MatchedOuter已經被置為false,因此,這裡並沒有繼續外表的游標下移,而是修改變數mj_MatchedOuter為true,然後調用MJFillOuter將其關聯的inner tuple置為空。這裡沒有進行狀態的切換。該輪循環結束。
- 關聯結果 6(1) null
outer inner 5 5 - marked tuple - offset tuple 5 5 outer tuple - 6 8 - inner tuple 6 8 7 12 8 14
-
6 由於上面一輪並沒有發生狀態的切換,因此,該輪繼續從狀態EXEC_MJ_SKIPOUTER_ADVANCE出發,只是,上一輪結束時,將變數mj_MatchedOuter置為true,因此,外表游標下移獲取到outer tuple——6(2),將mj_MatchedOuter置為false,然後切換到狀態EXEC_MJ_SKIP_TEST。
來到狀態EXEC_MJ_SKIP_TEST,調用方法MJCompare比較outer tuple與inner tuple——6(2) < 8(1)。切換到狀態EXEC_MJ_SKIPOUTER_ADVANCE。與上一輪相似,這裡同樣調用MJFillOuter將其關聯的inner tuple置為空。這裡沒有進行狀態的切換。該輪循環結束。
- 關聯結果 6(2) null
outer inner 5 5 - marked tuple - offset tuple 5 5 6 8 - inner tuple outer tuple - 6 8 7 12 8 14
-
7 這一輪的流程基本與第六輪相似,只是外表游標下移獲取到outer tuple——7。
- 關聯結果 7 null
outer inner 5 5 - marked tuple - offset tuple 5 5 6 8 - inner tuple 6 8 outer tuple - 7 12 8 14
-
8 該輪同樣從狀態EXEC_MJ_SKIPOUTER_ADVANCE出發,外表游標下移獲取到outer tuple——8,狀態切換到EXEC_MJ_SKIP_TEST。
來到狀態EXEC_MJ_SKIP_TEST,調用方法MJCompare比較outer tuple與inner tuple——8 == 8(1)。這裡標記offset為current,同時將marked tuple置為當前tuple。然後切換到狀態EXEC_MJ_JOINTUPLES。同時結束了本輪的匹配。(不要忘記,下一輪從狀態EXEC_MJ_NEXTINNER出發)
- 關聯結果 8 == 8(1)
outer inner 5 5 5 5 6 8 - marked tuple - offset tuple - inner tuple 6 8 7 12 outer tuple - 8 14
-
9 該輪從狀態EXEC_MJ_NEXTINNER出發,內表游標下移獲取數據——8(2),調用方法MJCompare與outer tuple相比較——8 == 8(2),因此,切換到狀態EXEC_MJ_JOINTUPLES,同時結束本輪的匹配。
- 關聯結果 8 == 8(2)
outer inner 5 5 5 5 6 8 - marked tuple - offset tuple 6 8 - inner tuple 7 12 outer tuple - 8 14
-
10 該輪同樣從狀態EXEC_MJ_NEXTINNER出發,內表游標下移獲取數據——8(2),同時將變數mj_MatchedInner設置為false,調用方法MJCompare與outer tuple相比較——8 < 12,故切換到狀態EXEC_MJ_NEXTOUTER。
在狀態EXEC_MJ_NEXTOUTER下,外表游標下移,同時將變數mj_MatchedOuter置為false,由於此時外表已經沒有數據了,因此通過MJEVAL_ENDOFJOIN切換到狀態EXEC_MJ_ENDOUTER。
來到狀態EXEC_MJ_ENDOUTER,由於變數mj_MatchedInner為false,這裡重置mj_MatchedInner為true後,調用方法MJFillInner,將inner tuple——12關聯的outer tuple填充為null。這裡沒有狀態切換。該輪循環結束。
- 關聯結果 null 12
outer inner 5 5 5 5 6 8 - marked tuple - offset tuple 6 8 7 12 - inner tuple 8 14 outer tuple - eof eof
-
11 該輪從上一輪結束的狀態EXEC_MJ_ENDOUTER出發。由於mj_MatchedInner為true,首先調用方法ExecMarkPos標記offset為current,然後內表游標下移,同時將變數變數mj_MatchedInner設置為false,狀態不變。
繼續來到狀態,這裡與繼續調用方法MJFillInner,將inner tuple——14關聯的outer tuple填充為null。這裡沒有狀態切換。該輪循環結束。
- 關聯結果 null 14
outer inner 5 5 5 5 6 8 - marked tuple 6 8 7 12 - offset tuple 8 14 - inner tuple outer tuple - eof eof
-
12 仍然是狀態EXEC_MJ_ENDOUTER,這裡同樣首先調用方法ExecMarkPos標記offset為current,然後內表游標下移,同時將變數變數mj_MatchedInner設置為false。由於此時內表的掃描結束,返回為空,同時該輪循環結束。
- 關聯結果 null
outer inner 5 5 5 5 6 8 - marked tuple 6 8 7 12 8 14 - offset tuple outer tuple - eof eof - inner tuple
- 至此,歸併排序的舉例就結束了。我推薦大家在閱讀本文的時候最好參照pg源碼——nodeMergejoin.c文件中的ExecMergeJoin方法,有事半功倍之效。
總結
- 狀態EXEC_MJ_NEXTINNER,該狀態的前置狀態只有EXEC_MJ_JOINTUPLES。在該狀態下內表游標下移獲取數據,因此,outer tuple與inner tuple比較只有兩種情況
- 1 outer tuple = inner tuple
- 2 outer tuple < inner tuple
- 狀態EXEC_MJ_TESTOUTER,該狀態的前置狀態只有EXEC_MJ_NEXTINNER與EXEC_MJ_NEXTOUTER。而在該狀態下外表游標下移獲取數據,因此,outer tuple與inner tuple比較只有兩種情況
- 1 outer tuple = inner tuple
- 2 outer tuple > inner tuple
-
狀態EXEC_MJ_NEXTOUTER與EXEC_MJ_SKIPOUTER_ADVANCE的區別:
前者會切換到狀態EXEC_MJ_TESTOUTER來比較。
而後者會切換到EXEC_MJ_SKIP_TEST來比較。
-
狀態EXEC_MJ_SKIP_TEST與EXEC_MJ_TESTOUTER區別:
前者outer tuple與inner tuple比較有三種可能的結果。這裡比較的是current outer tuple與current inner tuple。
而後者只有兩種可能的情況。而這裡比較的是current outer tuple與makred inner tuple。
如果current outer tuple > marked inner tuple,那麼inner tuple從current tuple與outer tuple切換到狀態EXEC_MJ_SKIP_TEST進行比較。
-
恢復標記
在狀態EXEC_MJ_TESTOUTER中遇到outer tuple == marked tuple時,這裡會置inner tuple為offset tuple,並且將inner tuple置為之前保存的offset tuple。