Hotdry.
database-systems

Biscuit索引中Roaring Bitmaps与PostgreSQL存储层集成深度解析

深入剖析Biscuit索引如何通过Roaring Bitmaps压缩算法与PostgreSQL存储层深度集成,实现高效的LIKE/ILIKE模式匹配,包括位图压缩适配、查询优化策略和内存管理机制。

在 PostgreSQL 生态系统中,模式匹配查询(特别是LIKEILIKE操作)一直是性能优化的难点。传统的pg_trgm扩展虽然提供了通配符支持,但其 recheck 开销和内存使用效率限制了大规模应用。Biscuit 索引作为一种新型的索引访问方法,通过深度集成 Roaring Bitmaps 压缩算法,为 PostgreSQL 带来了革命性的模式匹配性能提升。本文将深入解析 Biscuit 索引中 Roaring Bitmaps 与 PostgreSQL 存储层的集成细节。

一、Biscuit 索引架构与 Roaring Bitmaps 集成

1.1 核心设计理念

Biscuit 索引的核心设计理念是 "Bitmap Indexed Searching with Comprehensive Union and Intersection Techniques"。与传统的 B-tree 或 GIN 索引不同,Biscuit 为每个字符串构建了两类字符位置位图:

  • 正向位置索引:记录字符c在位置p出现的记录 ID 集合
  • 反向位置索引:记录字符c在位置-p(从末尾开始计数)出现的记录 ID 集合

这种双重索引结构使得 Biscuit 能够高效处理前缀、后缀和子字符串匹配。例如,对于字符串 "Hello":

  • 正向索引:H@0, e@1, l@2, l@3, o@4
  • 反向索引:o@-1, l@-2, l@-3, e@-4, H@-5

1.2 Roaring Bitmaps 的存储层适配

Biscuit 选择 Roaring Bitmaps 作为底层存储结构,主要基于其在稀疏数据集上的压缩优势。Roaring Bitmaps 采用分层存储策略:

// 简化的存储结构示意
typedef struct {
    uint16_t container_type;  // 容器类型:数组、位图或运行长度编码
    uint16_t key;             // 高16位作为键
    void* container;          // 指向实际数据的指针
} roaring_container_t;

在 PostgreSQL 集成中,Biscuit 需要处理几个关键适配问题:

  1. 内存对齐要求:PostgreSQL 的共享内存需要特定的对齐方式,Roaring Bitmaps 容器必须适配MAXALIGN要求
  2. 事务安全:位图更新需要支持 MVCC,Biscuit 通过 tombstone 机制标记删除记录
  3. WAL 日志:位图变更需要生成适当的 WAL 记录,确保崩溃恢复

1.3 多列索引的位图组织

Biscuit 支持多列索引,其内部结构组织如下:

BiscuitIndex
├── num_columns: int
├── column_indices[]: ColumnIndex[]
│   ├── pos_idx[256]: CharIndex    // 正向位置位图
│   │   └── entries[]: PosEntry[]
│   │       ├── pos: int
│   │       └── bitmap: RoaringBitmap
│   ├── neg_idx[256]: CharIndex    // 反向位置位图
│   ├── char_cache[256]: RoaringBitmap  // 字符存在性缓存
│   ├── length_bitmaps[]: RoaringBitmap[]  // 精确长度位图
│   └── length_ge_bitmaps[]: RoaringBitmap[]  // 最小长度位图
└── insensitive_column_indices[]: ColumnIndex[]  // 大小写不敏感索引

每个字符(0-255)在每个位置都有一个独立的 Roaring Bitmap,这种设计虽然增加了存储开销,但极大地提升了查询性能。

二、位图压缩算法在 PostgreSQL 中的适配

2.1 CRoaring 库的编译时集成

Biscuit 通过条件编译支持 CRoaring 库集成。在构建时,系统会检查多个常见的 CRoaring 安装路径:

# Makefile中的检测逻辑
ROARING_PATHS := /usr/local /usr /opt/local /opt/homebrew
ROARING_FOUND := $(foreach path,$(ROARING_PATHS),\
    $(wildcard $(path)/include/roaring/roaring.h))

ifneq ($(ROARING_FOUND),)
    CFLAGS += -DHAVE_ROARING
    INCLUDES += -I$(dir $(firstword $(ROARING_FOUND)))/..
    LIBS += -lroaring
endif

这种多路径检测机制提高了 Biscuit 在不同系统环境中的可移植性。

2.2 位图压缩策略选择

Roaring Bitmaps 根据数据密度自动选择最优的容器类型:

  1. 数组容器:当元素数量少于 4096 时,使用排序整数数组
  2. 位图容器:当元素密度高时,使用 4096 位的位图
  3. 运行长度编码:当存在长连续序列时,使用 (run_start, run_length) 对

在 PostgreSQL 上下文中,Biscuit 需要特别考虑:

  • TID 空间分布:PostgreSQL 的 TID(元组 ID)通常具有局部性,这有利于运行长度编码
  • 更新模式:频繁的插入 / 删除可能导致容器类型频繁转换,需要优化转换阈值

2.3 内存管理优化

Biscuit 实现了专门的内存管理策略来平衡性能和内存使用:

// 内存分配策略
typedef struct {
    Size total_allocated;      // 总分配内存
    Size active_bitmaps;       // 活动位图数量
    Size cached_bitmaps;       // 缓存位图数量
    double fragmentation_ratio; // 碎片化比率
} BiscuitMemoryStats;

// 关键内存参数
#define BISCUIT_MIN_BITMAP_SIZE  1024    // 最小位图大小
#define BISCUIT_MAX_CACHE_SIZE   (256*1024*1024)  // 最大缓存256MB
#define BISCUIT_EVICTION_THRESHOLD 0.85  // 缓存驱逐阈值

三、查询执行优化策略

3.1 模式解析与谓词重排序

Biscuit 的查询优化器首先解析 LIKE 模式,然后根据选择性重新排序谓词:

-- 示例查询
WHERE name LIKE '%common%'           -- 低选择性 (0.60)
  AND sku LIKE 'PROD-2024-%'         -- 高选择性 (0.02)  
  AND description LIKE '%rare_word%' -- 中等选择性 (0.15)

-- Biscuit自动重排序为:
1. sku LIKE 'PROD-2024-%'         -- 先执行高选择性谓词
2. description LIKE '%rare_word%'  -- 再执行中等选择性谓词  
3. name LIKE '%common%'           -- 最后执行低选择性谓词

选择性评分公式综合考虑了具体字符数、下划线数量、分区数和锚定强度:

score = 1.0 / (concrete_chars + 1)
      - (underscore_count × 0.05)
      + (partition_count × 0.15)
      - (anchor_strength / 200)

3.2 位图操作优化

Biscuit 实现了 12 项关键的性能优化,其中与 Roaring Bitmaps 直接相关的包括:

  1. 直接 Roaring 迭代:避免中间数组分配

    roaring_uint32_iterator_t *iter = roaring_create_iterator(bitmap);
    while (iter->has_value) {
        process(iter->current_value);
        roaring_advance_uint32_iterator(iter);
    }
    
  2. 批量 TID 插入:减少锁竞争

    for (i = 0; i < num_results; i += 10000) {
        tbm_add_tuples(tbm, &tids[i], batch_size, false);
    }
    
  3. 早期空集终止:快速失败机制

    result = bitmap[a][0];
    result &= bitmap[b][1];
    if (roaring_is_empty(result)) return empty_set;
    

3.3 多列查询的位图合并

对于多列查询,Biscuit 采用分层位图合并策略:

查询: WHERE col1 LIKE 'A%B' AND col2 LIKE '%C%D'

执行流程:
1. 解析col1模式 → 生成位图B1
2. 解析col2模式 → 生成位图B2  
3. 如果B1为空或B2为空 → 立即返回空集
4. 计算交集: result = roaring_bitmap_and(B1, B2)
5. 如果结果大小 < LIMIT值 → 提前终止
6. 对结果TID进行排序(如需要)

四、存储层集成与事务处理

4.1 PostgreSQL 索引访问方法接口

Biscuit 实现了完整的 PostgreSQL 索引访问方法接口:

// 关键接口函数
IndexAmRoutine biscuit_am = {
    .amstrategies = 1,
    .amsupport = 1,
    .amcanorder = false,      // 不支持有序扫描
    .amcanorderbyop = false,
    .amcanbackward = false,
    .amcanunique = false,
    .amcanmulticol = true,    // 支持多列
    .amoptionalkey = false,
    .amsearcharray = false,
    .amsearchnulls = false,
    .amstorage = false,
    .amclusterable = false,
    .ampredlocks = false,
    .amkeytype = InvalidOid,
    
    // 关键操作函数
    .ambuild = biscuit_build,
    .ambuildempty = biscuit_buildempty,
    .aminsert = biscuit_insert,
    .ambulkdelete = biscuit_bulkdelete,
    .amvacuumcleanup = biscuit_vacuumcleanup,
    .amcostestimate = biscuit_costestimate,
    .amoptions = biscuit_options,
    .amproperty = NULL,
    .amvalidate = biscuit_validate,
    .ambeginscan = biscuit_beginscan,
    .amrescan = biscuit_rescan,
    .amgettuple = biscuit_gettuple,
    .amgetbitmap = biscuit_getbitmap,
    .amendscan = biscuit_endscan,
    .ammarkpos = NULL,
    .amrestrpos = NULL,
    .amestimateparallelscan = NULL,
    .aminitparallelscan = NULL,
    .amparallelrescan = NULL,
};

4.2 MVCC 与并发控制

Biscuit 通过 tombstone 机制支持 MVCC:

  1. 删除操作:不立即清除位图,而是标记为 tombstone
  2. 批量清理:当 tombstone 数量达到阈值(默认 1000)时执行批量清理
    if (tombstone_count >= 1000) {
        for each bitmap {
            roaring_bitmap_andnot_inplace(bitmap, tombstones);
        }
        roaring_bitmap_clear(tombstones);
    }
    
  3. 可见性检查:在返回结果前过滤 tombstone 记录

4.3 WAL 日志生成

位图变更需要生成适当的 WAL 记录以确保崩溃恢复:

// 位图更新WAL记录结构
typedef struct {
    uint8   info;           // WAL记录类型
    Oid     index_oid;      // 索引OID
    ItemPointerData tid;    // 影响的元组
    uint32  bitmap_size;    // 位图数据大小
    char    bitmap_data[FLEXIBLE_ARRAY_MEMBER]; // 位图数据
} BiscuitWALRecord;

对于大型位图更新,Biscuit 采用增量 WAL 记录策略,只记录变更部分而非整个位图。

五、性能调优与监控

5.1 关键配置参数

虽然 Biscuit 目前不暴露大量可调参数,但内部有几个关键阈值:

// 性能相关阈值
#define BISCUIT_EARLY_TERMINATION_THRESHOLD  1000
#define BISCUIT_BATCH_CLEANUP_THRESHOLD      1000
#define BISCUIT_SORT_THRESHOLD               5000
#define BISCUIT_LIMIT_AWARE_COLLECTION       true

// 内存管理参数
#define BISCUIT_MAX_IN_MEMORY_BITMAPS        100000
#define BISCUIT_BITMAP_CACHE_SIZE            (128 * 1024 * 1024)

5.2 监控与诊断

Biscuit 提供了丰富的监控功能:

-- 检查Roaring Bitmaps支持
SELECT biscuit_has_roaring();      -- 是否编译了CRoaring支持
SELECT biscuit_roaring_version();  -- CRoaring库版本

-- 查看索引统计
SELECT biscuit_index_stats('idx_biscuit'::regclass);

-- 系统状态视图
SELECT * FROM biscuit_status;

biscuit_status视图提供:

  • 扩展版本信息
  • CRoaring 启用状态
  • 使用的位图后端
  • Biscuit 索引总数
  • 总磁盘索引大小

5.3 性能基准测试

根据官方基准测试,Biscuit 相比传统方案有显著优势:

指标 Biscuit (Roaring) pg_trgm (GIN) B-tree
平均执行时间 38.37 ms 111.45 ms 192.42 ms
中位数执行时间 11.34 ms 63.74 ms 170.26 ms
相对速度(中位数) 1.0× 0.18× (5.6× slower) 0.07× (15.0× slower)
索引大小 277.09 MB 86 MB 43 MB

关键发现:

  • Biscuit 比 B-tree 快 15 倍(中位数)
  • 比 pg_trgm 快 5.6 倍(中位数)
  • 100% 正确性验证通过 11,400 次测量
  • 所有结果具有高度统计显著性(p < 0.0001)

六、实际部署建议

6.1 适用场景

Biscuit 特别适合以下场景:

  1. 通配符密集型查询:包含大量%_的 LIKE/ILIKE 查询
  2. 多列模式匹配:需要在多个列上同时进行模式匹配
  3. 精确结果要求:不能接受 false positive 的场合
  4. 聚合查询优化:COUNT (*) 等聚合函数查询
  5. 高查询量系统:可以承受较高内存使用

6.2 部署注意事项

  1. 内存规划

    • 预估内存使用:每百万记录约需 200-300MB 内存
    • 监控biscuit_status视图中的内存使用情况
    • 定期执行REINDEX以回收碎片内存
  2. 写入性能考虑

    • INSERT 性能与 B-tree 相当
    • UPDATE 需要两次操作(删除旧位图,插入新位图)
    • DELETE 使用 tombstone 机制,批量清理
  3. 监控指标

    -- 关键监控查询
    SELECT 
        pg_size_pretty(pg_relation_size('idx_biscuit')) as index_size,
        (SELECT count(*) FROM biscuit_status) as roaring_enabled,
        (SELECT total_indexes FROM biscuit_status) as total_indexes
    FROM biscuit_status;
    

6.3 故障排除

常见问题及解决方案:

  1. 内存不足

    • 减少并发 Biscuit 索引数量
    • 增加shared_buffers配置
    • 考虑使用分区表减少单个索引大小
  2. 写入性能下降

    • 检查 tombstone 数量:SELECT * FROM biscuit_index_stats('index_name')
    • 手动触发清理:VACUUM FULLREINDEX
  3. 查询未使用索引

    • 确保查询使用 LIKE/ILIKE 操作符
    • 检查模式是否过于简单(如纯%模式)
    • 验证索引统计信息是否最新:ANALYZE table_name

七、未来发展方向

7.1 技术演进路线

Biscuit 的开发路线图包括:

  1. 有序扫描支持:实现amcanorder = true以支持原生有序扫描
  2. 并行索引构建:利用多核 CPU 加速索引创建
  3. 更多数据类型支持:JSON、数组等复杂类型的模式匹配
  4. 自适应压缩:根据数据特征动态调整压缩策略
  5. 云原生优化:针对云环境的内存和存储优化

7.2 生态系统集成

随着 Roaring Bitmaps 在 PostgreSQL 生态中的普及,预计将出现:

  1. 标准化接口:统一的 Roaring Bitmaps PostgreSQL 接口
  2. 工具链完善:专门的监控、调优和管理工具
  3. 云服务集成:主流云数据库服务的原生支持
  4. 扩展生态系统:基于 Roaring Bitmaps 的其他扩展开发

结论

Biscuit 索引通过深度集成 Roaring Bitmaps 压缩算法,为 PostgreSQL 带来了革命性的模式匹配性能提升。其核心优势在于:

  1. 算法创新:双重位置索引设计彻底解决了通配符匹配的性能问题
  2. 存储优化:Roaring Bitmaps 的智能压缩平衡了性能与内存使用
  3. 查询优化:12 项性能优化确保在各种场景下的最佳表现
  4. 系统集成:完整的 PostgreSQL 索引访问方法接口支持

虽然 Biscuit 在内存使用和功能范围上存在一定限制,但对于通配符密集型的应用场景,它提供了无可比拟的性能优势。随着技术的不断成熟和生态系统的完善,Biscuit 有望成为 PostgreSQL 模式匹配的标准解决方案。

对于正在面临 LIKE/ILIKE 查询性能瓶颈的团队,Biscuit 值得认真评估和尝试。通过合理的架构设计和性能调优,它可以为应用带来数量级的性能提升。


资料来源

  1. Biscuit GitHub 仓库:https://github.com/crystallinecore/biscuit
  2. Biscuit 官方文档:https://biscuit.readthedocs.io/
  3. Roaring Bitmaps 研究论文及相关技术文档
  4. PostgreSQL 索引访问方法开发指南
查看归档