openGauss源码解析——列存压缩算法

news/2024/7/9 20:20:20 标签: 数据库, c++, postgresql

在openGauss数据库中,相对于行存以页为单元进行压缩,列存以CU为单元具有天然的压缩优势。

在openGauss中有三种压缩级别:LOW, MIDDLE, HIGH。指定的压缩等级越高,则数据的压缩率越高。除此之外还可以选择不开启压缩。

typedef enum OptCompress {
    COMPRESS_NO = 0,
    COMPRESS_LOW,
    COMPRESS_MIDDLE,
    COMPRESS_HIGH,
} OptCompress;

对于压缩算法,一共有:

#define CU_DeltaCompressed 0x0001
#define CU_DicEncode 0x0002
// CU_CompressExtend is used for extended compression.
// For compression added afterwards, it can be defined as CU_CompressExtend + 0x0001~0x0008
#define CU_CompressExtend 0x0004    // Used for extended compression
#define CU_Delta2Compressed 0x0005  // CU_Delta2Compressed equals CU_CompressExtend plus 0x0001
#define CU_XORCompressed 0x0006     // CU_XORCompressed equals CU_CompressExtend plus 0x0002
#define CU_RLECompressed 0x0008
#define CU_LzCompressed 0x0010
#define CU_ZlibCompressed 0x0020
#define CU_BitpackCompressed 0x0040
#define CU_IntLikeCompressed 0x0080

对于一个CU的压缩,代码在CU::Compress(int valCount, int16 compress_modes, int align_size)函数里:

void CU::Compress(int valCount, int16 compress_modes, int align_size)
{
    errno_t rc;

    // Step1: 初始化,为compress_buffer分配内存,原数据大小 + NULL值位图大小 + 头部大小,压缩后大小不会超过分配的compress_buffer大小
    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;

    // Step 2: 填充CU的NULL值 bitmap 位图
    buf = CompressNullBitmapIfNeed(buf);

    // Step 3: 压缩CU内的数据
    bool compressed = false;
    if (COMPRESS_NO != heaprel_get_compression_from_modes(compress_modes))
        compressed = CompressData(buf, valCount, compress_modes, align_size);

    // case 1: 用户定义数据不需要被加密
    // case 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);

    // Step 4: 填充 compress_buffer 的头部
    FillCompressBufHeader();

    m_cache_compressed = true;

    // Step 5; 释放原数据buf
    FreeSrcBuf();
}

实际的数据压缩发生在CU::CompressData(_out_ char* outBuf, _in_ int nVals, _in_ int16 compress_modes, int align_size)函数里:

bool CU::CompressData(_out_ char* outBuf, _in_ int nVals, _in_ int16 compress_modes, int align_size)
{
    int compressOutSize = 0;
    bool beDelta2Compressed = false;
    bool beXORCompressed = false;

    // 获取压缩级别
    int8 compression = heaprel_get_compression_from_modes(compress_modes);

    // 压缩结果输出到outBuf
    CompressionArg2 output = {0};
    output.buf = outBuf;
    output.sz = (m_compressedBuf + m_compressedBufSize) - outBuf;

    // 压缩的输入,就是CU数据
    CompressionArg1 input = {0};
    input.sz = m_srcDataSize;
    input.buf = m_srcData;
    input.mode = compress_modes;

    // 为当前CU的数据设置 compression filter
    compression_options* ref_filter = (compression_options*)m_tmpinfo->m_options;

    // 如果允许tsdb,并且属性是timestamp或者float类型:
    // tsdb: 时序数据库
    if (g_instance.attr.attr_common.enable_tsdb && (ATT_IS_TIMESTAMP(m_atttypid) || ATT_IS_FLOAT(m_atttypid))) {
        SequenceCodec sequenceCoder(m_eachValSize, m_atttypid);     // 由SequenceCodec类完成
        compressOutSize = sequenceCoder.compress(input, output);
        if (ATT_IS_TIMESTAMP(m_atttypid)) {
            beDelta2Compressed = true;  // timestamp则使用了Delta2压缩
        } else if (ATT_IS_FLOAT(m_atttypid)) {
            beXORCompressed = true;     // float则使用了XOR压缩
        }
    }
    // 如果压缩结果小于0或者没有用Delta2和XOR压缩,说明要么上一步没有成功压缩,要么不是timestamp和float类型
    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);            // 由IntegerCoder类完成

                /* set min/max value */
                if (m_tmpinfo->m_valid_minmax) {
                    intCoder.SetMinMaxVal(m_tmpinfo->m_min_value, m_tmpinfo->m_max_value);
                }
                // 告诉IntegerCoder是否使用RLE压缩
                intCoder.m_adopt_rle = ref_filter->m_adopt_rle;
                compressOutSize = intCoder.Compress(input, output);
            } else if (ATT_IS_NUMERIC_TYPE(m_atttypid)) {       // 属性是numeric类型的
                if (compression > COMPRESS_LOW) {               // 压缩级别大于LOW,是MIDDLE或者HIGH
                    /// numeric data type compression.
                    /// lz4/zlib is used directly.              // 直接使用lz4/zlib压缩
                    input.buildGlobalDict = false;
                    input.useGlobalDict = false;
                    input.globalDict = NULL;
                    input.useDict = false;
                    input.numVals = HasNullValue() ? (nVals - CountNullValuesBefore(nVals)) : nVals;

                    StringCoder strCoder;                       // 由StringCoder类完成
                    compressOutSize = strCoder.Compress(input, output);
                }
            } else {
                // for future, another type             // 未来的其他类型
            }
        } else if (m_eachValSize > 0 && m_eachValSize <= 8) {           // 类型大小在(0, 8]范围
            IntegerCoder intCoder(m_eachValSize);                       // 由IntegerCoder完成
            /* set min/max value */
            if (m_tmpinfo->m_valid_minmax) {
                intCoder.SetMinMaxVal(m_tmpinfo->m_min_value, m_tmpinfo->m_max_value);
            }
            // 告诉IntegerCoder是否使用RLE压缩
            intCoder.m_adopt_rle = ref_filter->m_adopt_rle;         
            compressOutSize = intCoder.Compress(input, output);     // 由IntegerCoder类完成
        } else {
            // FUTURE CASE: complete global dictionary              // 未来会支持的:全局字典压缩
            Assert(-1 == m_eachValSize || m_eachValSize > 8);       // 类型大小大于8,或者为-1(变长字符)
            input.buildGlobalDict = false;
            input.useGlobalDict = false;
            input.globalDict = NULL;

            // 类型大小大于8的直接使用lz4/zlib,不用字典方法

            // 类型大小为-1的,说明是变长类型,如果压缩级别不是LOW则使用字典压缩
            input.useDict = (m_eachValSize > 8) ? false : (COMPRESS_LOW != compression);

            // values的数量是除掉了空值的
            input.numVals = HasNullValue() ? (nVals - CountNullValuesBefore(nVals)) : nVals;

            // 由StringCoder类完成
            StringCoder strCoder;
            // 提示StringCoder类是否使用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) {
            /* sample and set adopted compression methods */
            ref_filter->set_common_flags(output.modes);
        }

        return true;
    }

    return false;
}

对于SequenceCodec类的压缩,会先判断类型,如果是timestamp则使用Delta2压缩,是float类型则使用XOR压缩。然后还会判断压缩级别,如果是MIDDLE或HIGH则还需要分别使用lz4和zlib进行压缩。

对于IntegerCoder类的压缩,会先进行Delta压缩,如果使用RLE的话再使用RLE压缩,然后对压缩级别为MIDDLE和HIGH的情况分别使用lz4和zlib压缩。

对于StringCoder类的压缩,会先判断是否使用字典压缩,如果使用则先进行字典压缩,然后对数字部分使用IntegerCoder进行压缩。如果字典压缩失败或者没有允许使用字典压缩,则直接对于LOW和MIDDLE级别使用lz4压缩,对于HIGH级别使用zlib压缩。


可以看到整个流程为:
先判断是否允许了tsdb(时序数据库)并且类型为timestamp或者float类型,如果是,则压缩是由SequenceCodec类完成的,timestamp则使用了Delta2压缩,float则使用了XOR压缩。如果压缩级别为MIDDLE或HIGH,则还需要进行lz4或zlib压缩。
对于timestamp和float类型:

LOWMIDDLEHIGH
timestampDelta2Delta2 + lz4Delta2 + zlib
floatXORXOR + lz4XOR + zlib

如果不允许tsdb或者上一步没有成功压缩,则进入以下的流程:

如果是IntLike(类整型):char类型则由IntegerCoder类完成,先使用Delta压缩,如果开启了RLE则使用RLE压缩,如果压缩级别为MIDDLE或HIGH,则还需要进行lz4或zlib压缩。

LOWMIDDLEHIGH
charDelta + RLE(可选)Delta + RLE(可选) + lz4Delta + RLE(可选) + zlib

numeric类型则要判断压缩级别,如果级别为LOW,则不压缩numeric类型。当级别为MIDDLE或HIGH时,直接使用lz4/zlib压缩,由StringCoder类完成。

LOWMIDDLEHIGH
numeric不压缩lz4zlib

对于不是IntLike(类整型)的情况,如果类型大小在(0, 8]范围,则由IntegerCoder类完成,并使用RLE压缩,如果级别为MIDDLE或HIGH还需要分别使用lz4和zlib压缩。

LOWMIDDLEHIGH
长度不大于8的定长字符Delta + RLE(可选)Delta + RLE(可选) + lz4Delta + RLE(可选) + zlib

类型大小大于8的直接使用lz4/zlib,不使用字典压缩:

LOWMIDDLEHIGH
长度大于8的定长字符lz4lz4zlib

类型大小为-1的,说明是变长类型,如果压缩级别为LOW则不使用字典,直接使用lz4。如果压缩级别为MIDDLE或HIGH,尝试用字典方法,然后对数字部分使用IntegerCodr进行压缩:

LOWMIDDLEHIGH
变长字符lz4字典 + Delta + RLE(可选) + lz4字典 + Delta + RLE(可选) + zlib

综上:

LOWMIDDLEHIGH
timestampDelta2Delta2 + lz4Delta2 + zlib
floatXORXOR + lz4XOR + zlib
charDelta + RLE(可选)Delta + RLE(可选) + lz4Delta + RLE(可选) + zlib
numeric不压缩lz4zlib
长度不大于8的定长字符Delta + RLE(可选)Delta + RLE(可选) + lz4Delta + RLE(可选) + zlib
长度大于8的定长字符lz4lz4zlib
变长字符lz4字典 + Delta + RLE(可选) + lz4字典 + Delta + RLE(可选) + zlib

http://www.niftyadmin.cn/n/1842716.html

相关文章

openGauss列存数据压缩实验

对于时序场景&#xff0c;float和timestamp类型占比较大&#xff0c;需要重点关注&#xff0c;较高的压缩率可以降低磁盘空间的使用。 openGauss中&#xff0c;对于float使用Delta2算法&#xff0c;对于float使用XOR算法&#xff0c;推测参考了facebook关于时序数据库的论文&a…

解决:ORA-00054: resource busy and acquire with NOWAIT specified or timeout expired

目录问题解决问题 在oracle数据库中执行insert操作时&#xff0c;遇到下面的错误信息&#xff1a; ORA-00054: resource busy and acquire with NOWAIT specified or timeout expired 00054. 00000 - “resource busy and acquire with NOWAIT specified or timeout expired”…

PostgreSQL插入大量数据:pg_testgen插件

PostgreSQL test generator 在进行数据库开发、测试时&#xff0c;新建表之后&#xff0c;时常想自己插入数据&#xff0c;但十分麻烦。 pg_testgen插件可以产生大量随机数据&#xff0c;方便进行数据库开发测试。 插件地址&#xff1a;pg_testgen 安装方法&#xff1a; c…

Windows查看和导入证书(.cer / .pfx)

文章目录证书介绍问题汇总导入导出细节注意如何查看以上两种证书的到期日&#xff1f;Windows下导入证书证书介绍 作为文件形式存在的证书一般有以下几种格式&#xff1a; 带有私钥的证书 由Public Key Cryptography Standards #12&#xff0c;PKCS#12标准定义&#xff0c;包含…

PostgreSQL 空闲空间映射表(FSM)

随着数据表中不断插入和删除元组&#xff0c;页内必然会产生空闲空间。当我们需要插入新的元组时&#xff0c;需要优先将元组放到已有页内的空闲空间内&#xff0c;以节约存储空间。如果每次都用新的页来存放新元祖&#xff0c;显然会造成空间利用率的浪费。但我们怎么知道哪个…

PostgreSQL可见性映射表(VM)和VACUUM操作

PostgreSQL为了实现多版本并发控制&#xff08;MVCC&#xff09;&#xff0c;当事务删除或者更新元组时&#xff0c;并非从物理上删除&#xff0c;而是将其标记无效&#xff0c;最终再通过VACUUM命令清理这些无效元组&#xff0c;真正的物理删除发生在清理过程。清理无效元组时…

MySQL存储引擎介绍及配置

目录概述各存储引擎的特性MyISAMInnoDBMEMORYMERGE如何选择合适的存储引擎查看当前的默认存储引擎修改默认的存储引擎设置存储引擎概述 插件式存储引擎是MySQL数据库最重要的特征之一&#xff0c;用户可以根据应用的需要选择如何存储和索引数据、是否使用事务等。 MySQL 5.0支…

PostgreSQL插件开发

PostgreSQL中许多控制信息都是以系统表的形式来管理&#xff0c;这个特点决定了PostgreSQL比其他数据库更容易进行内核扩展。PostgreSQL还提供了丰富的数据库内核编程接口&#xff0c;允许开发者以插件的形式将自己的代码融入内核。 PostgreSQL插件开发非常简单&#xff0c;下…