声明:本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。
本文主要参考了 OpenGauss1.1.0 的开源代码和《OpenGauss数据库源码解析》一书以及OpenGauss社区学习文档和一些学习资料
在【OpenGauss源码学习 —— 列存储(CU)(一)】中我们初步认识了 CU 的结构和作用,本文我们接着来学习列存储数据的压缩和解压缩操作。本文所要学习的函数如下表所示:
函 数 | 作 用 |
---|---|
GetCUHeaderSize | 用于获取一个压缩单元(CU)头部的大小。 |
Compress | 用于压缩数据。它接受要压缩的数据数量 (valCount)、压缩模式 (compress_modes) 和对齐大小 (align_size) 作为参数,然后对数据进行压缩操作。 |
FillCompressBufHeader | 用于填充压缩缓冲区的头部信息。这些信息通常包括元数据和描述压缩数据的头部。 |
CompressNullBitmapIfNeed | 如果需要压缩空值位图,则这个函数会对其进行压缩。它接受一个指向字符缓冲区 (buf) 的指针作为参数,然后执行相应的压缩操作。 |
CompressData | 用于压缩数据。它接受一个输出缓冲区 (outBuf),要压缩的数据数量 (nVals),压缩选项 (compressOption) 和对齐大小 (align_size) 作为参数,并将压缩后的数据存储在 outBuf 中。 |
这几个函数用于在数据库系统中进行列存储数据的压缩和解压缩操作,包括获取压缩头部信息大小、对数据进行压缩、填充压缩数据的头部信息、以及在需要时压缩 NULL 位图和解压缩数据,以有效地存储和检索压缩的列存储数据。
该函数用于计算列存储数据单元(Column Unit)头部的大小,该头部包括用于数据校验和解析的信息,如 CRC、魔术数字、信息模式、压缩的 NULL 位图大小、未压缩数据大小和压缩数据大小。这些信息在存储和检索列存储数据时起到关键作用,以确保数据的完整性和正确性。其函数源码如下所示:(路径:src/gausskernel/storage/cstore/cu.cpp
)
// 获取列存储数据单元(Column Unit)头部的大小
int16 CU::GetCUHeaderSize(void) const
{
// 返回头部大小,包括以下部分:
return sizeof(m_crc) + // CRC,用于数据完整性检查
sizeof(m_magic) + // 魔术数字,用于标识数据单元类型
sizeof(m_infoMode) + // 信息模式,包含元组和压缩元组的信息
// 如果存在压缩的NULL位图,包括其大小
(HasNullValue() ? sizeof(m_bpNullCompressedSize) : 0) +
sizeof(m_srcDataSize) + // 未压缩数据大小
sizeof(int); // 压缩后数据的大小
}
这个函数用于确定列存储数据单元头部的大小,该头部包含了用于数据校验和信息描述的各个字段。注释提供了对每个字段和计算过程的解释。
Compress 函数用于压缩一个列存储数据单元(Column Unit),它首先分配一个用于存储压缩后数据的缓冲区,然后依次执行以下步骤:初始化缓冲区大小,填充 NULL 位图(如果需要),压缩数据,如果数据无法压缩则保留未压缩的数据,加密压缩后的数据,填充缓冲区头部,最后标记数据单元为已压缩并释放原始数据缓冲区。其函数源码如下所示:(路径:src/gausskernel/storage/cstore/cu.cpp
)
/*
* @Description: 压缩一个列存储数据单元(Column Unit)
* @IN compress_modes: 压缩模式
* @IN valCount: 值的数量
* @See also: 另请参阅
*/
void CU::Compress(int valCount, int16 compress_modes, int align_size)
{
errno_t rc;
// 步骤 1: 初始化分配压缩缓冲区的大小
// 源数据大小 + NULL位图大小 + 头部大小
// 我们保证压缩数据大小不会超过这个大小
m_compressedBufSize = CUAlignUtils::AlignCuSize(m_srcDataSize + m_bpNullRawSize + sizeof(CU), align_size);
m_compressedBuf = (char*)CStoreMemAlloc::Palloc(m_compressedBufSize, !m_inCUCache);
int16 headerLen = GetCUHeaderSize();
char* buf = m_compressedBuf + headerLen;
// 步骤 2: 填充压缩的NULL位图
buf = CompressNullBitmapIfNeed(buf);
// 步骤 3: 压缩数据
bool compressed = false;
if (COMPRESS_NO != heaprel_get_compression_from_modes(compress_modes))
compressed = CompressData(buf, valCount, compress_modes, align_size);
// 情况 1: 用户定义不应压缩输入数据。
// 情况 2: 即使用户定义压缩数据,但压缩后的数据大小
// 大于未压缩数据的大小,因此使用未压缩数据而不是压缩数据。
if (compressed == false) {
rc = memcpy_s(buf, m_srcDataSize, m_srcData, m_srcDataSize);
securec_check(rc, "\0", "\0");
m_cuSizeExcludePadding = headerLen + m_bpNullCompressedSize + m_srcDataSize;
m_cuSize = CUAlignUtils::AlignCuSize(m_cuSizeExcludePadding, align_size);
PADDING_CU(buf + m_srcDataSize, m_cuSize - m_cuSizeExcludePadding);
}
// 压缩后加密数据单元
CUDataEncrypt(buf);
// 步骤 4: 填充压缩缓冲区的头部
FillCompressBufHeader();
m_cache_compressed = true;
// 步骤 5: 释放源缓冲区
FreeSrcBuf();
}
函数执行过程解释:假设有一个列存储数据单元(CU),其中包含多个列的数据,需要将该 CU 进行压缩。首先,函数分配一个缓冲区,该缓冲区的大小由源数据大小、NULL 位图大小和头部信息大小组成,确保足够容纳压缩后的数据。接着,它检查是否有 NULL 值,如果有,则填充 NULL 位图到缓冲区。然后,它尝试对数据进行压缩,如果压缩后的数据大小小于未压缩数据大小,将压缩后的数据存入缓冲区。如果数据无法压缩或者压缩后的大小更大,它将保留未压缩的数据。接下来,对压缩后的数据进行加密,并填充缓冲区头部信息。最后,将该 CU 标记为已压缩状态,并释放原始数据缓冲区。这个函数用于减小存储空间并提高数据传输效率。
CU::FillCompressBufHeader 函数用于填充压缩缓冲区(m_compressedBuf)的头部信息。以下是该函数的详细解释:该函数的主要功能是在压缩缓冲区中设置头部信息,包括魔术标识、信息模式、NULL 位图压缩大小、未压缩数据大小、压缩数据大小以及 CRC 校验值。这些信息用于描述和校验压缩后的数据。这个过程有助于确保数据的完整性和可靠性。
其中,CU::FillCompressBufHeader 函数在 Compress 函数中调用。其函数源码如下所示:(路径:src/gausskernel/storage/cstore/cu.cpp
)
void CU::FillCompressBufHeader(void)
{
errno_t rc;
// m_crc将在压缩结束时设置
char* buf = m_compressedBuf;
int pos = sizeof(m_crc);
// 将m_magic(魔术标识)复制到压缩缓冲区
rc = memcpy_s(buf + pos, sizeof(m_magic), &m_magic, sizeof(m_magic));
securec_check(rc, "\0", "\0");
pos += sizeof(m_magic);
// 设置m_infoMode(信息模式)为CU_CRC32C,表示使用CRC32C校验
m_infoMode |= CU_CRC32C;
// 将m_infoMode(信息模式)复制到压缩缓冲区
rc = memcpy_s(buf + pos, sizeof(m_infoMode), &m_infoMode, sizeof(m_infoMode));
securec_check(rc, "\0", "\0");
pos += sizeof(m_infoMode);
// 如果CU中包含NULL值,将m_bpNullCompressedSize(NULL位图压缩大小)复制到压缩缓冲区
if (HasNullValue()) {
rc = memcpy_s(buf + pos, sizeof(m_bpNullCompressedSize), &m_bpNullCompressedSize, sizeof(m_bpNullCompressedSize));
securec_check(rc, "\0", "\0");
pos += sizeof(m_bpNullCompressedSize);
}
// 将m_srcDataSize(未压缩数据大小)复制到压缩缓冲区
rc = memcpy_s(buf + pos, sizeof(m_srcDataSize), &m_srcDataSize, sizeof(m_srcDataSize));
securec_check(rc, "\0", "\0");
pos += sizeof(m_srcDataSize);
// 计算压缩数据的大小(cmprDataSize)并复制到压缩缓冲区
int cmprDataSize = m_cuSizeExcludePadding - GetCUHeaderSize() - m_bpNullCompressedSize;
rc = memcpy_s(buf + pos, sizeof(cmprDataSize), &cmprDataSize, sizeof(cmprDataSize));
securec_check(rc, "\0", "\0");
pos += sizeof(cmprDataSize);
// 断言检查头部数据的位置是否正确
Assert(pos == GetCUHeaderSize());
// 最后,计算CRC校验值(m_crc)并将其存储在压缩缓冲区的开头
m_crc = GenerateCrc(m_infoMode);
*(uint32*)m_compressedBuf = m_crc;
}
CU::CompressNullBitmapIfNeed 函数用于检查是否需要压缩 NULL 位图数据,然后在压缩缓冲区中进行相应的处理。以下是该函数的详细解释:该函数用于处理 NULL 位图数据的压缩,但当前的实现中,它并没有执行任何实际的压缩操作。在注释中标明了 “FUTURE CASE”,表示将来可能会加入对 NULL 位图数据的压缩和解压缩支持。所以,这个函数目前只是将原始的 NULL 位图数据复制到压缩缓冲区中,并将压缩后的大小设置为原始大小。其函数源码如下所示:(路径:src/gausskernel/storage/cstore/cu.cpp
)
// FUTURE CASE: null bitmap data should be compressed and decompressed
// 注意:应该同时修改CompressNullBitmapIfNeed()和UnCompressNullBitmapIfNeed()函数。
char* CU::CompressNullBitmapIfNeed(_in_ char* buf)
{
errno_t rc;
if (HasNullValue()) {
Assert(m_bpNullRawSize > 0);
// FUTURE CASE: 延迟压缩NULL位图数据
// 将NULL位图数据复制到压缩缓冲区中
rc = memcpy_s(buf, m_bpNullRawSize, m_nulls, m_bpNullRawSize);
securec_check(rc, "\0", "\0");
m_bpNullCompressedSize = m_bpNullRawSize;
}
return (buf + m_bpNullCompressedSize);
}
CU::CompressData 函数的作用是对列存储数据进行压缩。以下是该函数的详细解释:
这个函数执行以下操作:
- 根据压缩模式选择适当的压缩方法,对数据进行压缩。
- 如果支持时序数据类型(TIMESTAMP 或 FLOAT),可能执行特殊的时序压缩。
- 如果压缩成功,计算压缩后 CU 的大小并设置相应的压缩信息。
- 如果采样尚未完成,对样本进行采样并设置采纳的压缩方法。
- 返回一个布尔值,指示是否成功压缩。
这个函数用于在列存储中对数据进行压缩,以减小存储占用空间。根据数据类型和压缩模式,它可能使用不同的压缩算法。如果数据成功压缩,将设置压缩后 CU 的大小和相应的元信息。这有助于在存储和检索数据时提高性能和减少存储成本。其函数源码如下所示:(路径:src/gausskernel/storage/cstore/cu.cpp
)
/*
* @Description: 压缩一个CU(列存储单元)数据。
* @IN compress_modes: 压缩模式
* @IN nVals: 值的数量
* @OUT outBuf: 输出缓冲区
* @Return: 布尔值,表示是否成功压缩
* @See also:
*/
bool CU::CompressData(_out_ char* outBuf, _in_ int nVals, _in_ int16 compress_modes, int align_size)
{
int compressOutSize = 0; // 用于存储压缩后的数据大小
bool beDelta2Compressed = false; // 用于表示是否使用了特殊的时序压缩方法,例如Delta压缩
bool beXORCompressed = false; // 用于表示是否使用了XOR压缩方法
/* 从压缩模式获取压缩值 */
int8 compression = heaprel_get_compression_from_modes(compress_modes);
// 准备输入参数
CompressionArg2 output = {0};
output.buf = outBuf;
output.sz = (m_compressedBuf + m_compressedBufSize) - outBuf;
CompressionArg1 input = {0};
input.sz = m_srcDataSize;
input.buf = m_srcData;
input.mode = compress_modes;
// 获取压缩过滤器
compression_options* ref_filter = (compression_options*)m_tmpinfo->m_options;
// 检查是否支持时序数据类型,例如TIMESTAMP或FLOAT
if (g_instance.attr.attr_common.enable_tsdb && (ATT_IS_TIMESTAMP(m_atttypid) || ATT_IS_FLOAT(m_atttypid))) {
// 使用特殊的时序压缩方法
SequenceCodec sequenceCoder(m_eachValSize, m_atttypid);
compressOutSize = sequenceCoder.compress(input, output);
if (ATT_IS_TIMESTAMP(m_atttypid)) {
beDelta2Compressed = true;
} else if (ATT_IS_FLOAT(m_atttypid)) {
beXORCompressed = true;
}
}
// 如果没有进行时序压缩或时序压缩失败,继续以下操作
if (compressOutSize < 0 || (!beDelta2Compressed && !beXORCompressed)) {
// 重置输出参数
output = {0};
output.buf = outBuf;
output.sz = (m_compressedBuf + m_compressedBufSize) - outBuf;
// 检查是否使用整型压缩模式
if (m_infoMode & CU_IntLikeCompressed) {
if (ATT_IS_CHAR_TYPE(m_atttypid)) {
// 对CHAR类型使用整数压缩
IntegerCoder intCoder(8);
/* 设置最小/最大值 */
if (m_tmpinfo->m_valid_minmax) {
intCoder.SetMinMaxVal(m_tmpinfo->m_min_value, m_tmpinfo->m_max_value);
}
/* 提供RLE编码的提示 */
intCoder.m_adopt_rle = ref_filter->m_adopt_rle;
compressOutSize = intCoder.Compress(input, output);
} else if (ATT_IS_NUMERIC_TYPE(m_atttypid)) {
if (compression > COMPRESS_LOW) {
/// 数值数据类型压缩。
/// 直接使用lz4/zlib。
input.buildGlobalDict = false;
input.useGlobalDict = false;
input.globalDict = NULL;
input.useDict = false;
input.numVals = HasNullValue() ? (nVals - CountNullValuesBefore(nVals)) : nVals;
StringCoder strCoder;
compressOutSize = strCoder.Compress(input, output);
}
} else {
// 未来,其他类型
}
} else if (m_eachValSize > 0 && m_eachValSize <= 8) {
// 使用整数压缩
IntegerCoder intCoder(m_eachValSize);
/* 设置最小/最大值 */
if (m_tmpinfo->m_valid_minmax) {
intCoder.SetMinMaxVal(m_tmpinfo->m_min_value, m_tmpinfo->m_max_value);
}
/* 提供RLE编码的提示 */
intCoder.m_adopt_rle = ref_filter->m_adopt_rle;
compressOutSize = intCoder.Compress(input, output);
} else {
// 未来,其他情况
Assert(-1 == m_eachValSize || m_eachValSize > 8);
input.buildGlobalDict = false;
input.useGlobalDict = false;
input.globalDict = NULL;
// 对于大小大于8的定长数据类型,
// 直接使用lz4/zlib方法,不包括字典方法。
// 对于大小为-1的可变长度数据类型,可以应用字典方法
// 首先尝试使用字典方法。
input.useDict = (m_eachValSize > 8) ? false : (COMPRESS_LOW != compression);
// 值的数量不包括NULL值的数量。
input.numVals = HasNullValue() ? (nVals - CountNullValuesBefore(nVals)) : nVals;
// 使用StringCoder.Compress
StringCoder strCoder;
/* 提供关于RLE和字典编码的提示 */
strCoder.m_adopt_rle = ref_filter->m_adopt_rle;
strCoder.m_adopt_dict = ref_filter->m_adopt_dict;
compressOutSize = strCoder.Compress(input, output);
}
}
if (compressOutSize > 0) {
// 压缩成功,计算CU大小并设置压缩信息
Assert((uint32)compressOutSize < m_srcDataSize);
Assert((0 == (output.modes & CU_INFOMASK2)) && (0 != (output.modes & CU_INFOMASK1)));
m_infoMode |= (output.modes & CU_INFOMASK1);
m_cuSizeExcludePadding = (outBuf - m_compressedBuf) + compressOutSize;
m_cuSize = CUAlignUtils::AlignCuSize(m_cuSizeExcludePadding, align_size);
Assert(m_cuSize <= m_compressedBufSize);
PADDING_CU(m_compressedBuf + m_cuSizeExcludePadding, m_cuSize - m_cuSizeExcludePadding);
if (!ref_filter->m_sampling_fihished) {
/* 对样本进行采样并设置采纳的压缩方法 */
ref_filter->set_common_flags(output.modes);
}
return true;
}
return false;
}