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
)。
/* ----------------------------------------------------------------
* 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
计划节点(因为无索引或小表全扫描),其中:
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
/* ----------------
* 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
用例:
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
/* ----------------------------------------------------------------
* 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
用例:
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
/*
* 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
执行效率的关键。
/* 函数定义: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;
}