3. 列式存储的编码技术:字典编码、RLE、Delta Encoding 原理与对比
聊到列式存储,绕不开的一个话题就是编码。说白了,列存之所以能比行存省那么多空间、查那么快,编码技术功不可没。我刚开始接触列存时,觉得不就是压缩嘛,后来踩了几个坑才明白——编码不只是压缩,它直接决定了查询引擎能不能跳过大量数据。
这一节,我们重点拆解三种最基础的编码:字典编码、RLE(游程编码)、Delta Encoding(差值编码)。我会结合项目里的实际案例,把它们的原理、适用场景、优缺点讲透。
3.1 字典编码:用ID换空间
字典编码的思路很朴素——把重复出现的值映射成更短的整数ID。你想想看,如果一列里只有几十种不同的值,却有几百万行,那存原始字符串多浪费?
具体做法分两步:
- 构建字典:扫描整列,为每个唯一值分配一个递增ID。
- 替换数据:把原始值替换成对应的ID。
举个例子,假设有一列“城市”:
原始数据:北京, 上海, 北京, 广州, 上海, 北京
字典映射:北京→0, 上海→1, 广州→2
编码后数据:0, 1, 0, 2, 1, 0
我在一个用户画像系统里用过字典编码。当时有个“性别”列,只有“男”“女”“未知”三种值,原始字符串占3个字节,编码后每个值只用1个bit(3种值需要2bit)。整列压缩比直接干到1:12。不过要注意,字典本身也要存,如果唯一值太多,字典可能比数据还大——那就得不偿失了。
适用场景:基数低(唯一值数量少)的列,比如枚举类型、状态码、分类标签。
不适用场景:唯一值接近行数的列,比如UUID、时间戳(精度到毫秒)。
3.2 RLE:把重复变成计数
RLE(Run-Length Encoding)的原理更直接——遇到连续重复的值,只存一次值和重复次数。我习惯叫它“数数编码”。
还是用数据说话:
原始数据:A, A, A, B, B, C, C, C, C
RLE编码:(A,3), (B,2), (C,4)
你看,9个值变成了3个二元组。如果列里连续重复很多,压缩效果非常恐怖。我曾经处理过一个“订单状态”列,数据按时间排序后,大量连续行都是“已完成”,RLE跑完,压缩比超过1:20。
一个小技巧:RLE对数据顺序敏感。如果数据是随机分布的,连续重复很少,RLE反而可能膨胀(因为要额外存计数)。所以很多列存系统会在写入前先做排序,让相同值尽量聚在一起。
但RLE有个坑——更新很麻烦。你想,如果中间某行状态变了,后面的RLE段都得重算。所以RLE更适合写少读多的场景,比如日志归档、历史快照。
3.3 Delta Encoding:存差值,不存原值
Delta Encoding的思路是:不存原始值,只存相邻值之间的差值。特别适合有序的数值列,比如时间戳、自增ID、传感器读数。
看个例子:
原始时间戳(秒):100, 102, 105, 110, 120
Delta编码:100(基准值), +2, +3, +5, +10
如果差值很小,可以用更少的bit来存。比如原始时间戳是64位整数,差值可能只需要8位甚至更少。我在物联网平台里用过这个,设备上报的时间戳间隔很规律,差值基本都在0~255之间,压缩比轻松到1:6以上。
注意:Delta Encoding要求数据有序。如果乱序,差值可能很大甚至负数,压缩效果会打折扣。另外,如果某一行数据损坏,后面的所有行都会解码错误——所以一般会配合校验和或周期性地存一个完整基准值。
3.4 三种编码的对比
下面这张表是我自己总结的,平时选编码时直接对照着看:
| 特性 | 字典编码 | RLE | Delta Encoding |
|---|---|---|---|
| 核心思想 | 值→ID映射 | 连续重复→计数 | 存差值代替原值 |
| 最佳场景 | 低基数列 | 高重复率列 | 有序数值列 |
| 压缩比 | 取决于基数/行数比 | 取决于连续重复长度 | 取决于差值大小 |
| 解码速度 | 快(查表) | 极快(展开即可) | 中等(需累加) |
| 支持随机访问 | 是 | 否(需从头解码) | 否(需从基准值开始) |
| 更新代价 | 低(字典可追加) | 高(需重算段) | 高(后续值需重算) |
| 典型应用 | Parquet字典页 | ORC的RLEv2 | 时序数据库列存 |
嗯,这里要注意,实际生产环境中很少只用一种编码。比如Parquet里,同一列的不同数据页可能用不同编码——基数高的页用Delta,基数低的页用字典。这叫“自适应编码”,后面章节会细讲。
3.5 编码选择的核心逻辑
我个人的经验是,选编码前先问自己三个问题:
- 这列的唯一值有多少?——少就用字典,多就考虑其他。
- 数据是否有序?——有序优先考虑Delta,无序看重复率。
- 查询模式是什么?——频繁点查要支持随机访问,字典更合适;全列扫描RLE和Delta都行。
下面这张图是我画的一个决策流程,帮你快速定位:
说实话,没有银弹。我在一个项目里试过对“用户ID”列用字典编码,结果唯一值接近行数,字典比数据还大,最后换成了Delta Encoding,配合排序,效果好了很多。所以,一定要先分析数据分布,再选编码。
3.6 避坑指南
最后分享几个我踩过的坑:
- 别迷信压缩比:压缩比高不代表查询快。RLE解压极快,但字典需要查表,Delta需要累加,解码开销不一样。
- 注意编码的“副作用”:比如字典编码后,数据页里存的是ID,但查询时如果过滤条件是原始值,需要先查字典转成ID——这个转换本身有成本。
- 小心编码嵌套:有些系统允许对编码后的数据再编码(比如对RLE的计数再做Delta),理论上能进一步压缩,但解码链路变长,容易出bug。我见过一个线上事故,就是因为嵌套编码的解码逻辑有边界条件没处理好,导致查询结果错乱。