数据库
PolarDB-X 二级前缀压缩如何将索引膨胀降低高达70%
二级索引常常占据数据库存储的主导地位,尤其是在 SaaS 和电商领域。PolarDB-X 引入了二级前缀压缩(SPC),一种轻量级的每页字典方案,可将索引空间减少30%至70%,且无需繁重的 CPU 开销。基准测试显示,在 I/O 受限的只读工作负载中,吞吐量最高可提升47%。

对于数据库运维人员而言,二级索引是一把双刃剑。它们能加速查询,但很容易消耗比主表更多的存储空间。在 SaaS 多租户系统、电商平台和分布式追踪存储中,索引通常占据实例总存储的一半以上。这种膨胀推高了成本,降低了缓冲池效率,放大了 IOPS 压力,并加剧了 B+ 树的争用。
阿里云 PolarDB-X 团队发布了一份详细的技术报告,介绍一种名为二级前缀压缩 (SPC) 的新型压缩方案,该方案已在存储引擎的数据节点 (DN) 中实现。SPC 并非像 InnoDB 的内置表压缩和透明页压缩那样在块级别应用通用压缩,而是利用了二级索引 B+ 树叶子页面的结构冗余。其结果是一种轻量级的、页面本地的前缀字典,可将索引空间减少30%至70%,同时将 CPU 开销控制在合理范围内。
索引膨胀的剖析
在 InnoDB 中,每个二级索引存储一个由二级键 (SK) 和主键 (PK) 组成的元组。由于索引是按 SK 排序的,叶子页面内的相邻记录往往共享较长的公共前缀。诸如订单 ID、SKU、URL、文件路径和追踪 ID 等字段在生产工作负载中表现出高度的前缀相似性。但在 InnoDB 标准的 COMPACT 记录格式下,每条记录都独立存储其完整的 SK,即使是连续且 SK 相同的记录(仅后继 PK 不同)也是如此。
这在功能上是正确的,但在空间上是浪费的。这种冗余降低了叶子页面的空间利用率,从而稀释了缓冲池的热数据覆盖率,增加了页面访问频率,并提高了 B+ 树分裂和 SX 锁争用的速率。诸如 zlib 或 lz4 之类的通用块压缩器可以解决这个表象问题,但它们在每次读取时的解压缩成本,以及表压缩情况下的内存双页副本,使其不适合要求低延迟访问的 OLTP 工作负载。
SPC: 每页前缀字典
SPC 采用了不同的路线。该方案的核心是一个每页公共前缀字典,作为一个小的元数据区域维护在每个 16 KB 的索引页面内。每条记录仅存储一个字典引用、一个指示其共享该前缀多少字节的长度标识,以及其自身的剩余后缀。记录的逻辑值和排序顺序保持不变,但重复的前缀在每个页面中只存储一次。
下表将 SPC 与现有的 MySQL InnoDB 压缩选项在关键维度上进行了比较:
| 特性 | 表压缩 | 透明页压缩 | SPC |
|---|---|---|---|
| 范围 | 块级 (zlib) | 块级 (zlib/lz4) | 每页前缀字典 |
| CPU 开销 | 5%, 20% (内存中双副本) | 5%, 20% (I/O 时压缩) | 约2%, 8% (分裂触发,直接比较) |
| 内存开销 | 压缩 + 未压缩副本 | 单副本 | 单副本 |
| 缓冲池影响 | 有效容量减半 | 无额外影响 | 改善 (每页更多记录) |
| 写放大 | 每次页面写入时重新压缩 | 刷新时压缩 | 仅在页面分裂时编码 |
| 针对二级索引优化 | 否 | 否 | 是 |
选择每页共享字典,而不是每条记录的增量前缀或跨页共享字典,是一个刻意的工程权衡。PolarDB-X 团队评估了所有三种方法,并得出结论:每页设计实现了最佳平衡:压缩范围严格限定在单个页面,避免了全局元数据管理和级联写放大,同时仍然捕获了主要的冗余来源, , 相邻记录的前缀相似性。
分裂触发编码和 Slice 抽象
SPC 最务实的设计决策之一是何时编码。该团队采用了分裂触发策略:记录以其未压缩的形式插入。只有当页面即将空间不足并触发 B+ 树分裂时,SPC 才对整个页面进行编码。这意味着冷页面或稀疏页面永远不会产生编码开销,并且该成本会在填充页面的成百上千次 DML 操作中摊销。此外,压缩释放出的空间通常允许原本会导致分裂的插入操作就地完成,压缩本身起到了缓解分裂的作用。
编码算法本身是一个线性时间内运行的三阶段贪心近似算法。它扫描页面上的已排序记录,根据公共前缀长度将它们划分为候选前缀区间,然后应用合并启发式算法来平衡字典大小与净字节节省量。字典上限为128个条目,而前缀太短或未被覆盖的记录则保留其原始形式,压缩和未压缩的记录共存于同一页面。
在读取路径上,SPC 在 InnoDB 的记录管理层引入了一个Slice 抽象。对于标准的 COMPACT 记录,Slice 退化为一与上游行为相同的直接指针访问。对于 SPC 记录,它会按需将字典前缀和记录后缀懒散地组装成一个连续的内存区域。关键的是,该抽象还支持部分解压缩:在将 SPC 记录与查询元组进行比较时,可以直接在压缩形式下逐列进行比较,使用字典条目和扩展偏移量,而无需首先构建一个完整的未压缩记录。这消除了热读取路径上的一次内存拷贝和一次临时分配。
Slice 层确保 SPC 对 InnoDB 的其余部分在语义上是透明的,所有上游代码路径(MVCC、undo、purge、锁定、变更缓冲区、复制、备份/恢复)继续保持不变。从外部来看,SPC 纯粹是一种物理层面的、页内的存储优化。
基准测试结果:I/O 压力下吞吐量提升高达47%
PolarDB-X 团队使用一个旨在模拟典型前缀重复特征的表结构运行了一系列 sysbench 基准测试:3个低基数的 INT 列,6个具有40字节公共前缀的 VARCHAR(64) 列,以及12个二级索引。关闭 SPC 时,一个100万行的表占用1.8 GB,其中二级索引消耗1.16 GB。开启 SPC 后,该表缩小至1.1 GB,二级索引为0.52 GB,二级索引压缩率为55%,整体压缩率为38%。
最引人注目的结果出现在 I/O 受限条件下,缓冲池设置为4 GB(未使用 SPC 时覆盖约30%的数据集):
| 工作负载 | SPC 关闭 (QPS) | SPC 开启 (QPS) | 提升 |
|---|---|---|---|
| read_only | 82,401 | 108,384 | +31.5% |
| read_write | 33,292 | 34,060 | +2.3% |
| write_only | 20,259 | 20,615 | +1.8% |
当缓冲池设置为8 GB时,覆盖率翻倍至接近50%,在受读 I/O 限制的工作负载中,提升幅度跃升至只读+46.8% 和读写+47.5%。只写工作负载几乎持平(+1.6%),因为它的读取路径通过主键定位行,从不遍历二级索引页面,而分裂触发编码确保了常规 DML 不会产生编码开销。
当缓冲池设置为32 GB(足够容纳整个数据集在内存中)时,I/O 优势消失。所有三个工作负载的差异均在±2.4%以内,这证实了 SPC 在纯 CPU 路径上的解压缩开销极小,且其写入影响可忽略不计。
与其他数据库的比较
在主流数据库系统中,Microsoft SQL Server 的页面压缩在理念上与 SPC 最为接近:它应用了行压缩、前缀压缩和字典压缩的三层管道,并在页面填满时触发。PostgreSQL 的 B 树去重合并了重复的键,但不处理部分前缀共享。Oracle 的高级索引压缩使用自适应的每块压缩,而其基本前缀压缩 (COMPRESS n) 则在列级别操作。SPC 的字节级精度使其比 Oracle 的列级方法粒度更细,并且其局限于冗余度最高的二级索引,使其成为一种有针对性而非一刀切的压缩器。
结论
SPC 并非对现有数据库压缩技术的根本性背离,但它是一个执行良好、实用的优化方案,解决了一个特定的痛点:OLTP 工作负载中的二级索引膨胀。通过将压缩限制在页面分裂时触发的每页前缀字典,PolarDB-X 团队成功地将索引存储减少了30%到70%,同时将 CPU 和复杂度成本控制在足够低的水平,以便投入生产部署。对于已经在阿里云 PolarDB-X 上运行 MySQL 兼容工作负载的云运维人员来说,SPC 提供了一种有意义的手段来削减存储费用并改善 I/O 受限的查询性能,而无需更改应用程序。
对于更广泛的数据库工程社区来说,其启示在于,已排序 B+ 树页面中的结构冗余是一个值得挖掘的丰富矿藏,且一个轻量级、范围精准的方法可以在 OLTP 的甜蜜点上超越重量级的通用压缩器。完整的技术细节可在PolarDB-X 团队的博客文章中找到。
每天早晨用 3 分钟掌握科技要闻
每个工作日一封邮件,只讲真正重要的 AI 与科技动态。