{"id":"mb-20260827-2f2d5f","account":"mubei","brand":"","title":"一个 48 KB 的轻量数据库索引，体积只有传统 B 树的四千五百分之一","summary":"一个 48 KB 的轻量数据库索引，体积只有传统 B 树的四千五百分之一","body":"一个 48 KB 的轻量数据库索引，体积只有传统 B 树的四千五百分之一。\n它管着一张 1000 万行、近 1 GB 体积的数据大表。\n在刚写完数据时，它查一天的数据只需要 21 毫秒。\n但只要表里有 5% 的数据被随手更新了一次，整个查询性能会直接掉下断崖。\n耗时从 21 毫秒暴涨到 558 毫秒。\n数据库读写的数据块飙升 28 倍。\n为了挑出 11 万行目标结果，系统不得不把 382 万行无关数据读进内存反复重检。\n诡异的是，整张表的体积只增加了 3%。\n监控面板上的物理相关度指标依然高达 0.921，看起来毫无破绽。\n为什么仅仅 5% 的微小变动，没有让性能下降 5%，却引发了系统级雪崩？\n前 Oracle 查询引擎工程师 Venkat Sakamuri 用一组公开测试，拆解了这台藏在 PostgreSQL 底层的精巧机器。\n它的名字叫极值污染。\n要看懂这场雪崩，得先看懂块范围索引（BRIN）最初是怎么创造压缩奇迹的。\n传统的 B 树索引，是给表里的每一行数据都建一个独立的指针节点。\n一千万行数据，就要建一千万个索引条目。\n索引体积随随便便就冲上 214 MB，不仅极其吃内存，每次写入还要维护庞大的树结构。\n块范围索引换了一种极致偷懒的思路。\n它根本不记录具体的行。\n它把磁盘上的物理存储页切成连续的区块，比如每 128 个页划成一个范围。\n每一个区块，它只记录两个数字：这批数据里的最小值，和最大值。\n当你要查询 2 月 15 日的数据时，数据库拿着条件扫过这些极值摘要。\n如果一个区块的最大值是 1 月，直接跳过。\n如果一个区块的最小值是 3 月，也直接跳过。\n只有包含 2 月 15 日的区块，才会被整块读进内存。\n214 MB 的复杂树形结构，被压缩成了几千对数字，总共只有 48 KB。\n它小到能直接塞进 CPU 的二级缓存里运行。\n这套设计近乎完美，但它暗中押上了一个极其苛刻的物理假设：\n数据在磁盘上的物理存放顺序，必须和查询字段的逻辑顺序高度一致。\n一旦数据发生更新，Postgres 经典的多版本并发控制机制，就成了这台机器的致命毒药。\nPostgres 从不在原地覆盖旧数据。\n每次执行更新，它都会在磁盘空白处写入一条全新的数据版本。\n如果原有的数据页满了，这条新数据就会被扔到表尾，或者塞进清理工具留下的空洞里。\n灾难就在这一瞬间发生。\n一条 1 月份的旧订单被修改了状态，生成的新版本被随手扔进了 3 月份的数据区块中。\n这 128 个页的摘要区间，瞬间从原本的 3 月，被强行撑大成了 1 月到 3 月。\n原本用来挡住无关查询的极值护栏，被一条离群数据彻底撕开。\n只需要 5% 的数据更新散落在各个页面中。\n全表绝大部分区块的极值区间，都会被这些零散的旧数据撑到无限宽。\n当查询再次发起时，数据库发现每一个区块的摘要都涵盖了 2 月 15 日。\n原本用来跳过无用数据的块级剪枝能力，实质彻底崩溃。\n它必须把五万多个数据页全部读进内存，一条一条过滤出匹配的数据，再把剩下的近四百万行全部丢弃。\n更致命的是监控盲区。\n很多工程师习惯看官方统计视图里的秩相关度。\n但全局相关度衡量的是整张表的宏观单调趋势。\n5% 的离群点根本动摇不了 0.921 的高分，在监控图表上它看起来依然健康。\n宏观的统计指标，彻底掩盖了局部边界的全面破损。\n索引文件依然只有 48 KB，执行计划依然显示在走索引。\n但它已经从一个高速剪枝神器，退化成了一个代价高昂的磁盘搬运工。\n要把性能救回来，只能用全表重排工具重建物理聚簇。\n而代价是漫长的排他锁、临时翻倍的磁盘占用，以及巨大的预写日志开销。\n软件工程里从来没有平白无故的压缩奇迹。\n当一个数据结构用四千分之一的体积换取极致性能时，它必然把成本转嫁给了一条脆弱的物理假设。\n稀疏摘要赌的是数据的绝对有序。\n在任何一个允许持续写入和更新的真实系统里，物理存储的熵增才是最不可逆的规律。","category":"科技","score":null,"translated_x_url":null,"translated_status_id":null,"published_at":"2026-08-27 21:12:43","created_at":"2026-08-27 21:12:43"}