行存表与列存表简述

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

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过滤、列级压缩、批量处理等技术,大幅提升了数据仓库场景的性能。

相关推荐
福尔摩斯张2 小时前
Linux的pthread_self函数详解:多线程编程中的身份标识器(超详细)
linux·运维·服务器·网络·网络协议·tcp/ip·php
2401_832298103 小时前
一云多芯时代:云服务器如何打破芯片架构壁垒
运维·服务器·架构
Web极客码3 小时前
如何在 Linux 中终止一个进程?
linux·运维·服务器
一枚正在学习的小白3 小时前
prometheus监控对外服务
运维·prometheus
tzhou644523 小时前
Docker Compose 编排与 Harbor 私有仓库
运维·docker·容器
老年DBA4 小时前
Ora2Pg 迁移Oracle至 PostgreSQL 之实战指南
数据库·postgresql·oracle
A13247053124 小时前
防火墙配置入门:保护你的服务器
linux·运维·服务器·网络
CS Beginner4 小时前
【Linux】快速配置wifi和SSH服务
linux·运维·ssh
我也要当昏君4 小时前
第一节(代入排除法)
运维