標籤:war ram 表結構 包含 mpi about first roc 結構
從前面介紹的可最佳化語句處理相關的背景知識、實現思想和執行流程,不難發現可最佳化語句執行的核心內容是對於各種計劃節點的處理,由於使用了節點表示、遞迴調用、統一介面等設計,計劃節點的功能相對獨立、代碼總體流程相似,下面介紹執行器中各種計劃節點的相關執行過程。
在PostgreSQL中,計劃節點分為四類,分別是控制節點(Control Node)、掃描節點(ScanNode),物化節點(Materialization Node)、串連節點(Join Node) 。
控制節點:是一類用於處理特殊情況的節點,用於實現特殊的執行流程。例如,Result節點可用來表示INSERT語句中VALUES子句指定的將要插人的元組。
掃描節點:顧名思義,此類節點用於掃描表等對象以從中擷取元組。例如,SeqScan節點用於順序掃描一個表.毎次掃描一個元組。
物化節點:這類節點種類比較複雜,但它們有一個共同特點,即能夠緩衝執行結果到輔助儲存中。物化節點會在第一次被執行時產生其中的所有結果元組,然後將這些結果元組緩衝起來,等待其上層節點取用;而非物化節點則是每次被執行時產生一個結果元組並返回給上層節點。例如,Sort節點能夠擷取下層節點返回的所有元組並根據指定的屬性進行排序,並將排序結果全部緩衝起來,每次上層節點從Sort節點取元組時就從緩衝中按順序返回下一個元組(見Postgres中的物化節點之sort節點)。
串連節點:此類節點對應於關係代數中的串連操作,可以實現多種串連方式(條件串連、左串連、右串連、全串連、自然串連等),每種節點實現一種串連演算法。例如,HashJoin實現了基於Hash的串連箅法。
掃描節點
掃描節點的作用是掃描表,每次擷取一條元組作為上層節點的輸入。掃描節點普遍存在於查詢計劃樹的葉子節點,它不僅可以掃描表,還可以掃描函數的結果集、鏈表結構、子查詢結果集等。
所有掃描節點都使用Scan作為公用父類,Scan不僅繼承了Plan的所有屬性,還擴充定義了scanrelid用於記錄被掃描的表在範圍表中的序號。
typedef struct Scan{ Plan plan; Index scanrelid; /* relid is index into the range table */} Scan;
掃描節點的執行狀態節點都以ScanState作為公用父類,ScanState除了繼承PlanState的所有屬性之外,還擴充定義了ss_currentScanDesc(記錄掃描的位置、關係等資訊),currentRelation(記錄被掃描的關係)和ss_ScanTupleSlot(記錄掃描到的結果)。
typedef struct ScanState{ PlanState ps; /* its first field is NodeTag */ Relation ss_currentRelation; HeapScanDesc ss_currentScanDesc; TupleTableSlot *ss_ScanTupleSlot;} ScanState;
下面是來自源碼中的所有的Scan類型:
T_SeqScanState, T_SampleScanState, T_IndexScanState, T_IndexOnlyScanState, T_BitmapIndexScanState, T_BitmapHeapScanState, T_TidScanState, T_SubqueryScanState, T_FunctionScanState, T_ValuesScanState, T_CteScanState, T_WorkTableScanState, T_ForeignScanState, T_CustomScanState,
下面將對其一一說明。
掃描節點有各自的執行函數,但是這些執行函數都由公用的執行函數ExecScan來實現。
TupleTableSlot *ExecScan(ScanState *node, ExecScanAccessMtd accessMtd, /* function returning a tuple */ ExecScanRecheckMtd recheckMtd)
ExecScan需要三個參數:
- 狀態節點ScanState,
- 擷取掃描元組的函數指標(accessMtd,由於每一種掃描節點掃描的對象不同,因此函數都不同),
- 判斷元組是否滿足符合過濾條件的函數指標(recheckMtd)。這個要說一下:這個函數用於並發控制,如果當前元組被其他事物修改並已提交,需要檢測該元組是否仍然滿足選擇條件。
ExecScan迭代地掃描對象,每次執行返回一條結果(內部返回元組是通過ExecScanFetch實現的)。ExecScan會使用accessMtd擷取元組,然後recheckMtd進行過濾條件判斷,最終返回元組。
看例子:
EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100; QUERY PLAN ------------------------------------------------------------------------------ Bitmap Heap Scan on tenk1 (cost=5.07..229.20 rows=101 width=244) Recheck Cond: (unique1 < 100) <---recheckMtd 的作用 -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..5.04 rows=101 width=0) Index Cond: (unique1 < 100
1.SeqScan 節點
SeqScan是最基本的掃描節點,它用於掃描物理表,完成沒有索引輔助的順序掃描過程。其計劃節點SeqScan實際是Scan節點的一個別名,並未定義擴充屬性。其執行狀態節點SeqScanState也直接使用ScanState。
SeqScan節點的初始化由函數ExecInitSeqScan完成。該函數首先建立一個SeqScanState結構,將SeqScan節點連結在SeqScanState結構的ps欄位中。然後調用ExecInitExpr對計劃節點的目標屬性和査詢條件進行初始化,並將它們連結到SeqScanState相應的欄位中。接下來還將為計劃節點分配用於儲存結果元組和掃描元組的資料結構。最後通過計劃節點中scanrelid欄位的資訊擷取被掃描對象的RelationData結構,並連結在ss_currentRelation欄位中,同時利用該資訊調用heap_beginscan初始化掃描描述符ss_currentScanDesc。
SeqScan節點的執行函數是ExecSeqScan,在該函數中:
調用ExecScan函數,並將SeqNext函數的指標作為ExecScan函數accessMtd參數的值。並將SeqRecheck函數指標作為ExecScan函數recheckMtd參數的值。SeqNext函數將通過儲存模組提供的函數heap_getnext擷取下一條元組並返回;
ExecScan在利用SeqNext獲得一個元組之後,還將根據計劃節點中的査詢條件和投影要求對得到的元組進行條件檢査和投影操作,最後將滿足要求的結果元組返回。這裡SeqRecheck其實沒做任何處理和判斷,因為這個函數不使用heap_beginscan返回的keys(也就是自己去表上找,不受並發的影響。這個後面說)。
其他掃描節點的執行函數都採用類似的方式管理,即統一調用ExecScan,但根據節點的類型給ExecScan的參數AccessMtd和recheckMtd賦予不同的函數指標。
SeqScan節點的淸理過程由函數ExecEndSeqScan完成,在該函數中需要額外調用函數heap_endscan來清理ss_currentScanDesc內的資訊。
2.SampleScan節點
這個是9.5 版本新增的資料取樣功能,支援查詢返回取樣資料。當前只在常規表和物化視圖上接受TABLESAMPLE子句。
文法大概是這樣:
SELECT select_list FROM table_name TABLESAMPLE sampling_method ( argument [, ...] ) [ REPEATABLE ( seed ) ]
用法的話看這裡:
http://www.postgres.cn/docs/9.5/sql-select.html(使用Sample)
http://www.postgres.cn/docs/9.5/tablesample-method.html(自訂Sample函數)
用白話說就是我對錶中合格資料可以進行採樣。幫你省了一個抽獎系統,棒棒棒!!(微笑臉)。
我們看一下節點結構:
typedef struct SampleScan{ Scan scan; /* use struct pointer to avoid including parsenodes.h here */ struct TableSampleClause *tablesample;} SampleScan;
可以看到,在Scan的基礎上加上了TableSample相關的結構,它的資料結構如下:
typedef struct TableSampleClause{ NodeTag type; Oid tsmhandler; /* OID of the tablesample handler function */ List *args; /* tablesample argument expression(s) */ Expr *repeatable; /* REPEATABLE expression, or NULL if none */} TableSampleClause;
描述SampleScan查詢狀態的資料結構SampleScanState如下,簡單而言,它在ScanState的基礎上增加了Sample採樣策略,隨機種子,採樣函數這些和Sample相關的資料。而這些資料就來自於SampleScan節點的TableSampleClause結構。
typedef struct SampleScanState{ ScanState ss; List *args; /* expr states for TABLESAMPLE params */ ExprState *repeatable; /* expr state for REPEATABLE expr */ /* use struct pointer to avoid including tsmapi.h here */ struct TsmRoutine *tsmroutine; /* descriptor for tablesample method */ void *tsm_state; /* tablesample method can keep state here */ bool use_bulkread; /* use bulkread buffer access strategy? */ bool use_pagemode; /* use page-at-a-time visibility checking? */ bool begun; /* false means need to call BeginSampleScan */ uint32 seed; /* random seed */} SampleScanState;
其他的話,諸君請看代碼吧~
3.IndexScan 節點
如果選擇條件涉及的屬性上建立了索引,則產生的査詢計劃中涉及表的掃描時會使用IndexScan節點。該節點能夠利用索引進行表的掃描以擷取滿足選擇條件的元組。
IndexScan節點的定義如下面所示。除了繼承Scan節點定義的屬性外,IndexScan擴充定義了indexid屬性(用於儲存索引的OID)、indexqual屬性(用於儲存索引掃描的條件)、indexqualorig屬性(用於儲存沒有處理的原始掃描條件鏈表以及indexonierdir屬性(用於儲存掃描的方向)。
typedef struct IndexScan{ Scan scan; Oid indexid; /* OID of index to scan */ List *indexqual; /* list of index quals (usually OpExprs) */ List *indexqualorig; /* the same in original form */ List *indexorderby; /* list of index ORDER BY exprs */ List *indexorderbyorig; /* the same in original form */ List *indexorderbyops; /* OIDs of sort ops for ORDER BY exprs */ ScanDirection indexorderdir; /* forward or backward or don't care */} IndexScan;
IndexScan節點的初始化過程由函數ExecInitlndexScan完成。該函數將構造IndexScanState節點,並使用indexid擷取索引的RelationData結構存放於iss_RelationDesc欄位中。同時,通過調用ExeclndexBuildScanKeys將indexqual中的索引掃描條件轉換為掃描關鍵字(ScanKey,儲存掃描滿足的條件)以及運行時關鍵字計算結構(IndexRuntimeKeylnfo,執行時才能得到結果的運算式資訊)分別儲存在 iss_ScanKeys 和 iss_RuntimeKeys 這兩個數組中。iss_NumScanKeys 和 iss_NumRuntimeKeys 則用於指示前面兩個數組的長度,同時還要設罝iss_NumRumimeKeys為false。最後將調用索引模組提供的index_beginscan初始化掃描描述符iss_ScanDesc。而索引掃描未經特殊處理的原始約束條件鏈表則用於構造indexqualorig欄位。
typedef struct IndexOnlyScanState{ ScanState ss; /* its first field is NodeTag */ List *indexqual; execution state for indexqual expressions ScanKey ioss_ScanKeys; Skey structures for index quals int ioss_NumScanKeys; number of ScanKeys ScanKey ioss_OrderByKeys; Skey structures for index ordering operators int ioss_NumOrderByKeys; number of OrderByKeys IndexRuntimeKeyInfo *ioss_RuntimeKeys; info about Skeys that must be evaluated at runtime int ioss_NumRuntimeKeys; number of RuntimeKeys bool ioss_RuntimeKeysReady; true if runtime Skeys have been computed ExprContext *ioss_RuntimeContext; expr context for evaling runtime Skeys Relation ioss_RelationDesc; index relation descriptor IndexScanDesc ioss_ScanDesc; index scan descriptor Buffer ioss_VMBuffer; buffer in use for visibility map testing, if any long ioss_HeapFetches; number of tuples we were forced to fetch from heap} IndexOnlyScanState;
IndexScan節點的執行過程由ExecIndexScan函數完成,其執行過程同樣由ExecScan統一管理,但對IndexScan節點將使用IndexNext函數來擷取元組。ExecIndexScan首先判斷是否有RuntimeKeys且需要計算(iss_RuntimeKeyReady為false),如果存在則調用ExecIndexReScanFunction Compute所有的iss_RuntimeKeys運算式,並將其儲存到關聯的iss_ScanKeys中。接著調用ExecScan通過IndexNext擷取元組,在IndexNext中將調用索引模組提供的index_getnext函數利用索引取得元組。
IndexScan的淸理過程由EndlndexScan函數完成,其中需要回收索引關係描述結構iss_RelationDesc (調用 index_close)和索引掃描描述符 iss_ScanDesc (調用 index_endacan)。
4.IndexOnlyScan節點
所謂index only scan ,就是因為建立index時,所包含的欄位集合,囊括了我們查詢語句中的欄位,這樣,提取出相應的index ,就不必再次提取資料區塊了。
舉個例子:對於表:
create table test(id int, name text, age int);insert into test select generate_series(1,100000),'test'::text,generate_series(1,100000);
我們對id和age建立複合索引:
create index test_id_age on test(id ,age);
然後,執行查詢:
explain select id, age from test where id < 20 and age >0;
查詢結果為:
postgres=# explain select id ,age from test where id < 20 and age >0; QUERY PLAN ------------------------------------------------------------------------------- Index Only Scan using test_id_age on test (cost=0.29..41.94 rows=20 width=8) Index Cond: ((id < 20) AND (age > 0))(2 rows)
這個查詢裡查詢的id和age就在索引test_id_age上,在我們取出索引的時候,我們已經擷取了(id,age)值的序列,因此就不必再去表中擷取記錄了,在Index上我們就獲得了我們需要的資料,因此稱為Index Only Scan。
對這個IndexOnlyScan我們可能有疑問,萬一我的索引沒有及時更新,豈不是會查詢出來舊的過時的資料?
對這點不必擔心,我們可以看看IndexOnlyScan的執行函數:
voidExecReScanIndexOnlyScan(IndexOnlyScanState *node)
它不是單純地根據節點的類型給ExecScan的參數AccessMtd和recheckMtd賦予不同的函數指標,而是還要:
* Recalculates the values of any scan keys whose value depends on * information known at runtime, then rescans the indexed relation.
也就是說,我們會先重新掃描擷取scan key,然後再拿著這個key去scan。調用路徑如下:
ExecIndexOnlyScan -->ExecReScan * 這裡是rescan,更新scan keys -->ExecReScanIndexOnlyScan -->ExecScan ## 用新的scan keys進行Scan
這裡,IndexOnlyScan不允許Recheck。
static boolIndexOnlyRecheck(IndexOnlyScanState *node, TupleTableSlot *slot){ elog(ERROR, "EvalPlanQual recheck is not supported in index-only scans"); return false; /* keep compiler quiet */}
5.BitmapIndexScan 節點
BitmapIndexScan節點也是利用屬性上的索引進行掃描操作,但是BitmapIndexScan得到的結果不是實際的元組,而是一個位元影像,該位元影像標記了滿足條件的元組在頁面中的位移量。BitmapIndexScan節點在第一次被執行時就將擷取所有滿足條件的元組並在位元影像中標記它們,而其上層節點中都會有特殊的掃描節點(例如下面將介紹的BitmapHeapScan)使用該位元影像來擷取實際的元組。因此,該掃描方式下不產生實際的元組,也就是說,該節點不出現在ExecProcNode函數的調用中,不是一個獨立的執行節點,只被特殊的上層節點調用。
BitmapIndexScan與IndexScan節點定義幾乎相同,由於一次返回關於所有的元組的位元影像,所以不需要記錄掃描方向的indexorderdir和indexorderby欄位。
typedef struct BitmapIndexScan{ Scan scan; Oid indexid; /* OID of index to scan */ List *indexqual; /* list of index quals (OpExprs) */ List *indexqualorig; /* the same in original form */} BitmapIndexScan;
其執行狀態節點BitmapIndexScanState與IndexScanState也很相似,但多出了表示索引關鍵字屬性數組及其長度的欄位。BitmapIndexScan和IndexScan的執行過程也類似,只是在BitmapIndexScan處理過程中,初始化函數ExecInitBitmapIndexScan使用index_beginscan_bitmap函數初始化掃描狀態,MultiExecBitmapIndexScan函數將調用index_getbitmap產生位元影像,並將其存放在執行狀態記錄節點的biss_result欄位中。
typedef struct BitmapIndexScanState{ ScanState ss; /* its first field is NodeTag */ TIDBitmap *biss_result; bitmap to return output into, or NULL ScanKey biss_ScanKeys; Skey structures for index quals int biss_NumScanKeys; number of ScanKeys IndexRuntimeKeyInfo *biss_RuntimeKeys; info about Skeys that must be evaluated at runtime int biss_NumRuntimeKeys; number of RuntimeKeys IndexArrayKeyInfo *biss_ArrayKeys; info about Skeys that come from ScalarArrayOpExprs int biss_NumArrayKeys; number of ArrayKeys bool biss_RuntimeKeysReady; true if runtime Skeys have been computed ExprContext *biss_RuntimeContext; expr context for evaling runtime Skeys Relation biss_RelationDesc; index relation descriptor IndexScanDesc biss_ScanDesc; index scan descriptor} BitmapIndexScanState;
6.BitmapHeapScan
上面介紹的BitmapIndexScan節點將輸出位元影像而不是元組,為了根據位元影像擷取實際的元組,PostgreSQL提供了BitmapHeapScan節點從BitmapIndexScan輸出的位元影像中擷取元組。
BitmapHeapScan節點定義如下所示,該節點在Scan的基礎上僅擴充了約束條件檢査欄位(bitmapqualorig),該欄位與IndexScan節點的indexqualorig功能相同。當並發事務修改並提交了當前處理的元組時,需要重新掃描更新後的元組是否滿足約束條件,而不是重新擷取位元影像,因此將直接使用該運算式進行條件計算。BitmapHeapScan有且僅有一個子節點(左子節點),顯然這個左子節點必須是提供位元影像輸出的計劃節點。
typedef struct BitmapHeapScan{ Scan scan; List *bitmapqualorig; /* index quals, in standard expr form */} BitmapHeapScan;
初始化函數ExecInitBitmapHeapScan會根據節點中的scanrelid初始化掃描描述符ss_currentScanDesc。其他的初始化設定在執行過程中進行。
執行函數 ExecBitmapHeapScan 會將 BitmapHeapNext 函數指標傳遞給 ExecScan, ExecScan 使用BitmapHeapNext 擷取元組。BitmapHeapNext 首先判斷 BitmapHeapScanState 的 tbm (位元影像)是否為空白,如果為空白則調用MultiExecProcNode從左子節點擷取位元影像,並調用tbm_begin_iterate初始化tbmiterator。如果需要預取還要調用 tbm_begin_iterate 初始化 prefetch_iterator,並將 prefetch_pages 置 0, prefetch_target設定為-1。然後執行過程會利用tbmiterator遍曆位元影像擷取物理元組的位移量,然後從對應的緩衝區中按照位移量取出元組並返回。
typedef struct BitmapHeapScanState{ ScanState ss; /* its first field is NodeTag */ List *bitmapqualorig; execution state for bitmapqualorig expressions TIDBitmap *tbm; bitmap obtained from child index scan(s) TBMIterator *tbmiterator; iterator for scanning current pages TBMIterateResult *tbmres; current-page data long exact_pages; total number of exact pages retrieved long lossy_pages; total number of lossy pages retrieved TBMIterator *prefetch_iterator; iterator for prefetching ahead of current page int prefetch_pages; # pages prefetch iterator is ahead of current int prefetch_target; target prefetch distance} BitmapHeapScanState;
清理過程ExecEndBitmapHeapScan需要調用左子節點的清理函數,然後清理tbmiterator、prefetch_iterator以及tbm位元影像,最後清理掃描描述符並關閉開啟的表。
7.TidScan 節點
PostgreSQL系統專用於標識元組物理位置的資料類型被稱作TID (Tuple Identifier),一個TID由塊號和塊內位移量組成,系統屬性ctid被定義為此種類型。
PostgreSQL 內建的表是堆表,資料按行儲存在HEAP PAGE中,在btree索引中,除了儲存欄位的value,還會儲存對應的ctid(即行號),檢索記錄也是通過行號進行檢索。 因此通過行號是可以快速檢索到記錄的。
行號的寫法是(page_number, item_number),資料區塊從0開始編號,行號從1開始編號。
舉例:
postgres=# select ctid ,* from zxc; ctid | id | name-------+----+---------- (0,4) | 1 | asdftest (0,5) | 3 | asdftest (0,6) | 9 | asdftest(3 行)
那麼我們可以用ctid來訪問資料:
postgres=# select * from zxc where ctid = '(0,5)'::tid; id | name----+---------- 3 | asdftest(1 行)
同時,在定義遊標後,可以使用“UPDATE/DELETE…WHERE CURRENT OF…”語句對當前遊標位置的元組進行修改/刪除。可以參考這裡:[]http://www.postgres.cn/docs/9.5/sql-update.html(http://www.postgres.cn/docs/9.5/sql-update.html)
這個時候產生的査詢計劃樹僅包含一個TidScan節點,其掃描的對象是TidScan節點中儲存的一個運算式鏈表,其中儲存的運算式可以得到ctid的值,TidScan節點將根據ctid值取得對應的元組。TidScan節點只在Scan節點的基礎上擴充了一個欄位tidquals用於儲存可以得到ctid的運算式鏈表。
TidScan 節點的初始化函數 ExecInitTidScan 會根據 tidquals 初始化 TidScanState 中的 tss_tidquals欄位,然後調用ExecInitExpr初始化tidquals中的運算式,並根據節點中的scanrelid初始化掃描描述符 ss_currentScanDesc。
TidScan節點的執行函數(ExecTidScan)也同樣調用函數ExecScan來完成執行工作,其中傳遞給ExecScan函數的指標是TidNext。函數TidNext首先需要通過計算TidScanState節點的tss_tidquals鏈表中的運算式來構造tss_TidList數組,該數組中存放的是一系列的ctid, tss_NumTidS用於記錄數組的長度,tss_TidPtr用於記錄當前處理的ctid在tss_TidList數組中的位移量,初始值設為-1。然後從tss_TidList中擷取下一個ctid值,接著調用儲存模組提供的heap_fetch根據該ctid擷取元組並返回。出於並發的需要,當TidScan節點用於“CURRENTOF".(遊標名)”語句時,擷取的ctid可能已經被其他事務修改,需要擷取此ctid對應元組的最新版本(利用HOT鏈),然後再調用heap_fetch進行擷取。
清理過程ExecEndTidScan不需要特殊的操作,直接釋放相關記憶體上下文和初始化時分配的空間。
大約還剩下7個Scan方法,下篇再說~
跟我一起讀postgresql源碼(九)——Executor(查詢執行模組之——Scan節點(上))