探秘新一代向量存储格式Lance-format (六) 编码与压缩技术

第六章:编码与压缩技术

🎯 核心概览

Lance 的性能秘密之一就是智能编码。相同的数据用不同的编码方式,压缩率可能从 0% 到 99%。本章深入讲解各种编码算法和选择策略。


📊 第一部分:编码的四个层次

物理编码 vs 逻辑编码

markdown 复制代码
用户看到的数据(逻辑):
[100, 2000, 300, 40000, 500]

Lance 的处理:
1. 逻辑编码:选择编码算法(基于数据特征)
2. 物理编码:实际的字节序列(硬盘存储)
3. 压缩:可选的通用压缩(LZ4/Zstd)
4. 存储:最终的字节流

为什么需要多种编码?

markdown 复制代码
不同数据的最优编码不同:

递增序列 [1,2,3,4,5]:
- Delta 编码最优:只需 1-2 bits/数

重复值 [A,A,A,B,B,C]:
- Dictionary 编码最优:只需 1-2 bits/值

随机值 [1234, 5678, 9012]:
- Bitpacking 最优:按实际范围存储

稀疏数据(大量 NULL):
- Bitmap 编码最优:NULL 用 1 bit 标记

Lance 的智能:**自动选择**最优编码

🔧 第二部分:主要编码方式详解

1. Bitpacking(比特打包)

原理:只用必要的比特数存储值

css 复制代码
示例:存储 0-100 的数字

方式 A:标准 int32(32 bits)
100 → [00000000 00000000 00000000 01100100]
浪费:99% 空间

方式 B:Bitpacking(7 bits)
100 → [01100100]  (去掉前 25 个 0)

100 个这样的数:
标准方式:400 bytes
Bitpacking:87.5 bytes
压缩率:78%

Lance 中的应用

rust 复制代码
// 自动计算所需比特数
fn compute_bit_width(values: &[i64]) -> usize {
    let max_val = values.iter().max().unwrap();
    let min_val = values.iter().min().unwrap();
    let range = max_val - min_val;
    
    // 计算所需比特数(向上取整)
    (64 - range.leading_zeros()) as usize
}

// 使用 SIMD 优化(AVX2)加速编解码
#[cfg(target_arch = "x86_64")]
fn bitpack_avx2(values: &[i64], bit_width: usize) -> Vec<u8> {
    // 一次处理 256 比特(4 个向量值)
    // 性能:5GB/s
}

何时使用

  • ✓ 数值范围有限的整数列
  • ✓ 多数值接近的数据
  • ✗ 非常大的数值(范围 > 2^32)

2. Dictionary 编码(字典编码)

原理:重复的值用索引代替

sql 复制代码
原始数据:
[user, admin, user, guest, admin, user, admin]
共 8 字节 × 7 = 56 字节(假设指针)

字典编码:
Dictionary: {
  "user": 0,
  "admin": 1,
  "guest": 2,
}
Indices: [0, 1, 0, 2, 1, 0, 1]

存储大小:
- 字典:30 字节
- 索引:7 字节(如果用 1 byte/索引)
- 总计:37 字节
压缩率:34%

实际 Lance:
使用 Bitpacking 对索引进一步编码(如果只有 3 种值,用 2 bits)
总计:5 字节 + 字典 30 字节 = 35 字节
压缩率:37%

重复率与压缩率的关系

erlang 复制代码
列数据 1000 000 行

重复率 10%(1000 种不同值):
字典大小:1000 × 平均值长度
索引大小:1000000 × 4 bytes = 4MB
压缩率:40%

重复率 50%(500 种不同值):
压缩率:60%

重复率 99%(100 种不同值):
压缩率:90%

Lance 的策略

rust 复制代码
fn choose_encoding(column: &ColumnData) -> Encoding {
    let distinct_count = estimate_distinct_count(column);
    let cardinality = distinct_count as f64 / column.len() as f64;
    
    if cardinality < 0.05 {  // 重复率 > 95%
        Encoding::Dictionary
    } else if cardinality < 0.3 {  // 重复率 > 70%
        Encoding::Dictionary
    } else {
        // 尝试其他编码...
    }
}

3. Delta 编码(差分编码)

原理:存储相邻值的差而不是值本身

ini 复制代码
原始数据(时间序列):
[100, 102, 105, 101, 103, 108]

Delta 编码:
第一个值:100(完整存储)
差值:[+2, +3, -4, +2, +5]

存储空间:
原始:6 × 8 bytes = 48 bytes
Delta:1 × 8 + 5 × 2 bytes = 18 bytes
压缩率:62%

进一步优化(Bitpacking 差值):
差值范围:-4 到 +5 → 需要 4 bits
总计:1 × 8 + 5 × 0.5 bytes = 10.5 bytes
压缩率:78%

适用场景

  • ✓ 时间序列数据
  • ✓ 单调递增的值
  • ✓ 值变化平缓的列

4. RLE 编码(游程编码)

原理:存储值和重复次数

scss 复制代码
原始数据(高重复):
[A, A, A, A, A, B, B, B, C, C, D, D, D, D, D]

RLE 编码:
(A, 5), (B, 3), (C, 2), (D, 5)

存储空间:
原始:15 × 1 byte = 15 bytes
RLE:4 × (1 + 4) = 20 bytes
压缩率:-33%(反而变大!)

但对于长序列:
[A] × 1000000 → (A, 1000000)
原始:1MB
RLE:10 bytes
压缩率:99.999%

Lance 的使用

  • 仅用于高度重复的数据
  • 在 RLE 段很短时自动禁用

5. Prefix 编码(前缀编码)

原理:只存储与前一个值不同的部分

arduino 复制代码
原始字符串列:
["apple", "application", "apply", "apricot", "banana"]

Prefix 编码:
"apple" (完整)
"application" → (+4, "ication")  // 前 4 字符相同
"apply" → (+3, "ly")
"apricot" → (-3, "ricot")  // 回溯后添加
"banana" → (-4, "nana")

存储大小:
原始:5+11+5+7+6 = 34 bytes
Prefix:5+4+2+3+4 = 18 bytes
压缩率:47%

字符串列的最优编码

  • 有序字符串:Prefix 编码 ✓
  • 无序但有重复:Dictionary ✓
  • 完全随机:Dictionary(如果有重复)或 Raw

🗜️ 第三部分:通用压缩算法

编码之后,还可应用通用压缩:

LZ4 vs Zstd

bash 复制代码
特征对比:

             LZ4      Zstd
编码速度     3GB/s    1GB/s
解码速度     5GB/s    3GB/s
压缩率       40%      50%
内存占用     低       中

选择原则:
- 写入频繁,需要快速编码 → LZ4
- 存储成本重要 → Zstd

Lance 的策略

rust 复制代码
pub enum CompressionAlgorithm {
    LZ4,           // 默认,快速
    Zstd,          // 高压缩率
    None,          // 不压缩
}

// 自动选择
fn choose_compression(column_type: &DataType) -> CompressionAlgorithm {
    match column_type {
        DataType::Utf8 => CompressionAlgorithm::Zstd,  // 字符串压缩率高
        DataType::Binary => CompressionAlgorithm::LZ4,  // BLOB 用速度
        _ => CompressionAlgorithm::LZ4,                 // 默认 LZ4
    }
}

📊 第四部分:编码选择的决策树

Lance 如何自动选择最优编码?

markdown 复制代码
收到列数据
    ↓
检查数据特征:
├─ 重复率 > 80%?→ Dictionary
├─ 范围小(< 2^16)?→ Bitpacking
├─ 单调递增?→ Delta
├─ 高度重复段?→ RLE
├─ 字符串且有序?→ Prefix
└─ 默认 → Raw (不编码)
    ↓
应用压缩算法(LZ4/Zstd)
    ↓
比较原始 vs 编码 + 压缩
    ↓
选择更小的方案

编码成本分析

diff 复制代码
列大小:100MB

方案 A:Raw
- 编码时间:0
- 结果:100MB
- 读取时间:10ms

方案 B:Bitpacking + LZ4
- 编码时间:100ms
- 结果:25MB (75% 压缩)
- 读取时间:3ms(IO 快)+ 2ms(解压)

选择:
写入时间允许 → 方案 B
实时写入需求 → 方案 A

🎯 第五部分:向量的特殊编码

向量量化(Quantization)

向量编码的关键在于量化。无索引的向量用 raw float32 存储太大:

ini 复制代码
1 百万个 768 维向量:

Raw float32:
768 × 4 bytes × 1M = 3GB
不压缩

IVF_PQ 量化:
768 维 → 8 个 codebook
每个 codebook:256 码字
编码:8 × log2(256)/8 = 8 bytes per vector
总大小:8MB
压缩率:99.7%

搜索性能:
Raw:扫描 3GB,计算 768 维距离
IVF_PQ:只检查 1% 候选,用 8 字节近似距离
性能提升:1000 倍

量化的精度损失

diff 复制代码
精度与性能的权衡:

Top-10 准确率:

无索引(100%):
- 第 1 名:完全正确

IVF_PQ:
- Top-10:95-99% 准确

IVF_SQ(标量量化):
- Top-10:98-99% 准确

选择:
- 需要完美精度:无索引(或黄金集合重排)
- 推荐系统:IVF_PQ(99% 准确足够)
- 图搜索:任何量化都可

📈 第六部分:实际压缩效果统计

真实数据集的压缩率

erlang 复制代码
数据集 1:电商产品数据(1000万行)

列类型          原始大小    编码方式      压缩后    压缩率
product_id     40MB       Bitpacking    10MB      75%
name           100MB      Dictionary    5MB       95%
category       50MB       Dictionary    1MB       98%
price          40MB       Bitpacking    10MB      75%
description    500MB      Zstd          25MB      95%
embedding      30GB       IVF_PQ        240MB     99.2%

总计:30.73GB → 251MB
整体压缩率:99.2%
erlang 复制代码
数据集 2:时间序列数据(5000万条)

timestamp      200MB      Delta+LZ4     20MB      90%
sensor_id      200MB      Dictionary    2MB       99%
temperature    200MB      Delta+LZ4     15MB      92.5%
humidity       200MB      Delta+LZ4     15MB      92.5%

总计:800MB → 52MB
整体压缩率:93.5%

💡 最佳实践

编码选择的三原则

场景 编码方案 原因
高基数(>10% 不同值) Bitpacking 编码简单快速
高重复(<5% 不同值) Dictionary 极高压缩率
时间序列 Delta + LZ4 针对性优化
向量数据 IVF_PQ 支持高效搜索
字符串(有序) Prefix + Zstd 大幅节省空间
BLOB(小) Zstd 最大压缩

编码的性能规律

bash 复制代码
编码成本 vs 读取速度:

写入时间(按数据大小):
Bitpacking:  1GB/s
Dictionary:  500MB/s
Delta:       2GB/s
RLE:         5GB/s
Raw:         10GB/s(不编码)

读取时间(已编码数据):
Raw:         10GB/s(无解码)
Bitpacking:  5GB/s
Dictionary:  3GB/s(需要查表)
Delta:       2GB/s(需要累加)

权衡:
- 写入频繁 → 选择简单编码
- 读取频繁 → 选择高压缩编码
- 混合场景 → LZ4(折中方案)

📚 总结

Lance 的编码系统通过以下创新实现高效存储:

  1. 多种编码算法:适应不同数据特征
  2. 自动选择:基于数据统计自动选择
  3. 级联压缩:编码 + 通用压缩双重优化
  4. 向量特化:PQ 量化支持向量搜索
  5. 性能优化:SIMD 加速编解码

下一章将讲解编码器和解码器的实现细节。

相关推荐
语落心生2 小时前
探秘新一代向量存储格式Lance-format (七) 编码器与解码器实现
架构
语落心生2 小时前
探秘新一代向量存储格式Lance-format (四) 容器与缓存机制
架构
语落心生2 小时前
探秘新一代向量存储格式Lance-format (三) Lance 数据类型系统
架构
语落心生2 小时前
探秘新一代向量存储格式Lance-format (二) 项目结构与模块划分
架构
语落心生2 小时前
探秘新一代向量存储格式Lance-format (一)Lance 项目概览与设计理念
架构
TracyCoder1233 小时前
微服务注册中心基础(一):AP架构原理
微服务·云原生·架构·注册中心
Kapaseker3 小时前
十年开发告诉你什么是“烂代码”
架构
Java烘焙师4 小时前
架构师必备:限流方案选型(原理篇)
架构·限流·源码分析
爱吃牛肉的大老虎9 小时前
网络传输架构之GraphQL讲解
后端·架构·graphql