BRIN的大小是B树的1/4570,直到5%的行被更新
我曾在Oracle共同拥有Zonemaps模块,而BRIN在Postgres中是相同的设计。它在pg_stats.correlation承认有问题之前就停止修剪——在相关性为0.921时,收缩为28倍。在1000万行上测量,带有内联工具。在Oracle,我共同拥有查询引擎中的Zonemaps模块,并为其核心贡献。Zonemap是一个小而不显眼的结构:为一系列连续的块存储某列的最小值和最大值。当一个谓词到达时,将其与每个区域的范围进行比较,并跳过不能匹配的块。没有树,没有逐行条目,没有与行数成比例的维护。对于大表,获胜的关键在于不读取块,而Zonemap几乎是最便宜的方式。Postgres在BRIN中有相同的思路:页面范围,每个范围的最小/最大摘要,一个微小的索引。这个设计不是一个较低的拷贝——在一个重要方面它比Oracle构建的更便宜。但它依赖于一个Postgres从不承诺的属性,而这篇文章测量了当该属性侵蚀时会发生什么。以下所有内容都是由一个脚本生成的,您可以自己运行。没有可以克隆的存储库——这个工具在本文结尾完整重现。条件:PostgreSQL 17.9在x86_64上,shared_buffers=256MB,work_mem=32MB,fsync=off,autovacuum=off,单容器,无其他负载。1000万行跨越90天,按时间戳顺序插入——976 MB堆。BRIN在created_at上,pages_per_range=128。探测是一天的范围:从一千万中提取111,112行。新鲜案例:BRIN是显著的。 在新加载的、物理有序的堆上: BRIN B树索引大小 48 kB 214 MB 探测执行时间 21.2 ms — 堆页面访问 1,536 — 48 KB。等效的B树为224,641,024字节——大约大4570倍。这个比例就是BRIN的全部论据,这不是营销:一个适合L2缓存的索引几乎没有维护成本,几乎没有写入成本,几乎没有读取成本。 更新的测量 然后我更新了越来越多的行——状态=status || '*',这是对未索引列的更新,是最温和的类型——并在每次测量前运行VACUUM (ANALYZE)。累积更新,每次都使用相同的探测: 状态 pg_stats相关性 堆页面(损失) 重新检查中移除的行 执行时间 新鲜 1.000 1,536 11,781 21.2 ms 1%行更新 0.979 1,806 32,243 24.2 ms 5%行更新 0.921 51,268 3,827,572 558.7 ms 20%行更新 0.782 63,923 4,216,606 690.6 ms 有趣的行是第三行。在1%到5%更新之间,索引的访问页数从1,806增加到51,268——对于返回相同行的相同查询,I/O增加了28倍——且执行时间增加了23倍。堆仅增长了3%。以下是5%更新的实际计划,来自该会话: 聚合(cost=134586.09..134586.10 rows=1 width=40)(实际时间=691.472..691.474 rows=1 loops=1) 缓冲区:共享命中=75,读取=51201,写入=30905 -> 在events上进行位图堆扫描(cost=49.89..134075.84 rows=102049 width=6)(实际时间=1.701..680.083 rows=111112 loops=1) 重新检查条件:((created_at >= '2026-02-15 00:00:00+00'::timestamp with time zone) AND (created_at < '2026-02-16 00:00:00+00'::timestamp with time zone)) 通过索引重新检查移除的行:3827572 堆块:损失=51268 缓冲区:共享命中=75,读取=51201,写入=30905 -> 在events_brin上进行位图索引扫描(cost=0.00..24.38 rows=117557 width=0)(实际时间=1.198..1.199 rows=512680 loops=1) 索引条件:((created_at >= '2026-02-15 00:00:00+00'::timestamp with time zone) AND (created_at < '2026-02-16 00:00:00+00'::timestamp with time zone)) 规划时间:0.201 ms 执行时间:691.503 ms 提取并丢弃了380万行以返回111,112行。这就是损失重新检查进行所有索引原本应避免的工作的原因。 为什么5%更新是一个悬崖而不是一个斜坡 因为页面范围由其极值总结。在Postgres中,UPDATE写入一个新的元组版本,当旧的页面没有空间时,新版本落在任何有空闲空间的地方——通常在堆的末尾,或者在VACUUM留下的回收空洞中。每个重新定位的行都携带其原始的created_at进入它落下的任何页面范围,该范围的摘要会扩大以涵盖它。一行来自一月的行落在一个充满三月行的范围中,会让整个128页的范围永远匹配每个一月的谓词。需要的不多的错误行会使大多数范围扩大,以横跨大多数时间域,一旦一个范围的摘要跨度覆盖你的谓词,整个范围都会被读取。这一过渡是明显的,因为范围是宽广的:不是“移动了5%的行因此读取更多5%的页面”,而是“移动了5%的行,并且它们分布在大多数范围中。”这也是我会停止信任pg_stats.correlation作为BRIN索引健康信号的时刻。在5%更新时,相关性仍然为0.921——一个大多数人会读作“很好,相关性很强”的数字——而修剪已经收缩了28倍。相关性测量全球排序排名
本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡