什么是低基数什么是高基数

在数据库和存储引擎的语境中,基数指的是某个字段中不同唯一值的数量

  • 低基数:唯一值很少,大量文档共享相同的值。
  • 高基数:唯一值很多,几乎每个文档的值都不同。

📊 直观对比

特征低基数高基数
唯一值数量几十 ~ 几千几十万 ~ 数十亿
重复率极高极低
典型例子性别、状态码、省份、星期几用户ID、订单号、时间戳、UUID
字典编码效果✅ 极好(字典小,ID位宽窄)❌ 很差(字典本身比原始数据还大)
位图索引效果✅ 每个值一个位图,高效❌ 需要海量位图,内存爆炸
B-Tree 效果⚠️ 区分度低,范围扫描退化✅ 天然适合
Lucene DocValues⚠️ 可用但浪费✅ 位宽压缩正好适配

🔢 量化感知

假设你有1000 万条文档:

  • gender:2 个唯一值 → 基数 = 2 →极低基数
  • province:34 个唯一值 → 基数 = 34 →低基数
  • age:~120 个唯一值 → 基数 = 120 →低基数
  • city_code:~3000 个唯一值 → 基数 = 3K →中等基数
  • user_id:~800 万个唯一值 → 基数 = 8M →高基数
  • timestamp:~1000 万个唯一值 → 基数 ≈ 文档数 →极高基数

💡 为什么基数重要?

因为不同的压缩/索引方案对基数的敏感度完全不同,这也是上一轮讨论各种数值存储方案时反复提到基数的原因:

  1. 字典编码的压缩率 ≈log₂(基数) / 原始位宽。基数越低,收益越大;当基数接近文档总数时,字典 + ID 序列反而比原始数据更大。
  2. 位图索引的空间 ≈基数 × 文档数 / 8。基数翻倍,空间翻倍。所以只对低基数字段建位图。
  3. Lucene 的位宽自适应numBitsPerValue = ceil(log₂(max - min + 1))。它不关心有多少唯一值,只关心值域跨度。所以即使基数很高,只要值域集中(比如 1000 万个文档的值都在 0~65535 之间),仍然只需 16 bits/值,压缩效果很好。
  4. B-Tree 的查询效率取决于区分度。低基数时等值查询返回海量行,索引选择性差;高基数时近似 O(log N),效率高。

一句话记忆基数 = 唯一值个数。选存储/索引方案前,先问自己"这个字段有多少种不同的值",答案直接决定了哪种方案最合适。

Lucene 当前采用的“位宽自适应 + 分块/全局二选一”方案,本质上是一种面向随机访问的轻量级列存压缩。但在更广泛的数据库和存储引擎领域,针对数值型数据还有多种截然不同的方案。

以下按设计哲学分类梳理:

1. 字典编码 + 位图索引

  • 代表系统:ClickHouse (LowCardinality)、Apache Doris、Elasticsearch Keyword
  • 核心思想:不直接存数值本身,而是将每个唯一值映射为一个整数 ID,再对 ID 序列做压缩。
  • 适用场景:低基数数值(状态码、枚举、年龄等)
  • 与 Lucene 的区别:Lucene 的 SortedNumericDocValues 不做字典编码;它假设值是连续或近似连续的整数,直接用位宽压缩。字典编码在低基数时空间效率远超位宽压缩,但高基数时字典本身的开销会成为瓶颈。

2. 通用列式压缩算法

  • 代表系统:Parquet / ORC / Arrow、DuckDB
  • 核心思想:在 RLE / Delta / Bit-Packing 之上,叠加通用块压缩器。
  • 典型组合Delta Encoding → RLE → ZSTD/LZ4/Snappy
  • 与 Lucene 的区别:Lucene 的.dvd不使用任何通用压缩器(不用 LZ4/ZSTD),因为 DocValues 需要 O(1) 随机访问,通用压缩器的解压代价不可接受。Parquet/ORC 是面向批量扫描优化的,可以容忍解压开销换取更高压缩率。

3. 向量化 SIMD 压缩

  • 代表系统:FastPFOR、StreamVByte、SIMD-BitPacking
  • 核心思想:利用 CPU 的 SIMD 指令集,一条指令同时处理 128/256/512 bit 的位宽打包/解包。
  • 与 Lucene 的区别:Lucene 的PackedInts/DirectWriter是纯标量 Java 实现。FastPFOR 等库在解码吞吐上可达 Lucene 的5~10 倍,但通常作为底层编解码库嵌入使用,而非独立的存储格式。Lucene 10+ 已开始引入 Panama Vector API 做类似优化。

4. B-Tree / LSM-Tree 行存式索引

  • 代表系统:InnoDB (B+Tree)、RocksDB / LevelDB (LSM)
  • 核心思想:数值作为 Key 或 Value 的一部分,按排序结构组织,通过树遍历定位。
  • 与 Lucene 的区别:这是完全不同的范式。Lucene DocValues 是纯列存、无树结构、O(1) 定位;B-Tree/LSM 是面向范围查询和有序遍历优化的,随机点查需要 O(log N) 次 I/O。DocValues 不适合做范围聚合以外的复杂数值查询,而 B-Tree 恰好擅长这个。

5. 专用时序/浮点压缩

  • 代表系统:Gorilla (Facebook TSDB)、InfluxDB TSM、QuestDB
  • 核心思想
    • 整数时间戳:Delta-of-Delta 编码(二阶差分),因为时间间隔通常是常数。
    • 浮点值:XOR 编码,相邻值的 IEEE754 表示通常只有少数 bit 不同。
  • 与 Lucene 的区别:Lucene 把浮点数转成 sortable long 后当整数处理,丢失了浮点数的局部相关性。Gorilla XOR 编码对浮点监控数据的压缩率比 Lucene 方案高一个数量级,但完全不适用于非时序数据。

📊 方案选型速查表

需求特征推荐方案Lucene DocValues 适合吗?
全文检索附带数值过滤/排序Lucene SortedNumeric✅ 最佳选择
大规模分析查询、高压缩率Parquet/ORC + ZSTD❌ 压缩率不够
低基数数值精确匹配字典编码 + 位图⚠️ 可用但非最优
高频随机点查 + 范围查询B-Tree / LSM-Tree❌ 不支持高效范围查
时序监控数据Gorilla / Delta-RLE❌ 未利用时序特性
极致解码吞吐SIMD Bit-Packing⚠️ 标量实现有差距

总结:Lucene 的方案是为"搜索引擎中的数值辅助字段"量身定制的——牺牲了极致压缩率和范围查询能力,换取了 O(1) 随机访问、零依赖、与倒排索引无缝配合的特性。如果你的场景脱离了搜索上下文(纯分析、纯时序、纯事务),上述替代方案通常在各自领域表现更优。