声明:本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。
本文主要参考了 OpenGauss1.1.0 的开源代码和《OpenGauss数据库源码解析》和《PostgresSQL数据库内核分析》一书
OpenGauss 在 PortalRun 函数中会实际执行相关的 DML 查询,对数据进行计算和处理。在执行过程中,所有执行算子分为两大类:行存储算子和向量化算子。这两类算子分别对应行存储执行引擎和向量化执行引擎。行存储执行引擎的上层入口是 ExecutePlan 函数,向量化执行引擎的上层人口是 ExecuteVectorizedPlan 函数。其中向量化引擎是针对列存储表的执行引擎。如果存在行存储表和列存储表的混合计算,那么行存储执行引擎和向量化执行引擎直接可以通过 VecToRow 和 RowToVec 算子进行相互转换。行存储算子执行入口函数的命名规则一般为 “Exec + 算子名” 的形式,向量化算子执行入口函数的命名规则一般为 “ExecVee十算子名” 的形式,通过这样的命名规则,可以快速地找到对应算子的函数入口。
本文所学习的重点为 VecToRow 算子的执行流程。在 OpenGauss 数据库管理系统中,VecToRow 算子扮演着一个重要的角色,特别是在处理涉及行存储和列存储表的混合计算场景。以下是对 VecToRow 算子的作用和功能的详细概述:
VecToRow 算子的作用:
VecToRow 算子的主要作用是在行存储执行引擎和向量化执行引擎之间提供数据转换的功能。在具体的操作中,它将向量化格式(主要用于列存储表)的数据转换为行存储格式(适用于行存储表)。这种转换对于数据库系统在处理混合类型的数据(既包括行存储表又包括列存储表)时至关重要。
VecToRow 算子的功能:
- 数据格式转换:
- 将数据从列存储格式转换为行存储格式。
- 这种转换使得向量化执行引擎处理的数据能够被行存储执行引擎理解和处理。
- 支持混合计算:
- 当查询涉及同时访问行存储表和列存储表时,VecToRow 算子允许这两种数据格式在同一个查询中有效结合。
- 通过转换,算子确保数据在不同类型的存储和处理引擎之间无缝传递。
- 优化查询性能:
- 在某些情况下,向量化处理(针对列存储)对于特定类型的查询更有效率。然而,对于某些操作,如某些类型的连接或复杂的数据处理,行存储格式可能更为合适。
- VecToRow 算子使得系统能够在这两种处理方式之间灵活转换,从而优化整体查询性能。
- 提高灵活性和兼容性:
- 此算子提升了数据库系统处理不同数据模型的灵活性。
- 它确保了即使在混合存储环境下,复杂的查询也能够正确执行,增强了系统的兼容性。
ExecInitVecToRow
函数为 VecToRow 算子的执行准备了必要的状态和上下文环境。
初始化 VecToRow 算子的执行状态,用于将向量化格式的数据转换为行格式。
VecToRow* node
: VecToRow 算子节点。EState* estate
: 执行状态。int eflags
: 执行标志。makeNode
函数创建 VecToRowState
结构体。ExecInitResultTupleSlot
初始化结果元组槽。ExecInitNode
初始化子节点,屏蔽对特定功能的需求。RecordCstorePartNum
记录列存储的分区数量。VecToRowState
状态结构。 函数源码如下所示:(路径:src\gausskernel\runtime\vecexecutor\vecnode\vectortorow.cpp
)
/*
* ExecInitVecToRow 函数初始化 VecToRow 算子的执行状态。
* 这个函数负责将向量化格式的数据转换为行格式数据。
* 参数包括 VecToRow 算子节点、执行状态和执行标志。
*/
VecToRowState* ExecInitVecToRow(VecToRow* node, EState* estate, int eflags)
{
VecToRowState* state = NULL;
// 创建 VecToRowState 结构体
state = makeNode(VecToRowState);
state->ps.plan = (Plan*)node; // 指定计划节点
state->ps.state = estate; // 设置执行状态
state->ps.vectorized = false; // 设置为非向量化状态
// 初始化结果元组槽
// VecToRow 算子不产生自己的元组,只处理子节点的元组。
ExecInitResultTupleSlot(estate, &state->ps);
// 初始化子节点
// 对子节点进行初始化,屏蔽对 REWIND, BACKWARD 或 MARK/RESTORE 的需求。
if ((uint32)eflags & EXEC_FLAG_BACKWARD)
ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("column store doesn't support backward scan")));
outerPlanState(state) = ExecInitNode(outerPlan(node), estate, eflags);
RecordCstorePartNum(state, node); // 记录列存储的分区数量
// 杂项初始化
// 为节点创建表达式上下文
ExecAssignExprContext(estate, &state->ps);
// 初始化元组类型
// 不需要初始化投影信息,因为这个节点不执行投影操作。
ExecAssignResultTypeFromTL(
&state->ps,
ExecGetResultType(outerPlanState(state))->tdTableAmType);
state->ps.ps_ProjInfo = NULL;
state->m_currentRow = 0; // 初始化当前行为0
state->m_pCurrentBatch = NULL; // 当前批次指针置为空
state->nattrs = ExecGetResultType(&state->ps)->natts; // 设置属性数量
state->tts = state->ps.ps_ResultTupleSlot;
(void)ExecClearTuple(state->tts);
state->tts->tts_nvalid = state->nattrs;
state->tts->tts_isempty = false;
state->devectorizeFunRuntime = (DevectorizeFun*)palloc0(state->nattrs * sizeof(DevectorizeFun));
for (int i = 0; i < state->nattrs; i++) {
state->tts->tts_isnull[i] = false;
int type_id = state->tts->tts_tupleDescriptor->attrs[i]->atttypid;
// 根据类型 ID 分配去向量化函数
// 各类型数据有不同的去向量化方法
if (COL_IS_ENCODE(type_id)) {
switch (type_id) {
case BPCHAROID:
case TEXTOID:
case VARCHAROID:
state->devectorizeFunRuntime[i] = DevectorizeOneColumn<VARCHAROID>;
break;
case TIMETZOID:
case TINTERVALOID:
case INTERVALOID:
case NAMEOID:
case MACADDROID:
case UUIDOID:
state->devectorizeFunRuntime[i] = DevectorizeOneColumn<TIMETZOID>;
break;
case UNKNOWNOID:
case CSTRINGOID:
state->devectorizeFunRuntime[i] = DevectorizeOneColumn<UNKNOWNOID>;
break;
default:
state->devectorizeFunRuntime[i] = DevectorizeOneColumn<-2>;
break;
}
} else {
if (type_id == TIDOID)
state->devectorizeFunRuntime[i] = DevectorizeOneColumn<TIDOID>;
else
state->devectorizeFunRuntime[i] = DevectorizeOneColumn<-1>;
}
}
state->m_ttsvalues = NULL;
state->m_ttsisnull = NULL;
return state; // 返回初始化好的状态
}
ExecVecToRow
函数通过以上步骤将向量化数据批次转换为行格式数据,以便在行存储执行引擎中进一步处理。
ExecVecToRow
函数负责执行 VecToRow 算子,将向量化批处理数据转换为行格式数据。
VecToRowState* state
: VecToRow 算子的状态结构体。TupleTableSlot*
:指向转换后的行格式数据的指针,或在没有更多数据时返回 NULL。初始化变量
PlanState* outer_plan
、TupleTableSlot* tuple
和 VectorBatch* current_batch
。tuple_subscript
用于在批次中定位特定行。处理当前批次
state
中获取当前向量化数据批次。获取新批次
VectorEngine
函数从子计划节点获取新的向量化数据批次。更新状态和转换批次
state->m_pCurrentBatch
并重置 state->m_currentRow
。DevectorizeOneBatch
函数将新批次的数据从向量化格式转换为行格式。检索当前批次的行
state->m_currentRow
和 state->nattrs
定位当前处理的行。tuple
并使用状态中的数据填充 tuple
。更新行索引
state->m_currentRow
以处理下一行。存储并返回行数据
ExecStoreVirtualTuple
将处理后的行数据存储到 tuple
。tuple
。 函数源码如下所示:(路径:src\gausskernel\runtime\vecexecutor\vecnode\vectortorow.cpp
)
/*
* ExecVecToRow 函数执行 VecToRow 算子,将向量化批处理数据转换为行格式数据。
* 参数 state 是 VecToRowState 结构体,包含算子的当前状态。
* 函数返回一个 TupleTableSlot 指针,指向转换后的行格式数据,或在没有更多数据时返回 NULL。
*/
TupleTableSlot* ExecVecToRow(VecToRowState* state) /* return: a tuple or NULL */
{
PlanState* outer_plan = NULL;
TupleTableSlot* tuple = state->tts;
VectorBatch* current_batch = NULL;
int tuple_subscript;
// 获取当前批次的数据
current_batch = state->m_pCurrentBatch;
if (BatchIsNull(current_batch)) {
// 如果当前批次为空,从子计划节点获取新的批次
outer_plan = outerPlanState(state);
current_batch = VectorEngine(outer_plan);
if (BatchIsNull(current_batch)) // 如果没有更多行,则返回 NULL
return NULL;
// 更新状态,记录新的批次和行索引
state->m_pCurrentBatch = current_batch;
state->m_currentRow = 0;
// 将当前批次的数据从向量格式转换为行格式
DevectorizeOneBatch(state);
}
// 从当前批次中检索行
tuple_subscript = state->m_currentRow * state->nattrs;
(void)ExecClearTuple(tuple);
for (int i = 0; i < state->nattrs; i++) {
// 从状态中获取转换后的行数据
tuple->tts_values[i] = state->m_ttsvalues[tuple_subscript + i];
tuple->tts_isnull[i] = state->m_ttsisnull[tuple_subscript + i];
}
state->m_currentRow++;
if (state->m_currentRow >= current_batch->m_rows) {
// 如果当前批次的所有行都已处理,标记批次为空
current_batch->m_rows = 0;
state->m_currentRow = 0;
}
// 将行数据存储到元组槽中
ExecStoreVirtualTuple(tuple);
return tuple; // 返回处理后的行数据
}
DevectorizeOneBatch 主要负责将数据从向量化的存储格式转换为行式格式,这是在将数据从列存储表传递给行存储执行引擎时一个关键的步骤。它通过逐列处理当前批次中的数据,将向量化的列数据转换为标准的行式数据,以便于行存储执行引擎进一步处理。函数源码如下所示:(路径:src\gausskernel\runtime\vecexecutor\vecnode\vectortorow.cpp
)
/* 将整个批次从向量存储转换为行存储的函数 */
void DevectorizeOneBatch(VecToRowState* state)
{
int i;
int j;
int rows;
int cols;
VectorBatch* current_batch = NULL;
ScalarVector* column = NULL;
MemoryContext old_context;
current_batch = state->m_pCurrentBatch;
rows = current_batch->m_rows; // 当前批次的行数
cols = state->nattrs; // 列数
/* 为 m_ttsvalues 分配内存,用于存储列值;
* 为 m_ttsisnull 分配内存,用于指示列值是否为 null。
* 这两个数组都存放在 VecToRowState 中。 */
if (state->m_ttsvalues == NULL) {
state->m_ttsvalues = (Datum*)palloc(sizeof(Datum) * cols * BatchMaxSize);
state->m_ttsisnull = (bool*)palloc(sizeof(bool) * cols * BatchMaxSize);
}
/* 循环处理整个批次,逐列进行。 */
ExprContext* econtext = state->ps.ps_ExprContext; // 表达式上下文
ResetExprContext(econtext); // 重置表达式上下文
old_context = MemoryContextSwitchTo(econtext->ecxt_per_tuple_memory);
for (i = 0; i < cols; i++) {
column = ¤t_batch->m_arr[i]; // 获取当前列的向量
/* 处理常量情况;同时处理 NULL 值情况。 */
for (j = 0; j < rows; j++)
state->m_ttsisnull[j * cols + i] = IS_NULL(column->m_flag[j]);
// 调用去向量化函数处理当前列
state->devectorizeFunRuntime[i](state, column, rows, cols, i);
}
// 恢复原始的内存上下文
(void)MemoryContextSwitchTo(old_context);
return;
}
ExecEndVecToRow 函数的作用和功能是在执行数据库查询计划时进行清理和资源释放。首先,它清空了与该节点关联的结果元组槽,以确保在下一次使用前不会残留任何旧数据。接下来,它关闭了与该节点关联的子计划,这些子计划是嵌套在主查询中的子查询或连接操作,执行结束后需要关闭以释放资源。这些操作是确保查询执行的正确性和资源管理的重要步骤。ExecEndVecToRow 函数源码如下所示:(路径:src\gausskernel\runtime\vecexecutor\vecnode\vectortorow.cpp
)
void ExecEndVecToRow(VecToRowState* node)
{
// 清空元组表
// 这行代码清空了与给定节点关联的结果元组槽。
// 结果元组槽通常用于存储查询结果的临时数据结构。
(void)ExecClearTuple(node->ps.ps_ResultTupleSlot);
// 关闭子计划
// 这行代码关闭了与给定节点关联的子计划。
// 子计划是查询计划中的嵌套计划,通常是子查询或连接操作。
// 执行结束后需要关闭这些子计划以释放资源。
ExecEndNode(outerPlanState(node));
}
VecToRow 算子在 OpenGauss 的架构中起着桥梁的作用,连接了行存储和向量化(列存储)执行引擎。它通过转换数据格式,确保了数据可以在这两种不同的存储和处理模型之间流动,从而支持更复杂和多样化的查询操作。这不仅提高了查询的灵活性和效率,还增强了数据库在处理不同类型数据时的整体性能。