科技·via

一个 48 KB 的轻量数据库索引,体积只有传统 B 树的四千五百分之一

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

相关 / RELATED

JSON

导出 / EXPORT

订阅 · SUBSCRIBE

每周一封信号简报,重大进展可选即时推送。

不追踪邮件打开与邮件链接点击,一键退订。 或用 RSS · 详情