跟我一起讀postgresql源碼(十)——Executor(查詢執行模塊之——Scan節點(下))


接前文跟我一起讀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 bool
SubqueryRecheck(SubqueryScanState *node, TupleTableSlot *slot)
{
	/* nothing to check */
	return true;
}

上面說了在執行時調用了ExecProcNode處理subplan,那么在清理過程中,很顯然需要額外調用ExecEndNode來清理子計划。


9.FunctionScan 節點

二話不說先上例子:

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 FUNCTION

postgres=# 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 1

postgres=# 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 ALL
postgres(# 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.只要工作表不為空,重復下列步驟:

  • 計算遞歸項,用當前工作表的內容替換遞歸自引用。對UNION(不是UNION ALL),拋棄重復行以及那些與之前結果行重復的行。將剩下的所有行包括在遞歸查詢的結果中,並且也把它們放在一個臨時的中間表中。

  • 用中間表的內容替換工作表的內容,然后清空中間表。

詳細可以看這里: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.h
typedef 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節點就這么結束了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM