声明:本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。
本文主要参考了 OpenGauss1.1.0 的开源代码和《OpenGauss数据库源码解析》一书以及OpenGauss社区学习文档和一些学习资料
列存储(autoanalyze)是一种数据库自动维护统计信息的功能,它定期分析表中的数据分布和变化情况,以优化查询性能。自动分析通过检查表的内容,确定最佳查询计划,从而帮助数据库优化查询操作,提高查询性能,特别是在大型数据库中。这个过程会定期运行,通常由数据库自动调度,以确保统计信息的准确性和最新性,以便数据库查询优化器可以更好地选择执行计划。这有助于提高数据库的性能和响应速度,减少了手动维护统计信息的工作负担。
在上一章中我们学习了relation_needs_vacanalyze 函数,其作用是确定一个表是否需要执行自动化的 VACUUM 或 ANALYZE 操作,并返回相应的决策。在relation_needs_vacanalyze 函数中,以下代码的主要目的是根据一些阈值和条件来确定是否需要对表进行自动的 VACUUM 和 ANALYZE 操作。
if (false == *dovacuum && allowVacuum)
*dovacuum = ((float4)vactuples > vacthresh);
// 确定表是否需要分析
if (allowAnalyze)
*doanalyze = ((float4)anltuples > anlthresh);
以下表格中列举了相关参数及含义:
参 数 | 含 义 |
---|---|
vac_base_thresh | 表的自动 VACUUM 操作的基本阈值,通常是一个整数,表示表中元组数量的下限,低于此下限将触发 VACUUM。 |
anl_base_thresh | 表的自动 ANALYZE 操作的基本阈值,通常是一个整数,表示表中元组数量的下限,低于此下限将触发 ANALYZE。 |
vac_scale_factor | VACUUM 操作的缩放因子,通常是一个浮点数,用于根据表的大小和性能需求调整自动 VACUUM 的触发条件。 |
anl_scale_factor | ANALYZE 操作的缩放因子,通常是一个浮点数,用于根据表的大小和性能需求调整自动 ANALYZE 的触发条件。 |
vacthresh | 实际的自动 VACUUM 操作阈值,根据基本阈值和缩放因子计算得出。 |
anlthresh | 实际的自动 ANALYZE 操作阈值,根据基本阈值和缩放因子计算得出。 |
vactuples | 跟踪表的元组数量,用于自动 VACUUM 操作中。 |
anltuples | 跟踪表的元组数量,用于自动 ANALYZE 操作中。 |
那么问题来了,vactuples和 anltuples 是如何获取到的呢?。
观察如下代码:
if (tabentry && (tabentry->changes_since_analyze || tabentry->n_dead_tuples)) {
anltuples = tabentry->changes_since_analyze;
vactuples = tabentry->n_dead_tuples;
AUTOVAC_LOG(DEBUG2, "fetch local stat info: vac \"%s\" changes_since_analyze = %ld n_dead_tuples = %ld ",
NameStr(classForm->relname), tabentry->changes_since_analyze, tabentry->n_dead_tuples);
}
if (avwentry && (avwentry->changes_since_analyze || avwentry->n_dead_tuples)) {
anltuples = avwentry->changes_since_analyze;
vactuples = avwentry->n_dead_tuples;
AUTOVAC_LOG(DEBUG2, "fetch global stat info: vac \"%s\" changes_since_analyze = %ld n_dead_tuples = %ld ",
NameStr(classForm->relname), avwentry->changes_since_analyze, avwentry->n_dead_tuples);
}
这段代码用于从统计信息中获取表的相关统计数据(changes_since_analyze 和 n_dead_tuples),并将这些数据分别赋值给 anltuples 和 vactuples,以便后续决定是否执行自动 ANALYZE 和自动 VACUUM 操作。那么 tabentry 又是什么呢?
PgStat_StatTabEntry 结构体,用于存储数据库中表的统计信息。源码如下:(路径:src/include/pgstat.h
)
typedef struct PgStat_StatTabEntry {
PgStat_StatTabKey tablekey; /* 表的统计信息的唯一标识符 */
PgStat_Counter numscans; /* 表的扫描次数 */
PgStat_Counter tuples_returned; /* 从表中返回的元组数 */
PgStat_Counter tuples_fetched; /* 从表中提取的元组数 */
PgStat_Counter tuples_inserted; /* 插入到表中的元组数 */
PgStat_Counter tuples_updated; /* 更新表中的元组数 */
PgStat_Counter tuples_deleted; /* 从表中删除的元组数 */
PgStat_Counter tuples_hot_updated; /* 热更新的元组数(在没有引起索引更新的情况下更新的元组数) */
PgStat_Counter n_live_tuples; /* 存活的元组数(未被标记为死亡的元组数) */
PgStat_Counter n_dead_tuples; /* 死亡的元组数(标记为死亡但尚未被清除的元组数) */
PgStat_Counter changes_since_analyze; /* 从上次分析以来发生的变化次数 */
PgStat_Counter blocks_fetched; /* 从磁盘读取的块数 */
PgStat_Counter blocks_hit; /* 从缓存中读取的块数 */
PgStat_Counter cu_mem_hit; /* 连续内存中的命中次数 */
PgStat_Counter cu_hdd_sync; /* 硬盘同步的次数 */
PgStat_Counter cu_hdd_asyn; /* 硬盘异步的次数 */
TimestampTz vacuum_timestamp; /* 用户发起的 VACUUM 的时间戳 */
PgStat_Counter vacuum_count; /* 用户发起的 VACUUM 的计数 */
TimestampTz autovac_vacuum_timestamp; /* 自动 VACUUM 的时间戳 */
PgStat_Counter autovac_vacuum_count; /* 自动 VACUUM 的计数 */
TimestampTz analyze_timestamp; /* 用户发起的分析的时间戳 */
PgStat_Counter analyze_count; /* 用户发起的分析的计数 */
TimestampTz autovac_analyze_timestamp; /* 自动分析的时间戳 */
PgStat_Counter autovac_analyze_count; /* 自动分析的计数 */
TimestampTz data_changed_timestamp; /* 数据变更的时间戳(例如,插入、删除、更新操作的时间戳) */
uint64 autovac_status; /* 自动 VACUUM 的状态信息 */
} PgStat_StatTabEntry;
PgStat_StatTabEntry 结构体用于存储数据库中每个表的统计信息,包括表的扫描次数、元组的增删改查数量、块的读取次数、硬盘访问情况、时间戳等信息,用于监控和分析表的使用情况和性能表现,特别是在自动 VACUUM 和分析过程中提供了关键的统计数据。
在了解了以上逻辑后,我们知道每次操作表后会更改统计信息,那么是怎么修改的呢?我们首先来从 pgstat_count_heap_insert 函数开始看起吧。
pgstat_count_heap_insert 函数用于在数据库中统计表中插入新元组的操作。它追踪并记录了在当前事务嵌套级别下插入的元组数量,以便进行性能监控和统计分析。
pgstat_count_heap_insert 函数源码如下所示:(路径:src/gausskernel/process/postmaster/pgstat.cpp
)
/*
* pgstat_count_heap_insert - count a tuple insertion of n tuples
* 统计插入 n 条元组的操作次数
*/
void pgstat_count_heap_insert(Relation rel, int n)
{
PgStat_TableStatus* pgstat_info = rel->pgstat_info;
// 检查是否需要进行统计
if (pgstat_info != NULL) {
/* We have to log the effect at the proper transactional level */
// 确定当前嵌套事务级别
int nest_level = GetCurrentTransactionNestLevel();
// 如果当前事务级别和记录的事务级别不一致,需要添加新的事务级别信息
if (pgstat_info->trans == NULL || pgstat_info->trans->nest_level != nest_level)
add_tabstat_xact_level(pgstat_info, nest_level);
// 更新当前事务级别下插入的元组数量
pgstat_info->trans->tuples_inserted += n;
}
}
此外,pgstat_count_cu_insert 函数是一个宏定义,用于将 pgstat_count_cu_insert 映射为 pgstat_count_heap_insert 函数的调用,其作用是用于统计某个表中的元组插入操作的次数。
pgstat_count_cu_insert 宏定义如下所示:(路径:src/include/pgstat.h
)
#define pgstat_count_cu_insert(rel, n) \
do { \
pgstat_count_heap_insert(rel, n); \
} while (0)
了解了 pgstat_count_heap_insert 函数和 pgstat_count_cu_insert 函数的作用,那这两个函数是在哪里调用的呢?答案是在 CStoreInsert::BatchInsertCommon 函数中进行调用的,该函数用于执行批量插入操作到列存储表,其功能包括创建列更新单元(CU)、统计插入或更新操作次数以及批量插入索引表中的数据。函数源码如下所示:(路径:src/gausskernel/storage/cstore/cstore_insert.cpp
)
void CStoreInsert::BatchInsertCommon(bulkload_rows* batchRowPtr, int options)
{
// 检查参数有效性,若为空或行数为0则直接返回
if (unlikely(batchRowPtr == NULL || batchRowPtr->m_rows_curnum == 0))
return;
int attno = m_relation->rd_rel->relnatts;
int col = 0;
Assert(attno == batchRowPtr->m_attr_num); // 断言表的列数和批处理行的列数相等
CHECK_FOR_INTERRUPTS(); // 检查是否收到中断信号
/* 步骤 1: 创建列更新单元(CU)和 CU 描述(CUDesc) */
for (col = 0; col < attno; ++col) {
// 如果列没有被删除,则创建相应的 CU 和 CUDesc
if (!m_relation->rd_att->attrs[col]->attisdropped) {
m_cuPPtr[col] = FormCU(col, batchRowPtr, m_cuDescPPtr[col]);
m_cuCmprsOptions[col].m_sampling_fihished = true;
}
}
// 根据是否为更新操作,统计 CU 插入或更新的次数
if (m_isUpdate)
pgstat_count_cu_update(m_relation, batchRowPtr->m_rows_curnum);
else
pgstat_count_cu_insert(m_relation, batchRowPtr->m_rows_curnum);
/*
* 步骤 2:
* a) 分配 CUID(列更新单元ID)和 CUPointer(列更新单元指针)
* b) 写入 CU 和 CUDesc
*/
SaveAll(options);
/* 步骤 3: 批量插入索引表 */
if (m_relation->rd_att->attrs[0]->attisdropped) {
int fstColIdx = CStoreGetfstColIdx(m_relation);
InsertIdxTableIfNeed(batchRowPtr, m_cuDescPPtr[fstColIdx]->cu_id);
} else
InsertIdxTableIfNeed(batchRowPtr, m_cuDescPPtr[0]->cu_id);
}
其中,参数 m_isUpdate 用于指示是否执行更新操作。如果 m_isUpdate 为真(true),则表示执行更新操作;如果 m_isUpdate 为假(false),则表示执行插入操作。在函数的后续部分,根据 m_isUpdate 的值来统计插入或更新的次数。
在 CStoreInsert::BatchInsertCommon 函数中还调用了 pgstat_count_cu_update 函数,其主要功能是在统计信息中记录列存(CStore)或 DFS 表的更新操作的数量。它首先获取与给定关系(Relation)关联的统计信息,然后检查是否需要在适当的事务级别记录操作效果。最后,它将更新的数量添加到统计信息中,以反映列存/DFS表的更新操作情况。函数源码如下所示:(路径:src/gausskernel/process/postmaster/pgstat.cpp
)
/*
* pgstat_count_cu/dfs_update - count a cstore/dfs update
* pgstat_count_cu/dfs_update - 记录列存/DFS更新操作
*/
void pgstat_count_cu_update(Relation rel, int n)
{
// 获取与关系关联的统计信息
PgStat_TableStatus* pgstat_info = rel->pgstat_info;
// 如果存在统计信息
if (pgstat_info != NULL) {
// 需要在适当的事务级别记录操作效果
int nest_level = GetCurrentTransactionNestLevel();
// 如果没有事务信息或者事务嵌套级别与当前不匹配,需要添加新的事务级别统计信息
if (pgstat_info->trans == NULL || pgstat_info->trans->nest_level != nest_level)
add_tabstat_xact_level(pgstat_info, nest_level);
// 更新统计信息中的"更新的元组数"字段,表示列存/DFS更新操作的数量
pgstat_info->trans->tuples_updated += n;
}
}
pgstat_count_cu_delete 函数的主要作用是,根据传入的关系(表)和删除的行数(n),统计列存或 DFS 的删除操作的数量,并记录在 PgStat_TableStatus 结构中,以便后续的性能统计和监视。函数首先获取表的 PgStat_TableStatus 信息,然后检查是否在当前事务层次中,如果不是,则添加一个新的事务层次。最后,增加已删除元组的数量。函数源码如下所示:(路径:src/gausskernel/process/postmaster/pgstat.cpp
)
以下代码定义了两个函数 pgstat_count_cu_delete 和 pgstat_count_dfs_delete,用于统计列存(CStore)和分布式文件系统(DFS)的删除操作。
/*
* pgstat_count_cu/dfs_delete - 统计列存/DFS删除操作的数量
*/
void pgstat_count_cu_delete(Relation rel, int n)
{
// 获取表的 PgStat_TableStatus 信息
PgStat_TableStatus* pgstat_info = rel->pgstat_info;
if (pgstat_info != NULL) {
// 需要在正确的事务层次记录操作效果
int nest_level = GetCurrentTransactionNestLevel();
// 如果当前事务层次与记录的事务层次不同,添加新的事务层次
if (pgstat_info->trans == NULL || pgstat_info->trans->nest_level != nest_level)
add_tabstat_xact_level(pgstat_info, nest_level);
// 增加已删除元组的数量
pgstat_info->trans->tuples_deleted += n;
}
}
pgstat_count_truncate 函数用于在执行表的截断操作后更新元组计数器。其主要作用是:根据传入的关系(表),在执行表的截断操作后,重置元组计数器,以便反映截断操作的影响。函数首先获取表的 PgStat_TableStatus 信息,然后检查是否在当前事务层次中,如果不是,则添加一个新的事务层次。接着,保存在截断前的计数器值,然后将插入、更新和删除的计数器重置为0,以便后续的性能统计和监视。
pgstat_count_truncate 函数源码如下所示:(路径:src/gausskernel/process/postmaster/pgstat.cpp
)
/*
* pgstat_count_truncate - 由于截断而更新元组计数器
*/
void pgstat_count_truncate(Relation rel)
{
// 获取表的 PgStat_TableStatus 信息
PgStat_TableStatus* pgstat_info = rel->pgstat_info;
if (pgstat_info != NULL) {
// 需要在正确的事务层次记录操作效果
int nest_level = GetCurrentTransactionNestLevel();
// 如果当前事务层次与记录的事务层次不同,添加新的事务层次
if (pgstat_info->trans == NULL || pgstat_info->trans->nest_level != nest_level)
add_tabstat_xact_level(pgstat_info, nest_level);
// 保存截断前的计数器值
pgstat_truncate_save_counters(pgstat_info->trans);
// 将插入、更新和删除的计数器重置为0
pgstat_info->trans->tuples_inserted = 0;
pgstat_info->trans->tuples_updated = 0;
pgstat_info->trans->tuples_deleted = 0;
}
}
pgstat_update_heap_dead_tuples 函数的主要作用是:根据传入的关系(表)和增量值,更新死元组的计数。函数首先获取表的 PgStat_TableStatus 信息,然后将增量值减去 t_delta_dead_tuples,以表示死元组的恢复。这个函数用于非事务性的 “delta” 死元组的计数,不会影响事务性状态,而是直接更新表的计数器。
/*
* pgstat_update_heap_dead_tuples - 更新死元组的计数
*
* 这个函数的语义是我们正在报告非事务性的 "delta" 死元组的恢复;
* 因此,t_delta_dead_tuples 减少而不是增加,并且更改直接进入每个表的计数器,
* 而不是进入事务性状态。
*/
void pgstat_update_heap_dead_tuples(Relation rel, int delta)
{
// 获取表的 PgStat_TableStatus 信息
PgStat_TableStatus* pgstat_info = rel->pgstat_info;
if (pgstat_info != NULL)
// 减少 delta 值,表示死元组的恢复
pgstat_info->t_counts.t_delta_dead_tuples -= delta;
}
以上这些函数用于数据库性能统计和监控,可以跟踪指定关系(表)的插入、更新、删除、截断、CU(Chunk Update)等操作,以记录和报告这些操作的频率和数量,帮助数据库管理员识别性能问题并监视数据库的活动。下表对以上函数进行了总结:
函 数 | 作 用 |
---|---|
pgstat_count_heap_insert | 用于统计在指定关系(表)中插入了多少行数据。 |
pgstat_count_heap_update | 用于统计在指定关系(表)中进行了多少次更新操作。 |
pgstat_count_heap_delete | 用于统计在指定关系(表)中进行了多少次删除操作。 |
pgstat_count_truncate | 用于统计在指定关系(表)上执行了多少次截断操作。 |
pgstat_update_heap_dead_tuples | 用于更新关系(表)中的死元组数量统计信息。 |
pgstat_count_cu_update | 用于统计在指定关系(表)的 CU(Chunk Update)上进行了多少次更新操作。 |
pgstat_count_cu_delete | 用于统计在指定关系(表)的 CU 上进行了多少次删除操作。 |