ALTER TABLE(CStoreRewriter)
声明:本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。
本文主要参考了 OpenGauss5.1.0 的开源代码和《OpenGauss数据库源码解析》一书
概述
OpenGauss源码学习 —— (ALTER TABLE(ExecRewriteCStoreTable))】这篇文章详细介绍了 OpenGauss 数据库中 ALTER TABLE 命令的实现细节,特别是涉及到列存储表的重写操作(ExecRewriteCStoreTable)。其中,CStoreRewriter 在此过程中起着核心作用,主要负责以下方面:
- 数据组织优化: 通过重写操作,CStoreRewriter 帮助优化存储在列存储表中的数据的物理布局,这包括数据的压缩和重组,以提高查询效率和减少存储空间。
- 维护数据完整性: 在重写过程中,CStoreRewriter 确保所有的表约束(如NOT NULL约束)和表结构更改(如列的添加或删除)都得到妥善处理,确保数据完整性不受影响。
- 更新元数据: 重写操作还涉及更新与表相关的元数据,以反映重写后的新状态,例如更新索引、表大小等信息。
本文将详细介绍一下列存重写类型 CStoreRewriter。
CStoreRewriter 类
CStoreRewriter 类是专门设计用于 OpenGauss 数据库系统中,处理列存储表(CStore)的 ALTER TABLE 操作。该类的核心目的是在表结构变动时,确保数据的有效重写、优化存储和提高查询效率。本类实现了多种复杂的数据处理功能,如数据重写、列添加、数据类型转换、表空间更改和压缩数据等。
在 ALTER TABLE 操作中,表结构的变化可能涉及添加新列、修改列类型或移动数据到新的表空间等。这些操作不仅需要修改表的元数据,还需要重新组织表中的数据,以适应新的表结构。CStoreRewriter 类通过其丰富的方法集合来完成这些任务,其中包括数据的读取、重写、压缩和存储等步骤。
该类的详细代码描述如下所示:(路径:src\include\access\cstore_rewrite.h
)
// 定义一个CStoreRewriter类,继承自BaseObject基类
class CStoreRewriter : public BaseObject {
public:
// 构造函数,初始化旧表关系和描述符,以及新的描述符
CStoreRewriter(Relation oldHeapRel, TupleDesc oldTupDesc, TupleDesc newTupDesc);
// 虚拟析构函数
virtual ~CStoreRewriter(){};
// 用于销毁重写器的实例
virtual void Destroy();
// 改变表空间的方法
void ChangeTableSpace(Relation CUReplicationRel);
// 开始重写列的方法
void BeginRewriteCols(_in_ int nRewriteCols, _in_ CStoreRewriteColumn **pRewriteCols,
_in_ const int *rewriteColsNum, _in_ bool *rewriteFlags);
// 重写列数据的方法
void RewriteColsData();
// 结束重写列的方法
void EndRewriteCols();
public:
// 处理值相同的列存储单元(CU)
template <bool append>
void HandleCuWithSameValue(_in_ Relation rel, _in_ Form_pg_attribute newColAttr, _in_ Datum newColVal,
_in_ bool newColValIsNull, _in_ FuncSetMinMax func, _in_ CUStorage *colStorage,
__inout CUDesc *newColCudesc, __inout CUPointer *pOffset);
// 为值相同的CU形成数据
static void FormCuDataForTheSameVal(__inout CU *cuPtr, _in_ int rowsCntInCu, _in_ Datum newColVal,
_in_ Form_pg_attribute newColAttr);
// 压缩列存储单元数据的方法
static void CompressCuData(CU *cuPtr, CUDesc *cuDesc, Form_pg_attribute newColAttr, int16 compressing_modes);
// 保存列存储单元数据的方法
FORCE_INLINE
static void SaveCuData(_in_ CU *cuPtr, _in_ CUDesc *cuDesc, _in_ CUStorage *cuStorage);
private:
// 判断是否需要重写的内联方法
FORCE_INLINE bool NeedRewrite() const;
// 添加列的方法
void AddColumns(_in_ uint32 cuId, _in_ int rowsCntInCu, _in_ bool wholeCuIsDeleted);
// 初始化添加列的第一阶段
void AddColumnInitPhrase1();
// 初始化添加列的第二阶段
void AddColumnInitPhrase2(_in_ CStoreRewriteColumn *addColInfo, _in_ int idx);
// 销毁添加列的方法
void AddColumnDestroy();
// 设置数据类型的方法
void SetDataType(_in_ uint32 cuId, _in_ HeapTuple *cudescTup, _in_ TupleDesc oldCudescTupDesc,
_in_ const char *delMaskDataPtr, _in_ int rowsCntInCu, _in_ bool wholeCuIsDeleted);
// 初始化设置数据类型的第一阶段
void SetDataTypeInitPhase1();
// 初始化设置数据类型的第二阶段
void SetDataTypeInitPhase2(_in_ CStoreRewriteColumn *sdtColInfo, _in_ int idx);
// 销毁设置数据类型的方法
void SetDataTypeDestroy();
// 处理全为NULL值的CU
FORCE_INLINE
void SetDataTypeHandleFullNullCu(_in_ CUDesc *oldColCudesc, _out_ CUDesc *newColCudesc);
// 处理值相同的CU
void SetDataTypeHandleSameValCu(_in_ int sdtIndex, _in_ CUDesc *oldColCudesc, _out_ CUDesc *newColCudesc);
// 处理正常情况下的CU
void SetDataTypeHandleNormalCu(_in_ int sdtIndex, _in_ const char *delMaskDataPtr, _in_ CUDesc *oldColCudesc,
_out_ CUDesc *newColCudesc);
// 获取冻结的事务ID
void FetchCudescFrozenXid(Relation oldCudescHeap);
// 插入新的列描述符元组
void InsertNewCudescTup(_in_ CUDesc *pCudesc, _in_ TupleDesc pCudescTupDesc, _in_ Form_pg_attribute pColNewAttr);
// 处理完全删除的CU
void HandleWholeDeletedCu(_in_ uint32 cuId, _in_ int rowsCntInCu, _in_ int nRewriteCols,
_in_ CStoreRewriteColumn **rewriteColsInfo);
// 刷新所有CU数据
void FlushAllCUData() const;
private:
// 旧表关系
Relation m_OldHeapRel;
// 旧元组描述符
TupleDesc m_OldTupDesc;
// 新元组描述符
TupleDesc m_NewTupDesc;
// 新的列描述符堆的对象标识符
Oid m_NewCuDescHeap;
// 列重写标志数组
bool *m_ColsRewriteFlag;
// 设置新的表空间标志
bool m_TblspcChanged;
// 目标表空间的对象标识符
Oid m_TargetTblspc;
// 目标关系文件节点的对象标识符
Oid m_TargetRelFileNode;
// 用于CU复制的新关系,如果表空间已更改,则使用特殊的CU复制关系
Relation m_CUReplicationRel;
// 设置数据类型列追加偏移量的指针数组
CUPointer *m_SDTColAppendOffset;
// 添加列的数量
int m_AddColsNum;
// 添加列信息的指针数组
CStoreRewriteColumn **m_AddColsInfo;
// 添加列存储的指针数组
CUStorage **m_AddColsStorage;
// 设置最小最大函数的指针数组
FuncSetMinMax *m_AddColsMinMaxFunc;
// 添加列追加偏移量的指针数组
CUPointer *m_AddColsAppendOffset;
// 设置数据类型的列数量
int m_SDTColsNum;
// 设置数据类型列信息的指针数组
CStoreRewriteColumn **m_SDTColsInfo;
// 读取设置数据类型列的存储指针数组
CUStorage **m_SDTColsReader;
// 设置数据类型列最小最大函数的指针数组
FuncSetMinMax *m_SDTColsMinMaxFunc;
// 写入设置数据类型列的存储指针数组
CUStorage **m_SDTColsWriter;
// 设置数据类型列值的数据数组
Datum *m_SDTColValues;
// 设置数据类型列是否为空的布尔数组
bool *m_SDTColIsNull;
// 新的列描述符关系
Relation m_NewCudescRel;
// 新的列描述符批量插入处理
HeapBulkInsert *m_NewCudescBulkInsert;
// 冻结的事务ID
TransactionId m_NewCudescFrozenXid;
// 在重写表数据期间使用的EState
EState *m_estate;
// 表达式上下文,每处理完一条数据后必须重置其每条数据内存
ExprContext *m_econtext;
};
详细功能解析
- 重写列数据:
BeginRewriteCols、RewriteColsData 和 EndRewriteCols 方法构成了数据重写的主要流程。在开始重写列之前,先通过 BeginRewriteCols 方法设置重写的列信息和相关参数。随后,RewriteColsData 方法实际进行数据的读取、转换和重新组织。最后,EndRewriteCols 方法结束重写过程,并进行必要的清理工作。 - 数据压缩与存储:
通过 CompressCuData 和 SaveCuData 方法,CStoreRewriter 对列存储单元(CU)中的数据进行压缩处理,以减少数据占用的存储空间,并提高数据的读取效率。这些方法确保数据在物理存储上的优化,是数据库性能提升的关键。 - 类型转换与列添加:
类中的 AddColumns、SetDataType 等方法处理数据类型的转换和新列的添加。这些功能对于数据库表结构的灵活性和扩展性至关重要,特别是在动态调整数据库模式以适应应用需求变更时。 - 表空间管理:
ChangeTableSpace 方法允许将数据从一个表空间迁移到另一个表空间。这在进行数据库维护或优化时非常有用,比如当某个表空间的磁盘空间不足,或者需要将数据迁移到性能更高的存储设备上时。 - 事务与元数据管理:
类中还包含了诸如 FetchCudescFrozenXid 和 InsertNewCudescTup 等方法,这些方法负责处理与数据重写相关的事务管理和元数据更新。例如,更新列描述信息,确保数据一致性和完整性。
添加列
AddColumns、AddColumnInitPhrase1、AddColumnInitPhrase2、AddColumnDestroy 这四个函数是 CStoreRewriter 类中用于处理列存储表中添加新列的一系列方法,它们协同工作以确保在列存储单元(CU)中正确地添加新列。
- AddColumns: 此函数负责在特定的列存储单元中添加新列。它接受列存储单元的标识符(cuId)、该单元中的行数(rowsCntInCu)以及一个标志(wholeCuIsDeleted),指示该单元是否已完全删除。这是添加新列的初步操作,涉及到更新数据结构以便包含新列。
- AddColumnInitPhrase1: 这个函数启动添加新列的初始化过程,设置必要的数据结构和状态,为新列的数据插入做准备。这是添加列操作的准备阶段,确保后续操作可以顺利进行。
- AddColumnInitPhrase2: 紧接着第一阶段,这个函数继续初始化过程,具体处理每个新添加的列的信息。它通过接收一个指向 CStoreRewriteColumn 的指针(表示新列的信息)和列的索引,来精确地配置每列的特定参数和状态。
- AddColumnDestroy: 在新列成功添加后,这个函数负责清理与添加新列相关的所有临时数据结构和状态,确保系统资源得到适当释放,并维护系统的稳定性和性能。
AddColumns 函数
AddColumns 函数主要实现在列存储表中添加新列的功能。当 ALTER TABLE 命令需要向表中添加列时,此函数会被调用。它首先检查目标列存储单元(CU)是否完全被删除,如果是,则只需要处理元数据而不需要写入任何数据。接下来,代码通过创建一个假的元组和元组槽来模拟一个环境,用于计算可能需要通过表达式定义的新列值。之后,它遍历所有要添加的列,为每个列计算值(如果有表达式的话),更新列存储描述符(CUDesc),然后调用 HandleCuWithSameValue 来处理值相同的 CU。最后,新的列描述符被插入到相应的表中,完成列的添加过程。函数源码如下所示:(路径:src\gausskernel\storage\cstore\cstore_rewrite.cpp
)
void CStoreRewriter::AddColumns(_in_ uint32 cuId, _in_ int rowsCntInCu, _in_ bool wholeCuIsDeleted)
{
// 确认cuId有效,cuId必须大于第一个CU的ID
Assert(cuId > (uint32)FirstCUID);
/* description: future plan-remove this branch after WHOLE DELETED CU data can be reused. */
// 如果整个CU已被删除,则不需要写入任何CU数据
if (wholeCuIsDeleted) {
// 但是每个列的描述符(cudesc)元组必须形成并插入
HandleWholeDeletedCu(cuId, rowsCntInCu, m_AddColsNum, m_AddColsInfo);
return;
}
// 构造一个假的元组来欺骗函数 ExecEvalExpr(),表示添加的新列值不依赖于任何现有列或其值
HeapTuple fakeTuple = CreateFakeHeapTup(m_NewTupDesc, -1, 0);
TupleTableSlot* fakeSlot = MakeSingleTupleTableSlot(m_NewTupDesc);
(void)ExecStoreTuple(fakeTuple, fakeSlot, InvalidBuffer, false);
// 遍历所有需要添加的列
for (int i = 0; i < m_AddColsNum; ++i) {
CStoreRewriteColumn* newColInfo = m_AddColsInfo[i];
// 确保新列信息有效,且是被添加的,而不是被删除的
Assert(newColInfo && newColInfo->isAdded && !newColInfo->isDropped);
int attrIndex = newColInfo->attrno - 1;
Form_pg_attribute newColAttr = &m_NewTupDesc->attrs[attrIndex];
CUDesc newColCudesc;
newColCudesc.cu_id = cuId;
newColCudesc.row_count = rowsCntInCu;
Datum newColVal = (Datum)0;
bool newColValIsNull = !newColInfo->notNull;
// 如果新列的值是通过表达式计算的,则计算新值
if (newColInfo->newValue) {
m_econtext->ecxt_scantuple = fakeSlot;
newColVal = ExecEvalExpr(newColInfo->newValue->exprstate, m_econtext, &newColValIsNull);
} else if (newColInfo->notNull) {
// 如果新列为NOT NULL但没有默认值,则抛出错误
ereport(ERROR,
(errcode(ERRCODE_NOT_NULL_VIOLATION),
errmsg("column \"%s\" contains null values", NameStr(newColAttr->attname)),
errdetail("existing data violate the NOT NULL constraint of new column."),
errhint("define DEFAULT constraint also.")));
}
newColCudesc.magic = GetCurrentTransactionIdIfAny();
// 处理值相同的列存储单元
HandleCuWithSameValue<true>(m_OldHeapRel,
newColAttr,
newColVal,
newColValIsNull,
m_AddColsMinMaxFunc[i],
m_AddColsStorage[i],
&newColCudesc,
m_AddColsAppendOffset + i);
// 插入新的列描述符元组
InsertNewCudescTup(&newColCudesc, RelationGetDescr(m_NewCudescRel), newColAttr);
// 重置表达式上下文
ResetExprContext(m_econtext);
}
// 释放假元组
heap_freetuple(fakeTuple);
fakeTuple = NULL;
// 释放假元组槽
ExecDropSingleTupleTableSlot(fakeSlot);
fakeSlot = NULL;
}
注: 在数据库操作中,尤其是在处理 ALTER TABLE 添加新列的情况下,创建一个假元组(fake tuple)通常是为了提供一个必要的上下文环境,以便能够评估和计算新列的默认值或者表达式值。这个假元组不含有实际数据,但它模拟了数据结构,使得函数和表达式能在适当的上下文中执行,而不需要实际的数据行。下面通过一个具体的案例来说明这一点:
假设我们有一个数据库表 employees,包含以下列:
列名 | 含义 |
---|---|
id | 员工ID |
name | 员工姓名 |
现在我们需要通过 ALTER TABLE 命令给这个表添加一个新列 entry_date(入职日期),这个新列的默认值应该是执行添加操作当天的日期。这个日期可以通过 内置函数 CURRENT_DATE 获得。为了在不实际插入或更新任何现存数据行的情况下评估 CURRENT_DATE,我们需要创建一个假元组。这个假元组将被用作执行 CURRENT_DATE 函数的上下文环境,具体代码示例如下所示:
// 假设这是ALTER TABLE添加列的函数实现部分
void AddEntryDateColumn() {
// 创建一个新的元组描述符,假设包含了新列`entry_date`
TupleDesc newTupDesc = CreateTupleDescWithEntryDate();
// 创建一个假元组,但这里我们不需要实际的数据值
HeapTuple fakeTuple = CreateFakeHeapTup(newTupDesc, -1, 0);
// 创建一个单元组槽,用于存放假元组
TupleTableSlot* fakeSlot = MakeSingleTupleTableSlot(newTupDesc);
// 将假元组存储到槽中
ExecStoreTuple(fakeTuple, fakeSlot, InvalidBuffer, false);
// 设定上下文中的扫描元组为我们的假元组槽
ExpressionContext* econtext = CreateStandaloneExprContext();
econtext->ecxt_scantuple = fakeSlot;
// 评估当前日期函数,获取默认值
Datum newColVal = ExecEvalExpr(CURRENT_DATE(), econtext, &newColValIsNull);
// 此处处理与新列相关的其他逻辑,例如更新列描述符等
// 清理资源
heap_freetuple(fakeTuple);
ExecDropSingleTupleTableSlot(fakeSlot);
FreeExprContext(econtext, true);
}
// 注意:上述代码是一个高层次的示意,实际实现会依赖具体的数据库系统API和内部结构。
通过这种方式,我们能够在不影响或依赖任何现有数据的情况下,为新列生成一个有效且正确的默认值。这是在数据库内部处理列添加和默认值设置的一个有效策略,特别是在大规模数据环境下,直接操作实际数据行可能导致性能问题或不必要的数据加载。使用假元组可以在逻辑上简化操作,确保新列的值能够正确评估并应用,同时避免对现有数据造成干扰。
为什么使用假元组?
- 上下文提供: SQL 表达式需要在特定的数据上下文中被评估。对于依赖于当前数据库状态的表达式,如 CURRENT_DATE 或其他系统函数,它们需要一个元组上下文来执行。而在实际的表中可能还没有数据,或者我们不希望触及实际数据。
- 避免实际数据修改: 使用假元组可以在不实际修改或加载任何真实数据的情况下模拟这一处理过程。这样做可以防止在大量数据的生产环境中因修改表结构而导致的性能问题。
AddColumnInitPhrase1 函数
AddColumnInitPhrase1 函数的作用是为处理新添加的列进行初步的内存分配和数据结构准备。当 ALTER TABLE 命令需要向表中添加新列时,此函数会被调用以确保所有相关的数据结构都已经准备就绪,用于后续的数据处理和存储操作。函数源码如下所示:(路径:src\gausskernel\storage\cstore\cstore_rewrite.cpp
)
具体而言,此函数为每一个即将添加的列分配四类资源:
- 列信息: 为存储新列的结构信息(如数据类型、是否有默认值等)的指针数组分配内存。
- 列存储: 为每个新列的物理存储(如列存储单元 CU 的指针)分配内存,这些存储将用来保存实际的数据。
- 最小最大函数: 为处理新列数据的最小和最大值的函数分配内存,这对于优化查询和数据维护非常重要。
- 追加偏移量: 为记录新数据应追加到哪里的偏移量分配内存,这是处理数据追加操作的关键。
// 确保需要添加的列数大于0,否则断言失败,因为没有列需要添加时调用这个函数没有意义
Assert(m_AddColsNum > 0);
// 为新添加的列信息分配内存,m_AddColsInfo 是一个指向 CStoreRewriteColumn 指针数组的指针
m_AddColsInfo = (CStoreRewriteColumn**)palloc(sizeof(CStoreRewriteColumn*) * m_AddColsNum);
// 为存储新添加列的存储对象分配内存,m_AddColsStorage 是一个指向 CUStorage 指针数组的指针
m_AddColsStorage = (CUStorage**)palloc(sizeof(CUStorage*) * m_AddColsNum);
// 为最小最大函数指针数组分配内存,这些函数用于处理新列的数据范围,m_AddColsMinMaxFunc 是指向 FuncSetMinMax 的指针数组
m_AddColsMinMaxFunc = (FuncSetMinMax*)palloc(sizeof(FuncSetMinMax) * m_AddColsNum);
// 为新添加列的追加偏移量分配内存,这些偏移量指示数据应该被追加到列存储单元的哪个位置,m_AddColsAppendOffset 是指向 CUPointer 的指针数组
m_AddColsAppendOffset = (CUPointer*)palloc(sizeof(CUPointer) * m_AddColsNum);
AddColumnInitPhrase2 函数
AddColumnInitPhrase2 函数为新添加的列进行详细的初始化设置,这包括为列存储配置存储空间,设置列的最小/最大值函数,以及处理与表空间和文件节点相关的逻辑。函数源码如下所示:(路径:src\gausskernel\storage\cstore\cstore_rewrite.cpp
)
- 列信息和存储配置: 函数开始时,首先确认索引有效,然后将传入的列信息存储在对应的数组索引位置。为每个列配置最小/最大值函数,这对于后续数据的快速检索和验证很重要。
- 列存储的物理位置: 创建并初始化与新列相关的文件节点对象,包括设置新列可能的表空间和文件节点。这对于数据的物理存储至关重要,尤其是当表空间发生变化时,需要正确地反映这些变化。
- 日志记录和错误处理: 如果表空间改变,函数会记录相关日志,帮助追踪操作的历史和定位可能的问题。
- 存储创建和锁管理: 为记录新数据应追加到哪里的偏移量分配内存,这是处理数据追加操作的关键。
- 追加偏移量的初始化: 最后,设置追加偏移量为0,准备开始数据追加操作,这是为后续的数据写入做准备。
void CStoreRewriter::AddColumnInitPhrase2(_in_ CStoreRewriteColumn* addColInfo, _in_ int idx)
{
// 确保要添加的列数大于0,以及索引 idx 在有效范围内
Assert(m_AddColsNum > 0);
Assert(idx >= 0 && idx < m_AddColsNum);
// 将传入的列信息对象存储到指定索引位置
m_AddColsInfo[idx] = addColInfo;
/* 为粗略检查设置最小/最大值函数 */
// 根据新列的数据类型,获取并设置相应的最小/最大值函数
m_AddColsMinMaxFunc[idx] = GetMinMaxFunc(m_NewTupDesc->attrs[addColInfo->attrno - 1].atttypid);
/* 可能会使用不同的表空间和文件节点 */
// 创建一个新的文件节点对象,用于列存储
RelFileNode relfilenode = m_OldHeapRel->rd_node;
relfilenode.spcNode = m_TargetTblspc; // 设置目标表空间
relfilenode.relNode = m_TargetRelFileNode; // 设置目标文件节点
CFileNode cFileNode(relfilenode, (int)addColInfo->attrno, MAIN_FORKNUM); // 为新列创建文件节点对象
m_AddColsStorage[idx] = New(CurrentMemoryContext) CUStorage(cFileNode); // 创建并分配列存储
// 如果表空间发生变化,记录日志
if (m_TblspcChanged) {
ereport(LOG,
(errmsg("Column [ADD COLUMN]: %s(%u C%d) tblspc %u/%u/%u => %u/%u/%u",
RelationGetRelationName(m_OldHeapRel),
RelationGetRelid(m_OldHeapRel),
(addColInfo->attrno - 1),
m_OldHeapRel->rd_node.spcNode,
m_OldHeapRel->rd_node.dbNode,
m_OldHeapRel->rd_node.relNode,
m_TargetTblspc,
m_OldHeapRel->rd_node.dbNode,
m_TargetRelFileNode)));
}
/* 处理表空间未变更的情况 */
// 锁定文件节点重用锁,共享模式
LWLockAcquire(RelfilenodeReuseLock, LW_SHARED);
// 创建存储,参数指示是否重用现有文件
m_AddColsStorage[idx]->CreateStorage(0, !m_TblspcChanged);
// 创建关联的列存储
CStoreRelCreateStorage(
&cFileNode.m_rnode, addColInfo->attrno, m_OldHeapRel->rd_rel->relpersistence, m_OldHeapRel->rd_rel->relowner);
// 释放锁
LWLockRelease(RelfilenodeReuseLock);
/* 为新的列存储单元数据使用仅追加方法 */
// 设置追加偏移量为0,表示从头开始追加数据
m_AddColsAppendOffset[idx] = 0;
}
AddColumnDestroy 函数
AddColumnDestroy 函数的作用是在数据库的 ALTER TABLE ADD COLUMN 操作完成后,进行清理和资源释放,以确保不会有内存泄露或其他资源占用问题。当添加新列的操作涉及到为新列分配多种资源(如存储空间、控制结构等)时,这些资源在操作完成后需要被适当地回收。函数源码如下所示:(路径:src\gausskernel\storage\cstore\cstore_rewrite.cpp
)
// 释放添加列操作后所有未使用的资源。
void CStoreRewriter::AddColumnDestroy()
{
// 检查是否有需要清理的列资源
if (m_AddColsNum > 0) {
// 循环通过所有添加的列
for (int i = 0; i < m_AddColsNum; ++i) {
// 使用 DELETE_EX 宏删除每个列的存储对象,安全释放内存
DELETE_EX(m_AddColsStorage[i]);
}
// 释放存储新列信息的数组
pfree_ext(m_AddColsInfo);
// 释放存储新列存储对象指针的数组
pfree_ext(m_AddColsStorage);
// 释放存储最小/最大函数指针的数组
pfree_ext(m_AddColsMinMaxFunc);
// 释放存储新列追加偏移量的数组
pfree_ext(m_AddColsAppendOffset);
}
}
设置数据类型
SetDataType、SetDataTypeInitPhase1、SetDataTypeInitPhase2 和 SetDataTypeDestroy 四个函数用于管理和处理列存储表中数据类型的变更过程。SetDataType 函数负责具体的数据类型设置工作,它根据提供的参数(如列存储单元 ID、描述元组、旧描述符、删除掩码、行数和删除状态)更新列存储单元的数据类型。SetDataTypeInitPhase1 和 SetDataTypeInitPhase2 函数分别代表初始化数据类型设置过程的两个阶段,第一阶段可能涉及准备必要的数据结构和临时资源,而第二阶段则具体处理每个列的数据类型转换和相关属性设置。最后,SetDataTypeDestroy 函数用于清理在数据类型设置过程中可能产生的任何临时资源或分配,确保资源的有效管理和释放,维护系统的稳定性和效率。这一系列函数确保在表结构变化时数据类型的正确转换和更新,是数据库表结构管理的重要组成部分。
SetDataType 函数
SetDataType 函数是处理 ALTER TABLE ALTER COLUMN SET DATA TYPE
操作的核心方法,它负责更新指定列存储单元中列的数据类型。此函数首先检查是否整个列存储单元已被删除,若是则只处理列描述符而不处理数据。若未被删除,则遍历每一个需要更新数据类型的列,对每个列的描述符进行解构和重构,更新旧的列描述符信息以匹配新的数据类型,并处理不同的数据状态(如全为空、值相同或正常情况)。最后,更新的列描述符会被插入新的关系描述中,同时旧的数据缓存被清理,确保数据的一致性和系统的稳定性。通过这个过程,列的数据类型得以安全且有效地更新,以适应表结构的变化。函数源码如下所示:(路径:src\gausskernel\storage\cstore\cstore_rewrite.cpp
)
// 主要功能实现部分,处理 ALTER COLUMN SET DATA TYPE 语句。
void CStoreRewriter::SetDataType(_in_ uint32 cuId, _in_ HeapTuple* cudescTup, _in_ TupleDesc cudescTupDesc,
_in_ const char* delMaskDataPtr, _in_ int rowsCntInCu, _in_ bool wholeCuIsDeleted)
{
// 如果整个列存储单元已被删除,则不需要处理现有的数据,但需要重新形成并插入每列的列描述符元组。
if (wholeCuIsDeleted) {
HandleWholeDeletedCu(cuId, rowsCntInCu, m_SDTColsNum, m_SDTColsInfo);
return;
}
// 获取列存储缓存管理器
DataCacheMgr* cucache = CUCache;
// 遍历所有设置数据类型的列
for (int sdtIndex = 0; sdtIndex < m_SDTColsNum; ++sdtIndex) {
CStoreRewriteColumn* setDataTypeColInfo = m_SDTColsInfo[sdtIndex];
// 断言确保列信息有效,且列既不是新添加的也不是已删除的
Assert(setDataTypeColInfo);
Assert(!setDataTypeColInfo->isAdded);
Assert(!setDataTypeColInfo->isDropped);
// 获取属性索引
int attrIndex = setDataTypeColInfo->attrno - 1;
// 解构列描述元组,并设置旧的列描述对象
CUDesc oldColCudesc;
CStore::DeformCudescTuple(cudescTup[attrIndex], cudescTupDesc, &m_OldTupDesc->attrs[attrIndex], &oldColCudesc);
// 断言确认列ID和行数正确
Assert(oldColCudesc.cu_id == cuId);
Assert(oldColCudesc.row_count == rowsCntInCu);
// 创建新的列描述对象
CUDesc newColCudesc;
// 根据列的不同状态调用不同的处理函数
if (oldColCudesc.IsNullCU()) {
SetDataTypeHandleFullNullCu(&oldColCudesc, &newColCudesc);
} else if (oldColCudesc.IsSameValCU()) {
SetDataTypeHandleSameValCu(sdtIndex, &oldColCudesc, &newColCudesc);
} else {
SetDataTypeHandleNormalCu(sdtIndex, delMaskDataPtr, &oldColCudesc, &newColCudesc);
}
// 插入新的列描述元组
InsertNewCudescTup(&newColCudesc, RelationGetDescr(m_NewCudescRel), &m_NewTupDesc->attrs[attrIndex]);
// 重置表达式上下文
ResetExprContext(m_econtext);
// 使列的缓存数据无效,因为此数据将不再被使用
cucache->InvalidateCU(
(RelFileNodeOld *)&(m_OldHeapRel->rd_node), setDataTypeColInfo->attrno - 1,
oldColCudesc.cu_id, oldColCudesc.cu_pointer);
}
// 清除缓存管理器的引用,因为 econtext 由 estate 管理
cucache = NULL;
}
其中,m_SDTColsNum 变量通常存储需要进行数据类型更改的列的数量。它的值与 CStoreRewriteType 相关联,特别是当 CStoreRewriteType 是 CSRT_SET_DATA_TYPE 时,m_SDTColsNum 用于计数此类操作。
CStoreRewriteType 枚举定义了不同类型的列存储重写操作。这个枚举有三个值:
typedef enum CStoreRewriteType {
CSRT_ADD_COL = 0, // --> ADD COLUMN 表示添加新列的操作。
CSRT_SET_DATA_TYPE, // --> ALTER COLUMN SET DATA TYPE 表示更改列的数据类型的操作。
CSRT_NUM // add new type above please. 枚举的结束标志,也用于计数枚举值的总数。
} CStoreRewriteType;
此外,函数 ATCStoreGetRewriteAttrs (在【OpenGauss源码学习 —— (ALTER TABLE(ExecRewriteCStoreTable))】一文中有描述)解析 ALTER TABLE 命令,确定需要执行哪些列存储的重写操作,并据此初始化相关数据结构。函数参数中的 nColsOfEachType 数组存储每种重写类型的列数量,其中索引 CSRT_ADD_COL
和 CSRT_SET_DATA_TYPE
分别计数添加列和更改数据类型的列。
SetDataTypeInitPhase1 函数
SetDataTypeInitPhase1 函数的主要目的是为数据类型变更操作的第一阶段准备必要的数据结构和资源。这包括为每个受影响的列分配存储其信息的空间、读写操作所需的存储结构、执行最小/最大值检查的函数,以及足够的内存来处理和转换数据。如果表空间发生了变化,还需要为数据的追加写入位置分配额外的偏移量。这个初始化阶段是确保数据类型变更能顺利进行的关键步骤,它为数据的读取、处理和重新存储奠定了基础。通过这样的准备,系统能够有效地处理数据类型的更改,保证数据的一致性和完整性。函数源码如下所示:(路径:src\gausskernel\storage\cstore\cstore_rewrite.cpp
)
void CStoreRewriter::SetDataTypeInitPhase1()
{
// 确保需要设置数据类型的列的数量大于0
Assert(m_SDTColsNum > 0);
// 为需要设置数据类型的列分配指针数组,存储每列的相关信息
m_SDTColsInfo = (CStoreRewriteColumn**)palloc(sizeof(CStoreRewriteColumn*) * m_SDTColsNum);
// 为每个列分配读取存储对象的数组,用于从列存储中读取数据
m_SDTColsReader = (CUStorage**)palloc(sizeof(CUStorage*) * m_SDTColsNum);
// 为每个列分配函数指针数组,用于执行最小/最大值函数,这对查询优化非常重要
m_SDTColsMinMaxFunc = (FuncSetMinMax*)palloc(sizeof(FuncSetMinMax) * m_SDTColsNum);
// 为每个列分配写入存储对象的数组,用于将处理后的数据写回列存储
m_SDTColsWriter = (CUStorage**)palloc(sizeof(CUStorage*) * m_SDTColsNum);
// 分配足够存储一个完整列存储单元最大数据量的值数组
m_SDTColValues = (Datum*)palloc(sizeof(Datum) * RelMaxFullCuSize);
// 分配足够存储一个完整列存储单元最大数据量的布尔数组,用于标记值是否为NULL
m_SDTColIsNull = (bool*)palloc(sizeof(bool) * RelMaxFullCuSize);
// 如果表空间发生变化,为每个列分配新的追加偏移量数组
if (m_TblspcChanged) {
m_SDTColAppendOffset = (CUPointer*)palloc(sizeof(CUPointer) * m_SDTColsNum);
}
}
SetDataTypeInitPhase2 函数
SetDataTypeInitPhase2 函数主要负责在数据类型变更过程中,为每个受影响的列配置具体的读写存储、设置最小/最大值函数、以及处理表空间变化带来的文件存储变更。函数首先更新列的信息对象和最小/最大值函数,然后根据表空间是否变更决定如何设置列的文件存储策略。如果表空间未变更,则使用原有的文件节点;如果表空间变更,则创建新的文件节点并设置为追加模式。此外,函数还确保在进行数据类型变更时注册相关的操作,以便系统能跟踪这些变更。通过这些步骤,函数确保每个列的数据存储策略和管理策略得到妥善设置,从而支持高效且安全的数据类型变更操作。函数源码如下所示:(路径:src\gausskernel\storage\cstore\cstore_rewrite.cpp
)
void CStoreRewriter::SetDataTypeInitPhase2(_in_ CStoreRewriteColumn* sdtColInfo, _in_ int idx)
{
// 将传入的列信息对象存储在指定的数组索引位置
m_SDTColsInfo[idx] = sdtColInfo;
/* 为变更数据类型的列设置最小/最大值函数 */
// 根据新的数据类型获取并设置相应的最小/最大值函数
m_SDTColsMinMaxFunc[idx] = GetMinMaxFunc(m_NewTupDesc->attrs[sdtColInfo->attrno - 1].atttypid);
// 根据原始关系文件节点和列号创建文件节点对象,用于读取操作
CFileNode cFileNode(m_OldHeapRel->rd_node, (int)sdtColInfo->attrno, MAIN_FORKNUM);
m_SDTColsReader[idx] = New(CurrentMemoryContext) CUStorage(cFileNode);
/* 当表空间未变化时:
* 1. 不创建新的列存储单元文件,而是重用现有文件。
* 2. 覆盖空闲空间或直接追加。
*
* 表空间可能发生变化,因此创建新的文件节点对象
*/
RelFileNode wrtRelFileNode = m_OldHeapRel->rd_node;
wrtRelFileNode.spcNode = m_TargetTblspc; // 设置目标表空间
wrtRelFileNode.relNode = m_TargetRelFileNode; // 设置目标文件节点
CFileNode wrtCFileNode(wrtRelFileNode, (int)sdtColInfo->attrno, MAIN_FORKNUM);
m_SDTColsWriter[idx] = New(CurrentMemoryContext) CUStorage(wrtCFileNode);
// 如果表空间已变更
if (m_TblspcChanged) {
// 创建新的列存储单元文件,并记录日志
m_SDTColsWriter[idx]->CreateStorage(0, false);
CStoreRelCreateStorage(&wrtCFileNode.m_rnode,
sdtColInfo->attrno,
m_OldHeapRel->rd_rel->relpersistence,
m_OldHeapRel->rd_rel->relowner);
// 采用仅追加策略
m_SDTColAppendOffset[idx] = 0;
// 记录日志信息
ereport(LOG,
(errmsg("Column [SET TYPE]: %s(%u C%d) tblspc %u/%u/%u => %u/%u/%u",
RelationGetRelationName(m_OldHeapRel),
RelationGetRelid(m_OldHeapRel),
(sdtColInfo->attrno - 1),
m_OldHeapRel->rd_node.spcNode,
m_OldHeapRel->rd_node.dbNode,
m_OldHeapRel->rd_node.relNode,
m_TargetTblspc,
m_OldHeapRel->rd_node.dbNode,
m_TargetRelFileNode)));
} else {
// 设置列分配策略为仅追加
m_SDTColsWriter[idx]->SetAllocateStrategy(APPEND_ONLY);
// 构建追加策略下的空间缓存
CStoreAllocator::BuildColSpaceCacheForRel(m_OldHeapRel, &m_SDTColsInfo[idx]->attrno, 1);
}
// 注册需要变更数据类型的关系标识
CStoreAlterRegister* alterReg = GetCstoreAlterReg();
alterReg->Add(RelationGetRelid(m_OldHeapRel));
}
SetDataTypeDestroy 函数
SetDataTypeDestroy 函数是数据类型变更过程中资源管理的关键部分,它确保在数据类型设置完成后,所有分配的内存和资源被适当地释放。这包括释放用于读取和写入数据的存储对象、列信息数组、最小/最大值函数数组、列值数组、列值 NULL 标记数组,以及表空间变更时使用的追加偏移量数组。释放这些资源是防止内存泄露和维护系统稳定性的重要措施。
通过逐项释放在数据类型变更操作中创建的所有动态分配的资源,此函数帮助确保系统资源得到有效管理,避免因资源占用过多而影响系统性能。这种资源清理策略是长时间运行的系统和服务中常见的良好实践,特别是在涉及大量数据处理和频繁修改表结构的数据库管理系统中。函数源码如下所示:(路径:src\gausskernel\storage\cstore\cstore_rewrite.cpp
)
// 如果已分配资源(即有列的数据类型需要被设置)
// free all the unused resource after SET DATA TYPE.
void CStoreRewriter::SetDataTypeDestroy()
{
// 如果已分配资源(即有列的数据类型需要被设置)
if (m_SDTColsNum) {
// 遍历每个数据类型变更的列
for (int i = 0; i < m_SDTColsNum; ++i) {
// 删除并释放每列的读取存储对象
DELETE_EX(m_SDTColsReader[i]);
// 删除并释放每列的写入存储对象
DELETE_EX(m_SDTColsWriter[i]);
}
// 释放存储列信息的数组
pfree_ext(m_SDTColsInfo);
// 释放存储列读取对象指针的数组
pfree_ext(m_SDTColsReader);
// 释放存储最小/最大函数指针的数组
pfree_ext(m_SDTColsMinMaxFunc);
// 释放存储列写入对象指针的数组
pfree_ext(m_SDTColsWriter);
// 释放存储列值的数组
pfree_ext(m_SDTColValues);
// 释放存储列值是否为NULL的布尔数组
pfree_ext(m_SDTColIsNull);
// 如果表空间发生了变化,还需要释放存储追加偏移量的数组
if (m_TblspcChanged) {
pfree_ext(m_SDTColAppendOffset);
}
}
}
BeginRewriteCols 函数
BeginRewriteCols 函数负责初始化列重写操作的准备工作,包括为新增列和数据类型更改的列设置相关的初始化环节。此方法首先验证是否有列需要重写,然后根据具体的需求(如添加新列或更改数据类型),执行相应的初始化步骤。它确保了在实际执行重写操作前,所有必要的数据结构和管理策略都已就绪。
此外,此方法还负责创建和配置新的列描述符关系(cudesc relation),这是管理列存储表结构变更的关键部分。通过这样的设置,CStoreRewriter 能够高效且安全地处理复杂的表结构变更操作,确保数据的一致性和系统的稳定性。
void CStoreRewriter::BeginRewriteCols(_in_ int nRewriteCols, _in_ CStoreRewriteColumn** pRewriteCols,
_in_ const int* rewriteColsNum, _in_ bool* rewriteFlags)
{
// 确认传入的重写列数与新元组描述的属性数匹配
Assert(nRewriteCols == m_NewTupDesc->natts);
// 设置需要添加的列的数量
m_AddColsNum = rewriteColsNum[CSRT_ADD_COL];
// 设置需要更改数据类型的列的数量
m_SDTColsNum = rewriteColsNum[CSRT_SET_DATA_TYPE];
// 存储每列是否需要重写的标志
m_ColsRewriteFlag = rewriteFlags;
// 如果没有列需要重写,直接返回
if (!NeedRewrite())
return;
// 如果有列需要添加,执行添加列的初始化第一阶段
if (m_AddColsNum > 0) {
AddColumnInitPhrase1();
}
// 如果有列需要数据类型变更,执行设置数据类型的初始化第一阶段
if (m_SDTColsNum > 0) {
SetDataTypeInitPhase1();
}
// 初始化添加列和数据类型变更的计数器
int nAddCols = 0;
int nChangeCols = 0;
// 遍历所有需要重写的列
for (int i = 0; i < nRewriteCols; ++i) {
// 如果当前列不需要重写,跳过
if (pRewriteCols[i] == NULL) {
Assert(!rewriteFlags[i]);
continue;
}
// 断言当前列确实需要重写
Assert(rewriteFlags[i]);
// 如果当前列是新添加的,执行添加列的初始化第二阶段
if (pRewriteCols[i]->isAdded) {
AddColumnInitPhrase2(pRewriteCols[i], nAddCols);
++nAddCols;
} else if (!pRewriteCols[i]->isDropped) { // 如果当前列是需要更改数据类型的
SetDataTypeInitPhase2(pRewriteCols[i], nChangeCols);
++nChangeCols;
} else
/* description: future plan-dropped columns case */
;
}
// 断言添加的列和数据类型变更的列的数量与预期相符
Assert(nAddCols == m_AddColsNum);
Assert(nChangeCols == m_SDTColsNum);
// 为重写创建新的列描述符关系
Oid oldCuDescHeap = m_OldHeapRel->rd_rel->relcudescrelid;
m_NewCuDescHeap = make_new_heap(oldCuDescHeap, m_TargetTblspc);
// 打开新的列描述符关系,以独占方式访问
m_NewCudescRel = heap_open(m_NewCuDescHeap, AccessExclusiveLock);
// 创建用于批量插入新列描述符的对象
m_NewCudescBulkInsert = New(CurrentMemoryContext) HeapBulkInsert(m_NewCudescRel);
}
下面是 CStoreRewriter::BeginRewriteCols 函数的执行流程,使用中文伪代码的形式描述:
函数 BeginRewriteCols(输入: nRewriteCols, pRewriteCols, rewriteColsNum, rewriteFlags)
断言: nRewriteCols 应等于 m_NewTupDesc 的属性数
// 初始化需要添加的列数和需要更改数据类型的列数
m_AddColsNum = rewriteColsNum[添加列的索引]
m_SDTColsNum = rewriteColsNum[设置数据类型的索引]
m_ColsRewriteFlag = rewriteFlags
// 检查是否需要进行重写操作
如果 不需要重写:
返回
// 如果有列需要添加, 执行添加列的初始化第一阶段
如果 m_AddColsNum > 0:
执行 AddColumnInitPhrase1()
// 如果有列需要更改数据类型, 执行设置数据类型的初始化第一阶段
如果 m_SDTColsNum > 0:
执行 SetDataTypeInitPhase1()
// 初始化添加列和更改数据类型的计数器
初始化 nAddCols = 0
初始化 nChangeCols = 0
// 遍历所有需要重写的列
对于 i 从 0 到 nRewriteCols:
// 如果当前列不需要重写, 跳过此列
如果 pRewriteCols[i] 是空:
断言 rewriteFlags[i] 为假
继续下一次循环
// 断言当前列确实标记为需要重写
断言 rewriteFlags[i] 为真
// 根据列的状态执行相应的初始化
如果 pRewriteCols[i] 是新增的:
执行 AddColumnInitPhrase2(pRewriteCols[i], nAddCols)
nAddCols 自增
否则如果 pRewriteCols[i] 没有被丢弃:
执行 SetDataTypeInitPhase2(pRewriteCols[i], nChangeCols)
nChangeCols 自增
否则:
// 处理已丢弃的列的情况(未来计划)
// 断言添加和变更的列数与预期一致
断言 nAddCols 等于 m_AddColsNum
断言 nChangeCols 等于 m_SDTColsNum
// 为重写创建新的列描述符关系
获取旧的列描述符关系ID
m_NewCuDescHeap = 创建新的堆(oldCuDescHeap, m_TargetTblspc)
m_NewCudescRel = 打开新的列描述符关系(m_NewCuDescHeap, 独占访问)
m_NewCudescBulkInsert = 创建新的批量插入对象(m_NewCudescRel)
结束函数
RewriteColsData 函数
RewriteColsData 函数主要负责执行列存储表中列数据的重写操作。这个函数的执行流程首先检查是否需要进行重写,如果不需要,则直接返回;否则,它将遍历所有的列存储单元(CU),并对每个CU进行处理。该函数通过系统扫描来检索与每个 CU 关联的元组信息,这包括既有数据的修改和虚拟删除标记的处理。对于需要添加新列或者需要数据类型变更的列,函数会执行相应的初始化和设置过程,确保数据的正确迁移和更新。
在数据重写过程中,函数会处理可能存在的 TOAST 数据,确保这些数据在新旧表之间正确转移,同时还会通过内容交换来处理 TOAST 表数据。该函数还管理了大量的内存操作,包括使用临时内存上下文来优化内存使用效率,并在每次迭代结束时重置这些内存上下文,以避免内存泄漏。
一旦完成所有数据的处理,RewriteColsData 将所有更改刷新到磁盘,并关闭与操作相关的数据库关系和索引。此外,它还负责清理所有的临时资源,包括批量插入对象和内存上下文,以确保系统资源的合理使用和释放。最终,通过日志记录关键的操作细节,提供对过程的审计和追踪,增强了系统操作的透明度和可追溯性。
void CStoreRewriter::RewriteColsData()
{
// 检查是否需要进行列重写,如果不需要则直接退出函数
if (!NeedRewrite())
return;
// 设置第一个有效的CU ID,通常是第一个CU ID加1
uint32 nextCuId = FirstCUID + 1;
// 获取关联到旧表的CU描述符的OID
Oid oldCudescOid = m_OldHeapRel->rd_rel->relcudescrelid;
// 获取当前最大的CU ID
uint32 maxCuId = CStore::GetMaxCUID(oldCudescOid, m_OldTupDesc);
// 打开旧的CU描述符关系表,并加上独占锁
Relation oldCudescHeap = heap_open(oldCudescOid, AccessExclusiveLock);
// 打开旧的CU描述符索引关系表,并加上独占锁
Relation oldCudescIndex = index_open(oldCudescHeap->rd_rel->relcudescidx, AccessExclusiveLock);
// 获取旧的CU描述符的元组描述符
TupleDesc oldCudescTupDesc = oldCudescHeap->rd_att;
// 获取旧的CU描述符关系表的TOAST表的OID
Oid oldCudescToastId = oldCudescHeap->rd_rel->reltoastrelid;
// 初始化布尔变量,用于后续检查是否值为NULL
bool isnull = false;
// 初始化ScanKey数组,用于数据库扫描操作
ScanKeyData key[ARRAY_2_LEN];
// 获取旧元组描述的属性数量
int nOldAttrs = m_OldTupDesc->natts;
int rc = 0;
// 分配内存存储CU描述元组
int const cudescTupsLen = sizeof(HeapTuple) * nOldAttrs;
HeapTuple* cudescTups = (HeapTuple*)palloc(cudescTupsLen);
HeapTuple virtualDelTup = NULL;
HeapTuple tup = NULL;
// 创建一个循环内存上下文,用于数据重写过程中的内存管理
MemoryContext oneLoopMemCnxt = AllocSetContextCreate(CurrentMemoryContext,
"Cstore Rewriting Memory",
ALLOCSET_DEFAULT_MINSIZE,
ALLOCSET_DEFAULT_INITSIZE,
ALLOCSET_DEFAULT_MAXSIZE);
// 遍历所有的CU ID,直到达到最大CU ID
while (nextCuId <= maxCuId) {
// 检查是否有中断发生,如果有,处理中断
CHECK_FOR_INTERRUPTS();
// 重置之前循环使用的内存,清空上一次的数据
MemoryContextReset(oneLoopMemCnxt);
// 将CU描述元组数组清零,准备新的数据加载
rc = memset_s(cudescTups, cudescTupsLen, 0, cudescTupsLen);
securec_check(rc, "", "");
virtualDelTup = NULL;
tup = NULL;
// 切换到私有内存上下文,为数据加载和处理准备环境
AutoContextSwitch newMemCnxt(oneLoopMemCnxt);
int scannedTuples = 0;
// 初始化ScanKey,设置列ID和CU ID的扫描键
ScanKeyInit(&key[0], (AttrNumber)CUDescColIDAttr, BTEqualStrategyNumber, F_INT4EQ, Int32GetDatum(1));
ScanKeyInit(&key[1], (AttrNumber)CUDescCUIDAttr, BTEqualStrategyNumber, F_OIDEQ, UInt32GetDatum(nextCuId));
// 遍历所有的属性,跳过已删除的列
for (int i = 0; i < nOldAttrs; i++) {
if (unlikely(m_OldTupDesc->attrs[i].attisdropped))
continue;
key[0].sk_argument = Int32GetDatum(i + 1);
/*
* 开始有序的系统扫描
* 如果一些现有的列存储单元(CU)数据将被重写,
* 那么已删除的元组将被跳过,并可能简单地标记为 NULL。
* 因此,需要记住具有相同 CU ID 的所有元组以及虚拟删除的元组。
*/
SysScanDesc oldCuDescScan = systable_beginscan_ordered(oldCudescHeap, oldCudescIndex, SnapshotNow, ARRAY_2_LEN, key);
// 如果找到匹配的元组,则复制并保存
if ((tup = systable_getnext_ordered(oldCuDescScan, ForwardScanDirection)) != NULL) {
Assert((i + 1) == DatumGetInt32(fastgetattr(tup, CUDescColIDAttr, oldCudescTupDesc, &isnull)));
Assert(!isnull);
Assert(nextCuId == DatumGetUInt32(fastgetattr(tup, CUDescCUIDAttr, oldCudescTupDesc, &isnull)));
Assert(!isnull);
Assert(scannedTuples < nOldAttrs);
// 必须复制此元组,因为稍后将在WHILE循环之外访问它。
cudescTups[i] = heap_copytuple(tup);
++scannedTuples;
}
systable_endscan_ordered(oldCuDescScan);
oldCuDescScan = NULL;
}
// 如果没有扫描到元组,意味着这个CU ID没有数据,继续下一个ID
if (0 == scannedTuples) {
++nextCuId;
continue;
}
// 设置查找键的参数为虚拟删除列ID,准备进行系统扫描
key[0].sk_argument = Int32GetDatum(VitrualDelColID);
// 开始有序的系统扫描,使用当前快照查找与上述键匹配的元组
oldCuDescScan = systable_beginscan_ordered(oldCudescHeap, oldCudescIndex, SnapshotNow, ARRAY_2_LEN, key);
// 如果能在系统扫描中找到对应的元组
if ((tup = systable_getnext_ordered(oldCuDescScan, ForwardScanDirection)) != NULL) {
// 确保之前没有找到过虚拟删除的元组(保证唯一性)
Assert(virtualDelTup == NULL);
// 复制找到的元组,用于后续处理
virtualDelTup = heap_copytuple(tup);
}
// 结束系统扫描
systable_endscan_ordered(oldCuDescScan);
// 清空扫描指针,准备下次使用或释放
oldCuDescScan = NULL;
// 如果没有找到虚拟删除的元组,抛出错误
if (virtualDelTup == NULL) {
Assert(false);
ereport(ERROR,
(errcode(ERRCODE_NO_DATA),
errmsg("Relation \'%s\' virtual cudesc tuple(cuid %u) not found",
RelationGetRelationName(m_OldHeapRel),
nextCuId)));
}
// 从虚拟删除的元组中获取行数属性的值
uint32 rowCount = DatumGetUInt32(fastgetattr(virtualDelTup, CUDescRowCountAttr, oldCudescTupDesc, &isnull));
// 确保获取行数属性值时没有发生错误
Assert(!isnull);
// 初始化指向删除掩码数据的指针
char* delMaskDataPtr = NULL;
// 初始化整个CU是否被删除的标志
bool wholeCuIsDeleted = false;
// 从虚拟删除的元组中获取指向删除掩码的数据指针
char* delMaskVarDatum =
DatumGetPointer(fastgetattr(virtualDelTup, CUDescCUPointerAttr, oldCudescTupDesc, &isnull));
// 如果删除成功掩码数据存在
if (!isnull) {
// 解压可能被压缩的TOAST数据
delMaskVarDatum = (char*)PG_DETOAST_DATUM(delMaskVarDatum);
// 验证解压后的数据大小是否符合预期
Assert(VARSIZE_ANY_EXHDR(delMaskVarDatum) == bitmap_size(rowCount));
// 获取删除掩码数据的实际指针
delMaskDataPtr = VARDATA_ANY(delMaskVarDatum);
// 判断整个CU是否被删除
wholeCuIsDeleted = CStore::IsTheWholeCuDeleted(delMaskDataPtr, rowCount);
}
// 处理添加列的操作
AddColumns(nextCuId, rowCount, wholeCuIsDeleted);
// 处理数据类型更改操作
SetDataType(nextCuId, cudescTups, oldCudescTupDesc, delMaskDataPtr, rowCount, wholeCuIsDeleted);
// 将剩余的CU描述元组和虚拟删除元组插入到CU描述表中
for (int i = 0; i < nOldAttrs; ++i) {
if (m_OldTupDesc->attrs[i].attisdropped || m_NewTupDesc->attrs[i].attisdropped)
continue;
if (!m_ColsRewriteFlag[i]) {
m_NewCudescBulkInsert->BulkInsertCopy(cudescTups[i]);
}
}
// 最后复制虚拟删除的元组
m_NewCudescBulkInsert->EnterBulkMemCnxt();
m_NewCudescBulkInsert->BulkInsert(CStore::FormVCCUDescTup(
oldCudescTupDesc, delMaskDataPtr, nextCuId, rowCount, GetCurrentTransactionIdIfAny()));
m_NewCudescBulkInsert->LeaveBulkMemCnxt();
++nextCuId;
}
// 刷新所有列存储(CU)数据,确保所有更改都被持久化到磁盘
FlushAllCUData();
// 完成批量插入操作,确保所有插入的数据都已提交
m_NewCudescBulkInsert->Finish();
// 删除批量插入对象,释放相关资源
DELETE_EX(m_NewCudescBulkInsert);
// 删除用于数据重写的临时内存上下文,清理过程中使用的临时内存
MemoryContextDelete(oneLoopMemCnxt);
// 释放存储列描述元组的内存
pfree_ext(cudescTups);
// 如果表空间发生了变更,记录相应的日志
if (m_TblspcChanged) {
ereport(LOG,
(errmsg("Row [Rewrite]: %s(%u) tblspc %u/%u/%u => %u/%u/%u",
RelationGetRelationName(oldCudescHeap),
RelationGetRelid(oldCudescHeap),
oldCudescHeap->rd_node.spcNode,
oldCudescHeap->rd_node.dbNode,
oldCudescHeap->rd_node.relNode,
m_NewCudescRel->rd_node.spcNode,
m_NewCudescRel->rd_node.dbNode,
m_NewCudescRel->rd_node.relNode)));
}
// 重置新CU描述关系的TOAST表OID,清理前的整理工作
m_NewCudescRel->rd_toastoid = InvalidOid;
// 关闭新的CU描述关系,释放锁
heap_close(m_NewCudescRel, NoLock);
// 将指针设为NULL,避免悬挂引用
m_NewCudescRel = NULL;
// 关闭旧的CU描述关系,释放锁
heap_close(oldCudescHeap, NoLock);
// 关闭旧的CU描述索引关系,释放锁
index_close(oldCudescIndex, NoLock);
// 完成重写CU描述关系的过程,交换旧的CU描述表和新的CU描述表
finish_heap_swap(m_OldHeapRel->rd_rel->relcudescrelid, m_NewCuDescHeap, false,
swapToastByContent, false, m_NewCudescFrozenXid, FirstMultiXactId);
}
EndRewriteCols 函数
EndRewriteCols 函数是用于在列重写操作完成后执行的清理工作。此函数首先检查是否存在需要重写的列,如果没有,则立即退出函数。这种检查是为了确保不进行不必要的清理操作,从而优化性能。
如果存在需要重写的列,函数将继续执行两个重要的清理步骤:AddColumnDestroy 和 SetDataTypeDestroy。这两个函数分别负责清理在列添加和数据类型更改过程中创建的各种资源。AddColumnDestroy 主要用于释放添加新列时分配的内存和其他资源,而 SetDataTypeDestroy 用于清理在数据类型变更过程中分配的资源。
通过这样的设计,EndRewriteCols 函数确保在重写列的操作完全结束后,所有的临时资源都得到了妥善的管理和释放,从而维护了系统的稳定性和高效性。这也有助于防止内存泄漏,确保数据库在长时间运行后仍能保持良好的性能。函数源码如下所示:(路径:src\gausskernel\storage\cstore\cstore_rewrite.cpp
)
void CStoreRewriter::EndRewriteCols()
{
// 将列重写标志数组的指针设置为NULL,表示不再引用这个数组
m_ColsRewriteFlag = NULL;
// 如果当前没有需要重写的列,则直接返回
if (!NeedRewrite())
return;
// 执行清理工作,结束列重写操作
// 调用 AddColumnDestroy 函数来清理与添加列相关的资源
AddColumnDestroy();
// 调用 SetDataTypeDestroy 函数来清理与设置数据类型相关的资源
SetDataTypeDestroy();
}
AddColumnDestroy 函数
AddColumnDestroy 函数的作用是在完成添加新列的操作后,释放在此过程中分配的所有资源。这包括释放用于存储列信息、列存储对象、列的最小/最大值函数以及列的追加偏移量等资源的内存。此函数确保了在列添加操作后不会有资源泄漏,维护了系统的健康和性能。
在数据库的列存储系统中,添加列可能涉及到复杂的内部操作,包括数据的分配和内存管理。通过在操作完成后调 AddColumnDestroy,系统能够回收所有相关资源,从而避免不必要的内存占用和潜在的性能问题。这种资源管理策略是保持数据库长时间高效运行的关键部分。函数源码如下所示:(路径:src\gausskernel\storage\cstore\cstore_rewrite.cpp
)
// free all the unused resource after ADD COLUMN.
void CStoreRewriter::AddColumnDestroy()
{
// 如果有添加的列(即添加列数量大于0)
if (m_AddColsNum > 0) {
// 遍历所有添加的列
for (int i = 0; i < m_AddColsNum; ++i) {
// 使用 DELETE_EX 宏来安全地删除每个列的存储对象,并释放相关内存
DELETE_EX(m_AddColsStorage[i]);
}
// 释放存储列信息指针的内存
pfree_ext(m_AddColsInfo);
// 释放存储列存储对象指针的内存
pfree_ext(m_AddColsStorage);
// 释放存储最小/最大值函数指针的内存
pfree_ext(m_AddColsMinMaxFunc);
// 释放存储追加偏移量的内存
pfree_ext(m_AddColsAppendOffset);
}
}
SetDataTypeDestroy 函数
SetDataTypeDestroy 函数在完成列的数据类型设置后执行,目的是释放在该过程中分配的所有资源。这包括对每个列的读写存储对象、列信息、最小最大值函数、中间数据值、null标志以及追加偏移量(如果表空间发生变化时分配的)等资源的释放。
在数据库的列存储结构中,更改列的数据类型是一个复杂的操作,涉及到多种资源的分配和管理。SetDataTypeDestroy 通过系统地释放这些资源,确保了内存的有效管理,防止了资源泄漏,从而帮助维护了系统的稳定性和性能。这个清理函数是确保系统在完成重要操作后能够回到一个干净、稳定状态的关键步骤,特别是在频繁修改表结构的环境中尤为重要。函数源码如下所示:(路径:src\gausskernel\storage\cstore\cstore_rewrite.cpp
)
// free all the unused resource after SET DATA TYPE.
void CStoreRewriter::SetDataTypeDestroy()
{
// 如果有进行数据类型更改的列
if (m_SDTColsNum) {
// 遍历所有需要更改数据类型的列
for (int i = 0; i < m_SDTColsNum; ++i) {
// 使用 DELETE_EX 宏安全地删除和释放每个列的读取存储对象
DELETE_EX(m_SDTColsReader[i]);
// 使用 DELETE_EX 宏安全地删除和释放每个列的写入存储对象
DELETE_EX(m_SDTColsWriter[i]);
}
// 释放存储列信息的内存
pfree_ext(m_SDTColsInfo);
// 释放存储列读取对象指针的内存
pfree_ext(m_SDTColsReader);
// 释放存储最小/最大值函数指针的内存
pfree_ext(m_SDTColsMinMaxFunc);
// 释放存储列写入对象指针的内存
pfree_ext(m_SDTColsWriter);
// 释放存储数据类型转换中间值的内存
pfree_ext(m_SDTColValues);
// 释放存储数据是否为null的标志的内存
pfree_ext(m_SDTColIsNull);
// 如果表空间发生了变化,也释放追加偏移量的内存
if (m_TblspcChanged) {
pfree_ext(m_SDTColAppendOffset);
}
}
}
处理 Cu 相关函数
HandleCuWithSameValue 函数
HandleCuWithSameValue 函数用于处理具有相同值的列更新。它首先检查是否需要更新列存储单元(CU),如果需要,它会创建一个新的 CU 实例,快速生成新的 CU 数据,然后根据传入的 append 参数决定是追加数据还是使用现有空间。数据压缩后会被写入磁盘,并且相关的 CU 描述信息会被更新。如果是追加模式,CU 描述的指针会被更新到新的偏移位置。如果操作涉及高可用性复制,更新的数据也会被推送到复制队列。此函数确保数据的一致性和高效处理,是列存储管理中的一个关键环节,尤其在处理大量具有相同值的数据时显得尤为重要。函数源码如下所示:(路径:src\gausskernel\storage\cstore\cstore_rewrite.cpp
)
// 函数模板定义,用于处理具有相同值的列存储单元(CU)更新操作
template <bool append>
void CStoreRewriter::HandleCuWithSameValue(_in_ Relation rel, _in_ Form_pg_attribute pColAttr, _in_ Datum colValue,
_in_ bool colIsNull, _in_ FuncSetMinMax MinMaxFunc, _in_ CUStorage* colStorage, __inout CUDesc* pColCudescData,
__inout CUPointer* pOffset)
{
// 步骤1: 设置CU描述的模式。如果返回true,说明需要将新数据写入CU文件。
if (CStore::SetCudescModeForTheSameVal(colIsNull, MinMaxFunc, pColAttr->attlen, colValue, pColCudescData)) {
// 创建新的CU对象
CU* cuPtr = New(CurrentMemoryContext) CU(pColAttr->attlen, pColAttr->atttypmod, pColAttr->atttypid);
// 步骤2: 快速形成新的CU数据。
CStoreRewriter::FormCuDataForTheSameVal(cuPtr, pColCudescData->row_count, colValue, pColAttr);
// 步骤3: 在内存中压缩CU数据,并准备将它们写入CU文件。
int16 compressing_modes = 0;
heaprel_set_compressing_modes(rel, &compressing_modes);
CStoreRewriter::CompressCuData(cuPtr, pColCudescData, pColAttr, compressing_modes);
// 步骤4: 为这个CU数据分配文件空间。
if (append) {
// 如果是追加模式,则更新CU描述的指针并调整偏移量。
pColCudescData->cu_pointer = *pOffset;
*pOffset += pColCudescData->cu_size;
} else {
// 如果不是追加模式,从列存储中分配空间。
Assert(pOffset == NULL);
pColCudescData->cu_pointer = colStorage->AllocSpace(pColCudescData->cu_size);
}
// 步骤5: 将CU数据保存到磁盘文件中。
CStoreRewriter::SaveCuData(cuPtr, pColCudescData, colStorage);
// 步骤6: 关于高可用性,将CU数据推送到复制队列中
CStoreCUReplication((this->m_TblspcChanged ? this->m_CUReplicationRel : rel),
pColAttr->attnum,
cuPtr->m_compressedBuf,
pColCudescData->cu_size,
pColCudescData->cu_pointer);
// 删除CU对象
DELETE_EX(cuPtr);
} else {
// 如果不需要写入新数据,则确认CU描述的大小和指针都为0
Assert(pColCudescData->cu_size == 0);
Assert(pColCudescData->cu_pointer == 0);
}
}
以下是该函数执行流程的伪代码:
函数 HandleCuWithSameValue(append, rel, pColAttr, colValue, colIsNull, MinMaxFunc, colStorage, pColCudescData, pOffset):
如果 SetCudescModeForTheSameVal 返回 true:
创建一个新的 CU 对象 cuPtr
调用 FormCuDataForTheSameVal 为相同值形成新的 CU 数据
获取压缩模式 compressing_modes
调用 CompressCuData 压缩 CU 数据
如果 append 为 true:
设置 pColCudescData 的 cu_pointer 为 pOffset
更新 pOffset 为下一次写入的位置
否则:
断言 pOffset 为空
分配文件空间给该 CU 数据并将其存储在 colStorage 中
调用 SaveCuData 将 CU 数据保存到文件中
如果需要 HA:
调用 CStoreCUReplication 将 CU 数据推送到复制队列
删除 cuPtr
否则:
断言 pColCudescData 的 cu_size 和 cu_pointer 均为 0
FormCuDataForTheSameVal 函数
FormCuDataForTheSameVal 函数根据给定的新列值、列属性和行数,在内存中为相同值的列单元(CU)形成新的 CU 数据。首先,它计算新列值的大小,然后计算出初始化大小,接着检查初始化大小是否溢出。如果没有溢出,则初始化 CU,并追加数据;否则,报告错误,说明列的默认值大小过大。
该函数与 HandleCuWithSameValue 函数的关系是:HandleCuWithSameValue 函数是一个更高级别的函数,它调用了 FormCuDataForTheSameVal 函数来为相同值的列单元形成新的 CU 数据。HandleCuWithSameValue 函数负责处理整个 CU 的流程,而 FormCuDataForTheSameVal 函数则负责具体的 CU 数据形成过程。函数源码如下所示:(路径:src\gausskernel\storage\cstore\cstore_rewrite.cpp
)
// 为相同值形成新的 CU 数据。
void CStoreRewriter::FormCuDataForTheSameVal(CU* cuPtr, int rowsCntInCu, Datum newColVal, Form_pg_attribute newColAttr)
{
// 计算新列值的大小。
const Size valSize = datumGetSize(newColVal, newColAttr->attbyval, newColAttr->attlen);
// 计算初始化大小,不跳过已删除的堆元组。
const Size initSize = valSize * rowsCntInCu;
// 检查 *initSize* 是否溢出。
if (initSize < UINT32_MAX) {
// 到这里,安全地将 *initSize* 转换为 uint32。
cuPtr->InitMem((uint32)initSize, rowsCntInCu, false);
// 追加 CU 数据。
CU::AppendCuData(newColVal, rowsCntInCu, newColAttr, cuPtr);
return;
}
// 报告错误,因为列的默认值大小过大。
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("column \"%s\" needs too many memory", NameStr(newColAttr->attname)),
errdetail("its size of default value is too big.")));
}
CompressCuData 函数
CompressCuData 函数的作用是在重写 CU 文件期间压缩 CU 数据。它接收压缩模式值、CU 描述、CU 对象和新列的属性作为输入,并使用临时的压缩选项和 cu_tmp_compress_info 来设置 CU 的临时信息。然后,它设置 CU 的 magic 值,并对 CU 数据进行压缩,最后获取压缩后的 CU 大小。
该函数与 HandleCuWithSameValue 函数的关系是:HandleCuWithSameValue 函数调用了 CompressCuData 函数来对 CU 数据进行压缩。在处理相同值的列单元时,需要对数据进行压缩以节省存储空间。 CompressCuData 函数负责具体的压缩过程。函数源码如下所示:(路径:src\gausskernel\storage\cstore\cstore_rewrite.cpp
)
/*
* @Description: 压缩 CU 数据
* @IN/ compressing_modes: 压缩模式值
* @IN/OUT cuDesc: CU 描述
* @IN/OUT cuPtr: CU 对象
* @IN newColAttr: 新列的属性
* @See also: 无
*/
void CStoreRewriter::CompressCuData(CU* cuPtr, CUDesc* cuDesc, Form_pg_attribute newColAttr, int16 compressing_modes)
{
// 在重写 CU 文件期间,我们将使用临时的压缩选项和 cu_tmp_compress_info
compression_options tmp_filter;
tmp_filter.reset();
cu_tmp_compress_info cu_temp_info;
cu_temp_info.m_options = &tmp_filter;
// 如果不需要重新计算最小值和最大值,则设置 m_valid_minmax 为 true
cu_temp_info.m_valid_minmax = !NeedToRecomputeMinMax(newColAttr->atttypid);
if (cu_temp_info.m_valid_minmax) {
// 如果 m_valid_minmax 有效,则设置最小值和最大值
cu_temp_info.m_min_value = ConvertToInt64Data(cuDesc->cu_min, newColAttr->attlen);
cu_temp_info.m_max_value = ConvertToInt64Data(cuDesc->cu_max, newColAttr->attlen);
}
// 将 cuPtr 的临时信息设置为 cu_temp_info
cuPtr->m_tmpinfo = &cu_temp_info;
// 设置 CU 的魔术值
cuPtr->SetMagic(cuDesc->magic);
// 压缩 CU 数据
cuPtr->Compress(cuDesc->row_count, compressing_modes, ALIGNOF_CUSIZE);
// 获取压缩后的 CU 大小
cuDesc->cu_size = cuPtr->GetCUSize();
// 断言 CU 大小大于 0
Assert(cuDesc->cu_size > 0);
}
SetDataTypeHandleFullNullCu 函数
SetDataTypeHandleFullNullCu 函数的作用是处理全空(FULL NULL)的列单元的数据类型。它接收一个原列描述符 oldColCudesc,并将其复制到新的列描述符 newColCudesc 中,然后将新的列描述符的 magic 字段更新为当前事务 ID(如果存在)。在处理全空的列单元时,由于数据本身为空,所以只需更新描述符的元数据信息。函数源码如下所示:(路径:src\gausskernel\storage\cstore\cstore_rewrite.cpp
)
FORCE_INLINE void CStoreRewriter::SetDataTypeHandleFullNullCu(_in_ CUDesc* oldColCudesc, _out_ CUDesc* newColCudesc)
{
// 断言原列描述符是一个全空(FULL NULL)的列单元。
Assert(oldColCudesc->IsNullCU());
// 断言原列描述符不是一个相同值(SAME VALUE)的列单元。
Assert(!oldColCudesc->IsSameValCU());
// 如果原列描述符是一个全空(FULL NULL)的列单元,则无需操作。
// 这里只需要更新magic字段。
// 将新列描述符设置为与原列描述符相同的值,并更新magic字段为当前事务ID(如果存在)。
*newColCudesc = *oldColCudesc;
newColCudesc->magic = GetCurrentTransactionIdIfAny();
}
SetDataTypeHandleSameValCu函数
SetDataTypeHandleSameValCu 函数的作用是处理相同值(SAME VALUE)的列单元的数据类型。它接收一个列描述符 oldColCudesc,并根据其中的信息来计算新的列单元的数据。首先,它获取要处理的列的相关信息,并构造一个虚假的元组以便计算新值。然后,根据计算得到的新值,调用 HandleCuWithSameValue 函数处理相同值的列单元,以更新数据。函数源码如下所示:(路径:src\gausskernel\storage\cstore\cstore_rewrite.cpp
)
/*
* @Description: 处理相同值的列单元的数据类型,根据新值计算并更新 CU 数据
* @IN sdtIndex: 设置数据类型的列索引
* @IN oldColCudesc: 原列描述符
* @OUT newColCudesc: 新列描述符
* @See also: 无
*/
void CStoreRewriter::SetDataTypeHandleSameValCu(
_in_ int sdtIndex, _in_ CUDesc* oldColCudesc, _out_ CUDesc* newColCudesc)
{
// 断言原列描述符不是一个全空(NULL)的列单元。
Assert(!oldColCudesc->IsNullCU());
// 断言原列描述符是一个相同值(SAME VALUE)的列单元。
Assert(oldColCudesc->IsSameValCU());
// 将新列描述符的 cu_id、row_count 字段设置为与原列描述符相同的值,并将magic字段更新为当前事务ID(如果存在)。
newColCudesc->cu_id = oldColCudesc->cu_id;
newColCudesc->row_count = oldColCudesc->row_count;
newColCudesc->magic = GetCurrentTransactionIdIfAny();
// 获取设置数据类型的列信息。
CStoreRewriteColumn* setDataTypeColInfo = m_SDTColsInfo[sdtIndex];
// 创建一个虚假的 TupleTableSlot。
TupleTableSlot* fakeSlot = MakeSingleTupleTableSlot(m_OldTupDesc);
// 获取要处理的属性索引。
int attrIndex = setDataTypeColInfo->attrno - 1;
Form_pg_attribute pColOldAttr = &m_OldTupDesc->attrs[attrIndex];
Form_pg_attribute pColNewAttr = &m_NewTupDesc->attrs[attrIndex];
// 限制运行时使用的内存大小。
MemoryContext oldCxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(m_estate));
// 如果新值不应依赖于任何其他现有列或其现有值,则构造一个虚假的元组。
bool shouldFree = false;
Datum attOldVal = CStore::CudescTupGetMinMaxDatum(oldColCudesc, pColOldAttr, true, &shouldFree);
HeapTuple fakeTuple = CreateFakeHeapTup(m_OldTupDesc, attrIndex, attOldVal);
(void)ExecStoreTuple(fakeTuple, fakeSlot, InvalidBuffer, false);
m_econtext->ecxt_scantuple = fakeSlot;
// 计算新值,如果新值为空且列不允许为空,则报错。
Assert(setDataTypeColInfo->newValue);
bool attNewIsNull = false;
Datum attNewValue = ExecEvalExpr(setDataTypeColInfo->newValue->exprstate, m_econtext, &attNewIsNull);
if (attNewIsNull && setDataTypeColInfo->notNull) {
ereport(ERROR,
(errcode(ERRCODE_NOT_NULL_VIOLATION),
errmsg("column \"%s\" contains null values", NameStr(pColOldAttr->attname)),
errdetail("existing data violate the NOT NULL constraint.")));
}
// 根据是否需要更改表空间,调用 HandleCuWithSameValue 函数处理相同值的列单元。
if (m_TblspcChanged) {
HandleCuWithSameValue<true>(m_OldHeapRel,
pColNewAttr,
attNewValue,
attNewIsNull,
m_SDTColsMinMaxFunc[sdtIndex],
m_SDTColsWriter[sdtIndex],
newColCudesc,
m_SDTColAppendOffset + sdtIndex);
} else {
HandleCuWithSameValue<false>(m_OldHeapRel,
pColNewAttr,
attNewValue,
attNewIsNull,
m_SDTColsMinMaxFunc[sdtIndex],
m_SDTColsWriter[sdtIndex],
newColCudesc,
NULL);
}
// 如果需要释放内存,则释放。
if (shouldFree) {
pfree(DatumGetPointer(attOldVal));
attOldVal = (Datum)0;
}
heap_freetuple(fakeTuple);
fakeTuple = NULL;
ExecDropSingleTupleTableSlot(fakeSlot);
fakeSlot = NULL;
(void)MemoryContextSwitchTo(oldCxt);
}
SetDataTypeHandleNormalCu 函数
SetDataTypeHandleNormalCu 函数实现了将旧列数据类型转换为新列数据类型的功能。它首先加载旧列的数据,然后逐个转换为新的数据类型,并在此过程中考虑空值情况。接着,它将转换后的数据写入新的列文件中,并确保根据需要进行数据压缩,并将数据推送到复制队列,以便于数据的备份和同步。整体上,该代码负责将数据库中的旧列数据按照新的数据类型进行处理和迁移,以满足数据模式变更的需求。具体而言,它执行以下操作:
- 加载旧列的一个列单元(Column Unit,CU)数据,这是一组连续的行数据。
- 将旧列中的每个值转换为新的数据类型,同时考虑是否存在空值。
- 根据转换后的值和空值情况,形成新的列单元数据。
- 如果需要,将新的列单元数据压缩,并根据压缩模式设置相应的压缩方式。
- 将新的列单元数据写入新的列单元文件,其中可能会利用旧列单元文件中的空闲空间。
- 将新的列单元数据推送到复制队列,以便进行数据复制。
函数源码如下所示:(路径:src\gausskernel\storage\cstore\cstore_rewrite.cpp
)
/*
* @Description: 处理普通的列单元,加载旧列的一个 CU,将每个值转换为新数据类型,然后写入新的 CU 文件。
* @IN sdtIndex: 设置数据类型的列索引
* @IN delMaskDataPtr: 删除掩码数据指针
* @IN oldColCudesc: 旧列的 CU 描述
* @OUT newColCudesc: 新列的 CU 描述
*/
void CStoreRewriter::SetDataTypeHandleNormalCu(
_in_ int sdtIndex, _in_ const char* delMaskDataPtr, _in_ CUDesc* oldColCudesc, _out_ CUDesc* newColCudesc)
{
// 断言旧列的 CU 不是全空(NULL)的列单元。
Assert(!oldColCudesc->IsNullCU());
// 断言旧列的 CU 不是相同值(SAME VALUE)的列单元。
Assert(!oldColCudesc->IsSameValCU());
// 获取设置数据类型的列信息。
CStoreRewriteColumn* setDataTypeColInfo = m_SDTColsInfo[sdtIndex];
// 创建一个虚假的 TupleTableSlot。
TupleTableSlot* fakeSlot = MakeSingleTupleTableSlot(m_OldTupDesc);
// 获取要处理的属性索引。
int attrIndex = setDataTypeColInfo->attrno - 1;
Form_pg_attribute pColOldAttr = &m_OldTupDesc->attrs[attrIndex];
Form_pg_attribute pColNewAttr = &m_NewTupDesc->attrs[attrIndex];
/* 其他字段稍后设置。 */
int rowsCntInCu = oldColCudesc->row_count;
Assert(rowsCntInCu <= RelMaxFullCuSize);
newColCudesc->cu_id = oldColCudesc->cu_id;
newColCudesc->row_count = oldColCudesc->row_count;
newColCudesc->magic = GetCurrentTransactionIdIfAny();
/* 加载旧 CU 的所有值。 */
CU* oldCu = LoadSingleCu::LoadSingleCuData(oldColCudesc,
attrIndex,
pColOldAttr->attlen,
pColOldAttr->atttypmod,
pColOldAttr->atttypid,
m_OldHeapRel,
m_SDTColsReader[sdtIndex]);
// 声明一个指针数组,用于存储获取值的函数指针
GetValFunc getValFuncPtr[1];
// 初始化获取值的函数指针数组
InitGetValFunc(pColOldAttr->attlen, getValFuncPtr, 0);
// 根据旧列单元是否含有空值来确定获取值的函数指针的索引
int oldCuGetValFuncId = oldCu->HasNullValue() ? 1 : 0;
// 标志是否为第一次处理值
bool firstFlag = true;
// 标志旧列单元是否全为空值
bool fullNull = true;
// 标志新列单元是否含有空值
bool hasNull = false;
// 记录可变长度字符串的最大长度
int maxVarStrLen = 0;
/* 切换内存上下文。 */
MemoryContext oldCxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(m_estate));
for (int cnt = 0; cnt < rowsCntInCu; ++cnt) {
if ((delMaskDataPtr && IsBitmapSet((unsigned char*)delMaskDataPtr, (uint32)cnt)) || oldCu->IsNull(cnt)) {
/*
* case 1: 旧值已删除,或
* case 2: 旧值为空值。
* 因此跳过旧数据,并在新 CU 中设置为 NULL。
*/
m_SDTColValues[cnt] = (Datum)0;
m_SDTColIsNull[cnt] = true;
hasNull = true;
continue;
}
/*
* 因为 getValFuncPtr[0]() 不处理 NULL 值,所以
* 我们必须先考虑 NULL 值。
*/
Datum attOldVal = getValFuncPtr[0][oldCuGetValFuncId](oldCu, cnt);
HeapTuple fakeTuple = CreateFakeHeapTup(m_OldTupDesc, attrIndex, attOldVal);
(void)ExecStoreTuple(fakeTuple, fakeSlot, InvalidBuffer, false);
m_econtext->ecxt_scantuple = fakeSlot;
m_SDTColIsNull[cnt] = false;
m_SDTColValues[cnt] =
ExecEvalExpr(setDataTypeColInfo->newValue->exprstate, m_econtext, (m_SDTColIsNull + cnt));
if (!m_SDTColIsNull[cnt]) {
fullNull = false;
if (m_SDTColsMinMaxFunc[sdtIndex]) {
/* 计算此列所有新值的最小/最大值。 */
m_SDTColsMinMaxFunc[sdtIndex](m_SDTColValues[cnt], newColCudesc, &firstFlag);
/* 如果新数据类型没有可变长度,则不关心 *maxVarStrLen*。 */
if (pColNewAttr->attlen < 0) {
maxVarStrLen = Max(maxVarStrLen,
(int)datumGetSize(m_SDTColValues[cnt], pColNewAttr->attbyval, pColNewAttr->attlen));
}
}
} else {
if (setDataTypeColInfo->notNull) {
ereport(ERROR,
(errcode(ERRCODE_NOT_NULL_VIOLATION),
errmsg("column \"%s\" contains null values", NameStr(pColOldAttr->attname)),
errdetail("existing data violate the NOT NULL constraint.")));
}
m_SDTColValues[cnt] = (Datum)0;
hasNull = true;
}
/*
* 注意:绝对不要释放 *fakeTuple*,因为 *m_SDTColValues[]*
* 可能正在使用其值和内存空间。
*/
}
ExecDropSingleTupleTableSlot(fakeSlot);
fakeSlot = NULL;
(void)MemoryContextSwitchTo(oldCxt);
/*
* 在调用 *InitMem()* 时,我们必须指示新的 CU 是否具有 NULL 值。
* 这取决于三种情况:1)旧 CU 具有空值;2)旧 CU 的某些值已删除;3)新值是空值。
*/
CU* newCu = New(CurrentMemoryContext) CU(pColNewAttr->attlen, pColNewAttr->atttypmod, pColNewAttr->atttypid);
newCu->InitMem(sizeof(Datum) * rowsCntInCu, rowsCntInCu, hasNull);
/* 形成新的 CU 数据 */
if (!hasNull) {
/* 尽量减少 IF 跳转次数。 */
for (int cnt = 0; cnt < rowsCntInCu; ++cnt) {
CU::AppendCuData(m_SDTColValues[cnt], 1, pColNewAttr, newCu);
}
} else {
for (int cnt = 0; cnt < rowsCntInCu; ++cnt) {
if (!m_SDTColIsNull[cnt]) {
CU::AppendCuData(m_SDTColValues[cnt], 1, pColNewAttr, newCu);
} else {
newCu->AppendNullValue(cnt);
}
}
}
/* 设置 cudesc 模式并根据需要将数据写入 CU 文件。 */
if (CStore::SetCudescModeForMinMaxVal(fullNull,
m_SDTColsMinMaxFunc[sdtIndex] != NULL,
hasNull,
maxVarStrLen,
pColNewAttr->attlen,
newColCudesc)) {
int16 compressing_modes = 0;
/* 设置压缩模式 */
heaprel_set_compressing_modes(m_OldHeapRel, &compressing_modes);
CompressCuData(newCu, newColCudesc, pColNewAttr, compressing_modes);
if (m_TblspcChanged) {
/* 直接获取写入偏移量 */
newColCudesc->cu_pointer = m_SDTColAppendOffset[sdtIndex];
/* 更新 *m_SDTColAppendOffset* 以便下一次写入。 */
m_SDTColAppendOffset[sdtIndex] += newColCudesc->cu_size;
} else {
/*
* 首先尝试在旧 CU 文件中使用空闲空间。
* 在最坏的情况下,直接追加新数据到旧 CU 文件。
*/
newColCudesc->cu_pointer = m_SDTColsWriter[sdtIndex]->AllocSpace(newColCudesc->cu_size);
}
SaveCuData(newCu, newColCudesc, m_SDTColsWriter[sdtIndex]);
/* 将 CU 数据推送到复制队列 */
CStoreCUReplication((this->m_TblspcChanged ? this->m_CUReplicationRel : m_OldHeapRel),
setDataTypeColInfo->attrno,
newCu->m_compressedBuf,
newColCudesc->cu_size,
newColCudesc->cu_pointer);
}
DELETE_EX(oldCu);
DELETE_EX(newCu);
}
FetchCudescFrozenXid 函数
FetchCudescFrozenXid 函数的主要功能是根据旧的 CU 描述堆(heap)计算出用于冻结和清除死元组的事务 ID。它首先确定了最老的活动事务ID(OldestXmin),然后根据该信息设置了新的 CU 描述冻结的事务ID(m_NewCudescFrozenXid)。随后,它通过检查系统表 pg_class 中的 relfrozenxid 字段来获取旧的 CU 描述堆的冻结事务ID,并与当前的 ShmemVariableCache 中的 nextXid 进行比较,确保冻结事务 ID 不会后退。最后,如果新的 CU 描述冻结的事务 ID 早于旧的 CU 描述冻结的事务 ID,则使用旧的 CU 描述冻结的事务 ID。函数源码如下所示:(路径:src\gausskernel\storage\cstore\cstore_rewrite.cpp
)
/* description: 未来计划 - 检查 pFreezeXid 的提取方法,参考 copy_heap_data()。*/
void CStoreRewriter::FetchCudescFrozenXid(Relation oldCudescHeap)
{
// 计算用于冻结和清除死元组的事务ID。我们使用 -1 作为 freeze_min_age 来避免比普通 VACUUM 更早地冻结元组。
//
Assert(oldCudescHeap->rd_rel->relisshared == false);
TransactionId OldestXmin = InvalidTransactionId;
vacuum_set_xid_limits(oldCudescHeap, -1, -1, &OldestXmin, &m_NewCudescFrozenXid, NULL, NULL);
// FreezeXid 将成为表的新 relfrozenxid,并且不能后退,因此取最大值。
//
bool isNull = false;
TransactionId oldFrozenXid;
Relation rel = heap_open(RelationRelationId, AccessShareLock);
HeapTuple tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(RelationGetRelid(oldCudescHeap)));
if (!HeapTupleIsValid(tuple)) {
ereport(ERROR,
(errcode(ERRCODE_CACHE_LOOKUP_FAILED),
errmsg("cache lookup failed for relation %u", RelationGetRelid(oldCudescHeap))));
}
Datum xid64datum = heap_getattr(tuple, Anum_pg_class_relfrozenxid64, RelationGetDescr(rel), &isNull);
heap_close(rel, AccessShareLock);
heap_freetuple(tuple);
tuple = NULL;
if (isNull) {
oldFrozenXid = oldCudescHeap->rd_rel->relfrozenxid;
if (TransactionIdPrecedes(t_thrd.xact_cxt.ShmemVariableCache->nextXid, oldFrozenXid))
oldFrozenXid = FirstNormalTransactionId;
} else
oldFrozenXid = DatumGetTransactionId(xid64datum);
// 如果新的 CU 描述冻结的事务ID早于旧的 CU 描述冻结的事务ID,则使用旧的 CU 描述冻结的事务ID。
//
if (TransactionIdPrecedes(m_NewCudescFrozenXid, oldFrozenXid)) {
m_NewCudescFrozenXid = oldFrozenXid;
}
}
InsertNewCudescTup 函数
InsertNewCudescTup 函数的作用是向 CUDesc 表中插入新的元组。首先,它准备了用于存储 CUDesc 元组值和是否为 NULL 的数组。然后,它进入批量内存上下文以准备进行批量插入操作。接着,它调用 CStore::FormCudescTuple 函数创建一个 CUDesc 元组,并将其插入到批量插入器中。最后,它离开批量内存上下文,完成插入操作。函数源码如下所示:(路径:src\gausskernel\storage\cstore\cstore_rewrite.cpp
)
void CStoreRewriter::InsertNewCudescTup(
_in_ CUDesc* pCudesc, _in_ TupleDesc pCudescTupDesc, _in_ Form_pg_attribute pColNewAttr)
{
// 准备存储 CUDesc 元组的值和是否为 NULL 的数组
Datum values[CUDescMaxAttrNum];
bool isnull[CUDescMaxAttrNum];
// 进入批量内存上下文以进行批量插入操作
m_NewCudescBulkInsert->EnterBulkMemCnxt();
// 创建 CUDesc 元组并插入到批量插入器中
HeapTuple tup = CStore::FormCudescTuple(pCudesc, pCudescTupDesc, values, isnull, pColNewAttr);
m_NewCudescBulkInsert->BulkInsert(tup);
// 离开批量内存上下文
m_NewCudescBulkInsert->LeaveBulkMemCnxt();
}
HandleWholeDeletedCu 函数
HandleWholeDeletedCu 函数的作用是处理整个被删除的压缩单元(CU)。它首先创建一个完全为 NULL 的 CUDesc 对象,并填充了该压缩单元的 ID、行数以及标记为 NULL。然后,它遍历每个需要重写的列的信息,并将完全为 NULL 的 CUDesc 信息插入到新的 CUDesc 表中,以记录这些列在此压缩单元中的删除操作。函数源码如下所示:(路径:src\gausskernel\storage\cstore\cstore_rewrite.cpp
)
void CStoreRewriter::HandleWholeDeletedCu(
_in_ uint32 cuId, _in_ int rowsCntInCu, _in_ int nRewriteCols, _in_ CStoreRewriteColumn** rewriteColsInfo)
{
// 创建一个完全为 NULL 的 CUDesc 对象,并填充 cuId、行数以及标记为 NULL
CUDesc fullNullCudesc;
fullNullCudesc.cu_id = cuId;
fullNullCudesc.row_count = rowsCntInCu;
fullNullCudesc.SetNullCU();
fullNullCudesc.magic = GetCurrentTransactionIdIfAny();
// 遍历每个重写列的信息
for (int i = 0; i < nRewriteCols; ++i) {
// 向新的 CUDesc 表中插入新的 CUDesc 元组,将完全为 NULL 的 CUDesc 信息插入其中
InsertNewCudescTup(
&fullNullCudesc, RelationGetDescr(m_NewCudescRel), &m_NewTupDesc->attrs[(rewriteColsInfo[i]->attrno - 1)]);
}
}