3. 压缩原理:列式存储如何利用数据局部性实现高压缩比
聊到列式存储,很多人第一反应是「查询快」。但为什么快?除了只读需要的列,另一个核心武器就是——高压缩比。
我刚开始接触列存时,有个困惑:行存也能压缩啊,凭什么列存就能压得更狠?后来亲手调过几个生产环境的表,才真正明白。说白了,列存能把「相似的数据」凑到一起,而相似的数据,天生就好压。
这一章,我们就来拆解三种最经典的列存压缩算法:字典编码、RLE(游程编码)、差分编码。我会结合项目里的真实案例,告诉你什么时候用哪种,以及——哪些坑我替你踩过了。
3.1 字典编码:把长字符串变成小整数
字典编码的思路特别朴素。你想想看,一列「城市」数据,可能有几百万行,但实际出现的城市名就几十个。那我何必每行都存一遍「北京市海淀区」这么长的字符串?
做法很简单:建一个字典表,把每个唯一值映射成一个整数ID。数据文件里只存ID,查询时再通过字典还原。
原始数据(城市列):
北京, 上海, 北京, 广州, 上海, 北京, 深圳, 广州, 北京, 上海
字典映射:
北京 → 0
上海 → 1
广州 → 2
深圳 → 3
编码后存储:
0, 1, 0, 2, 1, 0, 3, 2, 0, 1
你看,原来每个「北京市」要占 6 个字节(UTF-8),现在只存一个整数,可能 1 个字节甚至更少。这就是压缩的来源。
我在一个电商项目里遇到过这种情况:订单表的「用户备注」列,几乎每个订单都不一样。当时新同事用了字典编码,结果压缩后比不压还大。嗯,这就是典型的「选错算法」。
3.2 游程编码(RLE):抓住连续重复的「块」
RLE 的思路更直接:与其重复存同一个值,不如只存「值 + 重复次数」。
举个例子。假设你有一列「日志级别」,按时间排序后可能是这样:
INFO, INFO, INFO, ERROR, ERROR, INFO, INFO, INFO, INFO, WARN
用 RLE 压缩后:
(INFO, 3), (ERROR, 2), (INFO, 4), (WARN, 1)
原来 10 个条目,现在变成 4 个「值+计数」对。如果连续重复的块越长,压缩比就越惊人。
我曾经处理过一个物联网时序表,传感器状态列只有「正常/异常」两种值。因为传感器大部分时间都正常,连续几万行都是「正常」。RLE 一压,压缩比直接干到 200:1。原来 1GB 的数据,变成 5MB 不到。
3.3 差分编码:只存「变化量」
差分编码的思路是:不存原始值,只存当前值和前一个值的差值。
这招对「有序递增」的数据特别管用。比如时间戳列:
原始时间戳(秒):
1700000000, 1700000001, 1700000003, 1700000006, 1700000010
差分编码后:
1700000000, 1, 2, 3, 4
// 第一个值保留原始值,后续只存差值
你看,原始时间戳是 10 位的大整数,占 4 字节。但差值只有 1、2、3、4,用 1 个字节甚至更少就能存下。如果差值更小,还能用位打包进一步压缩。
我记得有一次做用户行为分析,事件时间戳列每秒可能有几百条记录。差分之后,大部分差值都是 0 或 1。配合位打包,压缩比从 4x 直接提升到 30x。
3.4 组合使用:1+1 > 2
实际生产环境中,很少只用一种压缩算法。存储引擎通常会组合使用:
| 组合方式 | 适用场景 | 典型压缩比 |
|---|---|---|
| 字典 + RLE | 低基数枚举列,且按值排序后连续重复 | 10x ~ 50x |
| 差分 + 位打包 | 有序数值列,差值范围小 | 5x ~ 30x |
| 字典 + 差分 | 字典ID后,ID序列有局部递增性 | 8x ~ 40x |
| RLE + 差分 | 时序数据,值变化缓慢 | 20x ~ 100x |
我在调优 Parquet 表时,最常用的套路是:先字典编码把字符串转整数,再对整数列做 RLE 或差分。两步下来,数据体积能缩到原来的 5% 以下。
3.5 避坑指南:我踩过的三个坑
讲了这么多好处,也该说说坑了。毕竟压缩不是免费的午餐。
坑一:压缩和解压的 CPU 开销
我曾经在一个高并发查询系统里,对所有列都用了最高压缩级别的字典+RLE。结果查询延迟反而变高了——因为每次读取都要解压,CPU 成了瓶颈。后来我调整策略:高频查询的列用轻量压缩(差分),低频扫描的列用重度压缩(字典+RLE)。
坑二:字典编码的内存占用
字典本身要加载到内存才能解码。如果一列的基数很高(比如几十万个唯一值),字典可能占几百 MB。我遇到过因为字典太大导致 OOM 的情况。解决方案是:对高基数列不做字典编码,改用其他算法。
坑三:RLE 的更新代价
RLE 压缩后的数据,如果中间某行要更新,可能要把整个 RLE 块解压、修改、再重新压缩。这在 OLTP 场景下是灾难。所以列式存储通常只用于 OLAP(分析型)场景,写入后很少单行更新。
3.6 小结
三种压缩算法,各有各的脾气:
- 字典编码:专治「值多行少」的枚举列,把字符串变整数
- RLE:专治「连续重复」的数据列,把重复值变计数
- 差分编码:专治「有序递增」的数值列,把大数变小数
它们能发挥威力的前提,都是列式存储带来的数据局部性。同一列的数据类型一致、值域集中、分布规律明显——这才是压缩算法真正的用武之地。