声明:本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。
本文主要参考了 OpenGauss1.1.0 的开源代码和《OpenGauss数据库源码解析》和《PostgresSQL数据库内核分析》一书
OpenGauss 在 PortalRun 函数中会实际执行相关的 DML 查询,对数据进行计算和处理。在执行过程中,所有执行算子分为两大类:行存储算子和向量化算子。这两类算子分别对应行存储执行引擎和向量化执行引擎。行存储执行引擎的上层入口是 ExecutePlan 函数,向量化执行引擎的上层人口是 ExecuteVectorizedPlan 函数。其中向量化引擎是针对列存储表的执行引擎。如果存在行存储表和列存储表的混合计算,那么行存储执行引擎和向量化执行引擎直接可以通过 VecToRow 和 RowToVec 算子进行相互转换。行存储算子执行入口函数的命名规则一般为 “Exec + 算子名” 的形式,向量化算子执行入口函数的命名规则一般为 “ExecVee十算子名” 的形式,通过这样的命名规则,可以快速地找到对应算子的函数入口。
在【OpenGauss源码学习 —— (VecToRow)算子】一文中,我们学习了VecToRow 算子的执行流程,本文则来补充学习一下 RowToVec 算子的执行流程。
RowToVec 算子的主要功能和作用是将行存储表中的数据按列存储格式重新组织和转换,以便能够有效地交给向量化执行引擎处理。它负责将行数据逐行提取并转化成列向量,使得数据库系统能够在混合计算环境中高效地处理包括行存储表和列存储表的数据,从而实现更快速、更优化的查询和数据处理操作。这个过程是在混合存储环境中实现无缝数据转换的关键部分,有助于提高数据库查询性能和整体系统效率。
RowToVec 算子的执行流程通常包括以下步骤:首先,RowToVec 算子从行存储表中获取一批行数据,这些行数据以行的形式存储在内存中。然后,它会逐行地将这些数据转换为列存储格式,这意味着将每列的数据分别提取出来,组成列向量。这个转换过程包括数据的重新排列和重新组织,以适应列存储的数据结构。一旦所有行数据都被转换为列向量,RowToVec 算子就会将这些向量传递给向量化执行引擎,以便后续的处理。这种转换操作允许在混合计算中将行存储表和列存储表的数据进行有效整合,以便进行更高效的向量化计算。这样的执行流程可以在混合存储环境中实现数据的无缝转换和处理,以提高查询性能和效率。下面我们来详细的看一看相关函数源码一遍更好地理解和学习吧。
ExecInitRowToVec 函数的主要功能是为 RowToVec 算子创建执行状态,并初始化该状态以准备执行操作。它包括创建状态结构、设置计划节点、初始化元组表、分配向量缓冲、初始化子节点等步骤。这个过程确保了 RowToVec 算子能够正确地将行数据转换为列向量,并在混合计算中无缝运行,以提高数据库查询性能。
ExecInitRowToVec 函数的执行流程可以简要描述如下:
- 创建 RowToVecState 结构:首先,函数创建了一个 RowToVecState 结构,该结构用于存储 RowToVec 算子的执行状态信息。
- 设置基本属性:函数设置 RowToVecState 中的一些基本属性,如关联的计划节点、执行状态、以及标记为向量化执行。
- 检查数据类型支持:函数检查 RowToVec 算子是否支持所需的数据类型,以确保正确的数据处理。
- 初始化元组表:函数初始化用于存储结果的元组表,并分配所需的内存。
- 初始化子节点:函数初始化 RowToVec 算子的子节点,确保子节点能够正常执行。
- 创建表达式上下文:函数为 RowToVecState 创建表达式上下文,以便在执行期间进行表达式计算。
- 初始化元组类型:函数设置 RowToVecState 的元组类型,以匹配子节点的输出类型。
- 分配向量缓冲:函数为列向量数据分配内存,以便存储转换后的数据。
- 返回状态结构:最后,函数返回初始化完成的 RowToVecState 结构,以供后续的执行操作使用。
ExecInitRowToVec 函数源码如下所示:(路径:src\gausskernel\runtime\vecexecutor\vecnode\vecrowtovector.cpp
)
// 初始化 RowToVecState 结构
// 这段代码的作用是初始化 RowToVecState 结构,为 RowToVec 算子的执行创建执行状态。
// 具体步骤包括创建状态结构、设置计划节点、初始化元组表、分配向量缓冲、初始化子节点等。
RowToVecState* ExecInitRowToVec(RowToVec* node, EState* estate, int eflags)
{
RowToVecState* state = NULL;
/*
* 创建状态结构
*/
state = makeNode(RowToVecState);
state->ps.plan = (Plan*)node;
state->ps.state = estate;
state->ps.vectorized = true;
// 检查 RowToVec 算子是否支持所需数据类型
CheckTypeSupportRowToVec(node->plan.targetlist);
/*
* 初始化元组表
*
* 排序节点只从其排序关系中返回扫描元组。
*/
ExecInitResultTupleSlot(estate, &state->ps);
/* 分配向量缓冲 */
state->m_fNoMoreRows = false;
/*
* 初始化子节点
*
* 我们屏蔽了子节点对支持 REWIND、BACKWARD 或 MARK/RESTORE 的需求。
*/
outerPlanState(state) = ExecInitNode(outerPlan(node), estate, eflags);
/*
* 杂项初始化
*
* 为节点创建表达式上下文
*/
ExecAssignExprContext(estate, &state->ps);
/*
* 初始化元组类型。不需要初始化投影信息,因为此节点不进行投影。
*/
ExecAssignResultTypeFromTL(
&state->ps,
ExecGetResultType(outerPlanState(state))->tdTableAmType);
TupleDesc res_desc = state->ps.ps_ResultTupleSlot->tts_tupleDescriptor;
state->m_pCurrentBatch = New(CurrentMemoryContext) VectorBatch(CurrentMemoryContext, res_desc);
state->ps.ps_ProjInfo = NULL;
return state;
}
ExecRowToVec 函数的主要功能是将行数据转换为向量批次,用于向量化处理。它通过不断从外部计划节点获取行数据,然后将每个行数据向量化,直到外部计划的所有行都处理完毕。函数中的 VectorizeOneTuple 函数用于将单个行元组转换为向量,并使用适当的内存管理。一旦向量批次已满或外部计划的行耗尽,函数将返回当前的向量批次。
ExecRowToVec 函数的的执行流程可以简要描述如下:
- 准备必要的变量和数据结构,包括初始化向量批次(VectorBatch)和执行上下文(ExprContext)。
- 重置执行上下文的内存,以确保在处理每个行数据时不会出现内存混淆。
- 获取外部计划节点的状态信息,准备从外部计划获取行数据。
- 如果已经没有更多的行数据需要处理(由 m_fNoMoreRows 标志控制),则直接跳到完成步骤。
- 通过循环迭代处理外部计划的每个行数据,每次获取一个外部计划的元组。
- 对每个获取的行数据执行向量化处理,将行数据转换为向量格式,并使用适当的内存进行管理。
- 如果向量批次已满,或者外部计划的所有行都已处理完毕,则结束循环。
- 在完成步骤中,将向量批次中的每列的行数设置为相同的值,以标记批次中的有效数据行数。
- 最后,返回向量批次,其中包含转换后的向量化数据。
ExecRowToVec 函数的源码如下:(路径:src\gausskernel\runtime\vecexecutor\vecnode\vecrowtovector.cpp
)
/*
* @Description: 向量化运算符--将行数据转换为向量批次。
*
* @IN state: RowToVecState 结构,用于管理 RowToVec 算子的执行状态。
* @return: 返回包含行表数据的向量批次,如果没有数据可转换则返回 NULL。
*/
VectorBatch* ExecRowToVec(RowToVecState* state)
{
int i;
PlanState* outer_plan = NULL;
TupleTableSlot* outer_slot = NULL;
VectorBatch* batch = state->m_pCurrentBatch;
/* 重置当前的 ecxt_per_tuple_memory 上下文 */
ExprContext* econtext = state->ps.ps_ExprContext;
ResetExprContext(econtext);
/* 从节点获取状态信息 */
outer_plan = outerPlanState(state);
batch->Reset();
/*
* 如果在返回 NULL 后再次调用 ExecProcNode(),它可能会重新启动,
* 因此我们需要自行进行保护。
*/
if (state->m_fNoMoreRows) {
goto done;
}
/*
* 处理每个外部计划元组,然后获取下一个元组,直到外部计划耗尽。
*/
for (;;) {
outer_slot = ExecProcNode(outer_plan);
if (TupIsNull(outer_slot)) {
state->m_fNoMoreRows = true;
break;
}
/*
* 向量化一个元组并切换到 exprcontext 的 ecxt_per_tuple_memory。
*/
if (VectorizeOneTuple(batch, outer_slot, econtext->ecxt_per_tuple_memory)) {
/* 批次已满,现在返回当前批次 */
break;
}
}
done:
for (i = 0; i < batch->m_cols; i++) {
batch->m_arr[i].m_rows = batch->m_rows;
}
return batch;
}
VectorizeOneTuple 函数的作用是将一个数据库查询结果中的单个元组(Tuple)转换为向量化数据结构(VectorBatch),以便进行向量化处理。它会逐一提取元组中的每列数据,并根据列的数据类型和长度将其合适地存储在向量批次中的相应列中。此功能是在向量化执行引擎中关键的一步,通过高效地将行数据转换为列存储的向量形式,有助于加速数据库查询的处理速度和效率。此外,该函数还会检查向量批次是否已满,以便控制向量处理的大小和内存使用。
VectorizeOneTuple 函数的执行流程可以简要描述如下:
- 初始化必要的变量和标志,包括一个标志变量 may_more 用于表示是否可能还有更多的行需要处理,以及循环迭代中使用的计数器变量 i 和 j。
- 切换当前内存上下文到传入的 transformContext 上下文,以确保内存管理的正确性。
- 确保输入的插槽 slot 不为空,并且具有有效的元组描述符。
- 使用 tableam_tslot_getallattrs 函数从插槽中提取所有列的属性值。
- 开始循环迭代插槽中的每个列数据,处理每列的数据类型和长度。
- 根据列的数据类型和长度选择合适的处理方式,包括整数、浮点数、变长数据、大整数、TID(行标识符)等。
- 如果列不为空
(slot->tts_isnull[i] == false)
,则将列的值存储到向量批次的相应列中,并标记该列的数据为非 NULL。- 如果列为空
(slot->tts_isnull[i] == true)
,则将向量批次中的相应列标记为 NULL。- 增加向量批次中的行数计数器
pBatch->m_rows
。- 如果向量批次已满(行数达到 BatchMaxSize),则设置 may_more 标志为 true。
- 切换回之前的内存上下文,以确保内存管理的正确性。
- 返回 may_more 标志,指示向量批次是否已满,以便在上层调用中做出相应的处理。
VectorizeOneTuple 函数的源码如下:(路径:src\gausskernel\runtime\vecexecutor\vecnode\vecrowtovector.cpp
)
/*
* @Description: 将一个元组打包成向量批次。
*
* @IN pBatch: 目标向量化数据。
* @IN slot: 一个插槽(TupleTableSlot)中的源数据。
* @IN transformContext: 切换到此上下文以避免内存泄漏。
* @return: 如果 pBatch 已满,则返回 true,否则返回 false。
*/
bool VectorizeOneTuple(_in_ VectorBatch* pBatch, _in_ TupleTableSlot* slot, _in_ MemoryContext transformContext)
{
bool may_more = false; // 标志是否可能还有更多行需要处理
int i, j;
/* 切换到当前的转换上下文 */
MemoryContext old_context = MemoryContextSwitchTo(transformContext);
/*
* 提取旧元组的所有值。
*/
Assert(slot != NULL && slot->tts_tupleDescriptor != NULL);
tableam_tslot_getallattrs(slot); // 从插槽中获取所有属性值
j = pBatch->m_rows;
for (i = 0; i < slot->tts_nvalid; i++) { // 遍历元组的每一列
int type_len;
Form_pg_attribute attr = slot->tts_tupleDescriptor->attrs[i]; // 获取列的属性信息
pBatch->m_arr[i].m_desc.typeId = attr->atttypid; // 设置向量批次中列的数据类型
if (slot->tts_isnull[i] == false) { // 检查列是否为 NULL
type_len = attr->attlen; // 获取列的数据长度
switch (type_len) { // 根据数据长度选择合适的处理方式
case sizeof(char):
case sizeof(int16):
case sizeof(int32):
case sizeof(Datum):
pBatch->m_arr[i].m_vals[j] = slot->tts_values[i]; // 处理整数和浮点数
break;
case 12:
case 16:
case 64:
case -2:
pBatch->m_arr[i].AddVar(slot->tts_values[i], j); // 处理变长数据
break;
case -1: {
Datum v = PointerGetDatum(PG_DETOAST_DATUM(slot->tts_values[i])); // 解压缩数据
/* 如果是 numeric 列,尝试将 numeric 转换为 big integer */
if (attr->atttypid == NUMERICOID) {
v = try_convert_numeric_normal_to_fast(v);
}
pBatch->m_arr[i].AddVar(v, j); // 处理大整数和压缩数据
/* 由于可能创建了新的内存,因此我们必须及时检查和释放。 */
if (DatumGetPointer(slot->tts_values[i]) != DatumGetPointer(v)) {
pfree(DatumGetPointer(v)); // 释放临时内存
}
break;
}
case 6:
if (attr->atttypid == TIDOID && attr->attbyval == false) { // 处理 TID 数据
pBatch->m_arr[i].m_vals[j] = 0;
ItemPointer dest_tid = (ItemPointer)(pBatch->m_arr[i].m_vals + j);
ItemPointer src_tid = (ItemPointer)DatumGetPointer(slot->tts_values[i]);
*dest_tid = *src_tid;
} else {
pBatch->m_arr[i].AddVar(slot->tts_values[i], j); // 处理其他复合类型数据
}
break;
default:
ereport(ERROR, (errcode(ERRCODE_INDETERMINATE_DATATYPE), errmsg("不支持的数据类型分支"))); // 不支持的数据类型分支,抛出错误
}
SET_NOTNULL(pBatch->m_arr[i].m_flag[j]); // 标记列中的数据为非 NULL
} else {
SET_NULL(pBatch->m_arr[i].m_flag[j]); // 标记列中的数据为 NULL
}
}
pBatch->m_rows++; // 增加向量批次中的行数
if (pBatch->m_rows == BatchMaxSize) { // 如果向量批次已满
may_more = true; // 设置 may_more 标志为 true
}
/* 切换回旧上下文 */
(void)MemoryContextSwitchTo(old_context); // 切换回之前的内存上下文
return may_more; // 返回 may_more 标志,指示向量批次是否已满
}
ExecEndRowToVec 函数的主要作用是用于清理和释放 RowToVec 算子执行时所分配的资源和状态,包括向量批次数据、执行上下文、结果元组槽以及关闭外部计划节点。首先,它将向量批次数据的指针设置为 NULL,以防止内存泄漏。然后,通过 ExecFreeExprContext 函数取消与计划节点的输出上下文的链接,但不实际释放内存,这是因为内存释放可能由上层节点负责。接下来,它清空结果元组槽的数据,确保不会有残留数据。最后,它关闭外部计划节点,释放与之相关的资源。这个函数用于 RowToVec 算子的执行结束时,进行资源清理和释放,以确保系统资源的有效利用。
ExecEndRowToVec 函数的源码如下:(路径:src\gausskernel\runtime\vecexecutor\vecnode\vecrowtovector.cpp
)
void ExecEndRowToVec(RowToVecState* node)
{
node->m_pCurrentBatch = NULL; // 清空向量批次数据,防止内存泄漏
/*
* 我们不实际释放任何 ExprContexts(参见 ExecFreeExprContext 中的注释),
* 只需从计划节点中取消链接输出上下文即可。
*/
ExecFreeExprContext(&node->ps); // 取消与计划节点的输出上下文的链接,不实际释放内存
/*
* 清空元组表
*/
(void)ExecClearTuple(node->ps.ps_ResultTupleSlot); // 清空结果元组槽的数据
/*
* 关闭子计划节点
*/
ExecEndNode(outerPlanState(node)); // 关闭外部计划节点
}
RowToVec 算子是用于将行存储数据转换为向量化数据的操作符,它通过 ExecInitRowToVec 函数进行初始化,将行数据逐行处理并转换为向量批次,这个过程由 ExecRowToVec 和 VectorizeOneTuple 函数完成,最后,ExecEndRowToVec 函数用于清理和释放资源,协同工作,使得在处理混合行存储和列存储数据时,能够高效地进行向量化计算,提高数据库查询性能。
这里用 AI 尝试性的生成了一张描述 RowToVec 算子的图,感觉很有意思。
这张图解释了数据库系统中 RowToVec 算子的功能,展示了数据从行存储格式转换到列存储格式的过程。下面是对每个部分的详细解释:
顶部部分 - 行存储表:
这部分代表数据库中常见的行存储表。
在行存储中,数据按行组织,这里以水平线的形式表示。
每条水平线象征一个数据行,包含顺序存储的各种字段。
中间部分 - RowToVec算子转换:
中间部分展示了RowToVec算子。
它作为一个转换机制,将数据从行格式转换为列格式。
这个过程对于在混合计算环境中操作的数据库非常关键,这些数据库使用行存储和列存储格式。
算子将行数据逐行提取并转化为列向量,这一过程在图中以从水平线到垂直线的转换形式展示。
底部部分 - 列存储格式:
图的底部展示了转换后的数据,现在以列存储格式呈现。
在列存储中,数据按列组织,这里以垂直线的形式表示。
每条垂直线代表一列数据,显示了数据在转换后的新结构。