在 PostgreSQL 生态系统中,模式匹配查询(特别是LIKE和ILIKE操作)一直是性能优化的难点。传统的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 需要处理几个关键适配问题:
- 内存对齐要求:PostgreSQL 的共享内存需要特定的对齐方式,Roaring Bitmaps 容器必须适配
MAXALIGN要求 - 事务安全:位图更新需要支持 MVCC,Biscuit 通过 tombstone 机制标记删除记录
- 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 根据数据密度自动选择最优的容器类型:
- 数组容器:当元素数量少于 4096 时,使用排序整数数组
- 位图容器:当元素密度高时,使用 4096 位的位图
- 运行长度编码:当存在长连续序列时,使用 (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 直接相关的包括:
-
直接 Roaring 迭代:避免中间数组分配
roaring_uint32_iterator_t *iter = roaring_create_iterator(bitmap); while (iter->has_value) { process(iter->current_value); roaring_advance_uint32_iterator(iter); } -
批量 TID 插入:减少锁竞争
for (i = 0; i < num_results; i += 10000) { tbm_add_tuples(tbm, &tids[i], batch_size, false); } -
早期空集终止:快速失败机制
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:
- 删除操作:不立即清除位图,而是标记为 tombstone
- 批量清理:当 tombstone 数量达到阈值(默认 1000)时执行批量清理
if (tombstone_count >= 1000) { for each bitmap { roaring_bitmap_andnot_inplace(bitmap, tombstones); } roaring_bitmap_clear(tombstones); } - 可见性检查:在返回结果前过滤 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 特别适合以下场景:
- 通配符密集型查询:包含大量
%和_的 LIKE/ILIKE 查询 - 多列模式匹配:需要在多个列上同时进行模式匹配
- 精确结果要求:不能接受 false positive 的场合
- 聚合查询优化:COUNT (*) 等聚合函数查询
- 高查询量系统:可以承受较高内存使用
6.2 部署注意事项
-
内存规划:
- 预估内存使用:每百万记录约需 200-300MB 内存
- 监控
biscuit_status视图中的内存使用情况 - 定期执行
REINDEX以回收碎片内存
-
写入性能考虑:
- INSERT 性能与 B-tree 相当
- UPDATE 需要两次操作(删除旧位图,插入新位图)
- DELETE 使用 tombstone 机制,批量清理
-
监控指标:
-- 关键监控查询 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 故障排除
常见问题及解决方案:
-
内存不足:
- 减少并发 Biscuit 索引数量
- 增加
shared_buffers配置 - 考虑使用分区表减少单个索引大小
-
写入性能下降:
- 检查 tombstone 数量:
SELECT * FROM biscuit_index_stats('index_name') - 手动触发清理:
VACUUM FULL或REINDEX
- 检查 tombstone 数量:
-
查询未使用索引:
- 确保查询使用 LIKE/ILIKE 操作符
- 检查模式是否过于简单(如纯
%模式) - 验证索引统计信息是否最新:
ANALYZE table_name
七、未来发展方向
7.1 技术演进路线
Biscuit 的开发路线图包括:
- 有序扫描支持:实现
amcanorder = true以支持原生有序扫描 - 并行索引构建:利用多核 CPU 加速索引创建
- 更多数据类型支持:JSON、数组等复杂类型的模式匹配
- 自适应压缩:根据数据特征动态调整压缩策略
- 云原生优化:针对云环境的内存和存储优化
7.2 生态系统集成
随着 Roaring Bitmaps 在 PostgreSQL 生态中的普及,预计将出现:
- 标准化接口:统一的 Roaring Bitmaps PostgreSQL 接口
- 工具链完善:专门的监控、调优和管理工具
- 云服务集成:主流云数据库服务的原生支持
- 扩展生态系统:基于 Roaring Bitmaps 的其他扩展开发
结论
Biscuit 索引通过深度集成 Roaring Bitmaps 压缩算法,为 PostgreSQL 带来了革命性的模式匹配性能提升。其核心优势在于:
- 算法创新:双重位置索引设计彻底解决了通配符匹配的性能问题
- 存储优化:Roaring Bitmaps 的智能压缩平衡了性能与内存使用
- 查询优化:12 项性能优化确保在各种场景下的最佳表现
- 系统集成:完整的 PostgreSQL 索引访问方法接口支持
虽然 Biscuit 在内存使用和功能范围上存在一定限制,但对于通配符密集型的应用场景,它提供了无可比拟的性能优势。随着技术的不断成熟和生态系统的完善,Biscuit 有望成为 PostgreSQL 模式匹配的标准解决方案。
对于正在面临 LIKE/ILIKE 查询性能瓶颈的团队,Biscuit 值得认真评估和尝试。通过合理的架构设计和性能调优,它可以为应用带来数量级的性能提升。
资料来源:
- Biscuit GitHub 仓库:https://github.com/crystallinecore/biscuit
- Biscuit 官方文档:https://biscuit.readthedocs.io/
- Roaring Bitmaps 研究论文及相关技术文档
- PostgreSQL 索引访问方法开发指南