燃烧吧! PG Bloom 索引扫描狂飙 7 倍性能
本期播客
燃烧吧! PG Bloom 索引扫描狂飙 7 倍性能
数据库查询优化的终极战场,不在 CPU,而在 I/O 的每一微秒。
想象这样一个场景:你的应用需要在一张亿级大表上做任意组合的等值查询——例如根据用户 ID、商品类别、订单状态等多个字段的任意组合筛选数据。你选择了 Bloom 索引,因为它能完美支持这种查询模式。但随着数据量增长,查询响应时间从秒级变成分钟级。你发现瓶颈在于 Bloom 索引扫描的 I/O——它需要读取大量索引页,而每次只能串行等待。
这种煎熬,在 PostgreSQL 19 中彻底终结。Michael Paquier 提交的 4c910f3 补丁,为 Bloom 索引的 bitmap 扫描路径引入了 流式读取(streaming read) ,在 1000 万行数据的测试中,性能提升高达 3 到 7 倍!这不仅是数字的飞跃,更是 I/O 效率革命的又一座里程碑。
第一性原理:为什么 Bloom 索引扫描会慢成瓶颈?
让我们回归 Bloom 索引的本质。Bloom 索引是一种概率型数据结构,用于快速判断一个元素是否属于某个集合。在 PostgreSQL 中,它常用于对多列进行任意组合的等值查询。
Bloom 索引的扫描过程(blgetbitmap())需要读取大量索引页来构建位图。传统的实现方式是同步循环:
for (每个需要扫描的块号) {
buffer = ReadBufferExtended(索引, 块号); // 等待 I/O 完成
处理该 buffer 中的数据;
ReleaseBuffer(buffer);
}
这个模式在现代硬件下面临一个根本性问题:它让 I/O 变得“串行”了。程序发出一个读请求,然后阻塞等待,直到数据从存储介质返回,才继续下一个。这就像在一条单车道高速公路上,即使路边有 10 条车道,你也只能一辆一辆地放行。
第一性原理告诉我们:现代存储设备本质上是高并发的。NVMe SSD 可以同时处理数百个 I/O 请求,由设备内部调度并行处理。串行 I/O 完全无法发挥硬件的真实能力。
PostgreSQL 2026 年度大戏来了, 扫海报中的二维码报名, 选择早鸟或通票(都含午餐和周边礼品), 可私信我要优惠码, 数量有限先到先得!
破局者:Streaming Read 如何实现 3-7 倍飞跃?
这个补丁的核心改动,就是将上述同步循环替换为流式读取:
// 初始化流式读取上下文,告诉内核“我要读这些块”
stream = read_stream_begin(..., 块号列表);
while ((buffer = read_stream_next_buffer(stream)) != NULL) {
处理该 buffer 中的数据;
ReleaseBuffer(buffer);
}
背后的魔法:
异步预取:当程序在处理当前页面时,内核已经在后台并行读取接下来的多个页面。 I/O 合并:流式读取可以将对相邻页面的多次小请求合并为一次大 I/O,减少系统调用开销。 流水线作业:I/O 等待和 CPU 处理重叠进行,让硬件始终保持忙碌。
权威数据:提交信息中给出了令人震撼的对比结果——在 1000 万行数据的测试中:
io_uring 方法:作者报告 3 倍运行时提升,而 Michael Paquier 本人测得接近 7 倍! worker 方法(3 个 worker) :作者报告的运行时提升比 Michael 的测试更好,且 IO 统计数据的减少在所有测试案例中都非常显著。
这意味着:你的 Bloom 索引越大,这个优化的收益就越显著。
量化分析:7 倍提升意味着什么?
假设一个典型场景:一张 5 亿行的用户行为表,上面有一个 Bloom 索引用于多字段组合查询。每次查询需要扫描 10% 的索引页(约 5000 页)。
旧方式:5000 次同步 I/O,假设每次 0.1 毫秒(高速 SSD),总 I/O 时间 500 毫秒,加上 CPU 处理,总响应时间约 800 毫秒。 优化后:通过流式读取,I/O 时间降低到 70 毫秒(7 倍提升),总响应时间约 150 毫秒。
对于高频查询:假设每秒 100 次这样的查询,旧方式需要 80 个 CPU 核心才能处理,而优化后只需 15 个核心。 硬件成本降低 80% 。
权威案例:流式读取正在成为新范式
这个优化不是孤例。Xuneng Zhou(该补丁的作者)在邮件列表中表示,他还在 pgstattuple 等其他模块中应用了相同的技术。此前,Bloom 索引的 VACUUM 操作也通过流式读取获得了 30% 的性能提升(commit d841ca2)。GIN 索引的 VACUUM 更是获得了 5 倍的提升(commit 6c228755)。
这表明:流式读取正在成为 PostgreSQL I/O 子系统的新标准,从核心索引到 contrib 模块,从 VACUUM 到查询扫描,全面受益。
第一性原理的边界:什么时候这个优化会失效?
任何优化都有前提。流式读取的核心前提是:你能提前知道需要读取哪些块,并且这些块的顺序是确定的。当这个前提崩塌时,效果可能打折扣。
崩塌场景 1:I/O 延迟极低(如内存表)
如果索引已经完全缓存在内存中,ReadBufferExtended() 几乎不产生 I/O 等待,那么流式读取的预取优势就无法体现。但此时,优化也不会造成伤害——它只是多了几次函数调用而已。
崩塌场景 2:存储设备本身是单线程的(老式 HDD)
在单块机械硬盘上,并发 I/O 的收益有限,因为磁头物理上只能服务一个请求。但即便如此,流式读取仍能通过减少系统调用次数和 I/O 合并带来小幅提升。7 倍的收益主要是在现代 SSD 和 io_uring 环境下测得的,这代表了未来主流存储的趋势。
崩塌场景 3:查询选择性极高,只需读取少量页
如果查询只需要读取 1-2 个索引页,流式读取的预取优势无法发挥。但此时,优化也不会造成额外负担。
崩塌场景 4:你没有使用 Bloom 索引
这个优化专为 Bloom 索引设计。如果你的业务没有使用 Bloom 索引(比如只用 B-tree 或 GIN),这个优化不直接受益。但好消息是,流式读取的哲学正在渗透到更多模块,未来的扫描优化可能会惠及所有索引类型。
DBA 的行动指南
1. 检查你是否在用 Bloom 索引
SELECT schemaname, tablename, indexname, indexdef
FROM pg_indexes
WHERE indexdef LIKE'%bloom%';
如果你看到输出,说明你正在使用 Bloom 索引。升级到 PostgreSQL 19 后,查询将自动受益。
2. 升级并验证性能
在测试环境,对一张包含大 Bloom 索引的表执行典型查询,并对比 PG18 和 PG19 的执行时间:
\timing on
SELECT * FROM your_table WHERE col1 = 'value1'AND col2 = 'value2';
记录时间。如果可能,用 EXPLAIN (ANALYZE, BUFFERS) 观察 I/O 统计的变化。
3. 调整 I/O 相关参数
为了最大化流式读取的效果,确保以下参数合理设置:
-- 启用异步 I/O(如果硬件支持)
ALTERSYSTEMSET io_method = 'io_uring'; -- 或 'worker'
-- 设置合适的 worker 数量(如果使用 worker 方法)
ALTERSYSTEMSET io_workers = 16;
SELECT pg_reload_conf();
4. 监控 I/O 效率
使用 pg_statio_user_indexes 观察优化前后的 I/O 变化:
SELECT indexname,
blks_read,
blks_hit,
round(blks_hit * 100.0 / NULLIF(blks_read + blks_hit, 0), 2) AS hit_ratio
FROM pg_statio_user_indexes
WHERE indexname = 'your_bloom_index';
流式读取应能提高缓存命中率,减少实际物理读。
未来展望:流式读取将无处不在
这个补丁再次印证了 PostgreSQL 社区对 I/O 子系统的持续投入。未来,我们很可能看到:
所有索引类型的扫描都采用流式读取 顺序扫描也能受益于流式预取 更智能的预取策略,结合统计信息和查询模式
结语
PostgreSQL 19 对 Bloom 索引扫描的优化,看似是一个索引类型的专属改进,实则是将现代 I/O 理念深植于数据库内核的又一力证。3 到 7 倍的性能提升,来源于对硬件特性的深刻理解和巧妙的工程实现。
对于 DBA 而言,这意味着更快的查询响应、更低的硬件成本、更平稳的业务运行。升级到 PostgreSQL 19,不仅是为了新功能,更是为了让你的硬件投资发挥出应有的效能。
从今天起,当你使用 Bloom 索引进行组合查询时,可以放心:PostgreSQL 19 已经为你铺就了 I/O 的高速公路。