PAX:你一直在寻找的缓存性能
摘要: PAX(分区属性扩展)是一种存储布局,将数据库页面内的数据组织成面向列的迷你页,将典型 NSM(n 元存储模型)中 94% 的缓存污染降至几乎可忽略的水平。通过在每个 8KB 页面内将列值分组存放,同时保持同一页面上的所有列以便于元组重建,PAX 实现了纯列式存储 80% 的优势,仅需 5% 的实现复杂度,非常适合宽表的混合 OLTP/OLAP 工作负载和选择性查询。
感谢 Boris Novikov,是他最初引导我走向 PAX 方向,并进行了多次富有见地的技术讨论。我感激他所花费的时间以及我们进行过和将继续进行的精彩讨论。
多年来,我对数据库存储布局有一种执念。不是那种光鲜的执念,而是那种凌晨 3 点醒来想着"为什么我们要加载 60 个无用字节到缓存中,却只读取 4 个字节?"
这正是 PostgreSQL 每天在全球生产服务器上数十亿次做的事情。我们就……接受它吗?不,我不接受它。让我给你讲讲 PAX。
让我抓狂的图书馆
想象一下:你走进一家图书馆。每本书就是一条数据库记录。PostgreSQL 的传统存储(NSM - N 元存储模型)将完整的书存放在每个书架上:第一章、第二章、第三章,全部装订在一起。
这就是让我夜不能寐的问题:当你只需要 1000 本书中的第三章时,你必须把每本完整的书从书架上取下来,翻过第一章和第二章(你永远不会读),取出第三章,然后移到下一本书。
你在浪费时间。你在浪费精力。你在浪费缓存带宽。
现在,列式存储(比如 Parquet)说"去他的,让我们把书拆了重新排列!"把所有第一章放在书架 A,所有第二章放在书架 B,所有第三章放在书架 C。这对快速读取第三章很棒!
但问题来了:当你需要同一本书的第一章和第三章时,你必须访问多个书架,收集碎片,然后重新装订书。用数据库术语来说,这是一个昂贵的连接操作。对于 OLTP 工作负载?别想了。太慢了。
**PAX 说:**把书放在同一个书架上(便于重新组装),但在该书架内将章节组织在一起(实现快速顺序访问)。
为什么 NSM 让我想尖叫
让我给你看看我们每天都在经历的灾难。
PostgreSQL 页面内部是什么
PostgreSQL 以 8KB 页面存储数据。一个典型的 NSM 页面如下所示:
**注意:**这是一个简化视图。实际上,元组之间可能存在间隙(由于更新、删除或对齐),但关键点仍然是:每个元组包含所有连续存储的列。这是我们的缓存问题。
元组从底部(pd_special)增长,行指针从顶部(在标题之后)增长,中间是空闲空间。很简单,对吧?
现在想象这个简单的查询:
SELECT age FROM users WHERE age > 30;
缓存灾难
现代 CPU 不会获取单个字节。它们加载缓存行(通常一次 64 字节)。就像去杂货店一样:即使你只需要牛奶,你也要提着一个能装 64 件商品的购物篮。
当 PostgreSQL 读取一个元组来检查 age 列(4 字节)时,以下是实际加载到缓存中的内容:
**注意:**我要为这张图的粗糙之处道歉。我没有时间按比例绘制。它只显示列数据。实际上,每个元组包含一个 23 字节的头部,浪费更严重(总共 83 字节,跨越 2 个缓存行)。
我们想要 age。我们得到了一切。
那是 94% 的缓存污染!94% 👏 的 👏 污染 👏(是的,我喜欢拍手强调词语,这里感觉特别恰当,我忍不住。)
在 100 万行的表扫描上:
- NSM 触发 100 万次缓存未命中
- 传输的有用数据:4MB(仅 age 列)
- 浪费的带宽:56MB(其他无人询问的列)
我见过生产环境中的 DBA 玩俄罗斯方块——重新排列数据类型以最小化填充开销,寻找每个元组浪费的 2-4 字节。
与此同时,PostgreSQL 正在将每行 56 个无用字节加载到缓存中。
就像在泰坦尼克号上重新排列甲板上的躺椅。
Anastasia Ailamaki 在 2001 年在一台 Pentium II Xeon 上测量过这个(是的,我足够老还记得那些)。她发现 75% 的缓存未命中完全可以通过 PAX 避免。
你知道更糟的是什么吗?现代系统有 128 字节的缓存行,这让问题更大!我们正在倒退!
PAX 登场:我们一直忽略的解决方案
PAX 的不同之处在于。它不是存储完整的元组,而是将每个 8KB 页面的内部重新组织成面向列的"迷你页":
每个迷你页将一列的所有值分组在一起。PAX 头部包含指向每个迷你页位置的偏移量。当需要
age 列时,PostgreSQL 只读取"age"迷你页,而不是整行。
但有趣的地方来了。还记得 NSM 中 94% 的缓存浪费吗?看看用 PAX 查询相同数据时会发生什么:
一个缓存行现在加载 16 个 age 值,而不是 1 个。这不是 16% 的改进——而是缓存利用率提高了 16 倍!
使用 NSM,扫描 100 行查找"age > 30"会触发 100 次缓存未命中。使用 PAX?只有 7 次缓存未命中(100 ÷ 16 ≈ 7)。相同的数据,相同的查询,缓存未命中减少 93%。
这就是页面级别列式存储的魔力。PAX 将所有列保持在同一页面上(便于元组重建),但为顺序访问组织它们(缓存友好)。你获得了纯列式存储 80% 的优势,而无需昂贵的跨文件连接。
PAX 和 Parquet:没人谈论的家族树
当我弄清楚这件事时,我的mind被震撼了:Parquet 基于 PAX。
Parquet 没有发明页面级别的列式存储。Anastasia Ailamaki 在 2001 年就发明了。Twitter 和 Cloudera 只是将其应用于 2013 年的整个文件。
时间线:
- 2001:Ailamaki 发明 PAX(事务数据库中的 8KB 页面)
- 2013:Twitter/Cloudera 创建 Parquet(PAX 扩展到 128MB+ 文件)
那有什么区别?
| 特性 | PAX(原始) | Parquet | PostgreSQL 需要什么 |
|---|---|---|---|
| 粒度 | 页面(8KB) | 行组(128MB+) | 页面(8KB) |
| 能 UPDATE 吗? | 是(MVCC) | 否(文件不可变) | 是(MVCC) |
| 所在位置 | OLTP+OLAP DBMS | 数据湖/批处理 | OLTP+OLAP DBMS |
| 花哨的编码 | 不太 | RLE、字典、Delta、全部 | 未来工作 |
| 压缩 | 否(论文中没有) | Snappy、Gzip、Zstd | 未来工作 |
这是每个人都忽略的关键见解:Parquet 证明了 PAX 在大规模下有效,但牺牲了可变性来达到这一点。你不能 UPDATE Parquet 文件——它是仅追加或整个文件替换。
PostgreSQL 需要带有完整 ACID 保证的 PAX。这意味着坚持 8KB 页面,保留 MVCC,并使其与 WAL、vacuum 和缓冲区管理器一起工作。
PAX 与其他一切
让我明确说明 PAX 的位置:
| 特性 | NSM | PAX | 纯列式 |
|---|---|---|---|
| 缓存局部性 | 极差 | 优秀 | 优秀 |
| 记录重建 | 免费 | 便宜(同页) | 昂贵(多文件连接) |
| 适合 OLTP? | 是 | 相当好 | 不适合 |
| 适合 OLAP? | 不太好 | 相当好 | 非常适合 |
| 存储开销 | 基线 | 相同 | 可变 |
PAX 让你以 5% 的成本获得列式存储 80% 的优势。
这是我愿意接受的权衡。
什么时候应该(以及不应该)使用 PAX
我不会告诉你 PAX 是万能的。它不是。
最佳场景
宽表(12+ 列)带选择性查询:
-- 非常适合 PAX
SELECT event_time, revenue
FROM analytics_events -- 25 列的表
WHERE user_id = 12345;
-- 访问 25 列中的 2 列 → 92% 缓存节省
混合 OLTP/OLAP 工作负载:
- OLTP:单行更新保持快速(所有内容在同一页上)
- OLAP:范围扫描飞起(持续的缓存局部性)
频繁的顺序扫描,低投影:
-- NSM 的噩梦,PAX 的天堂
SELECT COUNT(*)
FROM logs
WHERE timestamp > NOW() - INTERVAL '1 hour';
PAX 表现不佳的地方
- 窄表(< 8 列): 重建开销开始接近 NSM 的简单性。不值得。
- SELECT * 查询: 无论如何您都必须读取所有微页面。PAX 只是增加开销。
- 通过索引的重度随机访问: 索引查找 → 读取完整 tuple。与 NSM 成本相同。 (尽管索引-only 扫描确实受益于更好的 VM 覆盖。)
您可以期待怎样的性能提升?
Ailamaki 在 2001 年在 Pentium II Xeon 上测量了:
- 顺序扫描时数据缓存未命中减少 75%
- 范围选择时执行时间减少 17-25%
- TPC-H 查询(混合连接/聚合)减少 11-48%
现代硬件(DDR5、128 字节缓存行、巨大的 L3 缓存)上,绝对数字不同,但原则仍然成立。实际上,它们更加成立:
- 更大的缓存行 = NSM 浪费更多带宽
- NVMe 终结了 I/O 主导地位 → CPU/缓存成为瓶颈
我保守的生产估算:
- 宽表上的 OLAP 查询:快 10-30%
- OLTP 查询:慢 0-5%(微页面开销)
- 混合工作负载:净收益 5-15%
但我想诚实地说:在有人构建 POC 并进行基准测试之前,这些都是有根据的猜测。
下一步是什么?
PAX 解决了一个真正的问题。缓存污染问题不会消失——随着每一代 CPU,它只会变得更糟。
但关键是:说"PAX 会很棒"很容易。在 PostgreSQL 中实际实现它?那就有趣了。
死 tuple 可能很难管理。WAL 日志记录如果不想每次都使用完整页面写入,会变得非常棘手。Vacuum 肯定会增加挑战。
这是理论工作——目前还没有 POC。但我想把这些想法抛出去。也许比我聪明的人会采纳它。
因为说实话?PostgreSQL 应该得到比 94% 缓存污染更好的东西。
参考文献:
Ailamaki, A., DeWitt, D. J., Hill, M. D., & Skounakis, M. (2001). Weaving Relations for Cache Performance. Proceedings of the 27th International Conference on Very Large Data Bases (VLDB) .