Postgres Executor
Tue Feb 7 10:19:24 CST 2023 重构, 不想文章变成无用的流水线
节点
refs
openGauss数据库源码解析系列文章——执行器解析(二
Postgresql查询执行模块README笔记
执行方式分为两大类,一类是 utility 执行,用于ddl等,底层直接对接某个函数,一类是 dml 算子执行,按树的形式执行,这里关注 dml 算子
总的四大类
- 控制节点
- 扫描节点
- 物化节点
- 链接节点
每一个算子分为
- 初始化
对应函数以 ExecInitxxx 构造 state 结构体,执行时候使用,exestate 也是树形结构,和 plantree 一一对应,保存运行时的关键信息 打开表或者其他资源 初始化子节点 初始化表达式上下文 初始化表达式 初始化tuple type 初始化project- 这里其他的都好理解,但是tuple相关的操作每个算子调用的函数看起来大同小异,但是细节作用不一样
- 一般 execstats 和 plan tree 是一一对应的,但是也是有例外的,如分区裁剪的时候,确定不需要的分区,init 的时候直接不生成对应的子树的 state
- 执行 主要的执行逻辑,其中会涉及到表达式的执行
- 清理 清理资源,之前初始化的时候申请的空间
Rescan command to reset a node and make it generate its output sequence over again.
CreateQueryDesc
ExecutorStart
CreateExecutorState
creates per-query context
switch to per-query context to run ExecInitNode
AfterTriggerBeginQuery
ExecInitNode --- recursively scans plan tree
ExecInitNode
recurse into subsidiary nodes
CreateExprContext
creates per-tuple context
ExecInitExpr
ExecutorRun
ExecProcNode --- recursively called in per-query context
ExecEvalExpr --- called in per-tuple context
ResetExprContext --- to free memory
ExecutorFinish
ExecPostprocessPlan --- run any unfinished ModifyTable nodes
AfterTriggerEndQuery
ExecutorEnd
ExecEndNode --- recursively releases resources
FreeExecutorState
frees per-query context and child contexts
FreeQueryDesc
表达式
介绍了pg表达式的具体的运行机制
对比不同数据库中表达式机制的实现
- 表达式按照数组的形式组织的
- 表达式尽量在初始化的时候做完所有的准备工作,执行时候只需要执行即可
- 重点函数和流程
CreateExprContext -- or use GetPerTupleExprContext(estate)
creates per-tuple context
ExecPrepareExpr
temporarily switch to per-query context
run the expression through expression_planner
ExecInitExpr
Repeatedly do:
ExecEvalExprSwitchContext
ExecEvalExpr --- called in per-tuple context
ResetExprContext --- to free memory
FreeExecutorState
frees per-query context, as well as ExprContext
(a separate FreeExprContext call is not necessary)
其中函数
ExecInitExpr 用于初始化表达式,输出为 ExprState
ExecEvalExprSwitchContext 用于 eval 表达式,输入为 ExprState
表达式保存在 ExprState.ExprState 中,使用 evalfunc 执行,evalfunc 可以有解释和编译执行,当前大部分还是使用 ExecInterpExprStillValid 中函数解释执行,
这里一般使用 ExecInterpExpr ,其他的用在特殊场景,不关注
ExecInterpExpr 现在有两种执行逻辑,一种是使用 if 分支执行,一种是直接使用 goto 配合 swith 执行,具体在 EEO_USE_COMPUTED_GOTO 定义中
goto:
大意是先switch 到 分支,执行之后,然后 移动到下一个表达式,再 switch 到具体得分支执行,如此设计可能和 cpu 指令的 pipline 有关
另一种执行方式是使用 llvm 进行编译执行, llvm_compile_expr 会编译表达式且设置 evalfunc = ExecRunCompiledExpr
pg 的 表达式 运行机制有次大改,主要是从 dfs 转换到 bfs ,具体的修改的目的及有点如下, 这里记录下来,熟记理解,然后在加上codegen和向量化,可以唬人
Faster expression evaluation and targetlist projection.
This replaces the old, recursive tree-walk based evaluation, with non-recursive, opcode dispatch based, expression evaluation.
Projection is now implemented as part of expression evaluation.
This both leads to significant performance improvements, and makes future just-in-time compilation of expressions easier.
更快的表达式评估和目标列表投影。
这用非递归的、基于操作码分派的表达式求值取代了旧的、基于递归树遍历的求值。
投影现在作为表达式评估的一部分实现。
这既可以显着提高性能,也可以使将来的表达式即时编译变得更容易。
The speed gains primarily come from:
- non-recursive implementation reduces stack usage / overhead
- simple sub-expressions are implemented with a single jump, without function calls
- sharing some state between different sub-expressions
- reduced amount of indirect/hard to predict memory accesses by laying out operation metadata sequentially; including the avoidance of nearly all of the previously used linked lists
- more code has been moved to expression initialization, avoiding constant re-checks at evaluation time
速度增益主要来自:
- 非递归实现减少堆栈使用/开销
- 简单的子表达式通过单次跳转实现,无需函数调用
- 在不同的子表达式之间共享一些状态
- 通过按顺序排列操作元数据来减少间接/难以预测的内存访问量; 包括避免几乎所有以前使用的链表
- 更多代码已移至表达式初始化,避免在评估时不断重新检查
Future just-in-time compilation (JIT) has become easier, as demonstrated by released patches intended to be merged in a later release, for primarily two reasons: Firstly, due to a stricter split between expression initialization and evaluation, less code has to be handled by the JIT. Secondly, due to the non-recursive nature of the generated "instructions", less performance-critical code-paths can easily be shared between interpreted and compiled evaluation.
未来的即时编译 (JIT) 已经变得更加容易,正如旨在在以后的版本中合并的已发布补丁所证明的那样,主要有两个原因:首先,由于表达式初始化和评估之间的划分更加严格,因此需要更少的代码 由 JIT 处理。 其次,由于生成的“指令”的非递归性质,对性能不太重要的代码路径可以很容易地在解释和编译评估之间共享。
The new framework allows for significant future optimizations. E.g.:
- basic infrastructure for to later reduce the per executor-startup overhead of expression evaluation, by caching state in prepared statements. That'd be helpful in OLTPish scenarios where initialization overhead is measurable.
- optimizing the generated "code". A number of proposals for potential work has already been made.
- optimizing the interpreter. Similarly a number of proposals have been made here too.
新框架允许在未来进行重大优化。 例如。:
- 用于稍后通过在准备好的语句中缓存状态来减少表达式评估的每个执行程序启动开销的基本基础架构。 这在初始化开销是可测量的 OLTP 场景中很有帮助。
- 优化生成的“代码”。 已经提出了一些关于潜在工作的建议。
- 优化解释器。 同样,这里也提出了一些建议。
The move of logic into the expression initialization step leads to some backward-incompatible changes:
- Function permission checks are now done during expression initialization, whereas previously they were done during execution. In edge cases this can lead to errors being raised that previously wouldn't have been, e.g. a NULL array being coerced to a different array type previously didn't perform checks.
- The set of domain constraints to be checked, is now evaluated once during expression initialization, previously it was re-built every time a domain check was evaluated. For normal queries this doesn't change much, but e.g. for plpgsql functions, which caches ExprStates, the old set could stick around longer. The behavior around might still change.
将逻辑转移到表达式初始化步骤会导致一些向后不兼容的更改:
- 函数权限检查现在在表达式初始化期间完成,而以前它们是在执行期间完成的。 在边缘情况下,这可能会导致出现以前不会出现的错误,例如 一个 NULL 数组被强制转换为不同的数组类型,以前没有执行检查。
- 要检查的域约束集现在在表达式初始化期间评估一次,以前每次评估域检查时都会重新构建。 对于普通查询,这并没有太大变化,但是例如 对于缓存 ExprStates 的 plpgsql 函数,旧集合可能会保留更长时间。 周围的行为可能仍然会改变。
select * from a where a = $1 for update
--99.51%--PortalRun
|
--99.51%--PortalRunSelect
|
--99.50%--standard_ExecutorRun
|
--99.47%--ExecLockRows
|
--98.45%--ExecScan
|
|--55.88%--ExecInterpExpr
| |
| |--32.43%--slot_getsomeattrs_int
| | |
| | --29.48%--tts_buffer_heap_getsomeattrs
| |
| --3.54%--ExecEvalParamExtern
|
|--34.60%--SeqNext
| |
| --29.53%--heap_getnextslot
| |
| |--21.52%--heapgettup_pagemode
| | |
| | --12.41%--heapgetpage
| | |
| | |--4.04%--HeapCheckForSerializableConflictOut
| | | |
| | | --2.12%--CheckForSerializableConflictOutNeeded
| | |
| | --1.12%--ReadBufferExtended
| | |
| | --1.01%--ReadBuffer_common
| |
| --3.79%--ExecStoreBufferHeapTuple
|
|--1.70%--heap_getnextslot
|
|--1.65%--MemoryContextReset
|
--0.50%--int4eq
tpch q1 perf
简要分析可以看出,大部分操作在hash相关的运算上,q1也确实有大量聚合运算,这里不知道使用 jit 能否有一定的提升
select
s_acctbal,
s_name,
n_name,
p_partkey,
p_mfgr,
s_address,
s_phone,
s_comment
from
part,
supplier,
partsupp,
nation,
region
where
p_partkey = ps_partkey
and s_suppkey = ps_suppkey
and p_size = 15
and p_type like '%BRASS'
and s_nationkey = n_nationkey
and n_regionkey = r_regionkey
and r_name = 'EUROPE'
and ps_supplycost = (
select
min(ps_supplycost)
from
partsupp,
supplier,
nation,
region
where
p_partkey = ps_partkey
and s_suppkey = ps_suppkey
and s_nationkey = n_nationkey
and n_regionkey = r_regionkey
and r_name = 'EUROPE'
)
order by
s_acctbal desc,
n_name,
s_name,
p_partkey limit 100;
# Total Lost Samples: 0
#
# Samples: 99K of event 'cpu-clock:pppH'
# Event count (approx.): 100006005906
#
# Children Self Command Shared Object Symbol
# ........ ........ ........ ............................................ ..............................................................
#
100.00% 0.00% postgres /usr/lib/x86_64-linux-gnu/libc-2.31.so 0x7fe4b684e083 D [.] putenv
|
---putenv
startup_hacks
PostmasterMain
ServerLoop
BackendStartup
ExitPostmaster
PostgresMain
exec_simple_query
PortalRun
PortalRunSelect
ExecutorRun
standard_ExecutorRun
ExecutePlan
ExecProcNode
ExecProcNodeFirst
ExecSort
ExecProcNode
ExecProcNodeFirst
ExecAgg
|
--99.98%--agg_fill_hash_table
|
|--67.56%--advance_aggregates
| |
| --67.35%--ExecEvalExprSwitchContext
| |
| --66.93%--ExecInterpExpr
| |
| |--26.72%--numeric_mul
| | |
| | |--23.48%--numeric_mul_opt_error
| | | |
| | | |--14.06%--mul_var
| | | | |
| | | | |--2.89%--pfree
| | | | | |
| | | | | --1.63%--AllocSetFree
| | | | | |
| | | | | --0.54%--__nss_database_lookup2
| | | | |
| | | | |--2.43%--alloc_var
| | | | | |
| | | | | --1.82%--palloc
| | | | | |
| | | | | --1.15%--AllocSetAlloc
| | | | |
| | | | |--1.98%--palloc0
| | | | | |
| | | | | --1.14%--AllocSetAlloc
| | | | |
| | | | |--1.03%--round_var
| | | | |
| | | | |--0.85%--__nss_database_lookup2
| | | | |
| | | | --0.51%--strip_var
| | | |
| | | |--3.57%--make_result_opt_error
| | | | |
| | | | --1.80%--palloc
| | | | |
| | | | --1.30%--AllocSetAlloc
| | | |
| | | |--3.34%--free_var
| | | | |
| | | | --2.91%--pfree
| | | | |
| | | | --1.74%--AllocSetFree
| | | | |
| | | | --0.56%--__nss_database_lookup2
| | | |
| | | |--1.10%--__nss_database_lookup2
| | | |
| | | --0.51%--__nss_disable_nscd
| | |
| | |--2.13%--pg_detoast_datum
| | | |
| | | --1.67%--detoast_attr
| | | |
| | | --1.13%--palloc
| | | |
| | | --0.75%--AllocSetAlloc
| | |
| | --0.61%--__nss_database_lookup2
| |
| |--13.61%--numeric_sub
| | |
| | |--10.80%--numeric_sub_opt_error
| | | |
| | | |--4.12%--sub_var
| | | | |
| | | | --3.20%--sub_abs
| | | | |
| | | | --1.26%--palloc
| | | | |
| | | | --0.91%--AllocSetAlloc
| | | |
| | | |--2.76%--make_result_opt_error
| | | | |
| | | | --1.43%--palloc
| | | | |
| | | | --1.10%--AllocSetAlloc
| | | |
| | | |--2.29%--free_var
| | | | |
| | | | --1.92%--pfree
| | | | |
| | | | --1.25%--AllocSetFree
| | | |
| | | --0.76%--__nss_database_lookup2
| | |
| | --2.02%--pg_detoast_datum
| | |
| | --1.55%--detoast_attr
| | |
| | --1.05%--palloc
| | |
| | --0.78%--AllocSetAlloc
| |
| |--13.50%--ExecAggPlainTransByVal
| | |
| | |--10.34%--numeric_avg_accum
| | | |
| | | |--5.83%--do_numeric_accum
| | | | |
| | | | |--2.07%--accum_sum_add
| | | | |
| | | | |--1.95%--__nss_database_lookup2
| | | | |
| | | | --0.71%--__nss_disable_nscd
| | | |
| | | |--3.04%--pg_detoast_datum
| | | | |
| | | | --2.46%--detoast_attr
| | | | |
| | | | --1.62%--palloc
| | | | |
| | | | --1.13%--AllocSetAlloc
| | | |
| | | --0.58%--__nss_database_lookup2
| | |
| | |--1.10%--__nss_database_lookup2
| | |
| | --0.58%--__nss_disable_nscd
| |
| |--6.32%--numeric_add
| | |
| | |--5.07%--numeric_add_opt_error
| | | |
| | | |--2.12%--add_var
| | | | |
| | | | --1.96%--add_abs
| | | | |
| | | | --0.70%--palloc
| | | | |
| | | | --0.50%--AllocSetAlloc
| | | |
| | | |--1.21%--make_result_opt_error
| | | | |
| | | | --0.57%--palloc
| | | |
| | | --0.97%--free_var
| | | |
| | | --0.86%--pfree
| | | |
| | | --0.50%--AllocSetFree
| | |
| | --0.91%--pg_detoast_datum
| | |
| | --0.68%--detoast_attr
| |
| |--1.65%--__nss_database_lookup2
| |
| --0.54%--__nss_disable_nscd
|
|--18.34%--lookup_hash_entries
| |
| |--16.52%--LookupTupleHashEntry
| | |
| | |--8.86%--LookupTupleHashEntry_internal
| | | |
| | | --8.70%--tuplehash_insert_hash
| | | |
| | | --8.52%--tuplehash_insert_hash_internal
| | | |
| | | --7.82%--TupleHashTableMatch
| | | |
| | | |--6.93%--ExecQualAndReset
| | | | |
| | | | --6.54%--ExecQual
| | | | |
| | | | --6.40%--ExecEvalExprSwitchContext
| | | | |
| | | | --6.00%--ExecInterpExpr
| | | | |
| | | | |--2.58%--bpchareq
| | | | | |
| | | | | |--0.83%--__nss_database_lookup2
| | | | | |
| | | | | --0.79%--bcTruelen
| | | | |
| | | | --1.31%--slot_getsomeattrs
| | | | |
| | | | --1.15%--slot_getsomeattrs_int
| | | | |
| | | | --0.94%--tts_minimal_getsomeattrs
| | | | |
| | | | --0.80%--slot_deform_heap_tuple
| | | |
| | | --0.58%--ExecStoreMinimalTuple
| | |
| | --6.96%--TupleHashTableHash_internal
| | |
| | |--4.98%--FunctionCall1Coll
| | | |
| | | --4.42%--hashbpchar
| | | |
| | | |--2.26%--hash_any
| | | | |
| | | | --1.97%--hash_bytes
| | | | |
| | | | --0.73%--__nss_database_lookup2
| | | |
| | | --0.73%--__nss_database_lookup2
| | |
| | --0.52%--__nss_database_lookup2
| |
| --1.22%--prepare_hash_slot
| |
| --0.53%--ExecClearTuple
|
|--12.30%--fetch_input_tuple
| |
| --12.10%--ExecProcNode
| |
| --11.93%--ExecSeqScan
| |
| --11.74%--ExecScan
| |
| |--5.89%--ExecQual
| | |
| | --5.72%--ExecEvalExprSwitchContext
| | |
| | --5.29%--ExecInterpExpr
| | |
| | |--3.57%--slot_getsomeattrs
| | | |
| | | --3.42%--slot_getsomeattrs_int
| | | |
| | | --3.13%--tts_buffer_heap_getsomeattrs
| | | |
| | | --2.97%--slot_deform_heap_tuple
| | | |
| | | --0.67%--__nss_database_lookup2
| | |
| | --0.59%--date_le_timestamp
| |
| --5.34%--ExecScanFetch
| |
| --5.18%--SeqNext
| |
| --4.87%--table_scan_getnextslot
| |
| --4.56%--heap_getnextslot
| |
| --3.82%--heapgettup_pagemode
| |
| --2.50%--heapgetpage
| |
| --1.56%--ReadBufferExtended
| |
| --1.55%--ReadBuffer_common
| |
| |--0.79%--smgrread
| | |
| | --0.78%--mdread
| | |
| | --0.75%--__libc_pread64
| | |
| | --0.74%--entry_SYSCALL_64_after_hwframe
| | do_syscall_64
| | |
| | --0.71%--ksys_pread64
| | |
| | --0.69%--vfs_read
| | |
| | --0.67%--new_sync_read
| | |
| | --0.66%--filemap_read
| | |
| | --0.52%--copy_page_to_iter
| | |
| | --0.51%--copyout
| | copy_user_enhanced_fast_string
| |
| --0.65%--BufferAlloc
|
--1.17%--MemoryContextReset
|
--0.85%--MemoryContextResetOnly
|
--0.57%--AllocSetReset
Cluster 算子
ClusterGather
ExecInitClusterGather() {
gatherstate->ps.ExecProcNode = ExecClusterGather;
// 主要是构造 PlanState
outerPlanState(gatherstate) = ExecStartClusterPlan() { //分发执行计划 {}
// 只是explan 的话,并不需要发送执行计划。直接这里返回即可
if (EXEC_FLAG_EXPLAIN_ONLY)
return ExecInitNode(plan, estate, eflags);
// 执行计划需要传输到其他节点,需要进行序列化,当前序列化操作使用的是查表的方法,可以不关注先后顺序,使用flag遍历查找,主要方法在mem_toc.h中
// 针对不同的类型的数据的序列化和反序列化也是后来实现的
StartRemotePlan() {
// 发送执行计划到远程节点
foreach(lc, state->cur_handle->handles) {
PQsendPlan()
}
PG_TRY() {
// 如果执行计划中由reduce 节点,则启动 StartDynamicReduceWorker
if (context->have_reduce && context->start_self_reduce) {
StartDynamicReduceWorker();
}
// 并且接收dn的消息,查看dn状态是否正常
foreach(lc, list_conn) {
PQgetResult(conn);
}
}
// 作用未知
if (context->have_reduce) {
StartRemoteReduceGroup();
}
}
// 初始化下层节点,这里cn上其实应该只有一个 cluster gather 及之上的算子起作用才对,但是实际上下层节点中如果存在reduce 节点,且 如果 优化中选择再 cn 进行 reduce ,则下层还是会进行一些reduce 相关得操作,
// 包括上面reduce 得一些设置
return ExecInitNode(plan, estate, eflags);
}
}
ExecClusterGather() {
// 获得下层节点的数据,这里分为两个不同的部分,先尝试从remote获得的数据,再尝试local的执行计划
while(node->remote_running != NIL || node->local_end == false) {
if(node->remote_running != NIL && PQNListExecFinish(node->remote_running, NULL, &node->hook_funcs, blocking)) {
// PQNListExecFinish 负责获取dn上得数据,这里 remote_running 记录的是运行中的dn,如果dn 运行结束,则加入到 last_run_end ,后续会从 remote_running 中剔除 运行结束的dn
return node->ps.ps_ResultTupleSlot;
}
// 获取cn上的数据
if(node->local_end == false) {
slot = ExecProcNode(outerPlanState(node));
}
}
}
ExecFinishClusterGather() {
// 清理工作
}
ClusterMergeGather
功能和 ClusterGather 类似,但是会对 dn 收集的 数据进行排序, 在优化阶段中,如果语句最终的输出需要排序,则在构造顶层节点的时候,会生成 sort 算子,然后在生成 ClusterMergeGather 算子 ClusterMergeGather plan 中,dn的输出一定是有序的
ExecInitClusterMergeGather() {
ps->ps.ExecProcNode = ExecClusterMergeGather;
// 使用 ExecStartClusterPlan 初始化的 planstat
outerPlanState(ps) = ExecStartClusterPlan();
// 如果是需要排序输出,则在初始化 slot 的时候,需要使用 sort tuple 相关的函数,具体参考 tuplesort_begin_heap
for(i=0;i<node->numCols;++i) {
PrepareSortSupportFromOrderingOp();
}
}
ExecClusterMergeGather() {
// 获得元组,然后进行排序,这里也区分cn和dn混合使用 ExecProcNode 和 cmg_get_remote_slot
// 其中 cmg_get_remote_slot 实际调用的是 PQNOneExecFinish
// 之后使用 binaryheap***进行排序
}
ExecFinishClusterMergeGather() {
// 清理工作
}
ClusterReduce
recuce 用于在cn 和 dn, dn 和 dn 之间传递数据,并且还会启动 一个 新进程负责此工作,这里简单梳理
typedef enum ReduceType
{
RT_NOTHING = 1, // 不执行reduce
RT_NORMAL, // 常规reduce
RT_REDUCE_FIRST, // 优点向上发送数据而不是向其他节点
RT_REDUCE_FIRST_EPQ, // EPQ EvalPlanQual,子链接 中的子查询
RT_PARALLEL_REDUCE_FIRST, //
RT_ADVANCE, // 提前进行reduce
RT_ADVANCE_PARALLEL,
RT_MERGE // reduce 要求有序
}ReduceType;
#define CRF_FETCH_LOCAL_FIRST 0x0001 /* fetch all local first */ //
#define CRF_DISK_UNNECESSARY 0x0002 /* don't need cache on disk */
#define CRF_DISK_ALWAYS 0x0004 /* always cache tuples on disk */
#define CRF_MAYBE_EPQ 0x0008 /* maybe execute in EPQ */
ExecInitClusterReduce() {
crstate->ps.ExecProcNode = ExecDefaultClusterReduce;
// 如果有 PGXCNodeOid ,则设置 模式为 RT_NOTHING
if (list_member_oid(node->reduce_oids, PGXCNodeOid) == false)
crstate->reduce_method = (uint8)RT_NOTHING;
// 如果要求排序,则 设置 为 RT_MERGE,numCols 大致指得是需要排序的列得数量
else if (node->numCols > 0)
crstate->reduce_method = (uint8)RT_MERGE;
// 判断需要优先执行reduce
else if (node->reduce_flags & (CRF_FETCH_LOCAL_FIRST|CRF_DISK_ALWAYS) ||
crstate->eflags != 0)
crstate->reduce_method = (uint8)RT_REDUCE_FIRST;
else
crstate->reduce_method = (uint8)RT_NORMAL;
if ((eflags & EXEC_FLAG_IN_EPQ) &&
crstate->reduce_method != RT_NOTHING)
crstate->ps.ExecProcNode = ExecEPQDefaultClusterReduce;
}
// ExecProcNodeReal 函数指针, ExecDefaultClusterReduce 和 ExecEPQDefaultClusterReduce 都只是一个warpper, 调用的时候,会按照不同reduce 策略,调用不同的 init 函数, 对函数指针赋值
// 默认的reduce 函数
ExecDefaultClusterReduce() {
ClusterReduceState *crstate = castNode(ClusterReduceState, pstate);
if (crstate->private_state != NULL)
return pstate->ExecProcNodeReal(pstate);
InitReduceMethod(crstate);
return pstate->ExecProcNodeReal(pstate);
}
// EPQ reduce
ExecEPQDefaultClusterReduce() {
switch(origin->reduce_method) {
case RT_REDUCE_FIRST:
InitEPQReduceFirst(node, origin);
case RT_MERGE:
InitEPQMergeReduce(node, origin);
case RT_ADVANCE:
InitEPQAdvanceReduce(node, origin);
}
return pstate->ExecProcNodeReal(pstate);
}
void InitReduceMethod(ClusterReduceState *crstate) {
switch(crstate->reduce_method) {
case RT_NOTHING:
// 设置 ExecProcNodeReal 为 ExecNothingReduce, 内部不具体得任务
ExecSetExecProcNode(&crstate->ps, ExecNothingReduce);
case RT_NORMAL:
InitNormalReduce(crstate);
case RT_REDUCE_FIRST:
InitReduceFirst(crstate);
case RT_MERGE:
InitMergeReduce(crstate);
}
}
ExecInitReduceScan
Read other posts