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