代码编织梦想

引言(Introduction)

在查询编译与规划处理后,将得到一个最优的计划树,计划树将由查询执行器负责递归执行。PG中的查询执行部分含有三个核心模块:Portal,Executor与ProcessUtility。其中Portal是查询执行过程中创建的核心数据,包含执行的策略信息等。PG将查询策略分为两大类,一类是数据查询操作,实现了对数据表数据的增删查改,该类功能实现的入口函数为Executor();另一类是功能操作,类似于DDL,比如游标,表的模式创建以及事务相关等。这类功能通过调用ProcessUtility()函数实现。

本文将结合《PostgreSQL数据库内核分析》第六章内容,介绍查询执行器的框架结构以及执行方式。

查询执行策略

在正式执行查询之前,需要先根据执行计划树或者一个非计划树选择对应的策略模式。PG根据操作是否需要返回数据(是否需要使用缓存)以及操作的功能分为四类:

  1. PORTAL_ONE_SELECT:一个普通的SELECT命令,该命令可以优化,这类命令可以一遍查询并一遍返回查询结果;
  2. PORTAL_ONE_RETURNING:带有RETURNING子句的INSERT/UPDATE/DELETE查询命令。在处理这类命令时,需要在完成所有的操作后再返回结果。因此这些操作需要使用到缓存用于存储缓存数据;
  3. PORTAL_UTIL_SELECT:功能性命令,但是其返回结果类似SELECT语句,比如说创建表格的结果。因此也需要使用缓存来存储操作结果,然后返回给用户;
  4. PORTAL_MULTI_QUERY:用于处理以上三种情况之外的查询,包括嵌套查询等。

注:虽然说事务具有原子性,但是每一条SQL才是最小的具有原子性的单元。

策略选择

PG在生成计划树之后,执行计划树之前需要先选择执行策略。实际上PG会先通过调用CreatePortal()函数创建一个PortalData数据结构,然后调用PortalStart()里的ChoosePortalStrategy()函数选择一个执行策略。执行策略的选择主要通过判断操作的数量来选择执行策略,因为SELECT与DDL都不需要重写,而INSERT/UPDATE/DELETE则可能需要经过规则系统进行重写。执行策略的选择流程如下:

在这里插入图片描述

Portal是查询执行过程中的核心数据结构,存储了查询计划树链表以及选择的执行策略信息:

typedef struct PortalData
{
	const char *name;			/* portal's name */
	MemoryContext portalContext;	/* subsidiary memory for portal */
	SubTransactionId createSubid;	/* the creating subxact */
	SubTransactionId activeSubid;	/* the last subxact with activity */
	const char *sourceText;		/* text of query (as of 8.4, never NULL) */
	List	   *stmts;			/* list of PlannedStmts */
	CachedPlan *cplan;			/* CachedPlan, if stmts are from one */
	PortalStrategy strategy;	/* see above */
	QueryDesc  *queryDesc;		/* info needed for executor invocation */
    ...
}			PortalData;

一个Protal对象的生命周期如下:

  1. PortalCreate创建:创建一个干净的Portal,其中内存上下文、资源跟踪器、清理函数等都是默认的,但并没有包含执行树的信息;
  2. PortalDefineQuery初始化1:添加原始的命令语句与计划树信息;
  3. PortalStart初始化:根据计划树信息选择执行策略,并且调用ExecutorStart初始化执行器;
  4. PortalRun执行:按照执行策略选择调用Executor或ProcessUtility执行命令;
  5. PortalDrop清理:对Portal占用的资源进行释放,比如用于缓存输出的内存。

其中,命令执行过程中,查询的结果会通过调用FillPortalStore填充到缓存中,然后调用RunFromStore从缓存中获取元组数据并返回。

数据定义语句执行

DDL是一种功能性语言,与普通的增删查改不同,DDL主要用于表格模式的创建修改与删除,权限的修改等 ,因此不同种类的数据定义语句,其实现会有所不同。在PG中,DDL都会通过调用函数ProcessUtility()实现。本节主要介绍该函数中会涉及的一些数据结构以及一些命令的具体执行过程。

大致过程

函数ProcessUtility()会调用standard_ProcessUtility()完成对各种类型命令的分类与执行。由于PlannedStmt结构中包含了计划树,因此可以通过判断计划树的结点类型从而判断出当前数据定义语句的类型:

ProcessUtility
    ->standard_ProcessUtility
    	switch(nodeTag(parsetree)){
                case T_TransactionStmt: ...
                case T_DeclareCursorStmt: ...
                ...
        }

其中不同类型的语句处理过程会使用到不同的数据结构,比如事务控制的处理语句会使用到TransactionStmt,游标控制的语句则会使用到DeclareCursorStm。PG为DDL的执行设计了许多数据结构,本文仅介绍其中的一些。下面将简要介绍一下创建表格的SQL语句在PG中的处理过程。

SQL命令输入

创建一个表格course,含有三种字段:id,带有自增属性、姓名,变长字符、学分,非负。

create table course(
	id serial primary key,
	name varchar,
	credit int,
	constraint cond1 check(credit >= 0 and name<>'')
);

当输入该SQL命令后,会经过词法分析,语法分析,再经过语义分析,规则重写,优化后生成计划树:

exex_simple_query
    ->pg_parse_query
    ->pg_analyze_and_rewrite
    ->pg_plan_queries
    ->CreatePortal
    ...

创建Portal对象并初始化

在生成计划树后,Pg会调用CreatePortal创建一个Portal对象。然后调用PortalStart选择执行策略。这里创建表格选择的执行策略为PORTAL_MULTI_QUERY。生成的Portal结构体信息如下:

{name = 0x55dbe022d0a8 "", prepStmtName = 0x0, portalContext = 0x55dbe02d6990, resowner = 0x55dbe01f5138, cleanup = 0x55dbde48d771 <PortalCleanup>, createSubid = 1, activeSubid = 1, sourceText = 0x55dbe01c0ea0 "create table course(id serial primary key, name varchar, credit int, constraint cond1 check(credit >= 0 and name<>''));", commandTag = CMDTAG_CREATE_TABLE, qc = {commandTag = CMDTAG_CREATE_TABLE, nprocessed = 0}, stmts = 0x55dbe01c2a58, cplan = 0x0, portalParams = 0x0, queryEnv = 0x0, strategy = PORTAL_MULTI_QUERY, cursorOptions = 4, run_once = false, status = PORTAL_READY, portalPinned = false, autoHeld = false, queryDesc = 0x0, tupDesc = 0x0, formats = 0x0, portalSnapshot = 0x0, holdStore = 0x0, holdContext = 0x0, holdSnapshot = 0x0, atStart = true, atEnd = true, portalPos = 0, creation_time = 732354745561379, visible = false}

执行命令

在初始化完成后,Pg会调用PortalRun正式开始执行命令。在PortalRun中,Pg会调用函数PortalRunMulti,里面再调用PortalRunUtility,最终调用ProcessUtility。其调用关系如下:

PortalRun
    ->PortalRunMulti
    	->PortalRunUtility
    		->ProcessUtility
    			->standard_ProcessUtility
    				->ProcessUtilitySlow
    					->transformCreateStmt
    					->DefineRelation
    					->EventTriggerCollectSimpleCommand // collect even trigger
    					->transformRelOptions // transform the options of the toast
    					->heap_reloptions // initial physical heap relation
    					->NewRelationCreateToastTable // create a toast table

由于创建表格需要更新系统表,因此创建表格过程中会有触发器发生工作。因此在ProcessUtility中,会调用函数ProcessUtilitySlow(),因为该版本的处理函数会因为触发器的作用而变得缓慢。

该函数中可以看到,创建表格的处理方式与创建外部表格的方式一致:首先调用transformCreateStmt()转换创建表格SQL语句中的各种约束,包括创建表格是否是外表,表格中每个字段定义,表格约束,外键约束等信息;然后调用DefineRelation()创建表格;由于创建的表格course中id字段是自增的,所以需要另外一个附表course_id_seq作为辅助表,创建辅助表的入口函数是CommandCounterIncrement()。

DefineRelation创建表格的流程主要如下:

  1. 预备性检查,包括检查表的名称长度以及表格的类型;

  2. 权限检查,检查表空间与用户,从而检查用户是否具有权限在该表空间中创建表格。如果在一个临时表空间中创建,那将会创建一个临时表;

  3. 表格类型检查,判断当前表格是分表还是普通的堆表,如果是分表的话还需要对主表进行上锁。默认创建的表格是堆表;

  4. 解析表格设置,解析语句中创建表格的参数;

    // pg14.0 "src/backend/commands/tablecmds.c:791(DefineRelation)"
    reloptions = transformRelOptions((Datum) 0, stmt->options, NULL, validnsps,
                                     true, false);
    switch (relkind)
    {
        case RELKIND_VIEW:
            (void) view_reloptions(reloptions, true);
            break;
        case RELKIND_PARTITIONED_TABLE:
            (void) partitioned_table_reloptions(reloptions, true);
            break;
        default:
            (void) heap_reloptions(relkind, reloptions, true);
    }
    
  5. 检查表格的继承属性,并将祖先表的属性合并到当前属性当中(MergeAttributes);

  6. 构建表格的描述,比如说表格的模式,字段的名称,类型与非空约束;

  7. 确定表格的访问方法,如果该字段没有指定,那么选择使用默认的堆表访问;

  8. 为表格获取一个OID;

  9. 创建表文件,包括表格的物理文件,并在相应的系统表上注册;

  10. 打开创建的表格,并获取互斥锁,因为这个阶段要检查每个字段的约束,查看是否有默认值,表达式等;

  11. 如果创建的表格是分表的话,还需要检查表格是否是分表,如果是分表的话还需要处理处理主表的特殊属性,比如说主表的外键约束,索引等;

  12. 检查表格的某些字段是否有Check约束。

清理

在创建完表格后,会调用PortalDrop函数对Portal结构进行清理。

数据操作语句执行

DML是用户对数据库表格数据进行增删查改操作的语言,并且PG在解析这些语言后会生成一颗计划树用于执行查询。与功能性命令的处理不同,PG使用Executor对这些查询命令进行执行。这部分的关键函数入口有三个:ExecutorStart,ExecutorRun与ExecutorEnd,分别进行初始化,执行以及资源回收的工作。比如执行查询select时,这些函数的调用关系如下:

exec_simple_query
    ->PortalStart
    	->ExecutorStart
    ->PortalRun
    	->ProtalRunSelect
    		->ExecutorRun
    ->ProtalDrop
    	->PortalCleanup
    		->ExecutorEnd

这三个函数中使用到的关键数据结构是QueryDesc,该数据结构中包含了所有Executor执行该查询需要的信息,包括是否使用闪照,以及输出的元组存放地址等:

typedef struct QueryDesc
{
	/* These fields are provided by CreateQueryDesc */
	CmdType		operation;		/* CMD_SELECT, CMD_UPDATE, etc. */
	PlannedStmt *plannedstmt;	/* planner's output (could be utility, too) */
	const char *sourceText;		/* source text of the query */
	Snapshot	snapshot;		/* snapshot to use for query */
	Snapshot	crosscheck_snapshot;	/* crosscheck for RI update/delete */
	DestReceiver *dest;			/* the destination for tuple output */
	ParamListInfo params;		/* param values being passed in */
	EState	   *estate;			/* executor's query-wide state */
	PlanState  *planstate;		/* tree of per-plan-node state */
	/* This field is set by ExecutorRun */
	bool		already_executed;	/* true if previously executed */
	struct Instrumentation *totaltime;	/* total time spent in ExecutorRun */
} QueryDesc;

PG中查询计划被组织成树(PlanState)的形式,每个结点代表着一个执行任务。并且树的结构方便执行的递归进行,父节点的输入为子节点的输出。

物理代数模型与处理模型

《数据库系统实现》里面介绍“物理查询计划由操作符构造,每一个操作符实现计划中的一步。物理操作符常常是一个关系代数操作符的特定实现。但是物理操作符也需要用来完成一些与关系代数操作符无关的任务”。

此处提及的物理操作符即是查询计划树的查询结点,而关系代数操作符则指的是查询使用到的SQL语句元素。通常情况下,查询计划树虽然可以返回SQL语句请求的结果,但是二者的结构并不会是一一对应的关系,因为查询计划树会添加一些额外的节点信息,比如扫描节点,连接结点等,这些额外的结点由查询编译器添加的。

PG中的查询计划树的结点通常包含0~2个输入和1个输出,比如说扫描节点Scan没有输入,Sort结点只有一个输入,而Join结点有两个输入。并且查询计划树通常被组织成二叉树的形式,子节点的输出作为父节点的输入,这样叶节点的处理结果会向上传递到根结点,而根节点只需要递归调用执行子节点即可获得查询的最终结果。

PG使用Executor执行数据操作语句的计划树,而真正对每个计划树中每个结点进行处理的则是ExecInitNode,ExecProcNode以及ExecEndNode。这三个函数分别实现了对结点进行初始化,执行以及清理的工作。这三个函数的递归调用则实现了对整棵查询计划树的处理。这三个函数的调用流程主要如下:

exec_simple_query
    ->PortalStart
        ->ExecutorStart
            ->standard_ExecutorStart(queryDesc, eflags);
                ->InitPlan
                    ->ExecInitNode
    ->PortalRun
    	->PortalRunMulti
         	->ProcessQuery
             	->ExecutorRun
                    ->standard_ExecutorRun
                    	->ExecutePlan
                    		->ExecProcNode
            	->ExecutorEnd
                    ->standard_ExecutorEnd
                    	->ExecEndPlan
                    		->ExecEndNode

数据结构

所有PG中的任务结点都是继承于Plan结构体(PG中并没有类继承的概念,这里只是引用C++的继承含义,便于理解)。该结构体包含了计划执行的初始化代价,涉及的元组数量,目标字段等属性。并且可以从lefttree与righttree看出,Plan结构体还负责整合整棵计划树的结构。

typedef struct Plan
{
	NodeTag		type;
	Cost		startup_cost;	/* cost expended before fetching any tuples */
	Cost		total_cost;		/* total cost (assuming all tuples fetched) */
	double		plan_rows;		/* number of rows plan is expected to emit */
	int			plan_width;		/* average row width in bytes */
	int			plan_node_id;	/* unique across entire final plan tree */
	List	   *targetlist;		/* target list to be computed at this node */
	List	   *qual;			/* implicitly-ANDed qual conditions */
	struct Plan *lefttree;		/* input plan tree(s) */
	struct Plan *righttree;
    ...
} Plan;

每个任务结点中包含的一级字段是带有Plan字段的PlanState,用于存储执行函数(ExecProcNodeMth),子任务(subPlan)以及结果元组等数据。同样重要的一个数据结构是EState,其存储了执行过程中的全局信息,这将会在后面进行介绍。Executor通过调用ExecInitNode可以获得子节点与当前节点的PlanState。

typedef struct PlanState
{
	NodeTag		type;
	Plan	   *plan;			/* associated Plan node */
	EState	   *state;			/* at execution time, states of individual
								 * nodes point to one EState for the whole
								 * top-level plan */
	ExecProcNodeMtd ExecProcNode;	/* function to return next tuple */
	ExecProcNodeMtd ExecProcNodeReal;	/* actual function, if above is a wrapper */
	struct PlanState *lefttree; /* input plan tree(s) */
	struct PlanState *righttree;
	List	   *subPlan;		/* SubPlanState nodes in my expressions */
	TupleDesc	ps_ResultTupleDesc; /* node's return type */
	TupleTableSlot *ps_ResultTupleSlot; /* slot for my result tuples */
	ExprContext *ps_ExprContext;	/* node's expression-evaluation context */
	ProjectionInfo *ps_ProjInfo;	/* info for doing tuple projection */
    ...
} PlanState;

每种任务结点都含有带有PlanState结构的字段,比如说哈希连接结点与嵌套连接结点等含有JoinState:

/* PlanState -> JoinState */
typedef struct JoinState
{
	PlanState	ps;
	JoinType	jointype;
	bool		single_match;	/* True if we should skip to next outer tuple
								 * after finding one inner match */
	ExprState  *joinqual;		/* JOIN quals (in addition to ps.qual) */
} JoinState;
/* JoinState -> MergeJoin*/
typedef struct MergeJoinState
{
	JoinState	js;				/* its first field is NodeTag */
	int			mj_NumClauses;
	MergeJoinClause mj_Clauses; /* array of length mj_NumClauses */
	TupleTableSlot *mj_OuterTupleSlot;
	...
	ExprContext *mj_OuterEContext;
	ExprContext *mj_InnerEContext;
} MergeJoinState;
/* JoinState -> NestLoopState */
typedef struct NestLoopState
{
	JoinState	js;				/* its first field is NodeTag */
	bool		nl_NeedNewOuter;
	bool		nl_MatchedOuter;
	TupleTableSlot *nl_NullInnerTupleSlot;
} NestLoopState;

与之相似的还有扫描节点ScanState,由ScanState拓展开来的还有序列扫描SeqScanState,采样扫描SampleScanState,索引扫描IndexScanState以及仅索引扫描IndexOnlyScanState等。各种执行状态都是由Executor通过调用ExecInitNode得到。

除了PlanState之外,Executor在执行过程中还需要用到的一个关键数据结构是EState,其存储了Executor在执行期间的信息。比如说执行前的闪照信息(snapshot),当前执行的上下文位置es_qeury_cxt,用于传递节点间的元组信息ts_tupleTable等。

typedef struct EState
{
	NodeTag		type;

	/* Basic state for all query types: */
	ScanDirection es_direction; /* current scan direction */
	Snapshot	es_snapshot;	/* time qual to use */
	PlannedStmt *es_plannedstmt;	/* link to top of plan tree */
	const char *es_sourceText;	/* Source text from QueryDesc */
	...
	/* Other working state: */
	MemoryContext es_query_cxt; /* per-query context in which EState lives */
	List	   *es_tupleTable;	/* List of TupleTableSlots */
	uint64		es_processed;	/* # of tuples processed */
    ...
	List	   *es_subplanstates;	/* List of PlanState for SubPlans */
} EState;

执行器的输入数据是QueryDesc,其包含了查询编译生成的PlannedStmt结构。在执行器正式开始执行之前,需要构造各种计划状态以及全局状态。这些都在InitPlan函数中完成。

...
	->ExecutorStart
    	->standard_ExecutorStart(queryDesc, eflags);
			->InitPlan // 完成EState与PlanState的初始化
			    ->ExecInitNode // 完成PlanState的初始化

Executor

前面介绍PG的Executor提供了三个接口函数:ExecutorStart,ExecutorRun以及ExecutorEnd。分别用来对EState,PlanState结构的初始化,计划节点的递归执行以及资源回收等工作。因此这节主要介绍这三个函数的主要处理逻辑:

ExecutorStart

ExecutorStart真正完成初始化任务的是standard_ExecutorStart函数,比如说初始化一个序列扫描的任务节点,其工作流程如下:

standard_ExecutorStart
    ->1. create and init EState
    ->2. InitPlan
    	->2.1 do permission checks and initialize the node's execution state(estate)
		->2.2 ExecInitNode
			->2.2.1 ExecInitSeqScan
            	->2.2.1.1 create SeqScanState structure
            	->2.2.1.2 open the scan relation
            	->2.2.1.3 create slot with the appropriate rowtype
            	->2.2.1.4 initialize result type and projection
            	->2.2.1.5 initialize child expressions

ExecutorRun

同样的,ExecutorRun中真正完成任务的是standard_ExecutorRun,其会调用ExecutePlan真正执行计划。ExecutePlan中有一个无限循环,用于不断执行PlanState中的ExecProcNodeMtd函数,并获取结果数据。依然以顺序扫描为例,其工作流程如下:

standard_ExecutorRun
    ->ExecutePlan
    	for(;;)
    	->ExecProcNode
            ->ExecScan
            	for(;;)
			   ->ExecScanFetch

ExecutorEnd

ExecutorEnd主要完成对执行过程中使用到的一些资源进行释放,比如说创建的EState结构,PlanState结构等。同样的,ExecutorEnd中真正进行资源回收的函数是standard_ExecutorEnd。依然以顺序扫描为例,其工作流程如下:

standard_ExecutorEnd
    ->ExecEndPlan
    	->ExecEndNode
    		->ExecEndSeqScan
    			->1. free the exprcontext
    			->2. clean out the tuple table
    			->3. close heap scan

计划节点

PG中的计划节点分为四种:

  1. 控制节点(Control Node):用于完成特殊的执行方式,比如说Union查询获取表格的并集;
  2. 扫描节点(Scan Node):扫描表格,并且每次获取一条元组作为上层节点的输入。比如说顺序扫描,索引扫描,位图扫描等;
  3. 物化节点(Materialization Node):需要使用到缓存的节点,比如说聚集函数操作,排序操作,去重操作等;
  4. 连接节点(Join Node):用于表格的连接,支持哈希连接,循环嵌套连接等。

本文不对这些节点进行详细的深入,只是提及而已。

辅助功能

Minimal Tuple

前面介绍,存储模块使用HeapTuple定义每条记录的物理信息,但是在执行器执行过程中,由于缓存空间较小,因此需要元组的提及尽可能地小,以提高每次处理地数据量。为了节省每个元组在缓存中占用的内容空间,PG定义了一个MinimalTupleData结构体用于存储缓存中的元组数据。与磁盘页面存储的HeapTuple不同,MinimalTupleData去除了一些事务相关的信息,并且省略了一些字段(执行器会执行投影和字段选择的操作)。

struct MinimalTupleData
{
	uint32		t_len;			/* actual length of minimal tuple */
	char		mt_padding[MINIMAL_TUPLE_PADDING];
	/* Fields below here must match HeapTupleHeaderData! */
	uint16		t_infomask2;	/* number of attributes + various flags */
	uint16		t_infomask;		/* various flag bits, see below */
	uint8		t_hoff;			/* sizeof header incl. bitmap, padding */
	/* ^ - 23 bytes - ^ */
	bits8		t_bits[FLEXIBLE_ARRAY_MEMBER];	/* bitmap of NULLs */
};

PG还定义了TupleTableSlot用于存储执行过程中的各种形式的元组。

/* base tuple table slot type */
typedef struct TupleTableSlot
{
	NodeTag		type;
#define FIELDNO_TUPLETABLESLOT_FLAGS 1
	uint16		tts_flags;		/* Boolean states */
#define FIELDNO_TUPLETABLESLOT_NVALID 2
	AttrNumber	tts_nvalid;		/* # of valid values in tts_values */
	const TupleTableSlotOps *const tts_ops; /* implementation of slot */
#define FIELDNO_TUPLETABLESLOT_TUPLEDESCRIPTOR 4
	TupleDesc	tts_tupleDescriptor;	/* slot's tuple descriptor */
#define FIELDNO_TUPLETABLESLOT_VALUES 5
	Datum	   *tts_values;		/* current per-attribute values */
#define FIELDNO_TUPLETABLESLOT_ISNULL 6
	bool	   *tts_isnull;		/* current per-attribute isnull flags */
	MemoryContext tts_mcxt;		/* slot itself is in this context */
	ItemPointerData tts_tid;	/* stored tuple's tid */
	Oid			tts_tableOid;	/* table oid of tuple */
} TupleTableSlot;

TupleTableSlot中TupleTableSlotOps存储了元组的处理函数指针,对TupleTableSlot的一些操作都是通过该数据结构中存储的函数指针实现的:

struct TupleTableSlotOps
{	...
    /*
	 * Return a heap tuple "owned" by the slot. It is slot's responsibility to
	 * free the memory consumed by the heap tuple. If the slot can not "own" a
	 * heap tuple, it should not implement this callback and should set it as
	 * NULL.
	 */
	HeapTuple	(*get_heap_tuple) (TupleTableSlot *slot);

	/*
	 * Return a minimal tuple "owned" by the slot. It is slot's responsibility
	 * to free the memory consumed by the minimal tuple. If the slot can not
	 * "own" a minimal tuple, it should not implement this callback and should
	 * set it as NULL.
	 */
	MinimalTuple (*get_minimal_tuple) (TupleTableSlot *slot);

	/*
	 * Return a copy of heap tuple representing the contents of the slot. The
	 * copy needs to be palloc'd in the current memory context. The slot
	 * itself is expected to remain unaffected. It is *not* expected to have
	 * meaningful "system columns" in the copy. The copy is not be "owned" by
	 * the slot i.e. the caller has to take responsibility to free memory
	 * consumed by the slot.
	 */
	HeapTuple	(*copy_heap_tuple) (TupleTableSlot *slot);

	/*
	 * Return a copy of minimal tuple representing the contents of the slot.
	 * The copy needs to be palloc'd in the current memory context. The slot
	 * itself is expected to remain unaffected. It is *not* expected to have
	 * meaningful "system columns" in the copy. The copy is not be "owned" by
	 * the slot i.e. the caller has to take responsibility to free memory
	 * consumed by the slot.
	 */
	MinimalTuple (*copy_minimal_tuple) (TupleTableSlot *slot);
};

Expression

在处理SQL语句的函数调用、计算式和条件表达式时需要使用到表达式计算。在执行过程中,Executor也使用了与计划节点相似的状态结构体来表示表达式,表达式状态的共同祖先是Expr结构体:

typedef struct Expr
{
	NodeTag		type;
} Expr;

表达式状态的结构体为

typedef struct ExprState
{
    ...
	NodeTag		tag;
	uint8		flags;			/* bitmask of EEO_FLAG_* bits, see above */
	/*
	 * If projecting a tuple result, this slot holds the result; else NULL.
	 */
#define FIELDNO_EXPRSTATE_RESULTSLOT 4
	TupleTableSlot *resultslot;
	/*
	 * Instructions to compute expression's return value.
	 */
	struct ExprEvalStep *steps;
	/* original expression tree, for debugging only */
	Expr	   *expr;
	/* private state for an evalfunc */
	void	   *evalfunc_private;
#define FIELDNO_EXPRSTATE_PARENT 11
	struct PlanState *parent;	/* parent PlanState node, if any */
	ParamListInfo ext_params;	/* for compiling PARAM_EXTERN nodes */
} ExprState;

Projection

PG还支持投影操作,投影操作能够在扫描元组中剔除不必要的字段信息,从而降低存储的需求量。投影操作通过表达式计算来实现。这里也不过多介绍了。

总结

本章介绍了PG中对于SQL语句的执行过程。执行引擎将SQL语句分为两类,一类是DDL,主要由ProcessUtility模块实现;第二类是DML,主要由Executor实现。二者都被封装到一个Portal结构体当中。ProcessUtility与Executor的行为模型相同,都包含初始化,运行以及清理三个步骤。并且在初始化时,都需要选择执行阶段的策略。二者相比,Executor的实现更为复杂,因为Executor在初始化时需要转换计划树得到计划节点,在执行过程中还涉及递归调用的流程。

参考资料(References)

《PostgreSQL数据库内核分析》

Executor

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_40837929/article/details/129818149

数据库索引详细介绍_lucky多多的博客-爱代码爱编程_数据库索引

数据库索引 索引的定义索引的作用B-Tree和B+Tree异同什么场景不适合创建索引什么样的字段适合创建索引索引的分类1. 主键索引2. 唯一索引3. 常规索引4. 全文索引 索引的不足使用索引的细节问题

数据库索引分类_价值成长的博客-爱代码爱编程

一. 按照索引作用对象 1. 单列索引 1)普通索引:允许空值,允许重复值 2)唯一索引:不允许重复值,允许空值 3)主键索引:不允许空值,不允许重复值 2. 组合索引(联合索引) 多列值组成一个索引,用于组合搜索,效率大于索引合并。 遵循最左前缀原则 eg:(a,b,c)创建索引,共有(a),(a,b),(a,b,c)三种索引 3. 全文索引

odc,是另一个 navicat 吗?-爱代码爱编程

欢迎访问 OceanBase 官网获取更多信息:https://www.oceanbase.com/ 关于作者 陈小伟 OceanBase 生态产品技术专家 OceanBase Developer Center 项目

postgresql之堆表存储(heap table)-爱代码爱编程

我们知道PostgreSQL中的表存储结构属于堆表(Heap Table),这是与MYSQL不同的(MYSQL中聚集索引表Clustering Index)。那么堆表和聚集索引表到底有什么不同,我们就一起来学习一下。 首先

mysql主从 添加从节点 主库已有数据情况下_mysql增加从节点-爱代码爱编程

mysql主从 添加从节点 主机已有数据情况下 1、主库配置1.1、锁定主库1.2、查看主数据库主从状态,记录file 和 position的值1.3、备份主数据库,用于导入从数据库实现同步1.4、保证从机上数

postgres源码解析53 磁盘管理器-爱代码爱编程

上文介绍了磁盘管理器中VFD的实现原理,本篇将从上层角度讲解磁盘管理器的工作细节。相关知识见回顾: postgres源码解析52 磁盘管理器–1 关键数据结构说明 本地全局变量 static HTAB *SMgrRela

postgresql 再说 pgbouncer 如何部署的问题-爱代码爱编程

开头还是介绍一下群,如果感兴趣polardb ,mongodb ,mysql ,postgresql ,redis 等有问题,有需求都可以加群群内有各大数据库行业大咖,CTO,可以解决你的问题。加群请联系 liuaustin3 ,在新加的朋友会分到2群。 最近得到与PGBOUNCER的一个问题,问题大体上是这样描述的,一台POSTGRESQ