PolarDB-X 存储引擎核心技术 | 索引前缀压缩 - Prefix Compression

简介: PolarDB-X推出SPC二级索引压缩技术,针对InnoDB页内重复前缀进行轻量级字典压缩,仅作用于二级索引、页内局部编码、分裂时触发。实测二级索引空间降低30%–70%,IO Bound场景下QPS提升近47%,CPU开销可控。(239字)

背景

PolarDB-X 分布式数据库,采用集中式和分布式一体化的架构,为了能够灵活应对混合负载业务,作为数据存储的 Data Node 节点采用了多种数据结构,其中使用行存的结构来提供在线事务处理能力,作为 100% 兼容 MySQL 生态的数据库,DN 在 InnoDB 的存储结构基础上,进行了深度优化,大幅提高了数据访问的效率。

在真实的 OLTP 业务中,单张业务大表挂十几条二级索引是相当常见的场景,SaaS 多租户、电商商品/订单、交易流水、链路 trace、归档库等场景尤为典型。二级索引的总占用达到单实例总存储的 50% 以上的线上实例比比皆是,有些业务表二级索引占用甚至显著超过主表本身。

二级索引空间的膨胀,不仅直接推高存储成本,还会进一步影响 Buffer Pool 热数据覆盖率、放大 IOPS 压力、加剧 B+tree 分裂与 SX 锁争抢。在云原生数据库追求极致性价比的当下,如何针对二级索引的结构特征做专门的压缩,是 PolarDB-X 存储引擎在存储效率方向上重点要回答的问题。

针对这一问题,PolarDB-X DN 存储引擎设计了 Secondary Prefix Compression(SPC) 方案:一种只作用于二级索引页内局部字典页分裂触发编码的轻量级前缀压缩能力。在典型业务上,SPC 可让二级索引占用下降 30%~70%,读写路径有效控制额外 CPU 开销,在 IO Bound 场景下还能带来显著的性能提升。

InnoDB 二级索引存储结构

二级索引的 B+tree 布局

在 InnoDB 存储引擎中,一个用户定义的表实际上由多个 B+tree 索引来实现,主键对应的聚簇索引 (Clustered Index) 的叶子节点上保存着全量的数据行信息,普通列上的二级索引 (Secondary Index) 的叶子节点上仅存储相应的二级索引键和主键。二级索引的物理记录排列如下图所示:

InnoDB 当前的二级索引记录组织方式,本质上是将每条记录按 (SK, PK) 形式顺序存放在 B+tree 叶子页中。这样的设计简洁直接,也保证了良好的查找与范围扫描能力;但从页内存储视角看,它并没有进一步利用二级索引数据本身的一些典型分布特征。

这些特征主要体现在三点:

  1. 相同 SK 的记录连续出现,但不会共享存储。
    对于 non-unique index,多条 SK 相同的记录会连续排列,但每条记录仍然各自保存一份完整的 SK 内容,只有后缀 PK 不同。
  2. 业务键通常具有较长公共前缀。
    在线业务中的订单号、SKU、URL、文件路径、trace id、邮箱域名、业务编码等字段,往往前缀高度相似,真正的区分信息集中在后半段。
  3. 有序排列使相邻记录天然高度相似。
    二级索引按 SK 有序存放,同一叶子页中相邻记录通常共享较长的公共前缀;但在 InnoDB 当前的记录格式下,这些重复前缀仍然会在每条记录中被分别保存。

从功能正确性上看,这种组织方式没有问题;但从空间利用率角度看,它意味着二级索引页内存在大量可被进一步利用的重复信息。这也正是 PolarDB-X DN 设计 SPC 的出发点:不改变二级索引的逻辑结构,而是在页内局部利用这些重复前缀,提升存储效率。

MySQL 原生压缩方案的局限

MySQL InnoDB 提供了两种原生压缩方案,均采用通用压缩算法对块级别进行压缩,存在压缩计算成本高昂等问题,目前在实际的生产应用中往往难以同时兼顾压缩收益与访问效率,主要对空间资源比较敏感的业务场景中推广使用。

压缩对比项

表压缩(Table Compress)

页压缩(Page Compress)

使用语法

CREATE TABLE t1 (c1 INT PRIMARY KEY)ROW_FORMAT=COMPRESSED

KEY_BLOCK_SIZE=8;

CREATE TABLE t1 (c1 INT) COMPRESSION="zlib";

压缩原理

Page在内存中保留压缩和非压缩两个版本,行级修改需要持续维护两个page,并在压缩页进行压缩,磁盘文件只写入压缩页。

Page写入磁盘时进行压缩,少于OS 文件系统unit size的可以通过 File Hole Punch方式释放空间,达到空间压缩效果。

使用依赖

逻辑页压缩,不依赖OS。

物理页压缩,强依赖文件系统打洞功能。

使用成本

内存:Buffer Pool 保留了压缩和未压缩两份数据page,利用率减半。

CPU:数据修改需同时对压缩和非压缩页进行修改,5%-20%的消耗。

内存:Buffer Pool只保留一份数据页。

CPU:在进行系统IO过程中进行压缩和解压计算,5%-20%的消耗。

归根结底,InnoDB 当前二级索引的空间代价,主要来自页内大量重复保存的 SK 信息:一方面,相邻记录之间往往共享很长的公共前缀;另一方面,相同 SK 的多条记录会连续出现,但每条记录仍各自保存一份完整 SK。这样的存储方式会直接降低叶子页的空间利用率,进而稀释 Buffer Pool 对热数据的覆盖能力,带来更多的索引页访问、更多的 B+tree 分裂以及更高的并发争用。从这个意义上说,二级索引压缩的关键,不在于再引入一种更重的通用压缩,而在于如何在不破坏现有索引结构与访问路径的前提下,更有效地利用这些天然存在的重复前缀与相似性。

SPC 索引结构

针对 InnoDB 二级索引的存储代价问题,PolarDB-X DN 存储引擎设计了 SPC 索引结构,通过页内前缀字典显著降低二级索引的空间占用,并提升 Buffer Pool 与 IO 的有效利用率。

SPC 整体架构

SPC 的核心思想是:

在每个二级索引页内部,专门开辟一小块元数据区(Page Dict)保存本页的若干条公共前缀;页内的每条记录只保留"用哪条前缀 + 前缀多长 + 自己的尾巴"。

SPC 页的整体布局如下图所示:

Page Dict:页级共享的前缀字典

前缀压缩的一个关键选型是:前缀信息存在哪里、允许在什么粒度上变动?

  • 按记录维护差分前缀:每条记录都以前一条记录为 base 存储差分。理论压缩率最高,但记录之间形成强依赖,单条记录的解析需要先还原前一条;一旦中间记录发生插入、删除或更新,后续记录都可能被连带影响,乐观写容易退化为悲观重写,工程复杂度极高。
  • 跨页共享字典:在页外维护一份更大范围可复用的前缀字典,例如按索引、按分区,甚至按一段 page range 共享。这样可以让多个页复用同一批高频前缀,在某些前缀高度稳定的场景下有机会获得更高的总体压缩率。但它的问题也很明显:字典本身变成了新的全局元数据,带来版本管理、并发控制、崩溃恢复、页分裂迁移、冷热前缀演化等一系列复杂性;同时单页读写不再完全自洽,访问路径和回收机制都会变重。
  • 页级共享字典:每页维护一份局部字典,页内所有记录共享。单条 DML 通常只改记录本身,不触发字典变动;只有在页重组、页分裂或整页重编码时,才重新计算字典。记录可以自然拆成“字典引用 + 自身尾部”,页内访问路径简单,局部性好,也更容易与现有 B+tree 页面生命周期对齐。

综合来看,三种方案分别对应三种不同的 trade-off:按记录差分把压缩率推到最高,但写放大和实现复杂度也最高;全局共享字典扩大了复用范围,但引入了额外的全局一致性与元数据管理成本;页级共享字典虽然在理论压缩率上不是最激进的方案,但它把压缩作用域严格限制在单页内部,在压缩收益、读写代价和工程可控性之间取得了更均衡的折中。SPC 选择了 页级共享字典它主要由三部分组成:

  1. 字典项数量。
  2. 每个前缀在字典区中的偏移目录。
  3. 真正的前缀字节串数据。

这样,多个索引记录就可以共享同一段页内前缀内容,而不需要各自重复保存。

从结构上看,SPC 把“记录里重复出现的前缀”提升成了一个页级的一等公民对象。

SPC 记录格式

SPC 记录在普通 COMPACT 记录头之外,额外增加了一段 SPC Header。最核心的两个字段是:

  1. Pack ID:表示该记录引用页内哪一个前缀。
  2. Pack Len:表示该记录实际复用了这个前缀的多少字节。

这样,一条 SPC 记录在物理布局上就从“完整索引键”变成了“页内共享前缀 + 本记录剩余内容”。记录的逻辑值没有发生变化,索引排序语义也没有发生变化,但页内重复前缀只需要保存一次。

SPC 数据编码

Split-triggered encoding:只在页面即将分裂时触发

编码时机是 SPC 在工程上需要审慎考虑的一件事。每条 DML 都做一次整页编码,CPU 代价是硬成本,会直接拖累写路径;完全不做又失去压缩意义。

SPC 采用 Split-triggered 编码 策略:新写入的记录先以非压缩形态写入页;直到页内空间即将耗尽、即将分裂时,才整页做一次 SPC 编码。

具体的触发点设置在 InnoDB 的乐观插入路径上:

  1. 页内剩余空间不足以容纳新记录;
  2. reorganize(清理删除标记、整理碎片)之后依然腾不出空间;
  3. 此时原本的逻辑会放锁进入 SMO 流程(B+Tree 结构变更);SPC 插在这里,先尝试一次整页 SPC 编码
  4. 编码后能塞下新记录,走"乐观 + 压缩"路径,避免 SMO;
  5. 编码后依然塞不下,回退到正常的 page split 流程。

这样做的收益是系统性的:

  • 冷页、稀疏页永远不会被编码,写路径无任何额外 CPU 开销;
  • 压缩代价被均摊到一页内的 N 次 DML 上;
  • 能避免的 SMO 就避免掉——压缩省下来的空间,很多时候刚好让本来要分裂的插入就地完成,压缩本身就在帮 SMO 减压。

页内编码算法

SPC 编码的目标是在字典条数上限(128)的约束下,为页内有序记录选取一组不相交的连续区间,每个区间共享一条公共前缀,使总字节净收益最大。这是相邻记录公共前缀长度序列上的最优分段问题,精确最优解需要 O(n²) 的动态规划,不适合放在页分裂的热路径上。

SPC 用线性时间的三阶段贪心做近似。第一阶段顺序扫描该序列,按升降转折切分为若干极大单调区间作为候选,每个候选的字典内容取自公共前缀最长处的记录。第二阶段从左至右逐对比较相邻候选独立保留与合并的字节收益,择优执行;合并后共享前缀退化为衔接处的最小值。第三阶段将保留的候选落入页级字典,为每条记录回填字典引用与压缩长度;前缀过短或未覆盖的记录保留原始形态,与压缩记录共存于同一页。

二级索引页内记录按键值有序,相同前缀天然连续成簇,这一贪心策略在实际负载下与精确最优的收益差距很小。

SPC 数据解码

前缀压缩的工程难点不在算法本身,而在解压要覆盖读路径每一个角落。InnoDB 里访问二级索引记录的场景非常多,每一条路径上都涉及rec_get_offsets、基于 offsets的字段访问、rec_tdtuple_t 的比较,都必须能正确处理 SPC 记录。

Slice:统一的逻辑视图

原生 InnoDB 的上层代码直接基于 rec 指针 + offsets 访问记录。若让每一处上层代码都去区分"这是连续的 COMPACT 记录,还是分裂的 SPC 记录",维护成本极高,也容易引入隐蔽 bug。

为此,SPC 在 InnoDB 的 rem 层引入了一层新的抽象——Slice。所有上层读取路径统一基于 Slice 接口访问记录:

  • 对连续的 COMPACT 记录,Slice 退化为直接访问 rec 指针,与社区原本方式保持一致;
  • 对 SPC 记录,Slice 在需要时按需把"字典前缀 + 记录尾巴"拼成一段连续内存再返回;
  • 对 DDL 外部排序的 merge record,Slice 也提供了对应的适配形态。

Slice 抽象的关键在于:它不改变上层代码的语义,只改变实现。上层代码看到的仍然是"逻辑上完整"的字段,物理差异被完全封装在 Slice 内部。

在这层抽象之上,SPC 对 MVCC / Undo / Purge / 锁 / Change Buffer / 复制 / 备份恢复 都是 语义等价 的——外部看起来,SPC 只是一个纯粹的页内物理存储优化,逻辑行为和普通二级索引完全一致。

在性能敏感的读路径上,Slice 还支持"不完全解压"的访问:

  • 比较一条 SPC 记录和一个 dtuple 时,可以直接基于字典 + 尾巴 + 扩展 offsets 在压缩形态下做逐列比较,不需要先构造出一条完整的非压缩 rec;
  • 这种"压缩态直接比较"在热路径上省掉一次 memcpy 和一次临时分配,是 SPC 在 CPU Bound 场景下性能的重要保障。

业界对比

MySQL 原生压缩

如前文所述,表压缩 和 透明页压缩 都是通用块压缩,不利用 B+tree 叶子页"相邻即相似"的结构特征;压缩/解压链路使用 zlib/lz4,CPU 代价大;并且表压缩在 Buffer Pool 保留压缩+解压双份副本,对内存利用率不利。

PostgreSQL B-tree Deduplication

PostgreSQL 13+ 在 B-tree 索引上支持 "deduplication":对相同键值的多条 TID 做聚合存储。其本质是针对重复键值的聚合,而不是针对前缀的字典压缩,对长前缀、前缀不完全相同的场景(如 URL、trace id)收益有限。

Oracle Key Compression

Oracle 的基础索引压缩是 prefix/key compression(COMPRESS n):它将索引键拆成 prefix 与 suffix,并在索引块内共享前导列前缀,因此本质上是按列数指定前缀长度的列级前缀共享。相比之下 SPC 的字典是字节级的,能更精细地利用真实数据分布。

在此基础上,Oracle 又提供了 Advanced Index Compression(COMPRESS ADVANCED LOW/HIGH)。与固定前缀方式不同,Advanced Index Compression 采用按块、结合数据分布的自适应压缩:数据库会为每个块选择更合适的压缩方式,公开文档提到其内部会使用 intra-column level prefixes、duplicate key elimination 与 rowid compression 等技术。

SQL Server Page Compression

SQL Server 的 Page Compression 在现有主流数据库中,与 SPC 在“页内利用重复模式降低存储开销”这一方向上最为接近。其实现并非单一算法,而是由 Row CompressionPrefix CompressionDictionary Compression 三层机制顺序组成:先对行格式做紧缩,再在页内提取列前缀,最后对整页范围内的重复值建立字典。其进行压缩的时机也与 SPC 相似:当页写满并需要容纳新行时,数据库会对整页重新评估,决定是否应用 Prefix 和 Dictionary Compression。

测试数据

性能表现

为了验证 SPC 在真实 OLTP 负载下的实际效果,我们基于 sysbench 设计了一组针对二级索引前缀重复特征的测试负载,在同一套硬件和数据集上,分别对比开启与关闭 SPC 时的空间占用与吞吐表现。

测试环境与数据集

压测使用128 并发线程,表结构模拟典型前缀重复特征:每张表包含 3 个 INT 类型的枚举列(模拟 status、category 等低基数字段,每列 5 种取值)和 6 个 VARCHAR(64) 的业务键列(模拟订单号、编码等具有公共前缀特征的字段,前缀长度 40 字节,1000 种前缀)。在此基础上建立 12 个二级索引:3 个枚举列单列索引、6 个业务键单列索引、3 个(枚举列, 业务键)联合索引。共 16 张表,每表 100 万行。关闭 SPC 的单表大小为 1.8GB,二级索引占 1.16G,开启 SPC 的单表大小为 1.1GB,二级索引占 0.52B,二级索引压缩率约 55%,整体压缩率约 38%。

测试负载包括三种:

  • read_only:二级索引点查 + 联合索引点查 + 联合索引范围扫描,模拟典型的在线查询路径。
  • read_write:在 read_only 基础上叠加二级索引列更新与 delete/insert 操作,模拟混合事务负载。
  • write_only:仅包含二级索引列更新与 delete/insert,每次写操作涉及全部 12 个二级索引的维护,模拟写密集场景。

IO Bound 场景

通过调整 Buffer Pool 大小,使索引工作集无法全部驻留内存,制造磁盘随机读成为瓶颈的 IO Bound 条件。

Buffer Pool = 4 GB

负载

关闭 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%

Buffer Pool = 8 GB

负载

关闭 SPC (QPS)

开启 SPC (QPS)

提升

read_only

105,069

154,215

+46.8%

read_write

47,904

70,655

+47.5%

write_only

27,369

27,818

+1.6%

在 IO Bound 条件下,SPC 的性能收益主要来自两条路径:其一,压缩后每个 16 KB 页容纳更多记录,Buffer Pool 的有效覆盖率显著提升,缓存命中率的差距直接转化为更少的磁盘随机读;其二,压缩减少了 B+tree 的叶子页总数,降低了页分裂频率,间接缓解了 SMO 带来的 SX 锁争抢。8 GB Buffer Pool 是本数据集下的最佳对比区间:关闭 SPC 时 BP 仅覆盖约三成数据,大量索引访问需要读盘;开启 SPC 后覆盖率提升至近半,缓存命中率的差距被最大化放大,读和读写负载均获得近 47% 的吞吐提升。

write_only 负载在两种 BP 配置下均几乎无差异(+1.6%~1.8%),这是符合预期的:该负载的读路径通过主键定位行,不经过二级索引的 B+tree 查找,因此 SPC 对二级索引页的压缩不影响读侧缓存命中率;写路径虽然涉及全部 12 个二级索引的维护,但 Split-triggered 编码策略确保了常规 DML 不触发额外的压缩开销,写吞吐基本持平。

CPU Bound 场景

将 Buffer Pool 调至 32 GB,使全部数据驻留内存,消除磁盘 IO 因素,观察 SPC 在纯 CPU 路径上的表现。

负载

关闭 SPC (QPS)

开启 SPC (QPS)

差异

read_only

426,476

416,194

-2.4%

read_write

294,917

291,175

-1.3%

write_only

138,673

149,890

+8.1%

数据全部驻留内存后,SPC 的 IO 收益消失,三种负载的吞吐差异均在测试波动范围内, Split-triggered 编码策略和压缩态直接比较优化有效控制了解压开销。

相关文章
|
8天前
|
人工智能 开发工具 iOS开发
Claude Code 新手完全上手指南:安装、国产模型配置与常用命令全解
Claude Code 是一款运行在终端环境中的 AI 编程助手,能够直接在命令行中完成代码生成、项目分析、文件修改、命令执行、Git 管理等开发全流程工作。它最大的特点是**任务驱动、终端原生、轻量高效、多模型兼容**,无需图形界面、不依赖 IDE 插件,能够深度融入开发者日常工作流。
2967 7
|
10天前
|
Shell API 开发工具
Claude Code 快速上手指南(新手友好版)
AI编程工具卷疯啦!Claude Code凭借任务驱动+终端原生的特性,成了开发者的效率搭子。本文从安装、登录、切换国产模型到常用命令,手把手带新手快速上手,全程避坑,30分钟独立用起来。
3068 20
|
23天前
|
人工智能 JSON 供应链
畅用7个月无影 JVS Claw |手把手教你把JVS改造成「科研与产业地理情报可视化大师」
LucianaiB分享零成本畅用JVS Claw教程(学生认证享7个月使用权),并开源GeoMind项目——将JVS改造为科研与产业地理情报可视化AI助手,支持飞书文档解析、地理编码与腾讯地图可视化,助力产业关系图谱构建。
23567 15
畅用7个月无影 JVS Claw |手把手教你把JVS改造成「科研与产业地理情报可视化大师」
|
4天前
|
人工智能 Linux BI
国内用 Claude Code 终于不用翻墙了:一行命令搞定,自动接 DeepSeek
JeecgBoot AI专题研究 一键脚本:Claude Code + JeecgBoot Skills + DeepSeek 全平台接入 一行命令装好 Claude Code + JeecgBoot Skills + DeepSeek 接入,无需翻墙使用 Claude Code,支持 Wind
1953 3
国内用 Claude Code 终于不用翻墙了:一行命令搞定,自动接 DeepSeek
|
10天前
|
人工智能 JSON BI
DeepSeek V4-Pro 接入 Claude Code 完全实战:体验、测试与关键避坑指南
Claude Code 作为当前主流的 AI 编程辅助工具,凭借强大的代码理解、工程执行与自动化能力深受开发者喜爱,但原生模型的使用成本相对较高。为了在保持能力的同时进一步降低开销,不少开发者开始寻找兼容度高、价格更友好的替代模型。DeepSeek V4 系列的发布带来了新的选择,该系列包含 V4-Pro 与 V4-Flash 两款模型,并提供了与 Anthropic 完全兼容的 API 接口,理论上只需简单修改配置,即可让 Claude Code 无缝切换为 DeepSeek 引擎。
2460 3
|
8天前
|
人工智能 安全 开发工具
Claude Code 官方工作原理与使用指南
Claude Code 不是传统代码补全工具,而是 Anthropic 推出的终端 AI 代理,具备代理循环、双驱动架构(模型+工具)、全局项目感知、6 种权限模式等核心能力,本文基于官方文档系统解析其工作原理与高效使用技巧。
1339 0
|
8天前
|
存储 Linux iOS开发
【2026最新】MarkText中文版Markdown编辑器使用图解(附安装包)
MarkText是一款免费开源、跨平台的Markdown编辑器,主打所见即所得实时预览,支持Windows/macOS/Linux。内置数学公式、流程图、代码高亮、多主题及PDF/HTML导出,是Typora的轻量免费替代首选。(239字)

热门文章

最新文章