標籤:gre method each 查詢最佳化 semi 編譯器 present 表示 void
接前文跟我一起讀postgresql源碼(九)——Executor(查詢執行模組之——Scan節點(上)) ,本篇把剩下的七個Scan節點結束掉。
T_SubqueryScanState, T_FunctionScanState, T_ValuesScanState, T_CteScanState, T_WorkTableScanState, T_ForeignScanState, T_CustomScanState,
8.SubqueryScan 節點
SubqueryScan節點的作用是以另一個査詢計劃樹(子計劃)為掃描對象進行元組的掃描,其掃描過程最終被轉換為子計劃的執行。
Postgres子查詢主要包含如下幾個關鍵字: EXISTS, IN, NOT IN, ANY/SOME, ALL,詳細介紹可以看看:http://www.postgres.cn/docs/9.5/functions-subquery.html
舉例子:
postgres=# explain select id from test_new where exists (select id from test_dm); QUERY PLAN------------------------------------------------------------------------- Result (cost=0.02..35.52 rows=2550 width=4) One-Time Filter: $0 InitPlan 1 (returns $0) -> Seq Scan on test_dm (cost=0.00..22346.00 rows=1000000 width=0) -> Seq Scan on test_new (cost=0.00..35.50 rows=2550 width=4)(5 行)
下面這個查詢雖然也是子查詢,但是在查詢編譯階段被最佳化了(提升子串連,主要是把ANY和EXIST子句轉換為半串連)
postgres=# explain select id from test_new where exists (select id from test_dm where id = test_new.id); QUERY PLAN----------------------------------------------------------------------------- Hash Semi Join (cost=38753.00..42736.38 rows=1275 width=4) Hash Cond: (test_new.id = test_dm.id) -> Seq Scan on test_new (cost=0.00..35.50 rows=2550 width=4) -> Hash (cost=22346.00..22346.00 rows=1000000 width=4) -> Seq Scan on test_dm (cost=0.00..22346.00 rows=1000000 width=4)(5 行)
有關內容,這裡有一篇講得很好:PostgreSQL查詢最佳化之子查詢最佳化
SubqueryScan節點在Scan節點之上擴充定義了子計劃的根節點指標(subplan欄位),而subrtable欄位是査詢編譯器使用的結構,執行器運行時其值為空白。
typedef struct SubqueryScan{ Scan scan; Plan *subplan;} SubqueryScan;
顯然,SubqueryScan節點的初始化過程(ExecInitSubqueryScan函數)會使用ExecInitNode處理SubqueryScan的subplan欄位指向的子計劃樹,並將子計劃的PlanStale樹根節點指標賦值給SubqueryScanState 的subplan欄位。
typedef struct SubqueryScanState{ ScanState ss; /* its first field is NodeTag */ PlanState *subplan;} SubqueryScanState;
我認為SubqueryScan節點其實就是一個殼子,為什麼這麼說呢?因為SubqueryScan節點的執行(ExecSubqueryScan 函數)通過將SubqueryNext 傳遞給 ExecScan函數處理來實現的。SubqueryNext實際則是調用ExecProcNode處理subplan來獲得元組。也就是說,這裡SubqueryScan是運行了一個獨立的查詢計劃,然後擷取它的結果,而不是自己去掃描表。因此recheck工作就在獨立的查詢計劃裡做過了,SubqueryScan節點不必再做。
所以我們可以看到:
static boolSubqueryRecheck(SubqueryScanState *node, TupleTableSlot *slot){ /* nothing to check */ return true;}
上面說了在執行時調用了ExecProcNode處理subplan,那麼在清理過程中,很顯然需要額外調用ExecEndNode來清理子計劃。
9.FuncitonScan 節點
二話不說先上例子:
postgres=# CREATE FUNCTION dup(int) RETURNS TABLE(f1 int, f2 text)postgres-# AS $$ SELECT $1, CAST($1 AS text) || ' is text' $$postgres-# LANGUAGE SQL;CREATE FUNCTIONpostgres=# explain SELECT * FROM dup(42); QUERY PLAN------------------------------------------------------------- Function Scan on dup (cost=0.25..10.25 rows=1000 width=36)(1 行)
在PostgreSQL中,有一些函數可以返回元組的集合,為了能從這些函數的傳回值中擷取元組,PostgreSQL定義了 FunctionScan節點,其掃描對象為返回元組集的函數。FunctionScan節點在Scan的基礎上擴充定義了:
functions列表欄位,裡面存放了FuncitonScan涉及的函數;
以及funcordinality欄位(是否給返回結果加上序號列)。
When a function in the FROM clause is suffixed by WITH ORDINALITY, a bigint column is appended to the output which starts from 1 and increments by 1 for each row of the function's output. This is most useful in the case of set returning functions such as unnest()
詳細看這裡:http://www.postgres.cn/docs/9.5/functions-srf.html
typedef struct FunctionScan{ Scan scan; List *functions; /* list of RangeTblFunction nodes */ bool funcordinality; /* WITH ORDINALITY */} FunctionScan;
FunctionScan 節點的初始化過程(ExecInitFunctionScan 函數)會初始化 FunctionScanState 結構,然後根據FunctionScan的欄位functions,對每個函數構造運行時的狀態節點FunctionScanPerFuncState,如下:
typedef struct FunctionScanPerFuncState{ ExprState *funcexpr; /* state of the expression being evaluated */ TupleDesc tupdesc; /* desc of the function result type */ int colcount; /* expected number of result columns */ Tuplestorestate *tstore; /* holds the function result set */ int64 rowcount; /* # of rows in result set, -1 if not known */ TupleTableSlot *func_slot; /* function result slot (or NULL) */} FunctionScanPerFuncState;
這雷根據FunctionScan中的functions欄位對每一個函數構造用於運算式計算的結構(儲存在funcexpr中)和,還要建構函式返回元組的描述符儲存在tupdesc中,此時用於儲存函數結果集的tuplestoreslate欄位為NULL。
上面這些做完以後,就可以根據所涉及的所有函數的FunctionScanPerFuncState結構來構造傳回值的TupleDesc(即最後的傳回值一定是這幾個函數傳回值的組合):
例如:
postgres=# SELECT * FROM dup(42) WITH ORDINALITY AS t(ls,n,xxx),increment(42); ls | n | xxx | increment----+------------+-----+----------- 42 | 42 is text | 1 | 43(1 行)
typedef struct FunctionScanState{ ScanState ss; /* its first field is NodeTag */ int eflags; //node's capability flags bool ordinality; //is this scan WITH ORDINALITY? bool simple; //true if we have 1 function and no ordinality int64 ordinal; //current ordinal column value int nfuncs; //number of functions being executed /* per-function execution states (private in nodeFunctionscan.c) */ struct FunctionScanPerFuncState *funcstates; /* array of length nfuncs */ MemoryContext argcontext; //memory context to evaluate function arguments in} FunctionScanState;
在 FunctionScan 節點的執行過程(ExecFunctionScan 函數)中,將 FunctionNext 傳遞給 ExecScan函數,FunctionNext函數首先判斷tuplestorestate是否為空白(首次執行時為空白),如果為空白則執行函數ExecMakeTableFunctionResult產生所有結果集並儲存在tuplestorestate中,此後每次執行節點將調用tuplestore_gettupleslot擷取結果集中的一個元組。
最後,FunctionScan節點清理過程需要淸理tuplestorestate結構。
10.ValuesScan 節點
VALUES計算由值運算式指定的一個行值或者一組行值。更常見的是把它用來產生一個大型命令內的"常量表", 但是它也可以被獨自使用。
當多於一行被指定時,所有行都必須具有相同數量的元素。結果表的列資料類型 由出現在該列的運算式的顯式或者推導類型組合決定,決定的規則與UNION相同。
在大型的命令中,在文法上允許VALUES出現在 SELECT出現的任何地方。因為文法把它當做一個 SELECT,可以為一個VALUES 命令使用ORDER BY、 LIMIT(或者等效的FETCH FIRST) 以及OFFSET子句。
我們舉例吧,一個純粹的VALUES命令:
VALUES (1, 'one'), (2, 'two'), (3, 'three');
將返回一個具有兩列、三行的表。
postgres=# VALUES (1, 'one'), (2, 'two'), (3, 'three'); column1 | column2---------+--------- 1 | one 2 | two 3 | three(3 行)postgres=# EXPLAIN VALUES (1, 'one'), (2, 'two'), (3, 'three'); QUERY PLAN-------------------------------------------------------------- Values Scan on "*VALUES*" (cost=0.00..0.04 rows=3 width=36)(1 行)
更常用地,VALUES可以被用在一個大型 SQL 命令中。 在INSERT中最常用:
postgres=# insert into test values (1,'xxxx');INSERT 0 1postgres=# explain insert into test_new values (1); QUERY PLAN------------------------------------------------------ Insert on test_new (cost=0.00..0.01 rows=1 width=0) -> Result (cost=0.00..0.01 rows=1 width=0)(2 行)
具體的可以看這個:http://www.postgres.cn/docs/9.5/sql-values.html
這樣我們就對VALUES子句不陌生了,下面繼續說。
ValuesScan節點是用來對VALUES子句給出的元組集合進行掃描(INSERT語句中的VALUES子句走的是RESULT節點)。如下所示,ValuesScan節點中的values_lists儲存了VALUES子句中的運算式鏈表。
typedef struct ValuesScan{ Scan scan; List *values_lists; /* list of expression lists */} ValuesScan;
ValuesScan節點的初始化過程(ExeclnitValuesScan函數)處理values_lists中的運算式產生Values運算式,並儲存在ValuesScanState的exprlists數組中,array_len記錄數組長度,cuxr_idx和
markedJdx用於儲存數組中的位移量。同時還會分配記憶體上下文rowconext用於運算式計箅(ss.ps.ps_ExprContext本來就是用來做運算式計算的,但是為了防止對於一個過長的VALUES子句發生的記憶體泄露,使用rowconext對VALUES每一行做統一處理,在每一行處理完成後就使用rowconext釋放該段記憶體。)。
typedef struct ValuesScanState{ ScanState ss; /* its first field is NodeTag */ ExprContext *rowcontext; //per-expression-list context List **exprlists; //array of expression lists being evaluated int array_len; //size of array int curr_idx; //current array index (0-based)} ValuesScanState;
ValuesScan 節點執行過程(ExecValuesScan 函數)調用 ExecScan 實現,ExecScan 通過 ValuesNext擷取掃描元組,ValuesNext則通過curr_idx從exprlists中擷取需要處理的運算式,並計算出結果元組返回。
由於額外地申請了rowconext上下文,因此在ValuesScan節點清理過程(ExecEndValuesScan函數)中需要釋放記憶體上下文rowcontext。
11.CteScan 節點
WITH提供了一種方式來書寫在一個大型查詢中使用的輔助語句。這些語句通常被稱為公用表運算式或CTE,它們可以被看成是定義只在一個查詢中存在的暫存資料表。在WITH子句中的每一個輔助語句可以是一個SELECT、INSERT、UPDATE或DELETE,並且WITH子句本身也可以被附加到一個主語句,主語句也可以是SELECT、INSERT、UPDATE或DELETE
具體可以參考這個:http://www.postgres.cn/docs/9.5/queries-with.html
如果對CTE有所瞭解,就會知道,CTE一般不會單獨存在,而是依附於一個主查詢,換言之CTE是作為一個副查詢出現的。所以在主查詢中就將副查詢作為一個子計劃Subplan處理。CTE的執行狀態樹存放到執行器全域狀態Estate的es_subplanstates鏈表中。
typedef struct EState{ NodeTag type; ... /* Parameter info: */ ParamListInfo es_param_list_info; /* values of external params */ ParamExecData *es_param_exec_vals; /* values of internal params */ ... List *es_subplanstates; /* List of PlanState for SubPlans */ ...} EState;
並在CteScan中的ctePlanld儲存其子計劃在該鏈表中的位移量,對應於同一個子計劃的CteScan的ctePlanld相同。PostgreSQL在實現時,還為每個CTE在一個全域參數鏈表中分配了一個空間,其位移量儲存在cteParam中,對應同一個CTE的CteScan對應的位移量也相同。CteScan節點相關資料結構如下所示。
typedef struct CteScan{ Scan scan; int ctePlanId; /* ID of init SubPlan for CTE */ int cteParam; /* ID of Param representing CTE output */} CteScan;
CteScan節點的初始化過程(ExecInitCteScan函數)將首先初始化CteScanState結構,通過ctePlanld在es_subplanstates中找到對應的子計劃執行狀態樹,並儲存在CteScanState的cteplanstate欄位中。
然後通過cteParam在執行器全域狀態Estate的es_param_exec_vals欄位中擷取參數結構ParamExecData。若ParamExecData中value為NULL,表示沒有其他CteScan對此CTE初始化過儲存結構,此時會初始化CteScanState的cte_table欄位,並將leader和ParamExecData的value賦值為指向當前CteScanState的指標。若ParamExecData中的value不為NULL,則將其值陚值給leader,讓其指向第一個CteScan建立的CteScanState,而不為當前的CteScan初始化cte_table。這樣對應一個CTE全域只有一個元組緩衝結構,所有使用該CTE的CteScan都會共用該緩衝。
typedef struct CteScanState{ ScanState ss; /* its first field is NodeTag */ int eflags; /* capability flags to pass to tuplestore */ int readptr; /* index of my tuplestore read pointer */ PlanState *cteplanstate; /* PlanState for the CTE query itself */ /* Link to the "leader" CteScanState (possibly this same node) */ struct CteScanState *leader; /* The remaining fields are only valid in the "leader" CteScanState */ Tuplestorestate *cte_table; /* rows already read from the CTE query */ bool eof_cte; /* reached end of CTE query? */} CteScanState;
最後。在做一些初始化工作,比如初始化處理元組的運算式上下文、子運算式、元組表、結果元組表等等。
在執行CteScan節點時,將首先査看cte_table指向的緩衝中是否緩衝元組(緩衝結構Tuplestorestate),如果有可直接擷取,否則需要先執行ctePlanld指向的子計劃擷取元組。
CteScan節點的清理過程需要清理元組緩衝結構,但只需清理leader指向自身的CteScanState。
12.WorkTableScan 節點
這個節點是和RecursiveUnion節點緊密關聯的。下面先看例子,一個RecursiveUnion查詢:
postgres=# WITH RECURSIVE t(n) AS(postgres(# VALUES(1)postgres(# UNION ALLpostgres(# SELECT n+1 FROM t WHERE n<100)postgres-# SELECT sum(n) FROM t; sum------ 5050(1 行)查詢計劃 QUERY PLAN------------------------------------------------------------------------- Aggregate (cost=3.65..3.66 rows=1 width=4) CTE t -> Recursive Union (cost=0.00..2.95 rows=31 width=4) -> Result (cost=0.00..0.01 rows=1 width=0) -> WorkTable Scan on t t_1 (cost=0.00..0.23 rows=3 width=4) Filter: (n < 100) -> CTE Scan on t (cost=0.00..0.62 rows=31 width=4)(7 行)
對於遞迴查詢求值,流程如下:
1.計算非遞迴項。對UNION(但不對UNION ALL),拋棄重複行。把所有剩餘的行包括在遞迴查詢的結果中,並且也把它們放在一個臨時的工作表中。
2.只要工作表不為空白,重複下列步驟:
詳細可以看這裡:http://www.postgres.cn/docs/9.5/queries-with.html
這裡的工作表就是WorkTable。
WorkTableScan會與RecursiveUnion共同完成遞迴合并子査詢。RecursiveUnion會緩衝一次遞迴中的所有元組到RecursiveUnionState結構中,WorkTableScan提供了對此緩衝的掃描。
如下所示,WorkTableScan節點擴充定義了wtParam用於同RecursiveUnion節點間的通訊,而 WorkTableScanState 節點的 rustate 欄位中記錄了 RecursiveUnionState結構的指標,以便WorkTableScan在執行過程中可以從緩衝結構中擷取元組。
typedef struct WorkTableScan{ Scan scan; int wtParam; /* ID of Param representing work table */} WorkTableScan;
節點狀態:
typedef struct WorkTableScanState{ ScanState ss; /* its first field is NodeTag */ RecursiveUnionState *rustate;} WorkTableScanState;
13.ForeignScan節點
如果用過postgres_fdw或者dblink這些PostgreSQL提供了外部資料封裝器,那麼就大概能知道這個Scan節點的用途:掃描外部Postgresql資料表。
如果你對postgres_fdw有興趣,這裡是網址,拿去不謝:http://www.postgres.cn/docs/9.5/postgres-fdw.html
ForeignScan節點的資訊如下,主要在Scan之外擴充了外部資料相關的一些資訊。fdw_exprs和fdw_private都在外部資料封裝器的控制下,但是fdw_exprs被假定為包含運算式樹狀架構並且將由規劃器相應地進行後處理; fdw_private不會。
fdw_scan_tlist是描述由FDW返回的掃描元組的內容的目標列表;如果掃描元組與外部表格的聲明行類型匹配,則可以為NIL,這對於簡單的外部表格掃描來說是正常情況。(如果計劃節點表示外部聯結,則需要fdw_scan_tlist,因為系統目錄中沒有可用的rowtype)
fdw_scan_tlist永遠不會被執行;它只是持有描述掃描元組列中的內容的運算式樹狀架構。
fdw_recheck_quals應該包含核心系統傳遞給FDW但是沒有被添加到scan.plan.qual中的條件,也就是說,這些條件需要在FDW中做判斷(這些條件是要在recheck中做判斷的)。
typedef struct ForeignScan{ Scan scan; Oid fs_server; /* OID of foreign server */ List *fdw_exprs; /* expressions that FDW may evaluate */ List *fdw_private; /* private data for FDW */ List *fdw_scan_tlist; /* optional tlist describing scan tuple */ List *fdw_recheck_quals; /* original quals not in scan.plan.qual */ Bitmapset *fs_relids; /* RTIs generated by this scan */ bool fsSystemCol; /* true if any "system column" is needed */} ForeignScan;
還有一個資料結構也要特別關注,它保持了外部資料封裝器處理常式返回函數,即它提供供PLANNER和EXECUTOR使用的回呼函數的指標。
src/include/foreign/fdwapi.htypedef struct FdwRoutine
下面是ForeignScan的狀態節點ForeignScanState,它在ScanState之外擴充了需要recheck的列表欄位fdw_recheck_quals、外部資料封裝器處理常式返回函數集合結構體FdwRoutine和外部資料封裝器狀態fdw_state。
在初始化時,ExecInitForeignScan函數除了做一般的初始化之外,還對ForeignScanState的fdwroutine欄位做了初始化,擷取函數指標和掃描關係表。
typedef struct ForeignScanState{ ScanState ss; /* its first field is NodeTag */ List *fdw_recheck_quals; /* original quals not in ss.ps.qual */ /* use struct pointer to avoid including fdwapi.h here */ struct FdwRoutine *fdwroutine; void *fdw_state; /* foreign-data wrapper can keep state here */} ForeignScanState;
ForeignScan節點的執行(ExecForeignScan 函數)通過將ForeignNext傳遞給 ExecScan函數處理來實現的。ForeignNext實際則是調用fdwroutine->IterateForeignScan在外部資料源上掃描每次獲得一個元組。
關於函數ForeignRecheck,還記得上面說的fdw_recheck_quals欄位嗎?這裡調用ExecQual函數使用fdw_recheck_quals欄位中的條件來做recheck。
最後,掃描結束後,調用fdwroutine->EndForeignScan關閉掃描,並且關閉外部表格ExecCloseScanRelation(node->ss.ss_currentRelation)。
14.CustomScan節點
從Custom這個單詞我們就可以知道,這是postgres開放的自訂Scan方法的介面。這個節點只提供了一個空殼子,我們看下:
typedef struct CustomScan{ Scan scan; uint32 flags; /* mask of CUSTOMPATH_* flags, see relation.h */ List *custom_plans; /* list of Plan nodes, if any */ List *custom_exprs; /* expressions that custom code may evaluate */ List *custom_private; /* private data for custom code */ List *custom_scan_tlist; /* optional tlist describing scan * tuple */ Bitmapset *custom_relids; /* RTIs generated by this scan */ const CustomScanMethods *methods;} CustomScan;
留給使用者自己去擴充,同時,CustomScanState狀態節點也是一樣,裡面只有一些函數指標和預設的一些屬性,你可以使用,也可以把CustomScanState作為你要擴充的Scan方法的State的一個屬性,可以說還是很靈活的。
因此,不多說這個了,希望能在網上看到關於這方面做擴充的例子~
Scan節點就這麼結束了。
跟我一起讀postgresql源碼(十)——Executor(查詢執行模組之——Scan節點(下))