声明:本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。
本文主要参考了 OpenGauss1.1.0 的开源代码和《OpenGauss数据库源码解析》一书以及OpenGauss社区学习文档和一些学习资料
本章我们继续在【 OpenGauss源码学习 —— 列存储(CStore)(三)】和【 OpenGauss源码学习 —— 列存储(CStore)(四)】的基础上进行进一步学习,本文将主要介绍 CStore 类中的部分私有成员函数。
CStore::CStoreScan 函数实现了 CStore 表的扫描操作,主要包括以下步骤:首先加载 CUDesc 信息,然后进行粗略检查,排除不满足条件的 CU。接着,根据扫描状态检查是否有匹配的 CU,如果有,则填充 VectorBatch 以存储扫描结果。在处理完当前 CUDesc 之后,刷新游标,标记已处理的行数和无效的行数,以便下次扫描时跳过这些行。最后,在适当的情况下进行 CU 列表的预取操作,提前加载部分数据到内存,以优化后续的访问效率。这一系列步骤保证了对 CStore 表的高效扫描和查询。CStoreScan 源码如下:(路径:src/gausskernel/storage/cstore/cstore_am.cpp
)
// CStoreScan
// 扫描列存储表并填充vecBatchOut
void CStore::CStoreScan(_in_ CStoreScanState* state, _out_ VectorBatch* vecBatchOut)
{
// 步骤1:持有的CUDesc的数量为max_loaded_cudesc
// 如果一次性加载所有CUDesc,内存将不足。
// 因此,我们为max_loaded_cudesc加载CUDesc一次
CSTORESCAN_TRACE_START(LOAD_CU_DESC); // 开始加载CUDesc
LoadCUDescIfNeed(); // 如果需要,加载CUDesc
CSTORESCAN_TRACE_END(LOAD_CU_DESC); // 结束加载CUDesc
// 步骤2:如果需要,进行粗略检查
// 通过CU的最小/最大值来消除CU。
CSTORESCAN_TRACE_START(MIN_MAX_CHECK); // 开始进行粗略检查
RoughCheckIfNeed(state); // 如果需要,进行粗略检查
CSTORESCAN_TRACE_END(MIN_MAX_CHECK); // 结束粗略检查
// 步骤3:有命中的CU
// 我们不会填充矢量,因为没有命中的CU
ADIO_RUN()
{
if (unlikely(m_cursor == m_NumCUDescIdx)) {
return;
}
}
ADIO_ELSE()
{
if (unlikely(m_NumLoadCUDesc == 0)) {
return;
}
}
ADIO_END();
// 步骤4:填充VecBatch
CSTORESCAN_TRACE_START(FILL_BATCH); // 开始填充VectorBatch
int deadRows = FillVecBatch(vecBatchOut); // 填充VectorBatch,获取无效行数
CSTORESCAN_TRACE_END(FILL_BATCH); // 结束填充VectorBatch
// 步骤5:刷新游标
RefreshCursor(vecBatchOut->m_rows, deadRows); // 刷新游标,更新已处理的行数和无效的行数
// 步骤6:如果需要,进行预取
ADIO_RUN()
{
CSTORESCAN_TRACE_START(PREFETCH_CU_LIST); // 开始预取CU列表
CUListPrefetch(); // 如果需要,进行CU列表的预取操作
CSTORESCAN_TRACE_END(PREFETCH_CU_LIST); // 结束预取CU列表
}
ADIO_END();
}
其中,CStore::CStoreScan 函数的更多详细信息在【 OpenGauss源码学习 —— 列存储(CopyTo)】一文中进行了详细介绍。
CStore::CStoreMinMaxScan 函数实现了对 CStore 表的扫描操作,填充输出的 VectorBatch 数据结构。首先,根据最大加载的 CUDesc 数量加载 CUDesc 信息,然后进行粗略检查以排除不符合条件的 CU。接着,检查是否扫描结束,如果是则直接返回。随后,对每列的 CUDesc 进行遍历,根据是否存在无效行以及是否有 min/max 值的情况,选择性地填充 min/max 值或者全部数据。根据填充情况,修复 VectorBatch 的行数和进行必要的填充值处理。最后,刷新游标并进行预取操作,完成整个扫描过程。
/*
* @Describe: 仅在没有无效行的情况下读取cudesc以获取min/max值
* 绕过特殊SQL案例的优化,例如类似 'select min(col1), max(col2) from t' 的情况
* @in - state CStore扫描状态。
* @out - vecBatchOut 存储数据的结构。
*/
void CStore::CStoreMinMaxScan(_in_ CStoreScanState* state, _out_ VectorBatch* vecBatchOut)
{
int pos = 0;
/*
* 步骤1:持有的CUDesc的数量是max_loaded_cudesc
* 如果一次性加载所有CUDesc,内存将不足。
* 因此,我们为max_loaded_cudesc加载CUDesc一次
*/
CSTORESCAN_TRACE_START(LOAD_CU_DESC); // 开始加载CUDesc
LoadCUDescIfNeed(); // 如果需要,加载CUDesc
CSTORESCAN_TRACE_END(LOAD_CU_DESC); // 结束加载CUDesc
CSTORESCAN_TRACE_START(MIN_MAX_CHECK); // 开始进行粗略检查
RoughCheckIfNeed(state); // 如果需要,进行粗略检查
CSTORESCAN_TRACE_END(MIN_MAX_CHECK); // 结束粗略检查
/* 步骤2:是否扫描结束? */
if (IsEndScan())
return;
/*
* 步骤3:如果cudesc有min/max,则填充min/max,否则填充所有数据。
* case1. 所有列的cudesc都有min/max且没有无效行
* case2. 有无效行
* case3. 现在有些列根本没有min/max。而有些列有。
* 例如,numeric列。
* 描述:未来计划-在加载数据时为numeric列保存min/max
*/
int idx = m_CUDescIdx[m_cursor];
int maxVecRows = 0;
bool needFixRows = false, onlyFillMinMax = true;
int deadRows = 0;
CSTORESCAN_TRACE_START(FILL_BATCH); // 开始填充VectorBatch
for (int i = 0; i < m_colNum; ++i) {
int colIdx = m_colId[i];
ScalarVector* vec = vecBatchOut->m_arr + colIdx;
CUDesc* cuDescPtr = m_CUDescInfo[i]->cuDescArray + idx;
GetCUDeleteMaskIfNeed(cuDescPtr->cu_id, m_snapshot);
Assert(0 == pos);
if (!m_hasDeadRow && !cuDescPtr->IsNoMinMaxCU() && this->m_fillMinMaxFunc[i]) {
(this->*m_fillMinMaxFunc[i])(cuDescPtr, vec, pos);
} else {
int funIdx = m_hasDeadRow ? 1 : 0;
deadRows = (this->*m_colFillFunArrary[i].colFillFun[funIdx])(i, cuDescPtr, vec);
if (vec->m_rows > 1)
onlyFillMinMax = false;
}
/* 如果存在不一致的行,则需要修复。并标记最大的行数。 */
if (vec->m_rows != maxVecRows) {
if (0 != maxVecRows)
needFixRows = true;
if (vec->m_rows > maxVecRows)
maxVecRows = vec->m_rows;
}
}
CSTORESCAN_TRACE_END(FILL_BATCH); // 结束填充VectorBatch
/*
* 步骤4:修复VecBatch的m_rows并为某些列添加填充值,如果需要。
* 因为某些列只填充min/max,而某些列填充某些特殊情况下的批量数据
*/
if (needFixRows) {
for (int i = 0; i < m_colNum; ++i) {
int colIdx = m_colId[i];
ScalarVector* vec = vecBatchOut->m_arr + colIdx;
if (vec->m_rows != maxVecRows) {
for (int k = vec->m_rows; k < maxVecRows; ++k) {
/* 修复行,将此值设置为不影响min/max结果的null。 */
vec->SetNull(k);
}
}
vec->m_rows = maxVecRows;
}
}
vecBatchOut->m_rows = maxVecRows;
/* 步骤5:如果只填充min/max,则刷新游标 */
if (onlyFillMinMax) {
IncLoadCuDescIdx(m_cursor);
m_rowCursorInCU = 0;
if (likely(m_scanPosInCU != NULL)) {
Assert(m_colNum > 0);
errno_t rc = memset_s(m_scanPosInCU, sizeof(int) * m_colNum, 0, sizeof(int) * m_colNum);
securec_check(rc, "", "");
}
} else {
RefreshCursor(vecBatchOut->m_rows, deadRows);
}
pos = vecBatchOut->m_rows;
Assert(pos <= BatchMaxSize);
}
这里详细解释一下如下代码逻辑:
/* 如果存在不一致的行,则需要修复。并标记最大的行数。 */
if (vec->m_rows != maxVecRows) {
if (0 != maxVecRows)
needFixRows = true;
if (vec->m_rows > maxVecRows)
maxVecRows = vec->m_rows;
}
这段代码的目的是确保不同列的 vec(ScalarVector 对象)中的行数一致。具体情况可能如下:
假设有一个表,其中的每一列都有不同的填充逻辑,有些列可能只填充了部分行,而有些列可能填充了所有行。在执行 CStoreMinMaxScan 函数时,通过循环遍历每一列的 vec,检查它们的行数是否一致。
- 如果某个列的行数不等于 maxVecRows,说明该列的填充逻辑导致它的行数与其他列不一致。
- 如果 maxVecRows 为零,表示这是第一次检查,将 needFixRows 设为 true。
- 如果某列的行数大于 maxVecRows,更新 maxVecRows 为该列的行数,以保证后续的行数检查使用的是最大值。
举例说明:
假设有三列 A、B 和 C,它们的 vec 分别为:
列 A:[1, 2, 3]
列 B:[4, 5]
列 C:[6, 7, 8, 9]
在第一次检查时,maxVecRows 为零,needFixRows 设为 true。在接下来的检查中,maxVecRows 会被更新为 4,因为列 C 的行数最多。在最后的修正阶段,列 A 和列 B 会被填充为 [1, 2, 3, null] 和 [4, 5, null, null],以保证它们的行数与列 C 一致。这样,vecBatchOut 中的每一列都具有相同的行数,确保后续的处理是一致的。
CStore::CStoreScan 和 CStore::CStoreMinMaxScan 函数是用于扫描 CStore 表的两个不同的方法。
CStore::CStoreScan:
- 作用和功能: 该函数用于普通的 CStore 表扫描,它通过加载 CUDesc 信息、进行粗略检查、判断是否扫描结束,以及填充 VectorBatch 数据结构等步骤完成扫描操作。它适用于一般的表扫描场景,填充所有的数据。
CStore::CStoreMinMaxScan:
- 作用和功能: 与普通扫描不同,该函数用于进行特殊的 CStore 表扫描,主要用于执行类似
'select min(col1), max(col2) from t'
这样的 SQL 查询。它通过加载 CUDesc 信息、进行粗略检查、检查是否扫描结束,然后根据每列的具体情况,选择性地填充 min/max 值或者全部数据。这个函数的优化点在于根据需要选择性地填充数据,而不是一次性填充所有数据。
CStore::LoadCUDescIfNeed 函数的主要作用是在需要的情况下加载 CUDesc(Column Unit Descriptor)。若需要加载,则遍历所有访问的列,调用 LoadCUDesc 函数逐列加载 CUDesc。函数根据是否启用 ADIO(Adaptive I/O)功能,以及是否达到预取阈值等条件,决定是否继续加载 CUDesc。加载完成后,进行一系列的检查,包括检查 CUDesc 控制块的一致性和检查每列的第一个和最后一个 CUDesc,以确保加载的 CUDesc 信息正确。加载完成后,根据访问列的类型,确定使用哪个列的 CUDesc 信息,然后进行相应的内存操作和状态更新。最终,根据 ADIO 是否启用,确定是否需要进行粗略检查,并更新加载的 CUDesc 信息的长度。函数源码如下所示:(路径:src/gausskernel/storage/cstore/cstore_am.cpp
)
/*
* @Description: 如果需要加载 CUDesc 且当前未加载足够数量的 CUDesc,则加载 CUDesc。
* 遍历所有访问的列,调用 LoadCUDesc 函数加载 CUDesc。
* 对于 ADIO 功能,如果发现未达到预取阈值,且有足够的 CUDesc 插槽,则继续加载。
* 加载完成后,设置 lastLoadNum 以备份值。
* 如果只包含系统列或常量列,则使用第一列的 CUDesc。
* @IN state: CStore Scan State,包含了 CStore 扫描的状态信息。
* @OUT vecBatchOut: 存储数据的 VectorBatch 结构。
*/
void CStore::LoadCUDescIfNeed()
{
uint32 last_load_num = 0; // 上次加载的 CUDesc 数量
int32 cudesc_idx = 0; // cudesc_idx 在 buffer io 时设置为0,在 adio 时设置为 m_NumCUDescIdx
// 如果不需要加载 CUDesc,则直接返回
if (!NeedLoadCUDesc(cudesc_idx)) {
return;
}
m_NumLoadCUDesc = 0; // 初始化加载的 CUDesc 数量为0
Assert(m_perScanMemCnxt); // 断言扫描内存上下文不为空
// 当批量 CU 已扫描和处理时,重置内存上下文
MemoryContextReset(m_perScanMemCnxt);
#ifdef MEMORY_CONTEXT_CHECKING
MemoryContextCheck(m_perScanMemCnxt->parent, m_perScanMemCnxt->parent->session_id > 0);
#endif
// 加载所有访问的列的 CUDesc
if (m_colNum > 0) {
last_load_num = m_CUDescInfo[0]->curLoadNum; // 记录加载的列的 CUDesc 数量
}
do {
bool found = false; // 是否找到 CUDesc 的标志
for (int i = 0; i < m_colNum; ++i) {
Assert(m_colId[i] >= 0);
// 如果启用了 ADIO,则加载一个 CU 以计算预取数量
found = LoadCUDesc(m_colId[i], m_CUDescInfo[i], g_instance.attr.attr_storage.enable_adio_function, m_snapshot);
}
if (likely(m_colNum > 1 && m_CUDescInfo[0]->curLoadNum > 0)) {
CheckConsistenceOfCUDescCtl();
/* 检查所有列的第一个 CUDesc */
CheckConsistenceOfCUDesc(0);
/* 检查所有列的最后一个 CUDesc */
if (m_CUDescInfo[0]->curLoadNum > 1) {
CheckConsistenceOfCUDesc(m_CUDescInfo[0]->curLoadNum - 1);
}
}
if (m_colNum > 0) {
// 更新加载的 CUDesc 的索引
for (int j = (int)m_CUDescInfo[0]->lastLoadNum; j != (int)m_CUDescInfo[0]->curLoadNum;
IncLoadCuDescIdx(j)) {
m_CUDescIdx[cudesc_idx] = j;
IncLoadCuDescIdx(cudesc_idx);
}
// 计算加载的 CUDesc 数量
m_NumLoadCUDesc += LoadCudescMinus(m_CUDescInfo[0]->lastLoadNum, m_CUDescInfo[0]->curLoadNum);
}
ADIO_RUN()
{
// 如果找到 CUDesc,检查预取数量并决定是否需要加载更多的 CUDesc
if (found && m_prefetch_quantity < m_prefetch_threshold &&
HasEnoughCuDescSlot(last_load_num, m_CUDescInfo[0]->curLoadNum)) {
continue;
}
// 加载完成,设置 lastLoadNum 为备份值
for (int i = 0; i < m_colNum; ++i) {
m_CUDescInfo[i]->lastLoadNum = last_load_num;
}
if (m_colNum > 0) {
// 设置最小预取数量,因为我们需要预取窗口来控制是否需要预取
t_thrd.cstore_cxt.cstore_prefetch_count = Max(m_NumLoadCUDesc, CSTORE_MIN_PREFETCH_COUNT);
ereport(DEBUG1,
(errmodule(MOD_ADIO),
errmsg("LoadCUDesc: columns(%d), count(%d), quantity(%d)",
m_colNum,
m_NumLoadCUDesc,
m_prefetch_quantity)));
}
break;
}
ADIO_ELSE()
{
break;
}
ADIO_END();
} while (1);
// 系统列和常量列
if (m_colNum > 0 && m_sysColNum != 0) {
// 访问正常列和系统列,使用正常列的 CUDesc
m_virtualCUDescInfo = m_CUDescInfo[0];
} else if (OnlySysOrConstCol()) {
// 只有系统列或常量列,使用第一列的 CUDesc
Assert(m_virtualCUDescInfo);
LoadCUDesc(m_firstColIdx, m_virtualCUDescInfo, false, m_snapshot);
for (int j = (int)m_virtualCUDescInfo->lastLoadNum; j != (int)m_virtualCUDescInfo->curLoadNum;
IncLoadCuDescIdx(j)) {
m_CUDescIdx[cudesc_idx] = j;
IncLoadCuDescIdx(cudesc_idx);
}
m_NumLoadCUDesc = LoadCudescMinus(m_virtualCUDescInfo->lastLoadNum, m_virtualCUDescInfo->curLoadNum);
// ADIO 使用它,但无需添加 ADIO_RUN(),因为对于缓冲 IO 它没有用处
t_thrd.cstore_cxt.cstore_prefetch_count = m_NumLoadCUDesc;
}
// 加载新的 CU 需要进行粗略检查
m_needRCheck = true;
// 在进行粗略检查之前,m_NumCUDescIdx 是加载的 CUDesc 信息的长度
BFIO_RUN()
{
m_NumCUDescIdx = m_NumLoadCUDesc;
}
BFIO_END();
return;
}
CStore::NeedLoadCUDesc 函数用于检查是否满足加载 CUDesc 的条件,判断是否需要加载 CUDesc。在使用 ADIO(Adaptive I/O)功能的情况下,首先检查是否已加载完所有的 CUDesc(m_load_finish),然后检查是否超过了预取数量的一半。如果是,则设置 need_load 为 true,将 cudesc_idx 设置为当前已加载 CUDesc 的索引,并重置预取数量。在没有使用 ADIO 功能的情况下,检查当前游标是否超过了已加载的 CUDesc 的数量,如果是,则设置 need_load 为 true,将 cudesc_idx 设置为 0,同时重置游标。最终,函数返回 need_load 的值。
/*
* @Description: 检查是否满足加载 CUDesc 的条件,并设置加载 CUDesc 的数组索引等信息
* @Param[IN] cudesc_idx: 加载 CUDesc 的数组索引(输出参数)
* @Return: true--需要加载 CUDesc, false-- 不需要加载
* @See also:
*/
bool CStore::NeedLoadCUDesc(int32& cudesc_idx)
{
bool need_load = false;
ADIO_RUN()
{
// 检查加载条件,首先确保没有加载完成,其次不超过预取数量的一半
if (!m_load_finish && (m_cursor == m_NumCUDescIdx || LoadCudescMinus(m_cursor, m_NumCUDescIdx) <=
t_thrd.cstore_cxt.cstore_prefetch_count / 2)) {
need_load = true;
m_prefetch_quantity = 0;
cudesc_idx = m_NumCUDescIdx;
}
}
ADIO_ELSE()
{
// 在没有启用 ADIO 功能的情况下,检查当前游标是否超过已加载 CUDesc 的数量
if (m_cursor >= m_NumLoadCUDesc) {
need_load = true;
m_cursor = 0;
cudesc_idx = 0;
}
}
ADIO_END();
return need_load;
}
CStore::RoughCheckIfNeed 函数是 CStore 扫描过程中的一个步骤,用于执行 “Rough Check”,即粗略检查,以判断当前的扫描条件是否能够命中列存储中的压缩单元(CU)。函数源码如下所示:(路径:src/gausskernel/storage/cstore/cstore_am.cpp
)
/*
* @Description: 如果需要进行粗略检查,则执行粗略检查操作。
* @IN state: CStore扫描状态
*/
void CStore::RoughCheckIfNeed(_in_ CStoreScanState* state)
{
int nkeys = state->csss_NumScanKeys; // 获取扫描条件的数量
CStoreScanKey scanKey = state->csss_ScanKeys; // 获取扫描条件的数组
PlanState* planstate = (PlanState*)state; // 获取计划节点的状态
uint32 curLoadNum;
uint32 lastLoadNum;
// 当m_needRCheck为false时,表示已经执行过粗略检查,无需重复执行
// 当m_colNum为0时,表示没有普通列,也无需执行粗略检查
if (likely(!m_needRCheck)) {
return;
}
// 当没有扫描条件或者没有普通列时,也无需执行粗略检查
if (likely(nkeys == 0 || scanKey == NULL || m_colNum == 0)) {
// 当没有WHERE条件时,仍需设置m_lastNumCUDescIdx和m_NumCUDescIdx,以备预取
ADIO_RUN()
{
m_NumCUDescIdx = (m_NumCUDescIdx + m_NumLoadCUDesc) % u_sess->attr.attr_storage.max_loaded_cudesc;
m_needRCheck = false; // 设置标志,表示已完成粗略检查
}
ADIO_END();
return;
}
int pos = 0;
bool hitCU = true;
int cudesc_idx_tmp = 0;
ADIO_RUN()
{
cudesc_idx_tmp = m_NumCUDescIdx; // 记录当前的CU索引
pos = m_NumCUDescIdx; // 记录粗略检查的起始位置
}
ADIO_END();
lastLoadNum = m_CUDescInfo[0]->lastLoadNum;
curLoadNum = m_CUDescInfo[0]->curLoadNum;
// 遍历所有的CU,进行粗略检查
for (int i = (int)lastLoadNum; i != (int)curLoadNum; IncLoadCuDescIdx(i), IncLoadCuDescIdx(cudesc_idx_tmp)) {
hitCU = RoughCheck(scanKey, nkeys, i); // 执行粗略检查操作
if (hitCU) {
// 将命中的CU索引记录下来,用于后续的预取
ADIO_RUN()
{
m_CUDescIdx[pos] = m_CUDescIdx[cudesc_idx_tmp];
}
ADIO_ELSE()
{
m_CUDescIdx[pos] = m_CUDescIdx[i];
}
ADIO_END();
IncLoadCuDescIdx(pos);
}
// 统计未命中的CU的行数,用于性能统计
if (planstate->instrument) {
RCInfo* rcPtr = &(planstate->instrument->rcInfo);
if (!hitCU) {
// 获取未命中的CU的行数,并更新性能统计信息
int seq = scanKey[0].cs_attno;
CUDesc* cudesc = &(m_CUDescInfo[seq]->cuDescArray[i]);
planstate->instrument->nfiltered1 += cudesc->row_count;
Relation cuDescRel = heap_open(m_relation->rd_rel->relcudescrelid, AccessShareLock);
Relation idxRel = index_open(cuDescRel->rd_rel->relcudescidx, AccessShareLock);
ScanKeyData key[2];
// 使用索引进行查询,获取CU的行数和位图
ScanKeyInit(&key[0], (AttrNumber)CUDescColIDAttr, BTEqualStrategyNumber,
F_INT4EQ, Int32GetDatum(VitrualDelColID));
ScanKeyInit(&key[1], (AttrNumber)CUDescCUIDAttr, BTEqualStrategyNumber,
F_OIDEQ, UInt32GetDatum(cudesc->cu_id));
SysScanDesc cuDescScan = systable_beginscan_ordered(cuDescRel, idxRel, m_snapshot, 2, key);
HeapTuple tmpTup = NULL;
if ((tmpTup = systable_getnext_ordered(cuDescScan, ForwardScanDirection)) != NULL) {
bool isNull = false;
uint32 deadRowCount = 0;
uint32 rowCount = DatumGetUInt32(fastgetattr(tmpTup, CUDescRowCountAttr,
cuDescRel->rd_att, &isNull));
Datum v = fastgetattr(tmpTup, CUDescCUPointerAttr, cuDescRel->rd_att, &isNull);
if (!isNull) {
// 解析位图,统计未命中的行数
int8* bitmap = (int8*)PG_DETOAST_DATUM(DatumGetPointer(v));
unsigned char delBitMap[MaxDelBitmapSize];
uint32 nBytes = (rowCount + 7) / 8;
errno_t rc = memcpy_s(delBitMap, MaxDelBitmapSize,
VARDATA_ANY(bitmap), VARSIZE_ANY_EXHDR(bitmap));
securec_check(rc, "", "");
for (uint32 j = 0; j < nBytes; j++) {
deadRowCount += NumberOfBit1Set[delBitMap[j]];
}
// 释放位图的内存
if ((Pointer)bitmap != DatumGetPointer(v)) {
pfree_ext(bitmap);
}
}
// 更新性能统计信息,减去未命中的行数
planstate->instrument->nfiltered1 -= deadRowCount;
}
// 结束查询
systable_endscan_ordered(cuDescScan);
index_close(idxRel, AccessShareLock);
heap_close(cuDescRel, AccessShareLock);
rcPtr->IncNoneCUNum(); // 更新性能统计信息
} else {
rcPtr->IncSomeCUNum(); // 更新性能统计信息
}
planstate->instrument->needRCInfo = true;
}
}
ADIO_RUN()
{
// 更新m_NumCUDescIdx,表示粗略检查的结束位置
if (pos == m_NumCUDescIdx) {
m_NumCUDescIdx = (m_NumCUDescIdx + m_NumLoadCUDesc) % u_sess->attr.attr_storage.max_loaded_cudesc;
} else {
m_NumCUDescIdx = pos;
}
}
ADIO_ELSE()
{
m_NumLoadCUDesc = pos;
}
ADIO_END();
// 设置标志,表示已完成粗略检查
m_needRCheck = false;
}
粗略检查是通过比较扫描条件与压缩单元(CU)的元数据信息来判断是否命中的过程。下面举例说明:
假设有一个列存储表,其中包含一个整数列(col1)和一个字符串列(col2),现在有一个查询条件为 col1 > 100 AND col2 = 'abc'
。执行粗略检查时,会比较每个压缩单元(CU)的元数据信息,例如检查每个 CU 的最小值和最大值,以确定是否有可能包含满足查询条件的行。
例如,对于整数列 col1,如果某个 CU 的最大值小于等于100,则可以确定该 CU 不可能包含满足查询条件的行,因此可以跳过对该 CU 的详细扫描。对于字符串列 col2,如果某个 CU 的最小值大于 ‘abc’ 或者最大值小于 ‘abc’,同样可以确定该 CU 不可能包含满足查询条件的行。这样就可以在不详细扫描具体行数据的情况下,快速过滤掉一些不符合条件的 CU,提高查询效率。
CStore::RoughCheck 函数的主要作用是根据扫描键(scanKey)和CUDesc 信息,判断当前 CUDesc 是否满足给定的扫描条件。例如,对于一个查询条件 col1 > 100 AND col2 = 'abc'
,该函数将检查每个 CUDesc 的元数据信息,比较是否满足条件。如果某个 CUDesc 的最小值大于100,且对应的字符串列的最大值等于 ‘abc’,则认为该 CUDesc 符合条件,返回 true。如果某个条件未满足,则返回 false,表示该 CUDesc 未命中。函数源码如下所示:(路径:src/gausskernel/storage/cstore/cstore_am.cpp
)
/*
* @Description: 执行CUDesc的粗略检查
* @Param[IN] cuDescIdx: 加载的CUDesc信息的索引
* @Param[IN] nkeys: 扫描键的数量
* @Param[IN] scanKey: CStore扫描键
* @Return: true--命中, false--未命中
* @See also:
*/
bool CStore::RoughCheck(CStoreScanKey scanKey, int nkeys, int cuDescIdx)
{
bool hitCU = true; // 假设命中CU
// 遍历所有的扫描键
for (int j = 0; j < nkeys; j++) {
int seq = scanKey[j].cs_attno; // 获取扫描键对应的列序号
CUDesc* cudesc = &(m_CUDescInfo[seq]->cuDescArray[cuDescIdx]); // 获取对应列的CUDesc信息
bool isNullKey = scanKey[j].cs_flags & SK_ISNULL; // 判断是否为NULL键
// 如果CUDesc是NULL,但扫描键不是NULL,则继续下一个键
if ((cudesc->IsNullCU() && !isNullKey) || cudesc->IsNoMinMaxCU())
continue;
// 如果扫描键是NULL,判断CUDesc是否包含NULL值,或者CUDesc本身是NULL
if (isNullKey)
hitCU = cudesc->CUHasNull() || cudesc->IsNullCU();
else
// 否则,调用相应的比较函数(如大于、小于、等于等)进行比较
hitCU = m_RCFuncs[j](cudesc, scanKey[j].cs_argument);
// 如果未命中,则跳出循环
if (!hitCU)
break;
}
return hitCU; // 返回是否命中CU
}
该函数主要用于管理在 CStore 扫描过程中,当前所处的CUDesc 以及在该 CUDesc 中的行游标位置。在扫描过程中,每次刷新游标后,都会检查当前 CUDesc 是否已经扫描完毕,如果是,则更新游标到下一个 CUDesc,并将行游标重置为 0。这样可以有效地管理扫描的状态,确保在 CStore 表中正确地进行行扫描。函数源码如下所示:(路径:src/gausskernel/storage/cstore/cstore_am.cpp
)
/*
* @Description: 刷新游标,更新当前扫描位置
* @Param[IN] row: 当前批次的行数
* @Param[IN] deadRows: 当前批次中的死行数
*/
void CStore::RefreshCursor(int row, int deadRows)
{
int cuRowCount = 0;
int idx = m_CUDescIdx[m_cursor]; // 获取当前游标指向的CUDesc的索引
// 判断是否有正常列的CUDesc信息
if (likely(m_CUDescInfo != NULL)) {
cuRowCount = m_CUDescInfo[0]->cuDescArray[idx].row_count; // 获取当前CUDesc的行数信息
} else {
Assert(m_virtualCUDescInfo);
cuRowCount = m_virtualCUDescInfo->cuDescArray[idx].row_count; // 获取虚拟CUDesc的行数信息
}
// 更新在当前CUDesc中的行游标
m_rowCursorInCU = m_rowCursorInCU + row + deadRows;
// 断言行游标不超过CUDesc的行数
Assert(m_rowCursorInCU <= cuRowCount);
// 如果行游标达到CUDesc的行数,表示当前CUDesc已经扫描完毕,更新游标到下一个CUDesc
if (unlikely(m_rowCursorInCU == cuRowCount)) {
IncLoadCuDescIdx(m_cursor); // 更新游标到下一个CUDesc
m_rowCursorInCU = 0; // 重置行游标为0
// 如果有正常的扫描位置信息,则将其重置为0
if (likely(m_scanPosInCU != NULL)) {
Assert(m_colNum > 0);
errno_t rc = memset_s(m_scanPosInCU, sizeof(int) * m_colNum, 0, sizeof(int) * m_colNum);
securec_check(rc, "", "");
}
}
}
该函数主要用于在 CStore 扫描过程中初始化粗略检查所需的环境。首先,通过 AutoContextSwitch将当前上下文切换为 m_scanMemContext ,确保内存管理的正确性。然后,根据扫描状态中的扫描键信息,获取每个扫描键对应的粗略检查函数,并存储在 m_RCFuncs 数组中。这样,在后续的粗略检查过程中,可以直接调用相应的函数进行检查。函数源码如下所示:(路径:src/gausskernel/storage/cstore/cstore_am.cpp
)
/*
* @Description: 初始化粗略检查环境
* @Param[IN] state: CStore扫描状态
*/
void CStore::InitRoughCheckEnv(CStoreScanState* state)
{
// 使用m_scanMemContext,该上下文直到析构函数被调用才会释放。
AutoContextSwitch newMemCnxt(m_scanMemContext);
// 初始化粗略检查函数
int nkeys = state->csss_NumScanKeys;
if (nkeys > 0) {
CStoreScanKey scanKey = state->csss_ScanKeys;
Relation rel = state->ss_currentRelation;
Form_pg_attribute* attrs = rel->rd_att->attrs;
// 为每个扫描键获取对应的粗略检查函数
m_RCFuncs = (RoughCheckFunc*)palloc(sizeof(RoughCheckFunc) * nkeys);
for (int i = 0; i < nkeys; i++) {
int colIdx = m_colId[scanKey[i].cs_attno];
// 获取粗略检查函数,根据属性类型、策略和排序规则
m_RCFuncs[i] = GetRoughCheckFunc(attrs[colIdx]->atttypid, scanKey[i].cs_strategy, scanKey[i].cs_collation);
}
}
}
CStore::BindingFp 函数主要用于为 CStore 扫描过程中的函数指针赋值。首先,它检查是否存在最大/最小值投影列,如果是,则为每个列根据其数据类型设置相应的最大/最小值填充函数。然后,它为每个列初始化 LoadCUDescCtl,并根据数据类型设置相应的填充函数。函数源码如下所示:(路径:src/gausskernel/storage/cstore/cstore_am.cpp
)
/*
* @Description: 根据不同的数据类型为函数指针赋值
* @in - state: CStore扫描状态
*/
void CStore::BindingFp(CStoreScanState* state)
{
int i = 0;
Relation rel = state->ss_currentRelation;
ProjectionInfo* proj = state->ps.ps_ProjInfo;
// 如果存在最大/最小值投影列
if (proj->pi_maxOrmin) {
Assert(list_length(proj->pi_maxOrmin) == list_length(proj->pi_acessedVarNumbers));
m_scanFunc = &CStore::CStoreMinMaxScan;
m_fillMinMaxFunc = (fillMinMaxFuncPtr*)palloc0(sizeof(fillMinMaxFuncPtr) * m_colNum);
// 为每个列根据数据类型设置最大/最小值填充函数
for (i = 0; i < m_colNum; ++i) {
switch (rel->rd_att->attrs[m_colId[i]]->atttypid) {
case CHAROID:
case INT2OID:
case INT4OID:
case INT8OID:
case OIDOID:
case DATEOID:
case TIMEOID:
case TIMESTAMPOID: {
m_fillMinMaxFunc[i] = &CStore::FillColMinMax;
break;
}
default: {
m_fillMinMaxFunc[i] = NULL;
break;
}
}
}
}
// 为每个列初始化LoadCUDescCtl,并根据数据类型设置相应的填充函数
for (i = 0; i < m_colNum; ++i) {
m_CUDescInfo[i] = New(CurrentMemoryContext) LoadCUDescCtl(m_startCUID);
if (m_colId[i] > rel->rd_att->natts - 1 || m_colId[i] < 0) {
continue;
}
switch (rel->rd_att->attrs[m_colId[i]]->attlen) {
case sizeof(char):
InitFillColFunction(i, (int)sizeof(char));
break;
case sizeof(int16):
InitFillColFunction(i, (int)sizeof(int16));
break;
case sizeof(int32):
InitFillColFunction(i, (int)sizeof(int32));
break;
case sizeof(Datum):
InitFillColFunction(i, (int)sizeof(Datum));
break;
case 12:
InitFillColFunction(i, 12);
break;
case 16:
InitFillColFunction(i, 16);
break;
case -1:
InitFillColFunction(i, -1);
break;
case -2:
InitFillColFunction(i, -2);
break;
default:
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
(errmsg("unsupported data type length %d of column \"%s\" of relation \"%s\" ",
(int)rel->rd_att->attrs[m_colId[i]]->attlen,
NameStr(rel->rd_att->attrs[m_colId[i]]->attname),
RelationGetRelationName(rel)))));
break;
}
}
}
CStore::FillColMinMax 函数主要用于处理列存储中的元数据信息,通过提供最小值和最大值,有助于查询优化器生成更有效的执行计划。函数中的 cuDescPtr 是列单元描述符,包含有关列单元的信息,而 vec 是 ScalarVector 结构,用于存储列数据。函数通过检查是否是空列单元来确定是否设置为 NULL,并将最小值和最大值填充到 ScalarVector 中。函数源码如下所示:(路径:src/gausskernel/storage/cstore/cstore_am.cpp
)
/*
* @Describe: 从 CU(Column Unit)描述符中获取最大值和最小值。
*
* @in - cuDescPtr: CU信息的指针
* @in - pos: 当前行号
* @out - vec: ScalarVector结构,用于存储列数据
*/
void CStore::FillColMinMax(CUDesc* cuDescPtr, ScalarVector* vec, int pos)
{
// 如果不是空 CU,则从 CU 描述符中获取最小值和最大值,并填充到 ScalarVector 中
if (!cuDescPtr->IsNullCU()) {
vec->m_vals[pos] = *(ScalarValue*)cuDescPtr->cu_min;
vec->m_vals[pos + 1] = *(ScalarValue*)cuDescPtr->cu_max;
} else {
// 如果是空 CU,则设置相应位置的值为 NULL
vec->SetNull(pos);
vec->SetNull(pos + 1);
}
// 更新 ScalarVector 的行数信息
vec->m_rows = pos + 2;
}
该函数用于初始化 CStore 扫描环境,包括为列的访问、延迟读取等设置相应的参数。具体功能如下:
- 在函数内部使用 AutoContextSwitch 对象,将内存上下文切换为 m_scanMemContext,确保分配的内存在扫描结束前一直有效。
- 通过访问 ProjectionInfo 对象,获取投影信息,包括要访问的列、延迟读取的列、系统列等。
- 根据投影信息,初始化以下参数:
- m_colNum:需要访问的列的数量。
- m_colId:需要访问的列的编号数组。
- m_lateRead:标识是否需要延迟读取的列的数组。
- m_scanPosInCU:记录每列在当前 CU 中的扫描位置的数组。
- m_CUDescInfo:LoadCUDescCtl 对象数组,用于管理每列的 CU 描述符。
- m_colFillFunArrary:列填充函数数组,用于根据不同的数据类型选择相应的填充函数。
- m_fillVectorByTids:填充向量的函数数组,用于通过 TIDs 填充向量。
- m_fillVectorLateRead:延迟读取的填充向量的函数数组。
- 调用 BindingFp 函数,为不同的数据类型的列设置相应的填充函数。
- 如果存在系统列,初始化以下参数:
- m_sysColNum:系统列的数量。
- m_sysColId:系统列的编号数组。
- 设置 m_onlyConstCol 标志,表示是否仅访问系统列或常量列。
- 如果仅访问系统列或常量列,创建虚拟的 LoadCUDescCtl 对象。
CStore::InitFillVecEnv 函数源码如下所示:(路径:src/gausskernel/storage/cstore/cstore_am.cpp
)
/*
* @Description: 初始化CStore扫描环境,包括为列的访问、延迟读取等设置相应的参数。
* @in - state: CStore扫描状态。
*/
void CStore::InitFillVecEnv(CStoreScanState* state)
{
// 使用AutoContextSwitch对象,将内存上下文切换为m_scanMemContext,确保分配的内存在扫描结束前一直有效。
AutoContextSwitch newMemCnxt(m_scanMemContext);
// 获取投影信息,包括要访问的列、延迟读取的列、系统列等。
ProjectionInfo* proj = state->ps.ps_ProjInfo;
// 如果存在需要访问的列
if (proj->pi_acessedVarNumbers != NIL) {
List* pColList = proj->pi_acessedVarNumbers;
// 初始化需要访问的列的相关参数
m_colNum = list_length(pColList);
m_colId = (int*)palloc(sizeof(int) * m_colNum);
m_lateRead = (bool*)palloc0(sizeof(bool) * m_colNum);
int i = 0;
ListCell* cell = NULL;
// 初始化需要访问的列的编号数组
foreach (cell, pColList) {
// m_colIdx[] start from zero
Assert(lfirst_int(cell) > 0);
int colId = lfirst_int(cell) - 1;
// 检查列是否存在
if (colId >= m_relation->rd_att->natts) {
ereport(ERROR, (errcode(ERRCODE_UNDEFINED_COLUMN),
errmsg("column %d does not exist", colId)));
}
// 检查列是否被删除
if (m_relation->rd_att->attrs[colId]->attisdropped) {
ereport(ERROR, (errcode(ERRCODE_UNDEFINED_COLUMN),
errmsg("column %s does not exist",
NameStr(m_relation->rd_att->attrs[colId]->attname))));
}
m_colId[i] = colId;
m_lateRead[i] = false;
i++;
}
// 初始化延迟读取的列的标志
foreach (cell, proj->pi_lateAceessVarNumbers) {
int colId = lfirst_int(cell) - 1;
for (i = 0; i < m_colNum; ++i) {
if (colId == m_colId[i]) {
m_lateRead[i] = true;
break;
}
}
}
// 初始化每列在当前CU中的扫描位置的数组
m_scanPosInCU = (int*)palloc0(sizeof(int) * m_colNum);
// 初始化每列的CU描述符管理对象数组
m_CUDescInfo = (LoadCUDescCtl**)palloc(sizeof(LoadCUDescCtl*) * m_colNum);
// 初始化列填充函数数组
m_colFillFunArrary = (colFillArray*)palloc(sizeof(colFillArray) * m_colNum);
// 初始化填充向量的函数数组
m_fillVectorByTids = (FillVectorByTidsFun*)palloc(sizeof(FillVectorByTidsFun) * m_colNum);
// 初始化延迟读取的填充向量的函数数组
m_fillVectorLateRead = (FillVectorLateReadFun*)palloc(sizeof(FillVectorLateReadFun) * m_colNum);
// 调用BindingFp函数,为不同的数据类型的列设置相应的填充函数。
BindingFp(state);
}
// 如果存在系统列
if (proj->pi_sysAttrList != NIL) {
ListCell* cell = NULL;
List* pSysList = proj->pi_sysAttrList;
// 初始化系统列的相关参数
m_sysColNum = list_length(pSysList);
m_sysColId = (int*)palloc(sizeof(int) * m_sysColNum);
int i = 0;
// 初始化系统列的编号数组
foreach (cell, pSysList) {
m_sysColId[i++] = lfirst_int(cell);
}
}
// 设置m_onlyConstCol标志,表示是否仅访问系统列或常量列。
m_onlyConstCol = proj->pi_const;
// 检查标志,如果仅访问系统列或常量列,确保其他参数为0。
#ifdef USE_ASSERT_CHECKING
if (m_onlyConstCol) {
Assert(m_colNum == 0 && m_sysColNum == 0);
}
#endif
// 如果仅访问系统列或常量列,创建虚拟的LoadCUDescCtl对象。
if (OnlySysOrConstCol()) {
m_virtualCUDescInfo = New(CurrentMemoryContext) LoadCUDescCtl(m_startCUID);
}
}
CStore::LoadCudescMinus 函数用于计算已加载的 CUDesc 的数量,接收 CUDesc 数组的起始和结束索引位置,并考虑了数组索引环绕的情况。在结束位置大于等于起始位置时,直接计算两者的差值;而在结束位置小于起始位置时,考虑数组索引环绕的情况,通过绕过起始位置到结束位置的数量来计算已加载的 CUDesc 的总数。函数返回已加载的 CUDesc 的数量。函数源码如下所示:(路径:src/gausskernel/storage/cstore/cstore_am.cpp
)
/*
* @Description: 计算已加载的CUDesc的数量
* @Param[IN] end: 数组索引的结束位置
* @Param[IN] start: 数组索引的起始位置
* @Return: 已加载的CUDesc的数量
* @See also:
*/
int CStore::LoadCudescMinus(int start, int end) const
{
// 如果结束位置大于等于起始位置,直接计算差值
if (end >= start) {
return end - start;
}
// 否则,考虑数组环绕的情况,计算绕过起始位置到结束位置的数量
return u_sess->attr.attr_storage.max_loaded_cudesc - start + end;
}
CStore::GetCUXmin 函数的主要目的是从给定的 CU ID 中获取对应 CU 的 xmin,即最小事务ID。函数通过访问列的 cudec 记录,使用列 0 的 cudec 记录中的 xmin 作为 CU 的 xmin。如果未找到相应的 CU 描述符,函数会抛出致命错误。函数源码如下所示:(路径:src/gausskernel/storage/cstore/cstore_am.cpp
)
/*
* @Description: 根据 CU(列单元)的 ID 获取对应的 CU 的 xmin(最小事务ID)。
* @IN cuid: CU 的 ID
* @Return: CU 的 xmin
*/
TransactionId CStore::GetCUXmin(uint32 cuid)
{
// 获取第一个未丢弃的列的索引
int colid = CStoreGetfstColIdx(m_relation);
// 使用列0的 cudec 记录的 xmin 作为 cu xmin
CUDesc cudesc;
// 通过给定的 CU ID 获取对应的 CU 描述符
bool found = this->GetCUDesc(colid, cuid, &cudesc, m_snapshot);
if (!found) {
// 如果未找到 CU 描述符,抛出错误
Assert(false);
ereport(FATAL,
(errmsg("未找到压缩单元描述符(表 \"%s\",列 \"%s\",cuid %u)",
RelationGetRelationName(m_relation),
NameStr(m_relation->rd_att->attrs[colid]->attname),
cuid)));
}
// 返回获取到的 CU 的 xmin
return cudesc.xmin;
}
CStoreGetfstColIdx 函数的目的是获取在给定的关系(Relation)中第一个没有被删除(dropped)的列的索引。函数遍历关系的所有属性,找到第一个没有被标记为已删除的属性,然后返回其索引。函数源码如下所示:(路径:src/gausskernel/storage/cstore/cstore_am.cpp
)
/*
* @Description: get the first colum index that is not dropped
* @Param[IN] rel: the target relation,表示目标关系(表)
* @Return: the first column index that is not dropped,返回第一个没有被删除的列的索引
* @See also: 如果在给定的关系中找不到没有被删除的属性,则默认返回 0
*/
int CStoreGetfstColIdx(Relation rel)
{
// 遍历关系中的所有属性
for (int i = 0; i < rel->rd_att->natts; i++) {
// 判断属性是否已删除。如果属性已删除,attisdropped 将为真(true)。
if (!rel->rd_att->attrs[i]->attisdropped)
// 返回第一个没有被删除的属性的索引
return i;
}
// 如果在给定的关系中找不到没有被删除的属性,则默认返回 0
return 0;
}