SeqScan算子
声明 :本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。
本文主要参考了 postgresql-18 beta2 的开源代码和《PostgresSQL数据库内核分析》一书
引言
有关SeqScan算子的相关描述可以参考【OpenGauss源码学习 ------ 执行算子(SeqScan算子)】这篇文章。这篇博客详细解读了OpenGauss数据库(基于PostgreSQL)中执行算子的核心,特别是扫描算子中的SeqScan算子,用于对基础表进行顺序扫描。
文章首先概述了执行算子 的分类(如控制、扫描、物化、连接 算子),然后聚焦扫描算子类型,并以SeqScan为例,通过代码调试和源码分析解释了其主要函数(如ExecInitSeqScan用于初始化状态、ExecSeqScan用于迭代获取元组),包括初始化扫描关系、处理分区表、采样扫描等细节。最后总结了SeqScan的完整执行流程,从查询解析到结束扫描,强调其在查询优化中的作用,并附带结构体定义和调试截图,帮助读者理解数据库执行机制。
基于PG18进一步学习
虽然OpenGauss的SeqScan算子源码提供了良好的入门基础,但PostgreSQL作为其上游项目,在版本迭代中可能引入了性能优化、并行扫描增强或新存储格式支持(如针对PG18的潜在改进)。为了更全面掌握SeqScan在现代数据库环境中的实现细节,我们可以转向PostgreSQL 18的源码学习,探索其在executor/nodeSeqscan.c等文件中的变化,这不仅能对比OpenGauss的差异,还能揭示如何在高并发场景下高效处理数据扫描。
函数源码解读
ExecInitSeqScan
函数 ExecInitSeqScan 用于初始化顺序扫描算子(SeqScan)。
c
/* ----------------------------------------------------------------
* ExecInitSeqScan
* ----------------------------------------------------------------
*/
/* 函数定义:初始化SeqScan(顺序扫描)节点,返回SeqScanState结构体指针,用于准备顺序扫描的运行时状态 */
SeqScanState *
ExecInitSeqScan(SeqScan *node, EState *estate, int eflags)
{
SeqScanState *scanstate; // 声明SeqScanState指针,用于存储初始化后的扫描状态结构体
/*
* Once upon a time it was possible to have an outerPlan of a SeqScan, but
* not any more.
*/
/* 断言检查:确保SeqScan节点没有outerPlan子节点(历史遗留检查,现在不允许SeqScan有子计划) */
Assert(outerPlan(node) == NULL);
/* 断言检查:确保SeqScan节点没有innerPlan子节点(SeqScan是单输入扫描节点,无需inner子树) */
Assert(innerPlan(node) == NULL);
/*
* create state structure
*/
/* 创建SeqScanState状态结构体(继承自ScanState),用于存储运行时状态 */
scanstate = makeNode(SeqScanState);
/* 设置计划指针:将查询计划中的SeqScan节点关联到状态结构体的ps.plan字段 */
scanstate->ss.ps.plan = (Plan *) node;
/* 设置执行状态:将全局执行状态EState关联到状态结构体的ps.state字段 */
scanstate->ss.ps.state = estate;
/* 设置执行入口:将ExecSeqScan函数指针赋值给ExecProcNode,用于后续执行时回调SeqScan的执行逻辑 */
scanstate->ss.ps.ExecProcNode = ExecSeqScan;
/*
* Miscellaneous initialization
*
* create expression context for node
*/
/* 杂项初始化:为节点创建表达式上下文(ExprContext),用于评估过滤条件、投影等表达式 */
ExecAssignExprContext(estate, &scanstate->ss.ps);
/*
* open the scan relation
*/
/* 打开扫描关系:根据scanrelid(范围表索引)打开要扫描的表关系,并应用eflags标志(如共享锁) */
scanstate->ss.ss_currentRelation =
ExecOpenScanRelation(estate,
node->scan.scanrelid,
eflags);
/* and create slot with the appropriate rowtype */
/* 创建扫描元组槽:初始化ss_ScanTupleSlot,用于存储从表中读取的元组,使用表的TupleDesc描述符和表槽回调函数 */
ExecInitScanTupleSlot(estate, &scanstate->ss,
RelationGetDescr(scanstate->ss.ss_currentRelation),
table_slot_callbacks(scanstate->ss.ss_currentRelation));
/*
* Initialize result type and projection.
*/
/* 初始化结果类型:基于目标列表(targetlist)设置结果元组的类型和格式 */
ExecInitResultTypeTL(&scanstate->ss.ps);
/* 初始化扫描投影信息:设置投影逻辑,用于从扫描元组生成上层期望的输出元组 */
ExecAssignScanProjectionInfo(&scanstate->ss);
/*
* initialize child expressions
*/
/* 初始化子表达式:编译并初始化WHERE子句的限定条件(qual),用于后续过滤元组 */
scanstate->ss.ps.qual =
ExecInitQual(node->scan.plan.qual, (PlanState *) scanstate);
/* 返回初始化完成的SeqScanState指针,供上层执行器使用 */
return scanstate;
}
通过具体SQL用例解释每一行
为了更直观地说明函数的作用,我们使用一个具体的SQL用例:
sql
SELECT * FROM employees WHERE salary > 50000;
假设employees表有列id (INT)、name (VARCHAR)、salary (INT),已插入几行数据(如(1, 'Alice', 60000)、(2, 'Bob', 40000))。这个查询会生成一个SeqScan计划节点(因为无索引或小表全扫描),其中:
node:SeqScan计划节点,scanrelid=1(指向range table的employees表),qual为salary > 50000的表达式树。estate:全局执行状态,包含内存上下文、快照等。eflags:执行标志,如EXEC_FLAG_REWIND(如果需要支持重扫描)。
在查询执行器的初始化阶段(ExecInitNode递归调用时),ExecInitSeqScan会被调用。以下按代码顺序解释每一行(或逻辑块)在该用例中具体做了什么:
-
函数签名:
SeqScanState * ExecInitSeqScan(SeqScan *node, EState *estate, int eflags)在用例中,这被上层
ExecInitNode调用,传入SeqScan节点(从优化器生成的计划树中获取)、当前查询的EState(包含employees表的范围表信息)和eflags(假设为0,表示无特殊标志)。函数负责将静态计划转换为可执行的运行时状态。 -
SeqScanState *scanstate;声明一个局部指针,用于指向新创建的
SeqScanState结构体。在用例中,这将成为存储扫描employees表状态的容器(如当前关系、元组槽等)。 -
Assert(outerPlan(node) == NULL); Assert(innerPlan(node) == NULL);检查
SeqScan无子计划(outer/inner为空)。在用例中,SELECT * FROM employees ...的计划是简单的SeqScan,无Join或其他子树,所以断言通过。如果有子查询,会报错(但本例无)。 -
scanstate = makeNode(SeqScanState);使用
makeNode宏分配内存,创建SeqScanState结构体(继承ScanState)。在用例中,这分配约数百字节内存(取决于平台),初始化为零值,准备存储扫描employees的状态。 -
scanstate->ss.ps.plan = (Plan *) node;将输入的
SeqScan计划节点关联到状态的ps.plan字段。在用例中,这链接了计划中的scanrelid=1和qual=(salary > 50000),确保执行时能访问计划细节。 -
scanstate->ss.ps.state = estate;关联全局
EState。在用例中,这让scanstate能访问查询的快照(用于可见性检查)、内存上下文(用于表达式评估)和范围表(rtable[1]指向employees)。 -
scanstate->ss.ps.ExecProcNode = ExecSeqScan;设置执行回调为
ExecSeqScan。在用例中,后续每次调用ExecProcNode(scanstate)时,会跳转到ExecSeqScan,开始逐行扫描employees并过滤salary > 50000。 -
ExecAssignExprContext(estate, &scanstate->ss.ps);为节点创建专用
ExprContext(表达式上下文)。在用例中,这分配一个短生命周期内存上下文,用于评估qual(salary > 50000),避免全局污染;上下文包括ecxt_scantuple(稍后用于填充当前元组)。 -
scanstate->ss.ss_currentRelation = ExecOpenScanRelation(estate, node->scan.scanrelid, eflags);调用
ExecOpenScanRelation打开表关系。在用例中,scanrelid=1对应employees表:获取Relation对象(包含元数据如TupleDesc)、加AccessShareLock锁(防止DDL),并根据eflags设置共享模式。结果:ss_currentRelation指向employees的打开Relation。 -
ExecInitScanTupleSlot(estate, &scanstate->ss, RelationGetDescr(scanstate->ss.ss_currentRelation), table_slot_callbacks(scanstate->ss.ss_currentRelation));初始化扫描槽
ss_ScanTupleSlot。在用例中,使用employees的TupleDesc(3列:id, name, salary)创建虚拟元组槽(TTSOpsVirtual),并绑定表回调(如heap_getnext)。这准备了一个"容器",用于存储从employees读取的元组(如{1, 'Alice', 60000})。 -
ExecInitResultTypeTL(&scanstate->ss.ps);基于计划的目标列表(
targetlist=*,全列)初始化结果类型。在用例中,设置ps.resulttype为employees的TupleDesc,确保上层(如Result节点)知道输出格式是3列INT/VARCHAR/INT。 -
ExecAssignScanProjectionInfo(&scanstate->ss);设置投影信息(
ps_ProjInfo)。在用例中,因为targetlist是全列(无计算),投影简单(直接返回扫描元组);如果有SELECT name, salary,会编译投影表达式。 -
scanstate->ss.ps.qual = ExecInitQual(node->scan.plan.qual, (PlanState *) scanstate);编译限定条件。在用例中,
qual是salary > 50000的Expr树:ExecInitQual遍历并初始化为可执行形式(链接到ExprContext),存储到ps.qual。后续扫描时,每行元组会用此过滤(Bob的40000被丢弃)。 -
return scanstate;返回初始化的状态。在用例中,上层
ExecInitNode接收此指针,插入计划树。查询执行时,从此状态开始扫描employees,逐行检查qual,只返回Alice的行。
通过这个用例,整个函数将抽象的SeqScan计划转化为可执行状态:打开表、准备槽和上下文、编译过滤逻辑,确保高效逐元组扫描(符合"一次一元组"思想)。如果运行EXPLAIN ANALYZE,会看到SeqScan的成本和行数估计基于此初始化。
ExecInitResultTypeTL
c
/* ----------------
* ExecInitResultTypeTL
*
* Initialize result type, using the plan node's targetlist.
* ----------------
*/
/* 函数定义:使用计划节点的targetlist初始化结果类型,将生成的TupleDesc赋值给planstate->ps_ResultTupleDesc,用于定义节点的输出元组格式 */
void
ExecInitResultTypeTL(PlanState *planstate)
{
/* 声明局部TupleDesc指针,用于存储从targetlist生成的元组描述符 */
TupleDesc tupDesc = ExecTypeFromTL(planstate->plan->targetlist);
/* 将生成的TupleDesc赋值给planstate的ps_ResultTupleDesc字段,确保上层节点知道此节点的输出结构(如列类型、名称) */
planstate->ps_ResultTupleDesc = tupDesc;
}
/* ----------------------------------------------------------------
* ExecTypeFromTL
*
* Generate a tuple descriptor for the result tuple of a targetlist.
* (A parse/plan tlist must be passed, not an ExprState tlist.)
* Note that resjunk columns, if any, are included in the result.
*
* Currently there are about 4 different places where we create
* TupleDescriptors. They should all be merged, or perhaps
* be rewritten to call BuildDesc().
* ----------------------------------------------------------------
*/
/* 函数定义:从targetlist(解析/计划阶段的列表)生成结果元组的TupleDesc描述符,包括resjunk列(临时列,用于排序等,不输出到结果) */
/* 注意:传入的targetList必须是计划阶段的Expr列表,不是已编译的ExprState */
TupleDesc
ExecTypeFromTL(List *targetList)
{
/* 调用内部函数生成TupleDesc,默认不跳过junk列(skipjunk=false),返回描述符 */
return ExecTypeFromTLInternal(targetList, false);
}
/* 静态内部函数:核心实现,从targetlist构建TupleDesc,支持可选跳过junk列 */
static TupleDesc
ExecTypeFromTLInternal(List *targetList, bool skipjunk)
{
/* 声明TupleDesc指针,用于存储最终的元组描述符 */
TupleDesc typeInfo;
/* 声明ListCell指针,用于遍历targetlist */
ListCell *l;
/* 声明len:targetlist的有效长度(根据skipjunk决定是否包含junk列) */
int len;
/* 声明cur_resno:当前结果列号,从1开始递增 */
int cur_resno = 1;
/* 如果skipjunk为true,计算不含junk列的长度;否则计算总长度 */
if (skipjunk)
len = ExecCleanTargetListLength(targetList);
else
len = ExecTargetListLength(targetList);
/* 创建一个模板TupleDesc,指定列数为len,用于后续填充属性信息 */
typeInfo = CreateTemplateTupleDesc(len);
/* 遍历targetlist的每个元素 */
foreach(l, targetList)
{
/* 获取当前TargetEntry(目标条目),包含列名、表达式等 */
TargetEntry *tle = lfirst(l);
/* 如果skipjunk为true且当前tle是junk列(resjunk=true,用于内部计算,不输出),则跳过 */
if (skipjunk && tle->resjunk)
continue;
/* 初始化TupleDesc的当前列:设置resno(列号)、resname(列名)、typid(类型OID,从tle->expr获取)、typmod(类型修饰符)、notnull(默认0,表示可空) */
TupleDescInitEntry(typeInfo,
cur_resno,
tle->resname,
exprType((Node *) tle->expr),
exprTypmod((Node *) tle->expr),
0);
/* 初始化当前列的排序规则(collation),从tle->expr获取 */
TupleDescInitEntryCollation(typeInfo,
cur_resno,
exprCollation((Node *) tle->expr));
/* 递增列号,准备下一个列 */
cur_resno++;
}
/* 返回构建完成的TupleDesc */
return typeInfo;
}
通过具体SQL用例解释每一行
为了更直观地说明这些函数的作用,我们使用一个具体的SQL用例:
sql
SELECT name, salary * 1.1 AS bonus FROM employees WHERE salary > 50000;。
假设employees表有列id (INT)、name (VARCHAR)、salary (INT),已插入数据(如(1, 'Alice', 60000)、(2, 'Bob', 40000))。这个查询生成一个SeqScan计划节点,其中:
targetlist:计划阶段的TargetEntry列表,包括name(VARCHAR, resno=1, resname="name")、bonus(NUMERIC, resno=2, resname="bonus", expr="salary * 1.1")。无resjunk列(假设无ORDER BY)。planstate:SeqScan的运行时状态(PlanState),其plan指向SeqScan节点。- 执行上下文:在
ExecInitSeqScan中调用ExecInitResultTypeTL,用于设置输出格式(2列:VARCHAR和NUMERIC)。
在查询执行器的初始化阶段(ExecutorStart调用ExecInitNode递归时),这些函数会被调用。以下按代码顺序解释每一行(或逻辑块)在该用例中具体做了什么。注意,这些函数是辅助函数,主要在节点初始化时运行,确保上层(如Result节点)知道输出元组的结构。
ExecInitResultTypeTL 函数部分
-
函数签名:
void ExecInitResultTypeTL(PlanState *planstate)在用例中,这被
ExecInitSeqScan调用,传入SeqScan的planstate(包含targetlist)。函数负责基于SELECT子句(name, bonus)初始化结果描述符,确保扫描节点输出的元组格式匹配查询期望(2列输出)。 -
TupleDesc tupDesc = ExecTypeFromTL(planstate->plan->targetlist);调用
ExecTypeFromTL生成TupleDesc。在用例中,从targetlist(2个TargetEntry:name和bonus)提取类型:name→VARCHAR (OID=1043),bonus→NUMERIC(OID=1700,从expr "salary * 1.1"推导)。结果tupDesc描述一个2列元组:列1 VARCHAR(可变长),列2 NUMERIC(精确小数)。 -
planstate->ps_ResultTupleDesc = tupDesc;将
tupDesc赋值给planstate->ps_ResultTupleDesc。在用例中,这设置SeqScan的输出描述符为2列结构。上层执行器(如门户或客户端)据此分配内存、解析结果(如psql显示"name | bonus"列头)。如果后续投影,这确保了bonus列的类型正确(NUMERIC,支持小数)。
ExecTypeFromTL 函数部分
-
函数签名:
TupleDesc ExecTypeFromTL(List *targetList)入口函数,接收
targetlist(计划Expr列表)。在用例中,传入SeqScan的targetlist(2条目),调用内部函数生成描述符。注释强调:必须是计划tlist(非ExprState),因为ExprState是运行时编译版。 -
return ExecTypeFromTLInternal(targetList, false);调用内部函数,
skipjunk=false(包含所有列,包括潜在junk)。在用例中,无junk列,所以直接构建完整描述符。返回的TupleDesc用于定义输出:2列,包含列名、类型、排序规则(默认C collation)。
ExecTypeFromTLInternal 函数部分
-
函数签名:
static TupleDesc ExecTypeFromTLInternal(List *targetList, bool skipjunk)核心静态函数,构建
TupleDesc。在用例中,skipjunk=false,targetList长度=2(name和bonus)。 -
TupleDesc typeInfo; ListCell *l; int len; int cur_resno = 1;声明变量。在用例中,
len=2(总长度),cur_resno从1开始。 -
if (skipjunk) len = ExecCleanTargetListLength(targetList); else len = ExecTargetListLength(targetList);计算
len。在用例中,skipjunk=false,所以ExecTargetListLength返回2(name + bonus)。如果有junk(如ORDER BY id),len仍=2(junk不计入输出,但这里无)。 -
typeInfo = CreateTemplateTupleDesc(len);创建
len=2的空TupleDesc模板。在用例中,这分配一个描述符框架:natts=2,attrs数组准备填充(每个attr包含typid、typmod等)。 -
foreach(l, targetList) { ... }遍历
targetlist(2次循环)。在用例中:- 第一次:
tle = name条目 (resname="name", expr=Var(name))。 - 第二次:
tle = bonus条目 (resname="bonus", expr=Op(salary * 1.1))。
- 第一次:
-
TargetEntry *tle = lfirst(l);获取当前
tle。在用例中,tle包含resname、expr(类型源)。 -
if (skipjunk && tle->resjunk) continue;检查
junk。在用例中,skipjunk=false,且无resjunk=true的tle,所以不跳过。 -
TupleDescInitEntry(typeInfo, cur_resno, tle->resname, exprType((Node *) tle->expr), exprTypmod((Node *) tle->expr), 0);初始化列属性。在用例中:
cur_resno=1:resname="name", exprType(VARCHAR)=1043, typmod=-1(变长),notnull=0。cur_resno=2:resname="bonus", exprType(NUMERIC)=1700, typmod=精确值(从*1.1推导)。
-
TupleDescInitEntryCollation(typeInfo, cur_resno, exprCollation((Node *) tle->expr));设置排序规则。在用例中,默认
C collation(exprCollation从Var/Op获取),确保name的字符串比较一致。 -
cur_resno++;递增列号。在用例中,从
1→2,循环结束。 -
return typeInfo;返回完成的
TupleDesc。在用例中,上层ExecInitResultTypeTL接收此2列描述符。执行时,SeqScan输出元组(如{'Alice', 66000.0})匹配此格式:过滤后Alice行通过,Bob (40000<50000)被qual丢弃。客户端据此解析结果,显示"Alice | 66000"。
通过这个用例,这些函数将SQL的SELECT子句转化为运行时元组格式:从抽象targetlist生成具体TupleDesc,确保高效内存分配和类型安全(例如bonus的NUMERIC避免INT溢出)。在PG18中,这支持更复杂的表达式(如窗口函数),但核心逻辑不变。 如果运行EXPLAIN,计划会显示输出类型基于此描述符。
ExecSeqScan/ExecScan
c
/* ----------------------------------------------------------------
* ExecSeqScan(node)
*
* Scans the relation sequentially and returns the next qualifying
* tuple.
* We call the ExecScan() routine and pass it the appropriate
* access method functions.
* ----------------------------------------------------------------
*/
/* 函数定义:执行SeqScan算子,顺序扫描关系(表),返回下一个符合条件的元组槽 */
static TupleTableSlot *
ExecSeqScan(PlanState *pstate)
{
/* 将通用PlanState强制转换为SeqScanState,确保访问SeqScan特有的状态字段 */
SeqScanState *node = castNode(SeqScanState, pstate);
/* 调用通用ExecScan函数,传入SeqScanState、SeqNext(访问方法)和SeqRecheck(重检查方法),执行扫描并返回元组槽 */
return ExecScan(&node->ss,
(ExecScanAccessMtd) SeqNext,
(ExecScanRecheckMtd) SeqRecheck);
}
/* ----------------------------------------------------------------
* ExecScan
*
* Scans the relation using the 'access method' indicated and
* returns the next qualifying tuple.
* The access method returns the next tuple and ExecScan() is
* responsible for checking the tuple returned against the qual-clause.
*
* A 'recheck method' must also be provided that can check an
* arbitrary tuple of the relation against any qual conditions
* that are implemented internal to the access method.
*
* Conditions:
* -- the "cursor" maintained by the AMI is positioned at the tuple
* returned previously.
*
* Initial States:
* -- the relation indicated is opened for scanning so that the
* "cursor" is positioned before the first qualifying tuple.
* ----------------------------------------------------------------
*/
/* 函数定义:执行扫描操作,使用指定的访问方法(accessMtd)获取元组,检查限定条件(qual),返回下一个符合条件的元组槽 */
TupleTableSlot *
ExecScan(ScanState *node,
ExecScanAccessMtd accessMtd, /* 函数指针:返回元组的访问方法,如 SeqNext */
ExecScanRecheckMtd recheckMtd) /* 函数指针:重新检查元组是否满足访问方法内部条件,如 SeqRecheck */
{
/* 声明表达式上下文指针,用于评估限定条件和投影表达式 */
ExprContext *econtext;
/* 声明限定条件状态指针(WHERE子句的编译形式) */
ExprState *qual;
/* 声明投影信息指针,用于将扫描元组转换为目标格式 */
ProjectionInfo *projInfo;
/*
* Fetch data from node
*/
/* 从节点状态获取限定条件(qual),可能为NULL(无WHERE子句) */
qual = node->ps.qual;
/* 获取投影信息,可能为NULL(无投影,如SELECT *) */
projInfo = node->ps.ps_ProjInfo;
/* 获取表达式上下文,用于存储当前元组和评估表达式 */
econtext = node->ps.ps_ExprContext;
/* interrupt checks are in ExecScanFetch */
/* 注释:中断检查(如用户取消查询)在ExecScanFetch中处理 */
/*
* If we have neither a qual to check nor a projection to do, just skip
* all the overhead and return the raw scan tuple.
*/
/* 如果无限定条件且无投影,直接跳过开销,返回原始扫描元组 */
if (!qual && !projInfo)
{
/* 重置表达式上下文,释放上一元组循环的内存 */
ResetExprContext(econtext);
/* 调用ExecScanFetch获取元组,直接返回(无需过滤或投影) */
return ExecScanFetch(node, accessMtd, recheckMtd);
}
/*
* Reset per-tuple memory context to free any expression evaluation
* storage allocated in the previous tuple cycle.
*/
/* 重置每个元组的内存上下文,释放上一元组循环中分配的表达式计算内存 */
ResetExprContext(econtext);
/*
* get a tuple from the access method. Loop until we obtain a tuple that
* passes the qualification.
*/
/* 无限循环:从访问方法获取元组,直到找到满足限定条件的元组或无元组 */
for (;;)
{
/* 声明元组槽指针,用于存储从访问方法获取的元组 */
TupleTableSlot *slot;
/* 调用ExecScanFetch获取下一个元组,可能经过recheckMtd验证 */
slot = ExecScanFetch(node, accessMtd, recheckMtd);
/*
* if the slot returned by the accessMtd contains NULL, then it means
* there is nothing more to scan so we just return an empty slot,
* being careful to use the projection result slot so it has correct
* tupleDesc.
*/
/* 如果访问方法返回空槽(TupIsNull),表示无更多元组可扫描 */
if (TupIsNull(slot))
{
/* 如果有投影信息,返回投影结果槽(清空,确保正确的TupleDesc) */
if (projInfo)
return ExecClearTuple(projInfo->pi_state.resultslot);
/* 否则直接返回空槽 */
else
return slot;
}
/*
* place the current tuple into the expr context
*/
/* 将当前元组放入表达式上下文,用于后续条件评估 */
econtext->ecxt_scantuple = slot;
/*
* check that the current tuple satisfies the qual-clause
*
* check for non-null qual here to avoid a function call to ExecQual()
* when the qual is null ... saves only a few cycles, but they add up
* ...
*/
/* 检查元组是否满足限定条件(qual) */
/* 如果qual为空或ExecQual返回true(元组通过过滤) */
if (qual == NULL || ExecQual(qual, econtext))
{
/*
* Found a satisfactory scan tuple.
*/
/* 如果有投影信息,执行投影操作,返回投影后的元组槽 */
if (projInfo)
{
/*
* Form a projection tuple, store it in the result tuple slot
* and return it.
*/
return ExecProject(projInfo);
}
/* 否则直接返回扫描元组槽(无投影) */
else
{
/*
* Here, we aren't projecting, so just return scan tuple.
*/
return slot;
}
}
/* 如果元组不满足限定条件,记录被过滤的元组计数(用于EXPLAIN ANALYZE) */
else
InstrCountFiltered1(node, 1);
/* 元组不通过限定条件,重置表达式上下文,释放内存,继续循环 */
ResetExprContext(econtext);
}
}
通过具体SQL用例解释每一行
我们使用一个具体的SQL用例:
sql
SELECT name, salary * 1.1 AS bonus FROM employees WHERE salary > 50000;
假设employees表有列id (INT)、name (VARCHAR)、salary (INT),已插入数据:(1, 'Alice', 60000)、(2, 'Bob', 40000)。这个查询生成一个SeqScan计划节点,其中:
node:SeqScanState,包含ss_currentRelation(指向employees表)、ps.qual(salary > 50000)、ps_ProjInfo(投影name和salary*1.1)。accessMtd:SeqNext(获取下一个元组)。recheckMtd:SeqRecheck(检查元组是否满足访问方法内部条件,如索引约束,这里无索引)。- 执行上下文:在
ExecutorRun阶段,ExecSeqScan调用ExecScan,逐行扫描employees表,过滤salary > 50000,投影为(name,bonus)。
以下按代码顺序解释每一行(或逻辑块)在该用例中具体做了什么。ExecScan 是扫描算子的通用执行函数,由ExecSeqScan调用,处理元组获取、过滤和投影。
-
函数签名:
TupleTableSlot * ExecScan(ScanState *node, ExecScanAccessMtd accessMtd, ExecScanRecheckMtd recheckMtd)在用例中,
ExecSeqScan调用ExecScan,传入SeqScanState、SeqNext(访问方法)、SeqRecheck(重检查方法)。函数负责逐行扫描employees表,检查salary > 50000,输出{name,bonus}元组。 -
ExprContext *econtext; ExprState *qual; ProjectionInfo *projInfo;声明变量。在用例中,
econtext用于存储当前元组和表达式状态,qual是salary > 50000的编译表达式,projInfo定义了name和salary*1.1的投影逻辑。 -
qual = node->ps.qual;获取限定条件。在用例中,
qual是salary > 50000的ExprState(由ExecInitQual在ExecInitSeqScan中编译),用于过滤元组。 -
projInfo = node->ps.ps_ProjInfo;获取投影信息。在用例中,
projInfo包含两个TargetEntry(name和salary*1.1),由ExecAssignScanProjectionInfo初始化,确保输出{name,bonus}。 -
econtext = node->ps.ps_ExprContext;获取表达式上下文。在用例中,
econtext包含ecxt_scantuple(稍后填充元组)和内存上下文(短生命周期,存储表达式计算结果)。 -
if (!qual && !projInfo) { ... }检查是否无限定条件和投影。在用例中,存在
qual(salary > 50000)和projInfo(name, bonus),所以跳过此分支。如果是SELECT * FROM employees(无WHERE和投影),会直接调用ExecScanFetch返回原始元组。 -
ResetExprContext(econtext);(外层)重置表达式上下文,释放上一元组的内存。在用例中,首次循环时清空内存上下文,确保干净环境;后续循环释放前一元组的计算结果(如
salary*1.1的中间值)。 -
for (;;) { ... }进入无限循环,获取元组直到符合条件或无元组。在用例中,循环扫描
employees表,直到处理完所有行或中断。 -
TupleTableSlot *slot;声明元组槽。在用例中,
slot将存储从SeqNext获取的元组(如{1, 'Alice', 60000})。 -
slot = ExecScanFetch(node, accessMtd, recheckMtd);调用
ExecScanFetch通过SeqNext获取元组。在用例中,第一次循环调用SeqNext,从employees表获取{1, 'Alice', 60000},存储到ss_ScanTupleSlot。如果EvalPlanQual(并发更新检查)生效,recheckMtd验证元组。 -
if (TupIsNull(slot)) { ... }检查是否返回空槽。在用例中,若扫描到表末尾(如处理完
Alice和Bob后),返回空槽。如果有projInfo(本例有),清空投影槽(bonus格式为NUMERIC);否则返回扫描槽。 -
econtext->ecxt_scantuple = slot;将当前元组放入表达式上下文。在用例中,将
{1, 'Alice', 60000}的槽赋给ecxt_scantuple,供qual(salary > 50000)和projInfo(salary*1.1)使用。 -
if (qual == NULL || ExecQual(qual, econtext)) { ... }检查元组是否满足限定条件。在用例中,
qual非空,ExecQual评估salary > 50000:Alice: 60000 > 50000,true,通过。Bob: 40000 < 50000,false,跳到else分支。如果qual为空(如SELECT * FROM employees),直接通过。
-
if (projInfo) { return ExecProject(projInfo); }如果有投影,执行
ExecProject。在用例中,Alice通过过滤,projInfo将{1, 'Alice', 60000}投影为{'Alice', 66000.0}(salary*1.1为NUMERIC),存储到投影槽并返回。 -
else { return slot; }无投影时返回原始槽。在用例中不适用(因有
projInfo)。如果查询是SELECT * FROM employees WHERE salary > 50000,返回{1, 'Alice', 60000}的扫描槽。 -
else InstrCountFiltered1(node, 1);元组不通过
qual,记录过滤计数。在用例中,Bob的40000不满足,计数+1(EXPLAIN ANALYZE显示"Rows Removed by Filter: 1")。 -
ResetExprContext(econtext);(内层)元组不通过时,重置上下文,释放内存。在用例中,
Bob的元组被丢弃,清空ecxt_scantuple和计算内存,准备下一循环(获取Bob或表末尾)。
通过这个用例,ExecScan实现"一次一元组"思想:逐行从employees表获取元组,过滤salary > 50000,投影为{name, bonus},最终返回{'Alice', 66000.0},表末尾返回空槽。PG18的优化可能增强了并行或表AM支持,但逻辑一致。如果运行EXPLAIN ANALYZE,会看到SeqScan的过滤和投影统计。
ExecScanFetch
c
/*
* ExecScanFetch -- check interrupts & fetch next potential tuple
*
* This routine is concerned with substituting a test tuple if we are
* inside an EvalPlanQual recheck. If we aren't, just execute
* the access method's next-tuple routine.
*/
/* 函数定义:获取下一个潜在元组,检查中断并处理EvalPlanQual重检查逻辑,否则调用访问方法获取元组 */
static inline TupleTableSlot *
ExecScanFetch(ScanState *node,
ExecScanAccessMtd accessMtd, /* 函数指针:返回元组的访问方法,如 SeqNext */
ExecScanRecheckMtd recheckMtd) /* 函数指针:重新检查元组是否满足访问方法内部条件,如 SeqRecheck */
{
/* 获取全局执行状态(EState),包含查询上下文和EPQ状态 */
EState *estate = node->ps.state;
/* 检查中断信号(如用户取消查询或超时),确保执行可被中断 */
CHECK_FOR_INTERRUPTS();
/* 判断是否处于EvalPlanQual重检查状态(处理并发更新冲突) */
if (estate->es_epq_active != NULL)
{
/* 获取EPQ状态,包含替换元组和行标记信息 */
EPQState *epqstate = estate->es_epq_active;
/*
* We are inside an EvalPlanQual recheck. Return the test tuple if
* one is available, after rechecking any access-method-specific
* conditions.
*/
/* 获取扫描节点的scanrelid(范围表索引,标识扫描的表) */
Index scanrelid = ((Scan *) node->ps.plan)->scanrelid;
/* 如果scanrelid为0,表示ForeignScan或CustomScan(将连接下推到远程端) */
if (scanrelid == 0)
{
/*
* This is a ForeignScan or CustomScan which has pushed down a
* join to the remote side. The recheck method is responsible not
* only for rechecking the scan/join quals but also for storing
* the correct tuple in the slot.
*/
/* 获取节点的扫描元组槽(ss_ScanTupleSlot) */
TupleTableSlot *slot = node->ss_ScanTupleSlot;
/* 调用recheckMtd检查元组是否满足访问方法条件,若不满足则清空槽 */
if (!(*recheckMtd) (node, slot))
ExecClearTuple(slot); /* 元组不满足条件,清空槽 */
/* 返回槽(可能为空) */
return slot;
}
/* 如果已完成EPQ替换(relsubs_done为true),返回空槽 */
else if (epqstate->relsubs_done[scanrelid - 1])
{
/*
* Return empty slot, as either there is no EPQ tuple for this rel
* or we already returned it.
*/
/* 获取扫描元组槽 */
TupleTableSlot *slot = node->ss_ScanTupleSlot;
/* 清空并返回空槽,表示无更多EPQ元组 */
return ExecClearTuple(slot);
}
/* 如果存在EPQ替换元组(relsubs_slot非空),返回该元组 */
else if (epqstate->relsubs_slot[scanrelid - 1] != NULL)
{
/*
* Return replacement tuple provided by the EPQ caller.
*/
/* 获取EPQ提供的替换元组槽 */
TupleTableSlot *slot = epqstate->relsubs_slot[scanrelid - 1];
/* 断言检查:确保没有行标记(rowmark)与替换元组冲突 */
Assert(epqstate->relsubs_rowmark[scanrelid - 1] == NULL);
/* 标记已处理该替换元组,避免重复返回 */
epqstate->relsubs_done[scanrelid - 1] = true;
/* 如果替换元组为空,返回NULL */
if (TupIsNull(slot))
return NULL;
/* 检查替换元组是否满足访问方法条件,若不满足则清空槽 */
if (!(*recheckMtd) (node, slot))
return ExecClearTuple(slot); /* 元组不满足条件,清空槽 */
/* 返回通过检查的替换元组槽 */
return slot;
}
/* 如果存在EPQ行标记(relsubs_rowmark非空),通过行标记获取替换元组 */
else if (epqstate->relsubs_rowmark[scanrelid - 1] != NULL)
{
/*
* Fetch and return replacement tuple using a non-locking rowmark.
*/
/* 获取扫描元组槽 */
TupleTableSlot *slot = node->ss_ScanTupleSlot;
/* 标记已处理行标记,避免重复返回 */
epqstate->relsubs_done[scanrelid - 1] = true;
/* 调用EvalPlanQualFetchRowMark根据行标记获取替换元组,失败则返回NULL */
if (!EvalPlanQualFetchRowMark(epqstate, scanrelid, slot))
return NULL;
/* 如果获取的元组为空,返回NULL */
if (TupIsNull(slot))
return NULL;
/* 检查替换元组是否满足访问方法条件,若不满足则清空槽 */
if (!(*recheckMtd) (node, slot))
return ExecClearTuple(slot); /* 元组不满足条件,清空槽 */
/* 返回通过检查的替换元组槽 */
return slot;
}
}
/*
* Run the node-type-specific access method function to get the next tuple
*/
/* 如果不在EPQ重检查中,直接调用访问方法(如SeqNext)获取下一个元组 */
return (*accessMtd) (node);
}
通过具体SQL用例解释执行过程
我们使用 SQL 查询:SELECT name, salary FROM employees WHERE salary > 50000 FOR UPDATE; ,并假设在并发环境中触发 EvalPlanQual(EPQ)重检查。employees 表结构为:id (INT)、name (VARCHAR)、salary (INT),数据如下:
(1, 'Alice', 60000)(2, 'Bob', 40000)
查询计划为 SeqScan,限定条件(qual)为 salary > 50000,且 FOR UPDATE 可能触发 EPQ(因并发更新)。假设在扫描过程中,另一事务更新了 Alice 的记录为 (1, 'Alice', 55000),触发 EPQ 重检查。我们用表形式展示 ExecScanFetch 的执行过程。
执行上下文
node:SeqScanState,包含ss_currentRelation(指向employees表)、qual(salary > 50000)。accessMtd:SeqNext,从表获取元组。recheckMtd:SeqRecheck,通常为空操作(SeqScan无索引约束)。estate:包含EPQ状态(es_epq_active),假设EPQ触发,relsubs_slot[0]包含替换元组(1, 'Alice', 55000)。- 场景:
ExecutorRun调用ExecSeqScan,进而调用ExecScan,后者调用ExecScanFetch获取元组。
执行过程表
以下表格展示 ExecScanFetch 在上述用例中的执行步骤,结合代码逻辑和 EPQ 场景。假设第一次调用获取 Alice 元组,触发 EPQ 重检查。
| 步骤 | 代码行 | 执行动作 | 当前状态 | 输出 |
|---|---|---|---|---|
| 1. 声明变量 | EState *estate = node->ps.state; |
获取 EState,检查 EPQ 状态 |
estate->es_epq_active 非 NULL,进入 EPQ 逻辑 |
无 |
| 2. 中断检查 | CHECK_FOR_INTERRUPTS(); |
检查用户中断(如 Ctrl+C),无中断继续 |
无中断信号 | 无 |
3. 检查 EPQ 状态 |
if (estate->es_epq_active != NULL) |
确认处于 EPQ 重检查,获取 EPQ 状态 |
epqstate 包含 relsubs_slot[0](替换元组:{1, 'Alice', 55000}) |
无 |
4. 获取 scanrelid |
Index scanrelid = ((Scan *) node->ps.plan)->scanrelid; |
获取范围表索引,SeqScan 为 1 |
scanrelid = 1 |
无 |
5. 检查 scanrelid |
if (scanrelid == 0) |
检查是否 ForeignScan/CustomScan,本例为 SeqScan,跳过 |
scanrelid = 1,进入下一个分支 |
无 |
6. 检查 relsubs_done |
else if (epqstate->relsubs_done[scanrelid - 1]) |
检查是否已返回 EPQ 元组,首次调用为 false |
relsubs_done[0] = false |
无 |
| 7. 检查替换元组 | else if (epqstate->relsubs_slot[scanrelid - 1] != NULL) |
发现替换元组,获取槽 | slot = {1, 'Alice', 55000} |
无 |
| 8. 断言检查 | Assert(epqstate->relsubs_rowmark[scanrelid - 1] == NULL); |
确保无行标记冲突,断言通过 | 无行标记 | 无 |
| 9. 标记已处理 | epqstate->relsubs_done[scanrelid - 1] = true; |
设置已返回标志,避免重复 | relsubs_done[0] = true |
无 |
| 10. 检查空槽 | if (TupIsNull(slot)) |
检查替换元组是否为空,非空 | slot 非空,包含 {1, 'Alice', 55000} |
无 |
| 11. 重检查 | if (!(*recheckMtd) (node, slot)) |
调用 SeqRecheck,SeqScan 无特殊约束,返回 true |
替换元组通过检查 | 无 |
| 12. 返回元组 | return slot; |
返回替换元组槽 | slot = {1, 'Alice', 55000} |
{1, 'Alice', 55000} |
| 13. 下一次调用 | if (epqstate->relsubs_done[scanrelid - 1]) |
再次调用时,relsubs_done[0] = true |
返回空槽(ExecClearTuple) |
空槽 |
| 14. 正常扫描 | return (*accessMtd) (node); |
无 EPQ 时,调用 SeqNext 获取元组(如 Bob) |
获取 {2, 'Bob', 40000}(后续由 ExecScan 过滤) |
{2, 'Bob', 40000} |
表说明
- EPQ (EvalPlanQual,计划评估限定)场景 :并发更新触发
EPQ,替换元组(1, 'Alice', 55000)被优先处理,满足ExecScan的qual(55000 > 50000),返回{'Alice', 55000}。第二次调用因relsubs_done返回空槽,恢复正常扫描。 - 正常扫描 :获取
Bob元组{2, 'Bob', 40000},但ExecScan的qual过滤掉(40000 < 50000)。表末尾返回空槽。 - "一次一元组" :每次调用返回一个元组槽(或空槽),符合
PostgreSQL执行器设计。 - PG18 特性 :
EPQ逻辑可能优化了并发性能(如更高效的行标记处理),但核心流程不变。
什么是 EPQ 场景?
EPQ(EvalPlanQual,计划评估限定)场景 是PostgreSQL中用于处理并发更新冲突的一种机制,出现在事务执行查询(如SELECT ... FOR UPDATE)时,另一事务同时修改了相同的数据行,导致元组版本不一致。EPQ确保查询看到一致的数据,符合事务隔离级别(如可重复读或序列化),通过在扫描过程中检查和替换受影响的元组来解决冲突。
通过这个用例,ExecScanFetch 展示了其在 EPQ 和正常扫描中的双重角色:优先处理替换元组,确保并发一致性;否则通过 SeqNext 获取表数据,支持"一次一元组 "。如果需要进一步分析 SeqNext 或生成流程图,请告诉我!
SeqNext
SeqNext 是 PostgreSQL SeqScan 算子的核心工作函数,负责从指定表中顺序获取下一个元组并存储到元组槽(TupleTableSlot)中,返回给上层 ExecScan 处理。其主要步骤包括:从 SeqScanState 获取扫描描述符(TableScanDesc)、执行状态(EState)、扫描方向和元组槽;
如果描述符未初始化(非并行扫描或串行执行),调用 table_beginscan 打开表扫描;通过 table_scan_getnextslot 获取下一个元组,存储到槽中并返回,若无元组则返回空指针。
函数支持正向或反向扫描,兼容表访问方法接口,并在并发环境中通过快照(es_snapshot)确保 MVCC 一致性。SeqNext 实现"一次一元组"思想,是 SeqScan 执行效率的关键。
c
/* 函数定义:SeqScan 算子的工作函数,从表中获取下一个元组并返回其元组槽 */
static TupleTableSlot *
SeqNext(SeqScanState *node)
{
/* 声明扫描描述符,用于存储表扫描的状态和上下文 */
TableScanDesc scandesc;
/* 声明执行状态指针,用于访问全局查询信息 */
EState *estate;
/* 声明扫描方向,决定是正向(Forward)还是反向(Backward)扫描 */
ScanDirection direction;
/* 声明元组槽指针,用于存储获取的元组 */
TupleTableSlot *slot;
/*
* get information from the estate and scan state
*/
/* 从节点状态获取当前扫描描述符(可能为空,需初始化) */
scandesc = node->ss.ss_currentScanDesc;
/* 从节点状态获取全局执行状态(包含快照、方向等) */
estate = node->ss.ps.state;
/* 获取扫描方向(ForwardScanDirection 或 BackwardScanDirection) */
direction = estate->es_direction;
/* 获取扫描元组槽,用于存储从表中读取的元组 */
slot = node->ss.ss_ScanTupleSlot;
/* 如果扫描描述符为空(非并行扫描或串行执行并行计划) */
if (scandesc == NULL)
{
/*
* We reach here if the scan is not parallel, or if we're serially
* executing a scan that was planned to be parallel.
*/
/* 初始化扫描描述符,打开表扫描,使用当前快照,无键条件 */
scandesc = table_beginscan(node->ss.ss_currentRelation,
estate->es_snapshot,
0, NULL);
/* 将初始化的扫描描述符保存到节点状态 */
node->ss.ss_currentScanDesc = scandesc;
}
/*
* get the next tuple from the table
*/
/* 调用表访问方法获取下一个元组,存储到指定槽,若成功返回槽 */
if (table_scan_getnextslot(scandesc, direction, slot))
return slot;
/* 如果无更多元组,返回空指针(表示扫描结束) */
return NULL;
}
是 否 EPQ 触发 是 否 无 EPQ 是 否 不满足 满足 是 否 需要更多元组 开始: 执行查询 - SELECT name, salary * 1.1 AS bonus FROM employees WHERE salary > 50000 并行查询? ExecSeqScanInitializeDSM: 初始化并行共享内存 分配共享内存: ParallelTableScanDesc 初始化并行扫描: table_parallelscan_initialize 启动并行扫描: table_beginscan_parallel 保存扫描描述符: ss_currentScanDesc ExecInitSeqScan: 初始化 SeqScan 创建 SeqScanState: 设置 plan, state, ExecProcNode 初始化 ExprContext: 为 qual 和投影分配内存 打开表关系: ExecOpenScanRelation - employees 初始化扫描槽: ExecInitScanTupleSlot 初始化结果类型: ExecInitResultTypeTL - name, bonus 初始化限定条件: ExecInitQual - salary > 50000 完成初始化 ExecSeqScan: 开始执行扫描 调用 ExecScan: 传入 SeqNext, SeqRecheck 循环: 获取元组 ExecScanFetch: 检查中断和 EPQ 获取替换元组: relsubs_slot 或 rowmark 通过重检查? 返回替换元组槽 清空槽 SeqNext: 获取下一个元组 检查扫描描述符: 如为空初始化 heap_getnextslot: 从堆表获取元组 获取到元组? 存储到扫描槽: ExecStoreBufferHeapTuple 放入 ExprContext: ecxt_scantuple 检查限定条件: salary > 50000 记录过滤计数: InstrCountFiltered1 有投影? ExecProject: 生成 name, salary*1.1 - 如 Alice, 66000.0 返回扫描槽 返回投影槽 上层处理: 如 Result 或 Gather 返回空槽: 扫描结束 结束