当前位置: 首页 > news >正文

【PostgreSQL内核学习 —— (SeqScan算子)】

SeqScan算子

  • 引言
    • 基于PG18进一步学习
  • 函数源码解读
    • ExecInitSeqScan
      • 通过具体SQL用例解释每一行
    • ExecInitResultTypeTL
      • 通过具体SQL用例解释每一行
    • ExecSeqScan/ExecScan
      • 通过具体SQL用例解释每一行
    • ExecScanFetch
      • 通过具体SQL用例解释执行过程
    • SeqNext

声明:本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。
本文主要参考了 postgresql-18 beta2 的开源代码和《PostgresSQL数据库内核分析》一书

引言

  有关SeqScan算子的相关描述可以参考【OpenGauss源码学习 —— 执行算子(SeqScan算子)】这篇文章。这篇博客详细解读了OpenGauss数据库(基于PostgreSQL)中执行算子的核心,特别是扫描算子中的SeqScan算子,用于对基础表进行顺序扫描。
  文章首先概述了执行算子的分类(如控制、扫描、物化、连接算子),然后聚焦扫描算子类型,并以SeqScan为例,通过代码调试和源码分析解释了其主要函数(如ExecInitSeqScan用于初始化状态、ExecSeqScan用于迭代获取元组),包括初始化扫描关系、处理分区表、采样扫描等细节。最后总结了SeqScan的完整执行流程,从查询解析到结束扫描,强调其在查询优化中的作用,并附带结构体定义和调试截图,帮助读者理解数据库执行机制。

基于PG18进一步学习

  虽然OpenGaussSeqScan算子源码提供了良好的入门基础,但PostgreSQL作为其上游项目,在版本迭代中可能引入了性能优化、并行扫描增强或新存储格式支持(如针对PG18的潜在改进)。为了更全面掌握SeqScan在现代数据库环境中的实现细节,我们可以转向PostgreSQL 18的源码学习,探索其在executor/nodeSeqscan.c等文件中的变化,这不仅能对比OpenGauss的差异,还能揭示如何在高并发场景下高效处理数据扫描。

函数源码解读

ExecInitSeqScan

  函数 ExecInitSeqScan 用于初始化顺序扫描算子(SeqScan)。

/* ----------------------------------------------------------------*		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用例:

SELECT * FROM employees WHERE salary > 50000;

  假设employees表有列id (INT)、name (VARCHAR)、salary (INT),已插入几行数据(如(1, 'Alice', 60000)(2, 'Bob', 40000))。这个查询会生成一个SeqScan计划节点(因为无索引或小表全扫描),其中:

  • nodeSeqScan计划节点,scanrelid=1(指向range tableemployees表),qualsalary > 50000的表达式树。
  • estate:全局执行状态,包含内存上下文、快照等。
  • eflags:执行标志,如EXEC_FLAG_REWIND(如果需要支持重扫描)。

  在查询执行器的初始化阶段(ExecInitNode递归调用时),ExecInitSeqScan会被调用。以下按代码顺序解释每一行(或逻辑块)在该用例中具体做了什么:

  1. 函数签名:SeqScanState * ExecInitSeqScan(SeqScan *node, EState *estate, int eflags)
    在用例中,这被上层ExecInitNode调用,传入SeqScan节点(从优化器生成的计划树中获取)、当前查询的EState(包含employees表的范围表信息)和eflags(假设为0,表示无特殊标志)。函数负责将静态计划转换为可执行的运行时状态。

  2. SeqScanState *scanstate;
    声明一个局部指针,用于指向新创建的SeqScanState结构体。在用例中,这将成为存储扫描employees表状态的容器(如当前关系、元组槽等)。

  3. Assert(outerPlan(node) == NULL); Assert(innerPlan(node) == NULL);
    检查SeqScan无子计划(outer/inner为空)。在用例中,SELECT * FROM employees ...的计划是简单的SeqScan,无Join或其他子树,所以断言通过。如果有子查询,会报错(但本例无)。

  4. scanstate = makeNode(SeqScanState);
    使用makeNode宏分配内存,创建SeqScanState结构体(继承ScanState)。在用例中,这分配约数百字节内存(取决于平台),初始化为零值,准备存储扫描employees的状态。

  5. scanstate->ss.ps.plan = (Plan *) node;
    将输入的SeqScan计划节点关联到状态的ps.plan字段。在用例中,这链接了计划中的scanrelid=1qual=(salary > 50000),确保执行时能访问计划细节。

  6. scanstate->ss.ps.state = estate;
    关联全局EState。在用例中,这让scanstate能访问查询的快照(用于可见性检查)、内存上下文(用于表达式评估)和范围表(rtable[1]指向employees)。

  7. scanstate->ss.ps.ExecProcNode = ExecSeqScan;
    设置执行回调为ExecSeqScan。在用例中,后续每次调用ExecProcNode(scanstate)时,会跳转到ExecSeqScan,开始逐行扫描employees并过滤salary > 50000

  8. ExecAssignExprContext(estate, &scanstate->ss.ps);
    为节点创建专用ExprContext(表达式上下文)。在用例中,这分配一个短生命周期内存上下文,用于评估qualsalary > 50000),避免全局污染;上下文包括ecxt_scantuple(稍后用于填充当前元组)。

  9. scanstate->ss.ss_currentRelation = ExecOpenScanRelation(estate, node->scan.scanrelid, eflags);
    调用ExecOpenScanRelation打开表关系。在用例中,scanrelid=1对应employees表:获取Relation对象(包含元数据如TupleDesc)、加AccessShareLock锁(防止DDL),并根据eflags设置共享模式。结果:ss_currentRelation指向employees的打开Relation

  10. ExecInitScanTupleSlot(estate, &scanstate->ss, RelationGetDescr(scanstate->ss.ss_currentRelation), table_slot_callbacks(scanstate->ss.ss_currentRelation));
    初始化扫描槽ss_ScanTupleSlot。在用例中,使用employeesTupleDesc3列:id, name, salary)创建虚拟元组槽(TTSOpsVirtual),并绑定表回调(如heap_getnext)。这准备了一个“容器”,用于存储从employees读取的元组(如{1, 'Alice', 60000})。

  11. ExecInitResultTypeTL(&scanstate->ss.ps);
    基于计划的目标列表(targetlist=*,全列)初始化结果类型。在用例中,设置ps.resulttypeemployeesTupleDesc,确保上层(如Result节点)知道输出格式是3INT/VARCHAR/INT

  12. ExecAssignScanProjectionInfo(&scanstate->ss);
    设置投影信息(ps_ProjInfo)。在用例中,因为targetlist是全列(无计算),投影简单(直接返回扫描元组);如果有SELECT name, salary,会编译投影表达式。

  13. scanstate->ss.ps.qual = ExecInitQual(node->scan.plan.qual, (PlanState *) scanstate);
    编译限定条件。在用例中,qualsalary > 50000Expr树:ExecInitQual遍历并初始化为可执行形式(链接到ExprContext),存储到ps.qual。后续扫描时,每行元组会用此过滤(Bob40000被丢弃)。

  14. return scanstate;
    返回初始化的状态。在用例中,上层ExecInitNode接收此指针,插入计划树。查询执行时,从此状态开始扫描employees,逐行检查qual,只返回Alice的行。

  通过这个用例,整个函数将抽象的SeqScan计划转化为可执行状态:打开表、准备槽和上下文、编译过滤逻辑,确保高效逐元组扫描(符合“一次一元组”思想)。如果运行EXPLAIN ANALYZE,会看到SeqScan的成本和行数估计基于此初始化。

ExecInitResultTypeTL

/* ----------------*		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);elselen = 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用例:

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)。
  • planstateSeqScan的运行时状态(PlanState),其plan指向SeqScan节点。
  • 执行上下文:在ExecInitSeqScan中调用ExecInitResultTypeTL,用于设置输出格式(2列:VARCHARNUMERIC)。

  在查询执行器的初始化阶段(ExecutorStart调用ExecInitNode递归时),这些函数会被调用。以下按代码顺序解释每一行(或逻辑块)在该用例中具体做了什么。注意,这些函数是辅助函数,主要在节点初始化时运行,确保上层(如Result节点)知道输出元组的结构。


ExecInitResultTypeTL 函数部分

  1. 函数签名:void ExecInitResultTypeTL(PlanState *planstate)
    在用例中,这被ExecInitSeqScan调用,传入SeqScanplanstate(包含targetlist)。函数负责基于SELECT子句(name, bonus)初始化结果描述符,确保扫描节点输出的元组格式匹配查询期望(2列输出)。

  2. TupleDesc tupDesc = ExecTypeFromTL(planstate->plan->targetlist);
    调用ExecTypeFromTL生成TupleDesc。在用例中,从targetlist2TargetEntrynamebonus)提取类型:name→VARCHAR (OID=1043),bonus→NUMERIC (OID=1700,从expr "salary * 1.1"推导)。结果tupDesc描述一个2列元组:列1 VARCHAR(可变长),列2 NUMERIC(精确小数)。

  3. planstate->ps_ResultTupleDesc = tupDesc;
    tupDesc赋值给planstate->ps_ResultTupleDesc。在用例中,这设置SeqScan的输出描述符为2列结构。上层执行器(如门户或客户端)据此分配内存、解析结果(如psql显示"name | bonus"列头)。如果后续投影,这确保了bonus列的类型正确(NUMERIC,支持小数)。


ExecTypeFromTL 函数部分

  1. 函数签名:TupleDesc ExecTypeFromTL(List *targetList)
    入口函数,接收targetlist(计划Expr列表)。在用例中,传入SeqScantargetlist2条目),调用内部函数生成描述符。注释强调:必须是计划tlist(非ExprState),因为ExprState是运行时编译版。

  2. return ExecTypeFromTLInternal(targetList, false);
    调用内部函数,skipjunk=false(包含所有列,包括潜在junk)。在用例中,无junk列,所以直接构建完整描述符。返回的TupleDesc用于定义输出:2列,包含列名、类型、排序规则(默认C collation)。


ExecTypeFromTLInternal 函数部分

  1. 函数签名:static TupleDesc ExecTypeFromTLInternal(List *targetList, bool skipjunk)
    核心静态函数,构建TupleDesc。在用例中,skipjunk=falsetargetList长度=2namebonus)。

  2. TupleDesc typeInfo; ListCell *l; int len; int cur_resno = 1;
    声明变量。在用例中,len=2(总长度),cur_resno1开始。

  3. if (skipjunk) len = ExecCleanTargetListLength(targetList); else len = ExecTargetListLength(targetList);
    计算len。在用例中,skipjunk=false,所以ExecTargetListLength返回2name + bonus)。如果有junk(如ORDER BY id),len=2junk不计入输出,但这里无)。

  4. typeInfo = CreateTemplateTupleDesc(len);
    创建len=2的空TupleDesc模板。在用例中,这分配一个描述符框架:natts=2attrs数组准备填充(每个attr包含typidtypmod等)。

  5. foreach(l, targetList) { ... }
    遍历targetlist2次循环)。在用例中:

    • 第一次:tle = name条目 (resname="name", expr=Var(name))。
    • 第二次:tle = bonus条目 (resname="bonus", expr=Op(salary * 1.1))。
  6. TargetEntry *tle = lfirst(l);
    获取当前tle。在用例中,tle包含resnameexpr(类型源)。

  7. if (skipjunk && tle->resjunk) continue;
    检查junk。在用例中,skipjunk=false,且无resjunk=truetle,所以不跳过。

  8. 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推导)。
  9. TupleDescInitEntryCollation(typeInfo, cur_resno, exprCollation((Node *) tle->expr));
    设置排序规则。在用例中,默认C collationexprCollationVar/Op获取),确保name的字符串比较一致。

  10. cur_resno++;
    递增列号。在用例中,从1→2,循环结束。

  11. return typeInfo;
    返回完成的TupleDesc。在用例中,上层ExecInitResultTypeTL接收此2列描述符。执行时,SeqScan输出元组(如{'Alice', 66000.0})匹配此格式:过滤后Alice行通过,Bob (40000<50000)qual丢弃。客户端据此解析结果,显示"Alice | 66000"。

  通过这个用例,这些函数将SQLSELECT子句转化为运行时元组格式:从抽象targetlist生成具体TupleDesc,确保高效内存分配和类型安全(例如bonusNUMERIC避免INT溢出)。在PG18中,这支持更复杂的表达式(如窗口函数),但核心逻辑不变。 如果运行EXPLAIN,计划会显示输出类型基于此描述符。

ExecSeqScan/ExecScan

/* ----------------------------------------------------------------*		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);/* 否则直接返回空槽 */elsereturn 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) */elseInstrCountFiltered1(node, 1);/* 元组不通过限定条件,重置表达式上下文,释放内存,继续循环 */ResetExprContext(econtext);}
}

通过具体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计划节点,其中:

  • nodeSeqScanState,包含ss_currentRelation(指向employees表)、ps.qualsalary > 50000)、ps_ProjInfo(投影namesalary*1.1)。
  • accessMtdSeqNext(获取下一个元组)。
  • recheckMtdSeqRecheck(检查元组是否满足访问方法内部条件,如索引约束,这里无索引)。
  • 执行上下文:在ExecutorRun阶段,ExecSeqScan调用ExecScan,逐行扫描employees表,过滤salary > 50000,投影为(name, bonus)。

  以下按代码顺序解释每一行(或逻辑块)在该用例中具体做了什么。ExecScan 是扫描算子的通用执行函数,由ExecSeqScan调用,处理元组获取、过滤和投影。

  1. 函数签名:TupleTableSlot * ExecScan(ScanState *node, ExecScanAccessMtd accessMtd, ExecScanRecheckMtd recheckMtd)
    在用例中,ExecSeqScan调用ExecScan,传入SeqScanStateSeqNext(访问方法)、SeqRecheck(重检查方法)。函数负责逐行扫描employees表,检查salary > 50000,输出{name, bonus}元组。

  2. ExprContext *econtext; ExprState *qual; ProjectionInfo *projInfo;
    声明变量。在用例中,econtext用于存储当前元组和表达式状态,qualsalary > 50000的编译表达式,projInfo定义了namesalary*1.1的投影逻辑。

  3. qual = node->ps.qual;
    获取限定条件。在用例中,qualsalary > 50000ExprState(由ExecInitQualExecInitSeqScan中编译),用于过滤元组。

  4. projInfo = node->ps.ps_ProjInfo;
    获取投影信息。在用例中,projInfo包含两个TargetEntrynamesalary*1.1),由ExecAssignScanProjectionInfo初始化,确保输出{name, bonus}。

  5. econtext = node->ps.ps_ExprContext;
    获取表达式上下文。在用例中,econtext包含ecxt_scantuple(稍后填充元组)和内存上下文(短生命周期,存储表达式计算结果)。

  6. if (!qual && !projInfo) { ... }
    检查是否无限定条件和投影。在用例中,存在qual(salary > 50000)projInfo(name, bonus),所以跳过此分支。如果是SELECT * FROM employees(无WHERE和投影),会直接调用ExecScanFetch返回原始元组。

  7. ResetExprContext(econtext); (外层)
    重置表达式上下文,释放上一元组的内存。在用例中,首次循环时清空内存上下文,确保干净环境;后续循环释放前一元组的计算结果(如salary*1.1的中间值)。

  8. for (;;) { ... }
    进入无限循环,获取元组直到符合条件或无元组。在用例中,循环扫描employees表,直到处理完所有行或中断。

  9. TupleTableSlot *slot;
    声明元组槽。在用例中,slot将存储从SeqNext获取的元组(如{1, 'Alice', 60000})。

  10. slot = ExecScanFetch(node, accessMtd, recheckMtd);
    调用ExecScanFetch通过SeqNext获取元组。在用例中,第一次循环调用SeqNext,从employees表获取{1, 'Alice', 60000},存储到ss_ScanTupleSlot。如果EvalPlanQual(并发更新检查)生效,recheckMtd验证元组。

  11. if (TupIsNull(slot)) { ... }
    检查是否返回空槽。在用例中,若扫描到表末尾(如处理完AliceBob后),返回空槽。如果有projInfo(本例有),清空投影槽(bonus格式为NUMERIC);否则返回扫描槽。

  12. econtext->ecxt_scantuple = slot;
    将当前元组放入表达式上下文。在用例中,将{1, 'Alice', 60000}的槽赋给ecxt_scantuple,供qual(salary > 50000)projInfo(salary*1.1)使用。

  13. if (qual == NULL || ExecQual(qual, econtext)) { ... }
    检查元组是否满足限定条件。在用例中,qual非空,ExecQual评估salary > 50000

    • Alice: 60000 > 50000true,通过。
    • Bob: 40000 < 50000false,跳到else分支。如果qual为空(如SELECT * FROM employees),直接通过。
  14. if (projInfo) { return ExecProject(projInfo); }
    如果有投影,执行ExecProject。在用例中,Alice通过过滤,projInfo{1, 'Alice', 60000}投影为{'Alice', 66000.0}salary*1.1NUMERIC),存储到投影槽并返回。

  15. else { return slot; }
    无投影时返回原始槽。在用例中不适用(因有projInfo)。如果查询是SELECT * FROM employees WHERE salary > 50000,返回{1, 'Alice', 60000}的扫描槽。

  16. else InstrCountFiltered1(node, 1);
    元组不通过qual,记录过滤计数。在用例中,Bob40000不满足,计数+1EXPLAIN ANALYZE显示“Rows Removed by Filter: 1”)。

  17. ResetExprContext(econtext); (内层)
    元组不通过时,重置上下文,释放内存。在用例中,Bob的元组被丢弃,清空ecxt_scantuple和计算内存,准备下一循环(获取Bob或表末尾)。

  通过这个用例,ExecScan实现“一次一元组”思想:逐行从employees表获取元组,过滤salary > 50000,投影为{name, bonus},最终返回{'Alice', 66000.0},表末尾返回空槽。PG18的优化可能增强了并行或表AM支持,但逻辑一致。如果运行EXPLAIN ANALYZE,会看到SeqScan的过滤和投影统计。

ExecScanFetch

/** 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 的执行过程。

执行上下文

  • nodeSeqScanState,包含 ss_currentRelation(指向 employees 表)、qualsalary > 50000)。
  • accessMtdSeqNext,从表获取元组。
  • recheckMtdSeqRecheck,通常为空操作(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_activeNULL,进入 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. 获取 scanrelidIndex scanrelid = ((Scan *) node->ps.plan)->scanrelid;获取范围表索引,SeqScan1scanrelid = 1
5. 检查 scanrelidif (scanrelid == 0)检查是否 ForeignScan/CustomScan,本例为 SeqScan,跳过scanrelid = 1,进入下一个分支
6. 检查 relsubs_doneelse if (epqstate->relsubs_done[scanrelid - 1])检查是否已返回 EPQ 元组,首次调用为 falserelsubs_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))调用 SeqRecheckSeqScan 无特殊约束,返回 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) 被优先处理,满足 ExecScanqual55000 > 50000),返回 {'Alice', 55000}。第二次调用因 relsubs_done 返回空槽,恢复正常扫描。
  • 正常扫描:获取 Bob 元组 {2, 'Bob', 40000},但 ExecScanqual 过滤掉(40000 < 50000)。表末尾返回空槽。
  • “一次一元组”:每次调用返回一个元组槽(或空槽),符合 PostgreSQL 执行器设计。
  • PG18 特性EPQ 逻辑可能优化了并发性能(如更高效的行标记处理),但核心流程不变。

什么是 EPQ 场景?

  • EPQEvalPlanQual,计划评估限定)场景 是 PostgreSQL 中用于处理并发更新冲突的一种机制,出现在事务执行查询(如 SELECT ... FOR UPDATE)时,另一事务同时修改了相同的数据行,导致元组版本不一致EPQ 确保查询看到一致的数据,符合事务隔离级别(如可重复读或序列化),通过在扫描过程中检查和替换受影响的元组来解决冲突。

  通过这个用例,ExecScanFetch 展示了其在 EPQ 和正常扫描中的双重角色:优先处理替换元组,确保并发一致性;否则通过 SeqNext 获取表数据,支持“一次一元组”。如果需要进一步分析 SeqNext 或生成流程图,请告诉我!

SeqNext

  SeqNextPostgreSQL SeqScan 算子的核心工作函数,负责从指定表中顺序获取下一个元组并存储到元组槽(TupleTableSlot)中,返回给上层 ExecScan 处理。其主要步骤包括:从 SeqScanState 获取扫描描述符(TableScanDesc)、执行状态(EState)、扫描方向和元组槽;
  如果描述符未初始化(非并行扫描或串行执行),调用 table_beginscan 打开表扫描;通过 table_scan_getnextslot 获取下一个元组,存储到槽中并返回,若无元组则返回空指针。
  函数支持正向或反向扫描,兼容表访问方法接口,并在并发环境中通过快照(es_snapshot)确保 MVCC 一致性。SeqNext 实现“一次一元组”思想,是 SeqScan 执行效率的关键。

/* 函数定义: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
返回空槽: 扫描结束
结束

文章转载自:

http://oyOUAbxQ.LqjLg.cn
http://KX0Y1DCG.LqjLg.cn
http://LW7ftd33.LqjLg.cn
http://T6wjlakk.LqjLg.cn
http://jh3BWHqq.LqjLg.cn
http://cFylPARv.LqjLg.cn
http://l86FywXM.LqjLg.cn
http://nVSh1Hsi.LqjLg.cn
http://qx6JjZUp.LqjLg.cn
http://3d7o0tle.LqjLg.cn
http://Zs7XFTqO.LqjLg.cn
http://hfG5ICf6.LqjLg.cn
http://xLtMqMoj.LqjLg.cn
http://vOIBQ3BW.LqjLg.cn
http://ffckza5V.LqjLg.cn
http://tMaUfFD4.LqjLg.cn
http://EPrY2I0U.LqjLg.cn
http://8U4OEU1Z.LqjLg.cn
http://Y0UKkn8l.LqjLg.cn
http://ZFrrF3Nu.LqjLg.cn
http://dSU4SwTU.LqjLg.cn
http://Y5DUimve.LqjLg.cn
http://K7Pm9PNn.LqjLg.cn
http://y06uTjf0.LqjLg.cn
http://Y5yV31HJ.LqjLg.cn
http://IiezrKol.LqjLg.cn
http://snXFaGV7.LqjLg.cn
http://W4jagyAO.LqjLg.cn
http://wycnlY7C.LqjLg.cn
http://0hxD5NO4.LqjLg.cn
http://www.dtcms.com/a/379346.html

相关文章:

  • 资源图分配算法
  • SpringBoot 中单独一个类中运行main方法报错:找不到或无法加载主类
  • 2025全球VC均热板竞争格局与核心供应链分析
  • 用“折叠与展开”动态管理超长上下文:一种 Token 高效的外部存储操作机制
  • 深度解析指纹模块选型与落地实践
  • 从用户体验到交易闭环的全程保障!互联网行业可观测性体系建设白皮书发布
  • grafana启用未签名插件
  • MySQL 数据类型与运算符详解
  • 编程实战:类C语法的编译型脚本解释器(五)变量表
  • 原生js拖拽
  • 数据结构--Map和Set
  • P1122 最大子树和
  • 【3DV 进阶-3】Hunyuan3D2.1 训练代码详细理解之-Flow matching 训练 loss 详解
  • Python写算法基础
  • 数据结构 优先级队列(堆)
  • FunASR GPU 环境 Docker 构建完整教程(基于 CUDA 11.8)
  • 探讨:线程循环与激活(C++11)
  • 拆解格行随身WiFi多网协同模块:智能切网+马维尔芯片,如何实现5秒跨网?
  • 游泳溺水检测识别数据集:8k图像,2类,yolo标注
  • ARM裸机开发:链接脚本、进阶Makefile(bsp)、编译过程、beep实验
  • 开始 ComfyUI 的 AI 绘图之旅-Flux.1图生图之局部重绘(Inpaint)和扩图(Outpaint)(九)
  • 2025.9.11day1QT
  • ubuntu24.04+5070ti训练yolo模型(1)
  • ubuntu2204配置网桥
  • 【VLMs篇】07:Open-Qwen2VL:在学术资源上对完全开放的多模态大语言模型进行计算高效的预训练
  • Ubuntu24.04安装 Fcitx5并设置五笔字型的方法
  • 格式塔是什么?带你理解信息组织与用户体验优化
  • AVLTree(C++ Version)
  • You Only Look Once
  • 虚拟机上部署服务后ssh无法连接