• 跟我一起读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节点就这么结束了。

  • 相关阅读:
    JavaScript 学习16.简化对象写法 上海
    JavaScript 学习11.字符串 String 对象 上海
    JavaScript 学习12.模板字符串(Template Strings) 上海
    JavaScript 学习10.使用const声明常量 上海
    JavaScript 学习9.使用let声明变量 上海
    JavaScript 学习13.Set 集合对象 上海
    一文讲透为Power Automate for Desktop (PAD) 实现自定义模块 附完整代码
    是时候使用 YAML 来做配置或数据文件了
    在博客文章中使用mermaid 定义流程图,序列图,甘特图
    .netcore+vue 实现压缩文件下载
  • 原文地址:https://www.cnblogs.com/flying-tiger/p/8277127.html
Copyright © 2020-2023  润新知