声明:本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。
本文主要参考了 OpenGauss1.1.0 的开源代码和《OpenGauss数据库源码解析》和《PostgresSQL数据库内核分析》一书
在 OpenGauss 中,有一个名为 pg_table_size 的内置函数可以帮助我们实时获取表的存储空间大小。本文将深入探讨这个函数的工作原理、用途以及如何使用它来监控和优化数据库性能。pg_table_size 函数的使用方式如下所示:
select pg_size_pretty(pg_table_size('表名'));
下面我们以一个实际的案例来调试下:
---创建列存表
CREATE TABLE column_store_table (
id INT,
name VARCHAR(50),
age INT,
salary DECIMAL(10, 2),
email VARCHAR(100)
)WITH (ORIENTATION = COLUMN);
---插入数据
INSERT INTO column_store_table VALUES
(1, 'John', 30, 50000.00, '[email protected]'),
(2, 'Alice', 28, 60000.50, NULL),
(3, 'Bob', NULL, NULL, '[email protected]');
---执行pg_table_size查看表的大小
select pg_size_pretty(pg_table_size('column_store_table'));
pg_size_pretty
----------------
72 kB
(1 row)
pg_table_size 函数作用是根据输入的表对象标识符(Oid),获取该表的大小信息。在获取表大小时,根据数据库是否为分布式数据库(PGXC)以及其他条件,它可能会采用不同的计算方法。最终,函数将计算得到的表大小作为整数值返回。如果表无效(不存在或无法打开),则返回 NULL。这个函数的目的是为了提供数据库管理员或开发人员一个快速获取表大小的工具,以便进行性能监控和优化。函数源码如下所示:(路径:src\common\backend\utils\adt\dbsize.cpp
)
Datum pg_table_size(PG_FUNCTION_ARGS)
{
// 从参数中获取传入的表的对象标识符
Oid relOid = PG_GETARG_OID(0);
// 表关联对象
Relation rel = NULL;
int64 size;
/* 获取共享锁,以防止其他并行操作中删除该表 */
// 尝试以访问共享锁的方式打开指定的表,并将其关联到 rel 变量中
rel = try_relation_open(relOid, AccessShareLock);
// 如果表无效(不存在或无法打开)
if (!RelationIsValid(rel)) {
// 返回 NULL,表示无法获取表的大小
PG_RETURN_NULL();
}
#ifdef PGXC
// 如果是分布式数据库(PGXC)
if (COLLECT_FROM_DATANODES(relOid)) {
// 通过调用 pgxc_exec_sizefunc 从分布式数据节点收集表的大小信息
// 结果存储在 size 变量中
size = pgxc_exec_sizefunc(relOid, "pg_table_size", NULL);
} else {
#else
// 如果不是分布式数据库(PGXC)或分布式功能未启用
if (true) {
#endif
// 使用 calculate_table_size 函数计算表的大小
// 具体的实现可能包括扫描表的存储文件、统计页面数等等
// 计算结果存储在 size 变量中
size = calculate_table_size(rel, DEFAULT_FORKNUM);
}
// 关闭之前打开的表,并释放占用的资源
relation_close(rel, AccessShareLock);
rel = NULL;
// 返回计算得到的表的大小,以整数的形式返回
PG_RETURN_INT64(size);
}
calculate_table_size 函数主要功能是计算指定表(或其索引、TOAST 表等)的总存储空间大小。这个大小包括表的主要数据文件、Free Space Map(FSM,用于跟踪空闲块的数据结构)、Visibility Map(VM,用于加速查询的数据结构)以及 TOAST 表(如果存在)。该函数还能够处理分区表,并且能够针对列存储表执行不同的计算。函数源码如下所示:(路径:src\common\backend\utils\adt\dbsize.cpp
)
/*
* Calculate total on-disk size of a given table,
* including FSM and VM, plus TOAST table if any.
* Indexes other than the TOAST table's index are not included.
*
* Note that this also behaves sanely if applied to an index or toast table;
* those won't have attached toast tables, but they can have multiple forks.
*/
static int64 calculate_table_size(Relation rel, int forkNumOption)
{
int64 size = 0; // 初始化变量 size,用于存储表的总存储空间大小
// 检查表是否是索引,如果不是索引则进入下面的逻辑
if (!RelationIsIndex(rel)) {
#ifdef ENABLE_MOT
// 如果表是分布式表且是 MOT 存储引擎的表
if (RelationIsForeignTable(rel) && RelationIsMOTTableByOid(RelationGetRelid(rel))) {
// 调用 CalculateMotRelationSize 函数计算表的大小
size = CalculateMotRelationSize(rel, InvalidOid);
} else if (!RelationIsPartitioned(rel)) {
#else
// 如果不是分布式表,进入下面的逻辑
if (!RelationIsPartitioned(rel)) {
#endif
// 调用 calculate_table_file_size 函数计算表的文件大小
// 具体的计算方式包括扫描数据文件、FSM、VM等
size = calculate_table_file_size(rel, RelationIsColStore(rel), forkNumOption);
} else {
List* partitions = NIL;
ListCell* cell = NULL;
Partition partition = NULL;
Relation partRel = NULL;
// 获取分区表的分区列表
partitions = relationGetPartitionList(rel, AccessShareLock);
// 迭代处理每个分区,计算每个分区的文件大小,并累加到 size 变量中
foreach (cell, partitions) {
partition = (Partition)lfirst(cell);
partRel = partitionGetRelation(rel, partition);
// 调用 calculate_table_file_size 函数计算分区的文件大小
// 具体的计算方式与非分区表相似
size += calculate_table_file_size(partRel, RelationIsColStore(rel), forkNumOption);
// 释放分区关联的资源
releaseDummyRelation(&partRel);
}
// 释放分区列表的资源
releasePartitionList(rel, &partitions, AccessShareLock);
#ifdef ENABLE_MULTIPLE_NODES
// 如果表是分布式时序存储表,额外计算时序标签表的大小
if (RelationIsTsStore(rel)) {
size += CalculateTsTagRelationSize(rel, forkNumOption);
}
#endif
}
} else {
// 如果表是索引,找到索引对应的基表
Relation baseRel = relation_open(rel->rd_index->indrelid, AccessShareLock);
bool bCstore = RelationIsColStore(baseRel) && (rel->rd_rel->relam == PSORT_AM_OID);
#ifdef ENABLE_MOT
// 如果基表是分布式表且是 MOT 存储引擎的表
if (RelationIsForeignTable(baseRel) && RelationIsMOTTableByOid(RelationGetRelid(baseRel))) {
// 调用 CalculateMotRelationSize 函数计算表的大小
size = CalculateMotRelationSize(baseRel, RelationGetRelid(rel));
} else if (!RelationIsPartitioned(rel)) {
#else
if (!RelationIsPartitioned(rel)) {
#endif
// 如果基表不是列存储表
if (!bCstore)
// 调用 calculate_table_file_size 函数计算索引文件的大小
size = calculate_table_file_size(rel, false, forkNumOption);
else {
// 如果基表是列存储表,找到列存储索引对应的表,计算该表的文件大小
Relation cstoreIdxRel = relation_open(rel->rd_rel->relcudescrelid, AccessShareLock);
if (cstoreIdxRel != NULL) {
size = calculate_table_file_size(cstoreIdxRel, true, forkNumOption);
relation_close(cstoreIdxRel, AccessShareLock);
}
}
} else {
List* partOids = NIL;
ListCell* cell = NULL;
Oid partOid = InvalidOid;
Oid partIndexOid = InvalidOid;
Partition partIndex = NULL;
Relation partIndexRel = NULL;
Relation cstorePartIndexRel = NULL;
// 获取基表的分区 OID 列表
partOids = relationGetPartitionOidList(baseRel);
// 迭代处理每个分区
foreach (cell, partOids) {
partOid = lfirst_oid(cell);
// 找到分区对应的索引 OID
partIndexOid = getPartitionIndexOid(RelationGetRelid(rel), partOid);
partIndex = partitionOpen(rel, partIndexOid, AccessShareLock);
partIndexRel = partitionGetRelation(rel, partIndex);
// 如果基表是列存储表
if (bCstore) {
// 找到列存储索引对应的表,计算该表的文件大小
cstorePartIndexRel = relation_open(partIndexRel->rd_rel->relcudescrelid, AccessShareLock);
size += calculate_table_file_size(cstorePartIndexRel, true, forkNumOption);
relation_close(cstorePartIndexRel, AccessShareLock);
} else
// 如果基表不是列存储表,计算索引文件的大小
size += calculate_table_file_size(partIndexRel, false, forkNumOption);
// 释放分区相关的资源
partitionClose(rel, partIndex, AccessShareLock);
releaseDummyRelation(&partIndexRel);
}
// 释放分区 OID 列表的资源
releasePartitionOidList(&partOids);
#ifdef ENABLE_MULTIPLE_NODES
// 如果表是分布式时序存储表,额外计算时序标签表的大小
if (RelationIsTsStore(rel)) {
size += CalculateTsTagRelationSize(rel, forkNumOption);
}
#endif
}
// 关闭基表
relation_close(baseRel, AccessShareLock);
}
// 返回计算得到的表的大小
return size;
}
代码主要逻辑:
总的来说,这段代码提供了一个重要的功能,用于计算表的存储空间大小,包括各种相关文件的大小。这对于数据库管理员和性能优化来说非常有用,可以帮助他们更好地管理数据库存储和性能。
calculate_table_file_size 的主要功能是计算数据库中指定表的总文件大小,包括主表和相关的分桶表、列存储表、时序存储表等。它通过检查表的特性和指定的叉号选项来决定计算哪些文件的大小,并将这些大小累加到一个变量中,最终返回表的总文件大小。这个功能对于数据库管理员和性能优化是非常有用的,可以帮助他们了解表的实际存储需求以及数据库的性能特征,从而更好地管理和优化数据库系统。函数源码如下所示:(路径:src\common\backend\utils\adt\dbsize.cpp
)
static int64 calculate_table_file_size(Relation rel, bool isCStore, int forkNumOption)
{
int64 size = 0; // 初始化变量 size,用于存储表的文件大小
ForkNumber forkNum = (ForkNumber)0; // 初始化 forkNum,表示文件叉号
// 如果表启用了分桶
if (RELATION_CREATE_BUCKET(rel)) {
Relation heapBucketRel = NULL;
oidvector* bucketlist = searchHashBucketByOid(rel->rd_bucketoid);
// 迭代处理每个分桶
for (int i = 0; i < bucketlist->dim1; i++) {
heapBucketRel = bucketGetRelation(rel, NULL, bucketlist->values[i]);
if (forkNumOption == DEFAULT_FORKNUM) {
/*
* 计算堆关系的大小,包括空闲块空间映射(FSM)和可见性映射(VM)
*/
for (int ifork = 0; ifork <= MAX_FORKNUM; ifork++) {
forkNum = (ForkNumber)ifork;
size += calculate_relation_size(&(heapBucketRel->rd_node), InvalidBackendId, forkNum);
}
} else {
// 如果指定了 forkNumOption,计算指定叉号的堆关系大小
size += calculate_relation_size(&(heapBucketRel->rd_node), InvalidBackendId, forkNumOption);
}
// 关闭分桶关系
bucketCloseRelation(heapBucketRel);
}
/*
* 计算 TOAST 表的大小
*/
if (OidIsValid(rel->rd_rel->reltoastrelid)) {
size += calculate_toast_table_size(rel->rd_rel->reltoastrelid);
}
} else {
if (forkNumOption == DEFAULT_FORKNUM) {
/*
* 计算堆关系的大小,包括空闲块空间映射(FSM)和可见性映射(VM)
*/
for (int ifork = 0; ifork <= MAX_FORKNUM; ifork++) {
forkNum = (ForkNumber)ifork;
// 如果是列存储表(CStore),调用 CalculateCStoreRelationSize 计算大小
if (isCStore) {
size += CalculateCStoreRelationSize(rel, forkNum);
}
#ifdef ENABLE_MULTIPLE_NODES
// 如果是分布式时序存储表,调用 CalculateTStoreRelationSize 计算大小
else if (RelationIsTsStore(rel)) {
size += CalculateTStoreRelationSize(rel, forkNum);
}
#endif
else {
// 否则,调用 calculate_relation_size 计算大小
size += calculate_relation_size(&(rel->rd_node), rel->rd_backend, forkNum);
}
}
/*
* 计算 TOAST 表的大小
*/
if (OidIsValid(rel->rd_rel->reltoastrelid)) {
size += calculate_toast_table_size(rel->rd_rel->reltoastrelid);
}
} else {
// 如果指定了 forkNumOption,计算指定叉号的关系大小
if (isCStore) {
size += CalculateCStoreRelationSize(rel, forkNumOption);
} else if (RelationIsTsStore(rel)) {
#ifdef ENABLE_MULTIPLE_NODES
// 如果是分布式时序存储表,调用 CalculateTStoreRelationSize 计算大小
size += CalculateTStoreRelationSize(rel, forkNumOption);
#endif
} else {
// 否则,调用 calculate_relation_size 计算大小
size += calculate_relation_size(&(rel->rd_node), rel->rd_backend, forkNumOption);
}
}
}
return size; // 返回计算得到的表的文件大小
}
CalculateCStoreRelationSize 函数的主要功能是计算数据库中列存储表(CStore)的总文件大小,包括数据文件、列存储文件、元数据文件和增量表的相关文件。它通过迭代处理每个数据文件和列存储文件的段来计算文件的大小,并考虑增量表和 CUDesc 表的大小,最终返回列存储表的总文件大小。这个功能对于数据库管理员和性能优化人员非常有用,可以帮助他们了解列存储表的存储需求,进行性能分析和数据库优化。函数源码如下所示:(路径:src\common\backend\utils\adt\dbsize.cpp
)
int64 CalculateCStoreRelationSize(Relation rel, ForkNumber forknum)
{
int64 totalsize = 0; // 初始化变量 totalsize,用于存储计算得到的列存储表总文件大小
int64 size = 0; // 初始化变量 size,用于暂存中间计算结果
uint64 segcount = 0; // 初始化变量 segcount,用于迭代列存储文件段
char pathname[MAXPGPATH] = {'\0'}; // 初始化数组 pathname,用于存储文件路径信息
// 如果指定的叉号是主叉号(MAIN_FORKNUM)
if (forknum == MAIN_FORKNUM) {
/*
* 计算数据文件大小。
*/
if (RelationIsDfsStore(rel)) { // 如果表是 DFS 存储表
if (IS_PGXC_DATANODE && IsConnFromApp()) {
/*
* 计算 HDFS 上 ORC 格式的数据文件大小(仅在连接来自应用程序时计算)。
*/
size = getDFSRelSize(rel); // 调用 getDFSRelSize 函数计算数据文件大小
totalsize += size; // 累加到 totalsize 中
}
} else { // 如果表不是 DFS 存储表
// 针对表的每个属性(列)进行处理
for (int i = 0; i < RelationGetDescr(rel)->natts; i++) {
totalsize += calculate_relation_size(
&rel->rd_node, rel->rd_backend, ColumnId2ColForkNum(rel->rd_att->attrs[i]->attnum));
// 调用 calculate_relation_size 计算数据文件大小,并累加到 totalsize 中
// 创建列存储文件节点对象
CFileNode tmpNode(rel->rd_node, rel->rd_att->attrs[i]->attnum, MAIN_FORKNUM);
// 创建 CUStorage 对象用于操作列存储文件
CUStorage custore(tmpNode);
// 迭代处理每个段(segment)
for (segcount = 0;; segcount++) {
struct stat fst;
CHECK_FOR_INTERRUPTS(); // 检查是否有中断请求
custore.GetFileName(pathname, MAXPGPATH, segcount); // 获取段的文件路径
if (stat(pathname, &fst) < 0) { // 获取文件状态信息
if (errno == ENOENT) // 如果文件不存在,跳出循环
break;
else
ereport(
ERROR, (errcode_for_file_access(), errmsg("could not stat file \"%s\": %m", pathname)));
// 报告文件访问错误
}
totalsize += fst.st_size; // 累加段文件大小到 totalsize 中
}
custore.Destroy(); // 销毁 CUStorage 对象
}
}
}
// 添加增量表(delta table)的大小
Relation deltaRel = try_relation_open(rel->rd_rel->reldeltarelid, AccessShareLock);
if (deltaRel != NULL) {
totalsize += calculate_relation_size(&(deltaRel->rd_node), deltaRel->rd_backend, forknum);
// 调用 calculate_relation_size 计算增量表大小,并累加到 totalsize 中
// 仅当指定的叉号为主叉号时,计算增量表的 TOAST 表大小,并累加到 totalsize 中
if (MAIN_FORKNUM == forknum && OidIsValid(deltaRel->rd_rel->reltoastrelid)) {
totalsize += calculate_toast_table_size(deltaRel->rd_rel->reltoastrelid);
}
relation_close(deltaRel, AccessShareLock); // 关闭增量表关系
}
// 添加 CUDesc 表(CUDesc table)的大小
Relation cudescRel = try_relation_open(rel->rd_rel->relcudescrelid, AccessShareLock);
if (cudescRel != NULL) {
totalsize += calculate_relation_size(&(cudescRel->rd_node), cudescRel->rd_backend, forknum);
// 调用 calculate_relation_size 计算 CUDesc 表大小,并累加到 totalsize 中
// 仅当指定的叉号为主叉号时,计算 CUDesc 表的 TOAST 表大小,并累加到 totalsize 中
if (MAIN_FORKNUM == forknum && OidIsValid(cudescRel->rd_rel->reltoastrelid)) {
totalsize += calculate_toast_table_size(cudescRel->rd_rel->reltoastrelid);
}
// 添加 CUDesc 索引(CUDesc Index)的大小
Relation cudescIdx = try_relation_open(cudescRel->rd_rel->relcudescidx, AccessShareLock);
if (cudescIdx != NULL) {
totalsize += calculate_relation_size(&(cudescIdx->rd_node), cudescIdx->rd_backend, forknum);
// 仅当指定的叉号为主叉号时,累加 CUDesc 索引的大小到 totalsize 中
// 不计算 TOAST 表的大小,因为 CUDesc 索引没有 TOAST 子关系。
relation_close(cudescIdx, AccessShareLock); // 关闭 CUDesc 索引关系
}
relation_close(cudescRel, AccessShareLock); // 关闭 CUDesc 表关系
}
return totalsize; // 返回计算得到的列存储表总文件大小(totalsize)
}
这段代码的主要功能是计算数据库中指定关系(表或索引)的总文件大小。它通过迭代处理每个文件段来计算文件的大小,并将结果累加到 totalsize 变量中。这个功能对于数据库管理员和性能优化人员非常有用,可以帮助他们了解数据库中关系的实际存储需求,进行性能分析和优化决策。函数源码如下所示:(路径:src\common\backend\utils\adt\dbsize.cpp
)
int64 calculate_relation_size(RelFileNode* rfn, BackendId backend, ForkNumber forknum)
{
int64 totalsize = 0; // 初始化变量 totalsize,用于存储计算得到的关系(表或索引)的总文件大小
char* relationpath = NULL; // 声明变量 relationpath,用于存储关系的路径信息
char pathname[MAXPGPATH] = {'\0'}; // 声明数组 pathname,用于存储文件路径信息
unsigned int segcount = 0; // 初始化变量 segcount,用于迭代处理文件段
errno_t rc = EOK; // 初始化错误码变量 rc
relationpath = relpathbackend(*rfn, backend, forknum); // 获取关系的路径
// 迭代处理文件段
for (segcount = 0;; segcount++) {
struct stat fst; // 声明结构体 fst,用于存储文件状态信息
CHECK_FOR_INTERRUPTS(); // 检查是否有中断请求
// 构造文件的路径名
if (segcount == 0)
rc = snprintf_s(pathname, MAXPGPATH, MAXPGPATH - 1, "%s", relationpath);
else
rc = snprintf_s(pathname, MAXPGPATH, MAXPGPATH - 1, "%s.%u", relationpath, segcount);
securec_check_ss(rc, "\0", "\0");
// 获取文件状态信息
if (stat(pathname, &fst) < 0) {
if (errno == ENOENT) // 如果文件不存在,跳出循环
break;
else
ereport(ERROR, (errcode_for_file_access(), errmsg("could not stat file \"%s\": %m", pathname)));
// 报告文件访问错误
}
totalsize += fst.st_size; // 累加文件大小到 totalsize 中
}
pfree_ext(relationpath); // 释放关系路径内存
return totalsize; // 返回计算得到的关系的总文件大小(totalsize)
}