查询执行器的功能是按照查询编译器输出的执行计划,调用对应的存储、索引、并发等模块,按照各种计划节点的实现算法来完成数据的读取或修改过程。
查询执行器主要由4个子模块组成:Portal、ProcessUtility、Executor和特定功能子模块。
1 查询执行策略
查询编译器输出执行计划后,需要为每种执行计划选择相应的处理过程,由具体的执行部件进行处理。
1.1 可优化语句与数据定义语句
用户输入的SQL语句可以分为可优化语句和数据定义语句(DDL)两种,分别由执行器和功能处理器进行处理。
1.2 四种执行策略
查询编译器输出的每一个执行计划树或者非计划树代表的处理动作称为一个原子操作。一个语句可能被转换为多个原子操作,不能简单使用同一个部件。同时SQL语句还有数据定义语句和可优化语句两种(不包含/包含返回结果),也需要由不同的部件进行处理。
基于上述原因,PG定义了四种不同的执行策略,分别使用不同的部件和流程完成执行策略。
-
PORTAL_ONE_SELECT:处理SELECT语句,使用执行器。
-
PORTAL_ONE_RETURNING:UPDATE/INSERT/DELETE等需要进行元组操作且需要缓存结果的语句,使用执行器。
-
PORTAL_UTIL_SELECT:面向单一数据定义语句,使用功能处理器。
-
PORTAL_MULTI_QUERY:前三种的混合,处理多个原子操作。
1.3 执行策略的选择
执行策略选择器根据查询编译器给出的查询计划树链表为当前查询选择四种执行策略中的一种。
使用的数据结构如下:
typedef struct PortalData
{
const char *name; // Portal 的名称
const char *sourceText; // 原始SQL语句
List *stmts; // 查询编译器输出的查询计划树链表
PortalStrategy strategy; // 当前查询选择的执行策略
PortalStatus status; // Portal 的状态
QueryDesc *queryDesc; // 查询描述符,存储执行查询所需要的所有信息
TupleDesc tupDesc; // 描述可能的返回元组的结构
...
} PortalData;
查询执行器执行一个SQL语句时都以一个Portal作为输入数据,其中存放与执行该SQL语句相关的所有信息。PlannedStmt和Query是两种原子操作。
数据结构关系如下图:
Portal根据链表长度,原子操作类型,以及命令类型commandType
选择执行策略:
PortalStrategy ChoosePortalStrategy(List *stmts) {
/* 检查原子操作链表的长度 */
if (list_length(stmts) == 1) {
Node *stmt = (Node *) linitial(stmts);
/* 检查原子操作类型 */
if(IsA(stmt, Query)) {
Query *query = (Query *) stmt;
if (query->canSetTag) { // boolean cansetTag
/* 检查命令类型 */
if (query->commandType == CMD_SELECT) {
...;
}
}
} else if (IsA(stmt, PlannedStmt)) {
...;
}
} else {
...;
}
}
根据原子操作链表选择执行策略的流程图如下:
1.4 Portal执行的过程
所有SQL语句的执行都从一个Portal开始,所有Portal的执行都必须依次调用PortalStart
、PortalRun
、PortalDrop
三个过程。
void PortalStart(Portal portal, ...) {
...;
portal->strategy = ChoosePortalStrategy(portal->stmts);
switch (portal->strategy) {
case PORTAL_ONE_SELECT:
...;
...;
}
...;
}
bool PortalRun(Portal portal, ...) {
...;
switch (protal->strategy) {
case PORTAL_ONE_SELECT:
...;
...;
}
...;P
}
void PortalDrop(Portal portal, ...) {
...;
/* portal->cleanup = PortalCleanup */
portal->cleanup(portal);
}
流程图如下:
2 功能处理器
用于处理数据定义语句。
2.1 数据定义语句执行流程
功能处理器需要处理所有类型的数据定义语句,每种类型的数据结构表示不同的操作类型。功能处理器通过判断数据结构中的NodeTag字段来区分不同的节点,并执行不同的对应处理函数。
void standard_ProcessUtility(PlannedStmt *pstmt, ...) {
/* 功能语句相关执行计划 */
Node *parsetree = pstmt->utilityStmt;
...;
switch (nodeTag(parsetree)) {
...;
case T_VaccumStmt:
ExecVacuum(pstate, (VacuumStmt *) parsetree, isTopLevel);
break;
case T_ExplainStmt:
ExplainQuery(pstate, (ExplainStmt *) parsetree, params, dest);
break;
...;
}
...;
}
流程示意图如下:
3 执行器
可优化语句被查询编译器处理后都会生成查询计划树,交给执行器处理。执行器的输入是包含查询计划树的数据结构QueryDesc
,输出则是相关执行信息或结果数据。执行器对查询计划树的处理,最终被转换为针对计划树上每一个节点的处理。
3.1 处理模型
查询计划树被组织为一个二叉树,下层节点的输出作为上层节点的输入。
上层函数通过ExecInitNode
、ExecProcNode
、ExecEndNode
三个接口函数统一对节点进行初始化、执行和清理,这三个函数根据待处理节点的实际类型调用对应的具体函数。
PG采取一次一元组的执行模式,每个节点被执行一次仅向上层节点返回一条元组。该模式具有以下优点:
-
减少了返回元组的延迟。
-
对于某些操作(如游标、LIMIT子句等)不需要一次性获取所有的元组,节省开销。
-
减少实现过程中缓存结果带来的代码复杂性和临时存储开销。
3.2 相关数据结构
查询描述符:
/* 查询描述符,封装查询执行器执行查询所需的所有内容 */
typedef struct QueryDesc {
CmdType operation; /* CMD_SELECT, CMD_UPDATE, etc. */
PlannedStmt *plannedstmt; /* planner's output (could be utility, too) */
TupleDesc tupDesc; /* descriptor for result tuples */
EState *estate; /* executor's query-wide state */
PlanState *planstate; /* tree of per-plan-node state */
...;
}
/* 查询计划树 */
typedef struct PlannedStmt {
NodeTag type;
CmdType commandType; /* select|insert|update|delete|merge|utility */
struct Plan *planTree; /* tree of Plan nodes */
...;
}
计划节点和状态节点:
查询计划树由计划节点构成。
/* 一个抽象类 */
typedef struct Plan {
NodeTag type;
List *qual; /* implicitly-ANDed qual conditions */
struct Plan *lefttree; /* input plan tree(s) */
struct Plan *righttree;
...;
}
类似于类的继承,把Plan看作一个父类,Join节点从Plan继承左右子树lefttree
和righttree
、节点类型type
、选择表达式qual
、等公共字段,并有自己的扩展字段连接类型jointype
和连接条件joinqual
。HashJoin节点继承Join节点。
所有的计划节点分为四类:控制节点、扫描节点(公共父类Scan)、连接节点(公共父类Join)、物化节点。类似于类的继承,把Plan看作一个父类,Join节点从Plan继承左右子树lefttree
和righttree
、节点类型type
、选择表达式qual
、等公共字段,并有自己的扩展字段连接类型jointype
和连接条件joinqual
。HashJoin节点继承Join节点。
所有的计划节点分为四类:控制节点、扫描节点(公共父类Scan)、连接节点(公共父类Join)、物化节点。
在执行过程中,执行器使用状态节点来记录计划节点的执行状态和数据。
所有状态节点也构成一棵状态树,并且每个状态节点都持有到对应的计划节点的指针。
每种类型的计划节点都有其对应的状态节点类型。同样有PlanState作为公共父类。
typedef struct PlanState {
NodeTag type;
Plan *plan; /* associated Plan node */
struct PlanState *lefttree; /* input plan tree(s) */
struct PlanState *righttree;
...;
}
3.3 执行器的运行
三个接口函数:ExecutorStart
、ExecutorRun
、ExecutorEnd
。
3.3.1 初始化查询计划树
构造全局状态记录Estate结构,并递归地初始化查询计划树,即构造对应的PlanState树。
void ExecutorStart(QueryDesc *queryDesc, ...) {
...;
standard_ExecutorStart(queryDesc, ...);
}
void standard_ExecutorStart(QueryDesc *queryDesc, ...) {
/* 构造 Estate */
EState *estate;
estate = CreateExecutorState();
queryDesc->estate = estate;
...;
/* 构造PlanState */
InitPlan(queryDesc, ...);
}
static void InitPlan(QueryDesc *queryDesc, ...) {
PlanState *planstate;
Plan *plan = plannedstmt->planTree;
/* 初始化状态树 */
planstate = ExecInitNode(plan, estate, eflags);
...;
queryDesc->planstate = planstate;
}
PlanState* ExecInitNode(Plan *node, EState *estate, ...) {
PlanState *result;
...;
switch (nodeTag(node)) {
/*
* scan nodes
*/
case T_SeqScan:
result = (PlanState *) ExecInitSeqScan((SeqScan *) node,
estate, eflags);
break;
...;
/*
* join nodes
*/
case T_NestLoop:
result = (PlanState *) ExecInitNestLoop((NestLoop *) node,
estate, eflags);
break;
...;
}
}
NestLoopState *ExecInitNestLoop(NestLoop *node, EState *estate, int eflags) {
NestLoopState *nlstate;
/*
* create state structure
*/
nlstate = makeNode(NestLoopState);
nlstate->js.ps.plan = (Plan *) node;
nlstate->js.ps.state = estate;
nlstate->js.ps.ExecProcNode = ExecNestLoop; // 指定执行的函数
/*
* initialize child nodes
*/
outerPlanState(nlstate) = ExecInitNode(outerPlan(node), estate, eflags);
innerPlanState(nlstate) = ExecInitNode(innerPlan(node), estate, eflags);
...;
return nlstate;
}
3.3.2 查询计划执行
实际执行由ExecutePlan
完成,主体是一个循环,每次循环通过ExecProcNode
从状态树中获取一个元组并对其进行相应处理,返回处理结果。
void ExecutorRun(QueryDesc *queryDesc, ...) {
...;
standard_ExecutorRun(queryDesc, ...);
}
void standard_ExecutorRun(QueryDesc *queryDesc, ...) {
EState *estate;
estate = queryDesc->estate;
ExecutePlan(estate, queryDesc->planstate, ...);
}
/* 实际执行 */
static void ExecutePlan(Estate *estate, PlanState *planstate, ...) {
...;
for (;;) {
/*
* Execute the plan and obtain a tuple
*/
slot = ExecProcNode(planstate);
/*
* if the tuple is null, then we assume there is nothing more to
* process so we just end the loop...
*/
if (TupIsNull(slot)) {
break;
}
...;
}
}
/* 多态实现 */
static inline TupleTableSlot *ExecProcNode(PlanState *node) {
...;
return node->ExecProcNode(node); // 通过 node 的 ExecProcNode 字段调用到对应的函数指针
}
/* 实际执行的函数 */
NestLoopState *ExecNestLoop(PlanState *pstate) {
NestLoopState *node = castNode(NestLoopState, pstate);
NestLoop *nl;
nl = (NestLoop *) node->js.ps.plan;
outerPlan = outerPlanState(node);
innerPlan = innerPlanState(node);
...;
for (;;) {
/* 处理左右子树 */
outerTupleSlot = ExecProcNode(outerPlan);
...;
innerTupleSlot = ExecProcNode(innerPlan);
...;
}
...;
}
3.3.3 执行器清理
void ExecutorEnd(QueryDesc *queryDesc) {
...;
standard_ExecutorEnd(queryDesc);
}
void standard_ExecutorEnd(QueryDesc *queryDesc) {
EState *estate;
estate = queryDesc->estate;
ExecEndPlan(queryDesc->planstate, estate);
...;
/*
* Release EState and per-query memory context. This should release
* everything the executor has allocated.
*/
FreeExecutorState(estate);
}
static void ExecEndPlan(PlanState *planstate, EState *estate) {
/* 同初始化和执行逻辑一样,递归处理节点 */
ExecEndNode(planstate);
...;
}
/* 处理节点,根据节点类型分发 */
void ExecEndNode(PlanState *node) {
/*
* do nothing when we get to the end of a leaf on tree.
*/
if (node == NULL) {
return;
}
...;
/* 根据不同的节点类型执行不同的处理,相当于多态实现 */
switch (nodeTag(node)) {
/*
* scan nodes
*/
case T_SeqScanState:
ExecEndSeqScan((SeqScanState *) node);
break;
...;
/*
* join nodes
*/
case T_NestLoopState:
ExecEndNestLoop((NestLoopState *) node);
break;
}
}
/* NetsLoop节点清理 */
void ExecEndNestLoop(NestLoopState *node) {
/*
* Free the exprcontext
*/
ExecFreeExprContext(&node->js.ps);
/*
* clean out the tuple table
*/
ExecClearTuple(node->js.ps.ps_ResultTupleSlot);
/*
* close down subplans
*/
ExecEndNode(outerPlanState(node));
ExecEndNode(innerPlanState(node));
}