行存表与列存表简述

一、行存表数据块存储结构

1、行存数据块整体布局

复制代码
┌─────────────────────────────────────────────┐
│              行存数据块 (8KB)                 │
├─────────────────────────────────────────────┤
│ 块头 (Block Header) - 112字节                 │
│  • pd_lsn (8字节): LSN                       │
│  • pd_checksum (2字节): 校验和                │
│  • pd_flags (1字节): 块状态标志               │
│  • pd_lower (2字节): 空闲空间起始偏移         │
│  • pd_upper (2字节): 空闲空间结束偏移         │
│  • pd_special (2字节): 特殊空间起始偏移       │
│  • pd_pagesize_version (2字节): 页大小版本    │
│  • pd_prune_xid (4字节): 清理事务ID           │
│  • pg_crc32c (4字节): CRC32校验              │
├─────────────────────────────────────────────┤
│ 行指针数组 (ItemId Array)                    │
│  • 每个ItemId 4字节 (lp_off, lp_flags, lp_len)│
│  • 指向实际行数据的偏移量                    │
├─────────────────────────────────────────────┤
│ 空闲空间 (Free Space)                        │
│  (从pd_lower到pd_upper)                       │
├─────────────────────────────────────────────┤
│ 行数据 (Row Data)                           │
│  • 从块尾部向前生长                          │
│  • 每行包含行头+列值                         │
├─────────────────────────────────────────────┤
│ 特殊空间 (Special Space)                     │
│  (索引块特有,如B-tree的左右指针)             │
└─────────────────────────────────────────────┘

2、行数据详细结构

复制代码
// 行头结构 (HeapTupleHeaderData)
typedef struct HeapTupleHeaderData
{
    uint32      t_xmin;        // 插入事务ID
    uint32      t_xmax;        // 删除/更新事务ID
    CommandId   t_cid;         // 命令ID
    uint16      t_infomask2;   // 属性数量+标志位
    uint16      t_infomask;    // 标志位(可见性等)
    uint8       t_hoff;        // 行头长度
    bits8       t_bits[FLEXIBLE_ARRAY_MEMBER]; // NULL位图
} HeapTupleHeaderData;

// 行数据布局
┌─────────────────────────────────────────┐
│ HeapTupleHeader (23-27字节)             │
├─────────────────────────────────────────┤
│ NULL位图 (每8列1字节,向上取整)          │
├─────────────────────────────────────────┤
│ 列1数据 (定长/变长)                     │
├─────────────────────────────────────────┤
│ 列2数据                                 │
├─────────────────────────────────────────┤
│ ...                                     │
├─────────────────────────────────────────┤
│ 变长列头信息 (如果存在变长列)            │
└─────────────────────────────────────────┘

3、行存表的空间管理机制

插入操作:

复制代码
-- 插入时空间分配
INSERT INTO users (id, name, email) VALUES (1, '张三', 'zhangsan@example.com');

-- 空间分配过程:
-- 1. 查找有足够空闲空间的块(FSM - 空闲空间映射)
-- 2. 在pd_lower处分配ItemId
-- 3. 从pd_upper处分配行数据空间
-- 4. 更新pd_lower和pd_upper

删除操作:

复制代码
-- 删除标记
DELETE FROM users WHERE id = 1;

-- 删除过程:
-- 1. 设置t_xmax为当前事务ID
-- 2. 设置t_infomask中的HEAP_XMAX_COMMITTED
-- 3. ItemId的lp_flags设置为LP_UNUSED
-- 4. 空间不会立即回收,等待VACUUM

更新操作:

复制代码
-- 更新(行迁移场景)
UPDATE users SET email = 'new_email@example.com' WHERE id = 1;

-- 如果新行大小 <= 旧行大小:
-- 1. 在原地更新,使用旧空间
-- 2. 旧版本标记删除,新版本在同一位置

-- 如果新行大小 > 旧行大小且当前块空间不足:
-- 1. 尝试在同一个块内寻找连续空间(碎片整理)
-- 2. 如果失败,触发行迁移

4、行迁移(row migration)机制

当更新导致行变大且当前块空间不足时:

复制代码
// 行迁移流程
bool heap_update_impl(Relation relation, ItemPointer otid, HeapTuple newtup)
{
    // 1. 检查当前块是否有足够空间
    if (new_len <= old_len) {
        // 原地更新
        return true;
    }
    
    // 2. 尝试在当前块内碎片整理
    if (freespace >= new_len && can_defragment) {
        // 重新组织块内数据
        heap_page_defragment(page);
        return true;
    }
    
    // 3. 行迁移到新块
    if (need_migration) {
        // a. 查找新块(通过FSM)
        Buffer new_buffer = GetBufferWithFreeSpace(relation, new_len);
        
        // b. 在新块插入新版本
        ItemPointerData new_tid;
        heap_insert_to_new_page(relation, new_buffer, newtup, &new_tid);
        
        // c. 在原位置设置重定向指针
        // 原ItemId的lp_flags设置为LP_REDIRECT
        // lp_off指向新的块号和行号
        
        // d. 设置旧版本为删除状态
        oldtup->t_xmax = GetCurrentTransactionId();
        
        // e. 更新索引(如果有)
        reindex_after_update(relation, otid, &new_tid);
    }
    
    return true;
}

行迁移后的块结构:

复制代码
原块:
ItemId[0]: lp_flags=LP_REDIRECT, lp_off=(block=5, offset=2)
原行数据:标记为删除

新块(块5):
ItemId[2]: lp_flags=LP_NORMAL, lp_off=行数据偏移
行数据:新版本数据

5、空间空洞处理方法

vacuum机制:

复制代码
-- 普通VACUUM(不锁表)
VACUUM users;
-- 1. 清理死元组
-- 2. 更新FSM
-- 3. 不回收空间给OS

-- 全VACUUM(需要排它锁)
VACUUM FULL users;
-- 1. 创建新表文件
-- 2. 拷贝有效数据
-- 3. 重建索引
-- 4. 删除旧文件,空间归还OS

HOT(heap-only tuple)更新:

当更新不修改索引键时,可以使用hot更新避免索引膨胀:

复制代码
-- 非索引列更新,可能触发HOT
UPDATE users SET email = 'new@example.com' WHERE id = 1;
-- 新旧版本在同一页内,通过HOT链连接
-- 索引仍指向原版本

块内碎片整理:

复制代码
// 页内碎片整理算法
void heap_page_defragment(Page page)
{
    // 1. 收集所有活跃元组
    List *live_tuples = collect_live_tuples(page);
    
    // 2. 按偏移量排序
    sort_tuples_by_offset(live_tuples);
    
    // 3. 从页尾开始紧凑
    compact_tuples_to_end(page, live_tuples);
    
    // 4. 更新ItemId指针
    update_item_pointers(page, live_tuples);
    
    // 5. 重置pd_lower和pd_upper
    PageSetLSN(page, GetXLogRecPtrForRel(reln));
}

二、列存表数据块存储结构

1、列存整体架构

复制代码
┌─────────────────────────────────────────────────┐
│               列存表物理结构                      │
├─────────────────────────────────────────────────┤
│ CU (Compression Unit) 存储层                    │
│  • 每个CU包含多行(例如10000行)的一列数据          │
│  • 列内数据连续存储                              │
│  • 支持压缩                                      │
├─────────────────────────────────────────────────┤
│ Delta表 (行存表)                                 │
│  • 存储小批量插入数据                            │
│  • 达到阈值后合并到CU                            │
├─────────────────────────────────────────────────┤
│ 辅助结构                                         │
│  • CUDesc: CU描述符,记录CU元数据                │
│  • CUMap: CU映射表                               │
│  • Delete Bitmap: 删除位图                       │
└─────────────────────────────────────────────────┘
CU:压缩单元
	每个cu存储表中一列(或多列,但通常每列独立)的连续多行数据(例如10000行)。
	cu内部数据按列存储,并且采用压缩算法进行压缩。
CU描述符(CU desc)
	每个cu对应一个cu描述符,记录cu的元数据,如cu的位置、大小、行数、最小值、最大值等。
CU映射表:(cu map)
	记录每个cu在文件中的位置映射,便于快速定位。
删除位图:(delete bitmap)
	用于存储小批量插入的数据,达到一定阈值后,会合并到CU中。

2、CU(压缩单元)详细结构

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                          CU物理文件 (1MB典型大小)                  │
├─────────────────────────────────────────────────────────────────┤
│ CU头部 (128字节固定)                                              │
│  ├─ 基础信息区 (64字节)                                           │
│  │   • magic (2字节): 0xC0DE (魔数标识)                           │
│  │   • version (1字节): CU版本号 (如0x01)                          │
│  │   • checksum (4字节): CRC32校验和                              │
│  │   • total_size (4字节): CU总大小(包括头部)                     │
│  │   • data_size (4字节): 数据区大小                              │
│  │   • col_id (2字节): 列ID                                      │
│  │   • cu_id (8字节): CU全局唯一ID                               │
│  │   • row_count (4字节): CU内行数 (如10000)                      │
│  │   • first_row_id (8字节): 起始行号                             │
│  │   • compress_mode (1字节): 压缩模式                           │
│  │   • compress_level (1字节): 压缩级别                          │
│  │   • dict_size (2字节): 字典大小(如有)                          │
│  │   • null_bitmap_size (2字节): NULL位图大小                    │
│  │   • rle_count (4字节): RLE编码段数(如有)                       │
│  │   • min_value (变长): 列最小值 (最长64字节)                    │
│  │   • max_value (变长): 列最大值 (最长64字节)                    │
│  │   • 保留区 (14字节)                                            │
│  ├─ 扩展信息区 (64字节,可选)                                     │
│  │   • create_xid (8字节): 创建事务ID                            │
│  │   • delete_xid (8字节): 删除事务ID(已废弃)                     │
│  │   • cmprs_info_offset (4字节): 压缩信息区偏移                  │
│  │   • data_offset (4字节): 数据区偏移                           │
│  │   • 其他统计信息 (如distinct_count, avg_length等)              │
├─────────────────────────────────────────────────────────────────┤
│ NULL位图区 (可选)                                                 │
│  • 每行1位,表示该行此列是否为NULL                              │
│  • 按字节对齐,大小 = ceil(row_count/8)                          │
│  • 例如10000行 -> 1250字节                                       │
├─────────────────────────────────────────────────────────────────┤
│ 压缩信息区 (Compression Info Section,变长)                       │
│  ├─ 字典表区 (字典压缩时存在)                                     │
│  │   • dict_entry_count (4字节): 字典项数                        │
│  │   • dict_entries: 字典项数组,每项包含:                      │
│  │        - original_value (变长): 原始值                        │
│  │        - encoded_value (2/4字节): 编码后的值                   │
│  ├─ 差值编码信息区 (差值压缩时存在)                               │
│  │   • base_value (变长): 基准值                                 │
│  │   • scale_factor (4字节浮点): 缩放因子                        │
│  │   • min_delta (8字节): 最小差值                               │
│  │   • max_delta (8字节): 最大差值                               │
│  ├─ RLE编码信息区 (游程编码时存在)                                │
│  │   • rle_segment_count (4字节): 游程段数                       │
│  │   • rle_segments: 游程段数组,每段包含:                      │
│  │        - value (变长): 值                                     │
│  │        - run_length (4字节): 重复次数                         │
│  │        - start_row (4字节): 起始行号                          │
├─────────────────────────────────────────────────────────────────┤
│ 数据区 (Data Section,变长)                                       │
│  • 压缩后的列数据,格式取决于压缩模式:                          │
│    1. 字典压缩: 存储编码值的数组 (如2字节/值)                     │
│    2. 差值压缩: 存储差值数组                                      │
│    3. RLE压缩: 存储(value, length)对序列                         │
│    4. LZ4/Zstd: 存储压缩后的字节流                               │
│  • 数据区大小 = total_size - data_offset                         │
└─────────────────────────────────────────────────────────────────┘
压缩方式:
列存表支持多种压缩算法,例如:
	字典压缩:将列中的重复值用字典中的索引表示,存储索引序列。
	差值编码:存储相邻值的差值,适用于有序数据。
	游程编码(RLE):将连续重复的值压缩为(值,重复次数)对。
	通用压缩算法:如LZ4、Zstandard等。

3、cu内部数据组织示例

假设一个cu存储10000行的user_id列,数据类型为INT:

情况1:字典压缩

复制代码
原始数据: [101, 102, 101, 103, 102, 101, ...]
字典表:
  编码值  原始值
  0      → 101
  1      → 102
  2      → 103

压缩信息区:
  dict_entry_count = 3
  dict_entries[0]: original_value=101, encoded_value=0
  dict_entries[1]: original_value=102, encoded_value=1
  dict_entries[2]: original_value=103, encoded_value=2

数据区(编码数组,每值2字节):
  [0x0000, 0x0001, 0x0000, 0x0002, 0x0001, 0x0000, ...]
  总大小 = 10000 * 2 = 20000字节

情况2:差值编码

复制代码
原始数据(已排序): [1000, 1001, 1002, 1003, 1005, 1006, ...]
压缩信息区:
  base_value = 1000
  scale_factor = 1.0
  min_delta = 0
  max_delta = 10

数据区(差值数组,每差值1字节):
  [0, 1, 2, 3, 5, 6, ...]
  总大小 = 10000 * 1 = 10000字节

情况3:RLE编码

复制代码
原始数据: [1, 1, 1, 1, 2, 2, 3, 3, 3, 3, 3, ...]
压缩信息区:
  rle_segment_count = 3
  rle_segments[0]: value=1, run_length=4, start_row=0
  rle_segments[1]: value=2, run_length=2, start_row=4
  rle_segments[2]: value=3, run_length=5, start_row=6

数据区(最小表示):
  只需要存储3个(value, length)对,而不是10000个值

压缩算法选择逻辑:

复制代码
// 压缩决策算法
typedef enum {
    COMPRESS_NONE     = 0,
    COMPRESS_DICT     = 1,  // 字典压缩:适合低基数数据
    COMPRESS_DELTA    = 2,  // 差值编码:适合有序数值
    COMPRESS_RLE      = 3,  // 游程编码:适合重复值多
    COMPRESS_LZ4      = 4,  // LZ4:通用快速压缩
    COMPRESS_ZSTD     = 5,  // Zstd:高压缩比
    COMPRESS_PFOR     = 6   // PFor:整数列高效压缩
} CompressionType;

CompressionType choose_compression(ColumnData *data, DataStats *stats) {
    // 规则1:如果唯一值很少,用字典压缩
    if (stats->unique_count < 256 && stats->row_count > 1000) {
        return COMPRESS_DICT;
    }
    
    // 规则2:如果数据已排序或基本有序,用差值编码
    if (stats->sorted_ratio > 0.95 && is_numeric_type(data->type)) {
        return COMPRESS_DELTA;
    }
    
    // 规则3:如果有长重复序列,用RLE
    if (stats->avg_run_length > 10) {
        return COMPRESS_RLE;
    }
    
    // 规则4:根据数据类型选择通用压缩
    if (data->type == INT_TYPE || data->type == FLOAT_TYPE) {
        return COMPRESS_PFOR;  // PFor对数值型数据高效
    }
    
    // 默认用Zstd(压缩比和速度均衡)
    return COMPRESS_ZSTD;
}

4、CU描述符(CUDesc)结构

复制代码
CUDesc存储在系统表中(类似`pg_cstore.cudesc`),每行对应一个CU:
-- CUDesc系统表结构
CREATE TABLE pg_cstore.cudesc (
    col_id        SMALLINT    NOT NULL,  -- 列ID
    cu_id         BIGINT      NOT NULL,  -- CU全局ID
    cu_size       INTEGER     NOT NULL,  -- CU大小(字节)
    row_count     INTEGER     NOT NULL,  -- CU内行数
    cu_pointer    BIGINT      NOT NULL,  -- CU在文件中的偏移(8字节)
    min_value     BYTEA,                 -- 最小值(变长,最长64)
    max_value     BYTEA,                 -- 最大值(变长,最长64)
    compress_mode SMALLINT    NOT NULL,  -- 压缩模式
    crc           INTEGER     NOT NULL,  -- CRC32校验
    magic         SMALLINT    NOT NULL,  -- 魔数0xC0DE
    create_csn    BIGINT,                -- 创建时的CSN
    delete_csn    BIGINT,                -- 删除CSN(未使用)
    parent_xid    BIGINT,                -- 父事务ID
    cu_type       SMALLINT,              -- CU类型: 0=普通, 1=临时
    PRIMARY KEY (col_id, cu_id)
) WITH (ORIENTATION = ROW, COMPRESSION = NO);
// CUDesc内存表示(为查询优化缓存)
typedef struct CUDescCacheEntry {
    uint32      col_id;          // 列ID
    uint64      cu_id;           // CU ID
    uint32      cu_size;         // CU大小
    uint32      row_count;       // 行数
    uint64      cu_pointer;      // 文件偏移
    Datum       min_value;       // 最小值
    bool        min_is_null;     // 最小值是否为NULL
    Datum       max_value;       // 最大值
    bool        max_is_null;     // 最大值是否为NULL
    uint16      compress_mode;   // 压缩模式
    uint32      crc;             // CRC校验
    uint64      first_row_id;    // 起始行号
    uint64      create_csn;      // 创建CSN
    uint64      delete_csn;      // 删除CSN(用于MVCC)
    bool        is_active;       // CU是否活跃
    bool        is_deleted;      // 是否逻辑删除
    uint32      dead_rows;       // 死亡行数(从DeleteBitmap统计)
    // 统计信息(用于优化器)
    double      avg_length;      // 平均长度
    uint32      null_count;      // NULL值数量
    // 链表指针(用于LRU缓存)
    struct CUDescCacheEntry *prev;
    struct CUDescCacheEntry *next;
} CUDescCacheEntry;
CUDesc的作用:
1. **快速过滤**:通过min/max值跳过不相关的CU
2. **CU定位**:通过cu_pointer快速定位CU在文件中的位置
3. **统计信息**:为查询优化器提供统计信息  
4. **版本管理**:通过CSN实现MVCC  
5. **空间管理**:跟踪CU大小和行数

5、CU映射表(cumap)

cumap是一个内存结构,用于快速定位cu。它通常是一个哈希表,键为(column_id,cu_id),值为CU描述符的指针。这样,给定列号和CU号,可以快速找到CU的位置和元数据。

复制代码
CUMap是内存中的数据结构,但持久化在pg_cstore.cumap系统表中:
-- CUMap系统表结构(简化)
CREATE TABLE pg_cstore.cumap (
    rel_id        OID         NOT NULL,  -- 表OID
    col_id        SMALLINT    NOT NULL,  -- 列ID
    cu_id         BIGINT      NOT NULL,  -- CU ID
    file_id       INTEGER     NOT NULL,  -- 文件ID
    seg_no        INTEGER     NOT NULL,  -- 段号
    offset        BIGINT      NOT NULL,  -- 在段内的偏移
    length        INTEGER     NOT NULL,  -- CU长度
    PRIMARY KEY (rel_id, col_id, cu_id)
);
// CUMap哈希表入口
typedef struct CUMapHashEntry {
    uint32      key;            // 哈希键: (rel_id << 16) | col_id
    CUCache    *cu_cache;       // CU缓存链表
    IntervalTree *interval_tree;// 按行号范围的区间树
    pthread_rwlock_t lock;      // 读写锁
} CUMapHashEntry;

// CU缓存项
typedef struct CUCacheEntry {
    uint64      cu_id;          // CU ID
    uint32      col_id;         // 列ID
    uint64      first_row_id;   // 起始行号
    uint32      row_count;      // 行数
    char       *file_path;      // 文件路径
    uint64      file_offset;    // 文件偏移
    uint32      cu_size;        // CU大小
    uint8      *cu_data;        // CU数据(缓存时加载)
    bool        is_dirty;       // 是否脏数据
    time_t      last_access;    // 最后访问时间
    uint32      access_count;   // 访问次数
    // LRU链表
    struct CUCacheEntry *prev;
    struct CUCacheEntry *next;
} CUCacheEntry;

// 区间树节点(用于按行号快速定位CU)
typedef struct IntervalTreeNode {
    uint64      start;          // 起始行号
    uint64      end;            // 结束行号(start + row_count - 1)
    CUCacheEntry *cu_entry;     // 对应的CU缓存项
    struct IntervalTreeNode *left;
    struct IntervalTreeNode *right;
    int         height;         // AVL树高度
} IntervalTreeNode;
CUMap的核心功能:
1、快速CU定位:
// 根据行号找到对应的CU
CUCacheEntry* find_cu_by_rowid(CUMapHashEntry *map_entry, uint64 row_id) {
    // 1. 在区间树中查找
    IntervalTreeNode *node = interval_tree_search(map_entry->interval_tree, row_id);
    if (node) {
        return node->cu_entry;
    }
    
    // 2. 如果不在内存,从磁盘加载
    return load_cu_from_disk(map_entry->rel_id, map_entry->col_id, row_id);
}
2、CU缓存管理
// LRU缓存淘汰算法
void lru_evict_cu_cache(CUMapHashEntry *map_entry) {
    CUCacheEntry *entry = map_entry->cu_cache->tail;  // 最久未使用
    
    while (entry && map_entry->cu_cache->total_size > MAX_CACHE_SIZE) {
        if (!entry->is_dirty) {
            // 从缓存移除
            remove_from_cache(map_entry, entry);
            // 从区间树移除
            interval_tree_delete(map_entry->interval_tree, 
                                 entry->first_row_id, 
                                 entry->first_row_id + entry->row_count - 1);
            free(entry->cu_data);
            free(entry);
        }
        entry = entry->prev;
    }
}

5、删除位图(delete bitmap)

删除位图是一个二维位图,第一维是CU,第二维是CU内的行号。每个位标识对应行是否被删除。删除位图可以单独存储,也可以与CU描述符存储在一起。

删除位图存储在单独的系统表中,避免与CU数据混合:

复制代码
-- Delete Bitmap系统表结构
CREATE TABLE pg_cstore.delete_bitmap (
    rel_id        OID         NOT NULL,  -- 表OID
    col_id        SMALLINT    NOT NULL,  -- 列ID(0表示全局行删除位图)
    cu_id         BIGINT      NOT NULL,  -- CU ID
    bitmap_data   BYTEA       NOT NULL,  -- 位图数据
    row_count     INTEGER     NOT NULL,  -- 总行数(用于验证)
    dead_rows     INTEGER     NOT NULL,  -- 死亡行数
    version       INTEGER     NOT NULL,  -- 版本号(用于乐观锁)
    last_update   TIMESTAMP   NOT NULL,  -- 最后更新时间
    PRIMARY KEY (rel_id, col_id, cu_id)
);
delete bitmap内存结构:
// Delete Bitmap内存表示
typedef struct DeleteBitmap {
    uint32      rel_id;          // 表OID
    uint16      col_id;          // 列ID(0表示行级删除)
    uint64      cu_id;           // CU ID
    uint32      row_count;       // 总行数
    uint32      dead_rows;       // 死亡行数
    uint8      *bitmap;          // 位图数据
    uint32      bitmap_size;     // 位图大小(字节)
    uint32      version;         // 版本号
    // 分层位图(提高大规模删除时效率)
    uint8      *summary_bitmap;  // 摘要位图(每64位一个摘要位)
    uint32      summary_size;    // 摘要位图大小
    // 内存优化:活跃位图缓存
    struct {
        uint32  start_row;       // 缓存起始行
        uint32  end_row;         // 缓存结束行
        uint8  *cached_bitmap;   // 缓存位图
    } cache;
    pthread_rwlock_t lock;       // 读写锁
} DeleteBitmap;

分层位图设计:
原始位图(每行1位,10000行需要1250字节):
  位图[0]: 0x01 (二进制:00000001) ← 第0行删除
  位图[1]: 0x80 (二进制:10000000) ← 第7行删除
  ...
摘要位图(每64行1位,10000行需要157字节):
  摘要位[0]: 1 ← 表示第0-63行有删除
  摘要位[1]: 0 ← 表示第64-127行无删除
  摘要位[2]: 1 ← 表示第128-191行有删除

查询优化:
1、先查摘要位图,如果为0,跳过整个64行的检查
2、提高全表扫描时过滤死亡行的效率

删除位图操作函数:
// 设置删除位
void delete_bitmap_set(DeleteBitmap *bitmap, uint32 row_offset) {
    uint32 byte_idx = row_offset / 8;
    uint32 bit_idx = row_offset % 8;
    
    // 设置详细位图
    bitmap->bitmap[byte_idx] |= (1 << bit_idx);
    bitmap->dead_rows++;
    
    // 更新摘要位图
    uint32 summary_idx = row_offset / 64;
    uint32 summary_bit = summary_idx / 8;
    uint32 summary_offset = summary_idx % 8;
    bitmap->summary_bitmap[summary_bit] |= (1 << summary_offset);
}

// 批量设置删除位(更高效)
void delete_bitmap_set_batch(DeleteBitmap *bitmap, uint32 *row_offsets, uint32 count) {
    // 按字节分组操作
    uint8 byte_masks[256] = {0};  // 每字节的位掩码
    
    for (uint32 i = 0; i < count; i++) {
        uint32 row_offset = row_offsets[i];
        uint32 byte_idx = row_offset / 8;
        uint32 bit_idx = row_offset % 8;
        byte_masks[byte_idx] |= (1 << bit_idx);
    }
    
    // 批量应用到位图
    for (uint32 byte_idx = 0; byte_idx < bitmap->bitmap_size; byte_idx++) {
        if (byte_masks[byte_idx]) {
            bitmap->bitmap[byte_idx] |= byte_masks[byte_idx];
            
            // 统计死亡行数
            bitmap->dead_rows += __builtin_popcount(byte_masks[byte_idx]);
            
            // 更新摘要位图
            uint32 start_row = byte_idx * 8;
            uint32 end_row = start_row + 7;
            for (uint32 row = start_row; row <= end_row; row++) {
                if (byte_masks[byte_idx] & (1 << (row % 8))) {
                    uint32 summary_idx = row / 64;
                    uint32 summary_bit = summary_idx / 8;
                    uint32 summary_offset = summary_idx % 8;
                    bitmap->summary_bitmap[summary_bit] |= (1 << summary_offset);
                }
            }
        }
    }
}

6、delta表

delta表是一个行存表,用于存储小批量插入的数据。当插入的数据量小于cu的阈值时,先存储在delta表中。当delta表中的数据量达到阈值(例如10000行)时,会触发合并操作,将delta表中的数据按列提取,压缩成CU。

7、完整数据操作流程

复制代码
-- 1. 创建列存表
CREATE TABLE user_actions (
    user_id    BIGINT,
    action_time TIMESTAMP,
    action_type VARCHAR(20),
    detail     TEXT
) WITH (ORIENTATION = COLUMN, COMPRESSION = MIDDLE);

-- 初始状态:
-- • CUDesc表:空
-- • CUMap:空
-- • Delete Bitmap:空
-- • Delta表:空

步骤1:插入1000行数据(小批量)
INSERT INTO user_actions VALUES 
(1, '2024-01-01 10:00:00', 'login', 'from ip 192.168.1.1'),
(2, '2024-01-01 10:01:00', 'logout', 'session expired'),
... -- 共1000行

后台操作流程:
// 1. 数据进入Delta表(行存格式)
DeltaTable *delta = get_delta_table(rel_id);
for (i = 0; i < 1000; i++) {
    delta_insert(delta, row_data[i]);
}

// 2. 更新Delete Bitmap(新行标记为未删除)
//    注意:Delta表数据还没有对应的CU,所以不在Delete Bitmap中
//    Delete Bitmap只对CU中的数据有效

// 3. 更新统计信息
update_table_stats(rel_id, 1000);

// 此时:
// • CUDesc表:仍为空(无CU)
// • CUMap:空
// • Delete Bitmap:不变
// • Delta表:有1000行数据

步骤2:继续插入9000行,触发cu创建
-- 再插入9000行,Delta表达到10000行阈值
INSERT INTO user_actions SELECT ... FROM generate_series(1, 9000);

后台合并流程:
void merge_delta_to_cu(Relation rel, DeltaTable *delta) {
    // 1. 按列提取Delta表数据
    for (col_idx = 0; col_idx < rel->col_count; col_idx++) {
        ColumnData col_data = extract_column_data(delta, col_idx);
        
        // 2. 选择压缩算法
        CompressionType comp_type = choose_compression(col_data);
        
        // 3. 创建CU
        CUData *cu = create_cu(col_data, comp_type);
        
        // 4. 写入CU文件
        uint64 cu_pointer = write_cu_to_file(rel, col_idx, cu);
        
        // 5. 创建CUDesc条目
        CUDesc desc = {
            .col_id = col_idx,
            .cu_id = generate_cu_id(),
            .cu_size = cu->total_size,
            .row_count = cu->row_count,
            .cu_pointer = cu_pointer,
            .min_value = cu->min_value,
            .max_value = cu->max_value,
            .compress_mode = comp_type,
            .crc = cu->crc,
            .create_csn = get_current_csn()
        };
        insert_cudesc(rel->rel_id, &desc);
        
        // 6. 更新CUMap
        update_cumap(rel->rel_id, col_idx, desc.cu_id, cu_pointer, cu->row_count);
        
        // 7. 初始化Delete Bitmap(全0,表示无删除)
        init_delete_bitmap(rel->rel_id, col_idx, desc.cu_id, cu->row_count);
    }
    
    // 8. 清空Delta表
    truncate_delta_table(delta);
    
    // 9. 更新表统计信息
    update_table_stats(rel->rel_id, delta->row_count);
}

// 此时:
// • CUDesc表:新增4行(每列一个CU)
// • CUMap:新增4个条目
// • Delete Bitmap:新增4个位图(全0)
// • Delta表:清空

步骤3:查看数据
SELECT * FROM user_actions 
WHERE user_id BETWEEN 100 AND 200 
  AND action_time >= '2024-01-01 10:00:00';
查看执行流程:
void columnar_scan(Relation rel, ScanKey keys) {
    // 1. 加载CUDesc到内存(如果未缓存)
    List *cudesc_list = load_cudesc_for_table(rel);
    
    // 2. 使用min/max值过滤CU
    List *filtered_cu_list = filter_cu_by_minmax(cudesc_list, keys);
    
    // 3. 为每个需要扫描的列处理CU
    for (col_idx in scan_cols) {
        for each cu_desc in filtered_cu_list[col_idx] {
            // 4. 通过CUMap找到CU位置
            CUCacheEntry *cu_entry = find_cu_in_cumap(rel->rel_id, col_idx, cu_desc->cu_id);
            
            if (!cu_entry->cu_data) {
                // 5. 从磁盘加载CU数据
                load_cu_data(cu_entry);
            }
            
            // 6. 解压CU数据
            ColumnData col_data = decompress_cu(cu_entry);
            
            // 7. 应用Delete Bitmap过滤死亡行
            DeleteBitmap *bitmap = get_delete_bitmap(rel->rel_id, col_idx, cu_desc->cu_id);
            ColumnData filtered_data = apply_delete_bitmap(col_data, bitmap);
            
            // 8. 应用查询条件过滤
            ColumnData result_data = apply_scan_keys(filtered_data, keys);
            
            // 9. 收集结果
            collect_results(col_idx, result_data);
        }
    }
    
    // 10. 多列结果关联(按行号)
    return join_columns_by_rowid();
}

步骤4:更新数据
-- 更新100行数据
UPDATE user_actions 
SET action_type = 'timeout' 
WHERE user_id IN (SELECT generate_series(1, 100));

更新操作流程:
void columnar_update(Relation rel, ScanKey where_keys, Datum *new_values) {
    // 1. 执行查询找到需要更新的行(与查询流程相同)
    RowIdList *row_ids = find_rows_to_update(rel, where_keys);
    
    // 2. 对每个找到的行:
    for each row_id in row_ids {
        // 3. 在Delete Bitmap中标记旧版本为删除
        // 找到行所在的CU
        CUDesc *cu_desc = find_cu_by_rowid(rel, row_id);
        // 计算在CU内的行偏移
        uint32 row_offset = row_id - cu_desc->first_row_id;
        // 设置删除位
        delete_bitmap_set(bitmap, row_offset);
        
        // 4. 更新CUDesc中的dead_rows计数
        increment_dead_rows(cu_desc);
        
        // 5. 新版本数据插入Delta表
        delta_insert(rel->delta_table, new_row_data);
        
        // 6. 如果更新了索引列,更新索引
        if (is_index_column(column)) {
            update_index(rel, row_id, new_values);
        }
    }
    
    // 7. 如果Delta表达到阈值,触发合并
    if (delta_table_size(rel->delta_table) > DELTA_THRESHOLD) {
        merge_delta_to_cu(rel, rel->delta_table);
    }
    
    // 8. 如果某个CU的死亡行比例超过阈值,标记需要VACUUM
    for each cu_desc in updated_cu_list {
        if ((float)cu_desc->dead_rows / cu_desc->row_count > VACUUM_THRESHOLD) {
            mark_cu_for_vacuum(cu_desc);
        }
    }
}

// 此时:
// • CUDesc表:dead_rows字段更新
// • Delete Bitmap:相应位被置1
// • Delta表:插入100行新数据

步骤5:删除数据
DELETE FROM user_actions 
WHERE user_id BETWEEN 500 AND 600;

删除操作流程:
void columnar_delete(Relation rel, ScanKey where_keys) {
    // 1. 找到需要删除的行
    RowIdList *row_ids = find_rows_to_delete(rel, where_keys);
    
    // 2. 批量设置Delete Bitmap
    for each cu_desc in affected_cu_list {
        DeleteBitmap *bitmap = get_delete_bitmap(rel->rel_id, cu_desc->col_id, cu_desc->cu_id);
        delete_bitmap_set_batch(bitmap, row_offsets, row_count);
        
        // 3. 更新CUDesc统计
        cu_desc->dead_rows += row_count;
        update_cudesc_dead_rows(cu_desc);
        
        // 4. 如果删除比例高,标记需要VACUUM
        if ((float)cu_desc->dead_rows / cu_desc->row_count > VACUUM_THRESHOLD) {
            mark_cu_for_vacuum(cu_desc);
        }
    }
    
    // 5. 更新索引(如果删除的列有索引)
    delete_index_entries(rel, row_ids);
}

// 此时:
// • Delete Bitmap:相应位被置1
// • CUDesc表:dead_rows字段更新

步骤6:vacuum操作
手动执行vacuum full
vacuum full user_actions;

vacuum full流程:
void columnar_vacuum_full(Relation rel) {
    // 1. 扫描所有CU,收集需要回收的CU列表
    List *cu_list = get_all_cu_descs(rel);
    List *cu_to_vacuum = NIL;
    
    for each cu_desc in cu_list {
        // 只回收死亡行比例高的CU
        float dead_ratio = (float)cu_desc->dead_rows / cu_desc->row_count;
        if (dead_ratio > 0.3 || dead_ratio > VACUUM_THRESHOLD) {
            cu_to_vacuum = lappend(cu_to_vacuum, cu_desc);
        }
    }
    
    // 2. 按列处理
    for (col_idx = 0; col_idx < rel->col_count; col_idx++) {
        ColumnData all_live_data = NULL;
        
        // 3. 读取该列所有CU的有效数据
        for each cu_desc in cu_to_vacuum[col_idx] {
            // 加载CU数据
            CUData *cu = load_cu(rel, cu_desc);
            // 应用Delete Bitmap,过滤死亡行
            ColumnData live_data = filter_dead_rows(cu, get_delete_bitmap(...));
            // 合并到总数据
            all_live_data = concat_column_data(all_live_data, live_data);
        }
        
        // 4. 重新压缩成新CU
        List *new_cu_list = repack_into_cus(all_live_data);
        
        // 5. 写入新CU文件
        for each new_cu in new_cu_list {
            uint64 new_pointer = write_cu_to_file(rel, col_idx, new_cu);
            
            // 6. 创建新CUDesc
            CUDesc new_desc = create_cudesc_for_cu(new_cu);
            new_desc.cu_pointer = new_pointer;
            
            // 7. 原子切换:先插入新,再删除旧
            insert_cudesc(rel->rel_id, &new_desc);
            update_cumap(rel->rel_id, col_idx, new_desc.cu_id, new_pointer);
        }
        
        // 8. 删除旧CU文件(标记为可删除)
        for each old_cu_desc in cu_to_vacuum[col_idx] {
            mark_cu_for_deletion(old_cu_desc);
        }
    }
    
    // 9. 处理Delta表(合并到CU)
    if (delta_table_size(rel->delta_table) > 0) {
        merge_delta_to_cu(rel, rel->delta_table);
    }
    
    // 10. 清理旧CU文件(异步)
    async_delete_marked_cus();
    
    // 11. 重置Delete Bitmap(新CU没有死亡行)
    truncate_delete_bitmap(rel);

8、高级特性与优化

cu预取与缓存:

复制代码
// 基于访问模式的CU预取
void prefetch_cus_by_pattern(Relation rel, ScanKey keys) {
    // 分析查询模式
    ScanPattern pattern = analyze_scan_pattern(keys);
    
    // 预测可能访问的CU
    List *predicted_cu_list = predict_cus_by_pattern(rel, pattern);
    
    // 异步预取
    for each cu_desc in predicted_cu_list {
        async_prefetch_cu(rel, cu_desc);
    }
}

自适应cu大小:

复制代码
// 根据数据特征调整CU大小
uint32 adaptive_cu_size(ColumnData *data) {
    // 默认1MB
    uint32 default_size = 1024 * 1024;
    
    // 如果数据压缩率很高,增大CU
    float compress_ratio = estimate_compress_ratio(data);
    if (compress_ratio > 0.1) {  // 压缩率超过10:1
        return default_size * 2;  // 2MB
    }
    
    // 如果数据访问频繁,减小CU
    if (get_column_access_frequency(data->col_id) > HIGH_FREQ_THRESHOLD) {
        return default_size / 2;  // 512KB
    }
    
    return default_size;
}

列级事务管理:

复制代码
// 列存表的多版本控制
typedef struct ColumnarXactInfo {
    uint64 xid;                    // 事务ID
    uint64 csn;                    // 提交序列号
    // 列级修改记录
    struct {
        uint16 col_id;
        DeleteBitmap *old_bitmap;  // 旧版本位图
        DeleteBitmap *new_bitmap;  // 新版本位图
        CUDesc *new_cu_desc;       // 新插入的CU
    } *col_changes;
    uint32 change_count;
} ColumnarXactInfo;

混合存储策略:

复制代码
// 智能数据放置:热数据行存,冷数据列存
void smart_data_placement(Relation rel) {
    // 分析数据访问模式
    AccessPattern pattern = analyze_access_pattern(rel);
    
    for each column in rel->columns {
        if (pattern.is_hot_column[column.id]) {
            // 热列:使用Delta表+小CU
            set_storage_policy(column, STORAGE_HOT);
        } else {
            // 冷列:使用大CU+高压缩
            set_storage_policy(column, STORAGE_COLD);
        }
    }
}

9、空间管理策略

cu合并与分裂:

复制代码
// CU合并流程(Delta表合并到CU)
void cstore_merge_delta(CStoreRelation rel)
{
    // 1. 按列从Delta表提取数据
    for each column in rel.columns {
        ColumnData col_data = extract_column_from_delta(column);
        
        // 2. 压缩数据
        CompressedData comp_data = compress_column(col_data);
        
        // 3. 判断是否需要分裂
        if (comp_data.size > MAX_CU_SIZE) {
            // CU分裂
            split_cu_into_multiple(comp_data);
        } else {
            // 创建新CU
            create_new_cu(column, comp_data);
        }
    }
    
    // 4. 更新CUDesc和CUMap
    update_cu_metadata(rel);
    
    // 5. 清空Delta表
    truncate_delta_table(rel);
}

空间回收(vacuum):

复制代码
-- 列存表的VACUUM
VACUUM FULL sales;

-- 执行过程:
-- 1. 扫描所有CU和Delete Bitmap
-- 2. 收集有效数据(未被删除的)
-- 3. 重新压缩有效数据到新CU
-- 4. 更新CUDesc和CUMap
-- 5. 删除旧CU文件

空洞处理策略:

复制代码
// 列存空间回收算法
void cstore_vacuum_full(CStoreRelation rel)
{
    // 1. 收集所有CU的删除统计
    VacuumStats stats = collect_vacuum_stats(rel);
    
    // 2. 判断是否值得回收
    if (stats.dead_tuples_ratio < VACUUM_THRESHOLD) {
        return; // 不值得回收
    }
    
    // 3. 按列重写数据
    for each column in rel.columns {
        // 创建临时CU文件
        TempCUFile temp_file = create_temp_cu_file();
        
        // 扫描所有CU,跳过删除的行
        for each cu in column.cu_list {
            if (cu.dead_rows_ratio > 0.3) {
                // 只拷贝有效数据
                copy_live_data(cu, temp_file, rel.delete_bitmap);
            } else {
                // 整个CU保留
                keep_cu(cu);
            }
        }
        
        // 合并临时文件到新CU
        merge_temp_cus(temp_file);
    }
    
    // 4. 重建Delete Bitmap(全清零)
    reset_delete_bitmap(rel);
    
    // 5. 原子切换文件
    switch_to_new_files(rel);
}

10、压缩与存储优化

列存压缩算法:

复制代码
// 列存支持的压缩算法
typedef enum CompressionMode {
    COMPRESS_NONE = 0,      // 不压缩
    COMPRESS_DELTA,         // 差值编码
    COMPRESS_DICT,          // 字典压缩
    COMPRESS_RLE,           // 游程编码
    COMPRESS_LZ4,           // LZ4压缩
    COMPRESS_ZSTD,          // Zstd压缩
} CompressionMode;

// 智能压缩选择
CompressionMode choose_compression(ColumnData data)
{
    // 分析数据特征
    DataStats stats = analyze_column_stats(data);
    
    if (stats.unique_ratio < 0.1) {
        // 低基数,适合字典压缩
        return COMPRESS_DICT;
    } else if (stats.sorted_ratio > 0.9) {
        // 高度有序,适合差值编码
        return COMPRESS_DELTA;
    } else if (stats.run_length_avg > 100) {
        // 长游程,适合RLE
        return COMPRESS_RLE;
    } else {
        // 通用压缩
        return COMPRESS_ZSTD;
    }
}

三、行存vs列存的空间管理对比

存储特征对比:

特性 行存表 列存表
存储单元 页(8KB),存储多行 CU(1MB),存储多行的一列
数据组织 行式存储,行内列连续 列式存储,列内数据连续
更新粒度 行级更新 列级更新(实际式删除+插入)
删除机制 标记删除,vacuum回收 标记删除,vacuum full回收
空间碎片 页内碎片,行间空洞 cu内空洞,cu间无碎片
压缩能力 有限(toast) 强大(列内压缩)
适合场景 oltp,点查询 olap,分析查询
空间不足处理对比:
场景 行存表处理 列存表处理
插入空间不足 分配新页 写入dalta表,定期合并
更新导致行变大 行迁移或toast 标记删除+插入新版本
删除空间回收 vacuum标记,vacuum full回收 标记删除位,vacuum full重写cu
空间删除整理 页内碎片整理,cluster重排 cu合并于分裂

四、高级空间管理技术

1、FSM(空闲空间映射)

复制代码
-- 查看表的FSM
SELECT * FROM pg_freespace('users');

-- FSM结构:
-- 三级树状结构:
-- 1. 叶子节点:记录每个数据块的空闲空间
-- 2. 中间节点:记录下级节点的最大空闲空间
-- 3. 根节点:记录整表的最大空闲空间

2、可见性映射(vm)

复制代码
VM位图结构:
• 每个数据块对应2位
• 位0: 所有元组可见?
• 位1: 所有冻结元组?
• 加速VACUUM跳过全可见块

3、toast(超长字段存储)

复制代码
-- TOAST存储策略
CREATE TABLE messages (
    id BIGINT,
    content TEXT  -- TOAST字段
) WITH (
    toast_tuple_target = 2000,  -- 行内存储阈值
    toast_compression = 'lz4'   -- TOAST压缩
);

-- TOAST存储层次:
-- 1. 行内存储(< 2000字节)
-- 2. 行外存储(TOAST表)
-- 3. 压缩存储(可选)

4、并行vacuum

复制代码
-- GaussDB(DWS)并行VACUUM
SET vacuum_cost_delay = 0;
SET max_parallel_maintenance_workers = 4;
VACUUM (PARALLEL 4, VERBOSE) sales;

-- 并行流程:
-- 1. 主进程扫描VM,分配工作给子进程
-- 2. 子进程并行清理不同数据块
-- 3. 主进程收集结果,更新FSM和VM

五、最佳实践

1、行存表优化:

复制代码
-- 1. 定期监控空间使用
SELECT 
    schemaname, tablename,
    pg_size_pretty(pg_total_relation_size(relid)) as total_size,
    pg_size_pretty(pg_relation_size(relid)) as table_size,
    n_dead_tup as dead_tuples,
    n_live_tup as live_tuples
FROM pg_stat_user_tables
WHERE n_dead_tup > n_live_tup * 0.2;  -- 死亡元组超过20%

-- 2. 设置合适的FILLFACTOR
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100)
) WITH (FILLFACTOR = 70);  -- 预留30%空间给更新

-- 3. 分区管理大表
CREATE TABLE logs_partitioned (
    log_time TIMESTAMP,
    message TEXT
) PARTITION BY RANGE (log_time);

2、列存表优化:

复制代码
-- 1. 设置合适的压缩级别
CREATE TABLE fact_table (
    ...
) WITH (
    ORIENTATION = COLUMN,
    COMPRESSION = (LEVEL = 3)  -- 压缩级别1-9
);

-- 2. 批量加载优化
-- 使用COPY而不是单条INSERT
COPY sales FROM '/data/sales.csv' WITH CSV;

-- 3. 定期合并Delta表
-- 自动合并阈值设置
SET cstore_insert_mode = 'bulkinsert';  -- 批量插入模式

六、诊断与监控

1、空间使用诊断:

复制代码
-- 行存表空间诊断
SELECT 
    relname,
    pg_size_pretty(pg_relation_size(oid)) as size,
    pg_size_pretty(pg_freespace(oid)) as free_space,
    (pg_freespace(oid)::float / pg_relation_size(oid) * 100) as free_percent
FROM pg_class 
WHERE relkind = 'r' 
  AND pg_relation_size(oid) > 1000000
ORDER BY free_percent DESC;

-- 列存表空间诊断
SELECT 
    relname,
    pg_size_pretty(pg_total_relation_size(oid)) as total_size,
    (SELECT count(*) FROM pg_cstore.cu) as cu_count,
    (SELECT sum(cu_size) FROM pg_cstore.cu) as cu_total_size
FROM pg_class 
WHERE relkind = 'c'  -- 列存表

2、性能监控:

复制代码
-- VACUUM监控
SELECT 
    schemaname,
    relname,
    last_vacuum,
    last_autovacuum,
    vacuum_count,
    autovacuum_count,
    n_dead_tup
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;

-- 行迁移监控
SELECT 
    schemaname,
    relname,
    heap_blks_hit,
    heap_blks_read,
    heap_blks_updated  -- 高更新可能意味行迁移
FROM pg_stat_user_tables;

七、总结

gaussdb(dws)列存表通过精细的组件设计实现了高效地列式存储:

1、cu(压缩单元):列数据的基本存储单元,支持多种智能压缩算法。

2、cudesc:cu的元数据目录,实现快速过滤和定位

3、cumap:内存中的cu映射,加速cu查找和缓存管理

4、delete bitmap:高效地删除标记机制,支持分层优化

5、delta表:处理小批量插入,避免频繁创建小CU

这种架构使得列存表在分析型查询中表现出色,通过min/max过滤、列级压缩、批量处理等技术,大幅提升了数据仓库场景的性能。

相关推荐
荣--1 小时前
一键部署不是为了省时间 —— 它是把"买来的 PaaS"变成"自己的平台"的拐点
运维·zabbix·工程化·一键部署·平台化·边界设计
江华森2 小时前
动手实战学 Docker — 从零到集群编排完全指南
运维
Avan_菜菜18 小时前
FRP 内网穿透完整实战:从 HTTP 映射到 HTTPS 自签代理
运维·nginx·https
SelectDB2 天前
Litefuse 开源并推出单进程轻量模式,25 秒就能跑起来的 Agent 可观测与评估平台
运维·后端·自动化运维
XIAOHEZIcode3 天前
Linux系统鼠标偏移常见原因以及修复方案
linux·运维·游戏
用户0328472220704 天前
如何搭建本地yum源(上)
运维
大树887 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠7 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质7 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
Inhand陈工7 天前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信