第 14 关 · ★★★

索引体系

Prefix、ZoneMap、Bloom、Inverted 与 NGram:用实验验证索引的裁剪效果。

已点亮 · 最佳 分
索引体系 第 1 页

Day 11|Doris 索引体系:Prefix、ZoneMap、Bloom、NGram、Inverted 与 Bitmap 迁移

Day 11 学习总览
Day 11 学习总览

前十天的课程已经完成了两件很重要的事。第一件是建立 Doris 的整体坐标系,理解 FE、BE、MPP、列存、向量化和 Pipeline 怎样协同。第二件是把数据真正组织起来:选择数据模型,设计排序键、分区和分桶,再通过批量导入、Routine Load、Flink Connector 与 CDC 持续写入。

从 Day 11 开始,我们进入查询加速层。此时最常见的疑问是:一张表增长到十亿、百亿甚至万亿行以后,查询为什么仍然可以在很短时间内返回?答案通常不来自某一个神奇参数。Doris 会在查询路径上逐级缩小数据范围,让无关分区、Tablet、Segment、Page 和行号尽早退出执行链路。

索引就是这套收缩机制的重要组成部分。

Doris 的索引体系具有鲜明的分析型数据库特征。Prefix Index 依赖有序存储定位数据块,ZoneMap 依赖列值范围跳过 Page,BloomFilter 和 NGram BloomFilter 使用概率结构排除不可能命中的数据块,Inverted Index 通过值或词项到 RowID 集合的映射完成精确过滤与全文检索。它们可以和分区裁剪、分桶裁剪、列式读取、向量化执行一起工作。

本篇还会处理一个容易混淆的版本问题。旧课程与旧项目中经常出现 Bitmap Index。当前 Doris 4.x 官方索引总览已经把 BITMAP Index 标记为由 Inverted Index 替代。BITMAP 数据类型仍然广泛用于精确去重和人群交并补,Delete Bitmap 仍然是 Unique Key Merge-on-Write 的内部机制。三者名称相近,职责不同。

完成今天的学习后,你应当能够面对一条慢 SQL,判断它应该依靠排序键、自动索引、Bloom、NGram 或倒排索引;也能够说明索引会增加哪些写入、存储与运维成本,并使用 Explain、Query Profile 和 A/B 测试证明优化结果。

本日学习目标

  1. 理解 Data Skipping 在 Doris 查询加速中的位置;
  2. 区分自动维护索引、手工创建索引和历史索引能力;
  3. 掌握 Prefix Index 的 Sort Key、36 字节与最左前缀规则;
  4. 掌握 ZoneMap 的 Min、Max、Null 裁剪机制;
  5. 掌握 BloomFilter 的概率判断、适用操作符和限制;
  6. 掌握 NGram BloomFilter 对 LIKE '%pattern%' 的加速方法;
  7. 掌握 Inverted Index 的结构化过滤与全文检索能力;
  8. 理解 Bitmap Index、BITMAP 数据类型和 Delete Bitmap 的区别;
  9. 建立索引选型、创建、构建、验证、上线和复审流程;
  10. 完成一份可进入生产评审的索引设计与验证报告。

版本口径

截至 2026-08-16,Apache Doris 官网将 4.1.3 标记为 Latest,将 4.0.8 标记为 Stable。本文以 4.x 当前官方索引总览为主线。Prefix Index 和 ZoneMap 由系统自动维护;Inverted、BloomFilter、NGram BloomFilter 需要根据查询模式创建。4.1.2 起,CHAR 列上的 BloomFilter Index 不再生效。BM25、SEARCH() 和向量混合检索会在 Day 28 集中展开。

实验前提

完整包提供 50,000 行固定种子事件数据、CSV 与 JSONL 样例、建表 SQL、索引查询 SQL、Stream Load 脚本和验证模板。当前制作环境没有连接实际 Doris 集群,SQL 已完成静态检查,运行结果需要在 Day 5 搭建的目标版本环境中确认。


一、索引的共同目标:尽可能少读数据

Data Skipping 思维模型
Data Skipping 思维模型

一条分析查询的成本可以粗略拆成下面几层:

  1. FE 接收 SQL,完成解析、绑定、改写和物理计划生成;
  2. BE 打开目标 Tablet、Rowset 和 Segment;
  3. 存储层读取、解压、解码列数据;
  4. 执行层完成过滤、聚合、排序、Join 和 TopN;
  5. 多个节点通过 Exchange 交换中间结果;
  6. Coordinator 合并结果并返回客户端。

索引最直接影响第二层和第三层。扫描量下降以后,后续过滤、聚合、Join、Shuffle 和结果合并也会随之减轻。

假设有一张日志表,包含 100 亿行数据,查询如下:

SELECT service, COUNT(*)
FROM app_events
WHERE event_time >= '2026-08-16 10:00:00'
  AND event_time <  '2026-08-16 11:00:00'
  AND trace_id = 'trace-9f8e7d'
  AND message LIKE '%timeout%'
GROUP BY service;

理想的裁剪链路可能包括:

  • 时间分区只保留当天或目标小时;
  • Hash 分桶在条件满足时减少 Tablet;
  • Prefix Index 根据有序时间范围定位数据块;
  • ZoneMap 排除时间范围完全不重叠的 Segment 和 Page;
  • BloomFilter 排除不包含目标 trace_id 的 Page;
  • NGram BloomFilter 排除不包含 timeout 字符片段的 Page;
  • 剩余少量数据进入精确过滤与聚合。

这条路径揭示了一个重要事实:索引收益依赖查询模式和数据分布。条件本身命中 90% 数据时,索引很难带来数量级提升。条件只命中万分之一数据时,定位型索引通常更有价值。时间列在物理上高度有序时,ZoneMap 能排除大量 Page;时间值在每个 Page 内随机分散时,Min/Max 范围会很宽,裁剪效果随之下降。

索引评审至少需要记录四个维度:

维度 需要回答的问题
选择性 条件最终保留多少比例的数据?
聚集性 相似值在 Segment 和 Page 中是否集中?
查询频率 SQL 每天执行多少次,是否位于核心 SLA?
写入强度 每秒写入量、批次规模和可接受的索引构建成本是多少?

索引数量不能直接代表性能。优秀的分区、Sort Key 和自动索引已经可以覆盖大量查询。手工索引适合补齐高频、稳定、收益明确的访问路径。


二、Doris 索引全景:自动维护、手工创建与历史能力

Doris 索引全景
Doris 索引全景

当前 4.x 可以先按管理方式分为三组。

1. 自动维护索引

Prefix Index 依附于 Sort Key。Doris 自动取排序键前 36 字节构建稀疏索引,每张表只有一组。它适合排序键最左连续列上的等值、IN 和范围过滤。

ZoneMap 覆盖所有列。Doris 在 Segment 和 Page 层保存 Min、Max 和 Null 信息,利用条件范围排除无关数据块。用户无需写 DDL,也无需维护独立索引文件。

2. 手工创建索引

Inverted Index 可以建立在多列上。它能够加速结构化列的等值、范围、集合、数组过滤,也能够完成关键词、短语、前缀、正则和多字段全文检索。当前官方索引总览将它作为非 Key 列过滤和文本检索的主要选择。

BloomFilter Index 把完整列值写入概率集合,服务 =IN。它的功能范围较窄,索引体积通常低于复杂倒排索引,适合明确的高基数等值查询。

NGram BloomFilter Index 把字符串切成连续字符片段,再把片段写入 BloomFilter,专门服务 LIKE '%pattern%'

Vector Index 服务近似向量检索,留到 Day 28。

3. Bitmap Index 的当前定位

旧版本课程常用“低基数 Bitmap、高基数 Bloom、文本 Inverted”来建立初始认知。当前 4.x 官方 Index Overview 明确说明 BITMAP Index 已由 Inverted Index 替代。新表设计可以直接评估 Inverted Index,已有 Bitmap Index 则需要经过真实负载回归后逐步迁移。

下面三个概念必须分清:

名称 所处层次 主要用途
Bitmap Index 旧式二级索引 低基数列过滤
BITMAP 数据类型 聚合状态与集合类型 精确去重、用户集合交并补
Delete Bitmap Unique Key MoW 内部结构 标记旧版本行的可见性

名称相似不代表可以互换。索引迁移不会影响 BITMAP 聚合状态,也不会改变 Delete Bitmap 的内部工作方式。


三、Prefix Index:Sort Key 决定最强的内建定位路径

Prefix Index
Prefix Index

Doris 在 Tablet 内按 Sort Key 有序保存数据。每若干行组成一个逻辑 Data Block,系统从每个 Block 的第一行提取排序键前缀,记录该 Block 的起始位置。Prefix Index 很小,可以常驻内存,查询时通过二分定位到可能命中的区间。

1. 三种模型的 Sort Key 来源

数据模型 Sort Key 来源
Duplicate Key DUPLICATE KEY(...) 中的列
Aggregate Key AGGREGATE KEY(...) 中的列
Unique Key UNIQUE KEY(...) 中的列

这意味着模型 Key 同时承担写入语义和物理排序。Day 7 关注相同 Key 再次写入后的结果,Day 11 关注 Key 顺序对查询裁剪的影响。

2. 36 字节规则

Prefix Index 最多取 Sort Key 前 36 字节。固定长度列会按顺序拼接。遇到 VARCHAR 时,前缀在该列结束位置立即截断,后续排序列不再进入 Prefix Index。

假设 Key 为:

(user_id BIGINT, age INT, message VARCHAR(100), event_time DATETIME)

Prefix Index 可以包含 user_id + age + message 的一部分,event_time 无法继续加入。

如果 Key 的第一列就是 user_name VARCHAR(20),前缀只包含 user_name。即使总长度没有达到 36 字节,后面的列也不会进入前缀。

因此,高频过滤的固定长度列通常适合放在前面,长字符串需要谨慎进入排序键前部。

3. 最左连续命中

Sort Key 为 (event_date, service, user_id) 时,下面的条件能够利用 Prefix Index:

WHERE event_date = '2026-08-16'

WHERE event_date = '2026-08-16'
  AND service = 'payment'

WHERE event_date BETWEEN '2026-08-01' AND '2026-08-16'

只过滤 serviceuser_id 时,查询跳过了左侧排序列,Prefix Index 无法直接完成区间定位。ZoneMap、Inverted Index 或其他路径仍可能发挥作用。

4. 一张表只有一组 Prefix Index

一张表只有一套 Sort Key,因此只有一组 Prefix Index。业务存在多套稳定高频过滤顺序时,可以考虑:

  • 调整主表 Sort Key;
  • 为非 Key 条件创建 Inverted Index;
  • 为 Duplicate Key 表创建不同列序的同步单表物化视图。

第三种方法会在 Day 12 展开。

5. 验证方式

Prefix Index 自动生效,无需 Hint。Query Profile 中重点关注:

RowsKeyRangeFiltered

这个指标表示 Prefix Index 排除的行数。还要同时查看 Scan Rows、Scan Bytes、Tablet 数和 Segment 数。指标大于 0 只能证明索引参与了过滤,最终收益仍需结合总扫描量与延迟判断。


四、ZoneMap:利用 Min、Max 和 Null 完成低成本裁剪

ZoneMap Index
ZoneMap Index

ZoneMap 由系统自动维护。每个 Segment 和 Page 都保存列级统计信息,包括最小值、最大值和是否包含 Null。查询条件与某个数据块的范围完全不相交时,存储层可以直接跳过该块。

假设一个 Page 的 latency_ms 范围为 [10, 80]

  • latency_ms > 500 可以排除该 Page;
  • latency_ms = 100 可以排除该 Page;
  • latency_ms BETWEEN 40 AND 60 仍需读取;
  • latency_ms IS NULL 可以利用 Null 统计判断。

1. 物理聚集性决定收益

时间有序写入时,一个 Page 可能只覆盖几分钟,查询一小时数据能够排除大量 Page。时间值随机分布时,每个 Page 的 Min 很早、Max 很晚,ZoneMap 很难证明数据块无关。

这也是 Sort Key 和 ZoneMap 经常协同的原因。高频范围列进入合理排序位置后,相似值更加集中,Min/Max 统计更有区分度。

2. 自动覆盖所有列

ZoneMap 存在于所有列,非 Key 列也能受益。收益大小取决于数据分布。例如 status 只有少量值,如果每个 Page 同时包含 SUCCESSFAILEDCANCELLED,状态条件无法排除很多 Page。上游按状态成批写入,或排序键让相同状态聚集时,裁剪能力会增强。

3. 常见误区

  • 查询范围本身覆盖大部分数据,ZoneMap 过滤少属于正常现象;
  • 每个 Page 的 Min/Max 很宽,增加同类范围条件也难以改善;
  • 只看到表上存在 ZoneMap,并不能推导出固定加速倍数;
  • 分区裁剪已经把数据缩到很小时,ZoneMap 的增量收益可能有限。

Profile 字段会随版本演进,课程站需要维护版本映射。判断逻辑保持一致:观察 Segment、Page 和 Rows 的过滤量,确认实际读取是否显著下降。


五、BloomFilter:高基数等值查询的概率型 Skip Index

BloomFilter Index
BloomFilter Index

BloomFilter 使用位数组和多组 Hash 函数保存集合信息。写入时,每个值经过多次 Hash,相关位置被设置为 1;查询时再计算相同位置:

  • 任意位置为 0,目标值一定不存在;
  • 所有位置为 1,目标值可能存在。

第二类判断存在误判。误判会让 Doris 多读一个 Page,不会把真实命中数据过滤掉,因此结果正确性不受影响。

1. 适用条件

BloomFilter 适合:

  • column = value
  • column IN (...)
  • 高基数、重复率低的列;
  • 查询值在绝大多数 Page 中都不存在。

常见字段有 user_idorder_idtrace_id、设备 ID 和 UUID。

低基数列通常收益有限。性别、布尔状态等字段很容易出现在每个 Page,BloomFilter 无法排除数据块。

2. 当前 4.x 的关键限制

官方文档列出的限制包括:

  • 只加速 =IN
  • !=NOT IN>< 和范围条件无法使用;
  • TINYINTFLOATDOUBLE 列不支持创建;
  • 从 4.1.2 起,CHAR 列上的 BloomFilter Index 不再生效。

版本差异需要进入 Web 站的能力元数据和升级检查单。

3. 创建语法

BloomFilter 沿用表属性语法:

CREATE TABLE order_events_bloom (
    event_time DATETIME,
    order_id   BIGINT,
    trace_id   VARCHAR(64),
    amount     DECIMAL(18,2)
)
DUPLICATE KEY(event_time, order_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 8
PROPERTIES (
    "replication_num" = "1",
    "bloom_filter_columns" = "order_id,trace_id"
);

已有表可以通过 ALTER TABLE ... SET 修改 bloom_filter_columns。调整后需要等待相关结构变更完成,再做性能回归。

4. 验证指标

Profile 中重点查看:

RowsBloomFilterFiltered
BlockConditionsFilteredBloomFilterTime

第一个指标反映排除的数据量,第二个指标反映过滤本身的成本。过滤时间很高、扫描节省很少时,索引价值可能不足。

当前官方 Index Overview 对新设计给出的倾向很明确:非 Key 列过滤可以优先评估 Inverted Index;BloomFilter 适合功能范围清晰、等值查询稳定、索引空间更敏感的场景。


六、NGram BloomFilter:加速 LIKE '%pattern%'

NGram BloomFilter
NGram BloomFilter

普通 BloomFilter 保存完整列值,无法直接判断一个长字符串是否包含某个字符片段。NGram BloomFilter 会先把字符串切成连续 N 个字符的片段,再把这些片段写入 BloomFilter。

字符串 DorisDBgram_size=3 时会生成:

Dor, ori, ris, isD, sDB

查询 message LIKE '%timeout%' 时,查询模式也会拆成多个 3-Gram。某个 Page 缺少其中任意片段,就可以直接跳过。

1. 适用边界

NGram BloomFilter 适合:

  • 字符串列;
  • LIKE '%pattern%'
  • 模式中连续字符数不小于 gram_size
  • 查询频率高,原始字符片段匹配符合业务语义。

它只服务 LIKE。普通 BloomFilter 与 NGram BloomFilter 在同一列上互斥,只能选择一种。

2. 核心参数

INDEX idx_message_ngram (message) USING NGRAM_BF
PROPERTIES (
    "gram_size" = "3",
    "bf_size" = "1024"
)

gram_size 决定每个字符片段的长度。值较小时,短模式更容易使用,索引项也会增加;值较大时,短模式无法命中。官方建议从 3 起步,并结合业务最短搜索词调整,最低不建议小于 2。

bf_size 是每个数据块 BloomFilter 的位数。增大它可以降低 Hash 冲突概率,同时增加索引空间。官方建议从 1024 Bits 起步,再结合 Profile 调整。

3. 会话设置与验证

当前官方文档要求开启函数下推:

SET enable_function_pushdown = true;

随后执行:

SELECT COUNT(*)
FROM logs_ngram
WHERE message LIKE '%timeout%';

Profile 仍使用 BloomFilter 相关指标评估过滤效果。

4. 与全文检索的边界

LIKE '%timeout%' 关注原始字符片段,NGram 很自然。关键词、短语、布尔组合、相关性排序、多字段检索更适合 Inverted Index。业务需要先定义“字符包含”还是“词项检索”,再选择索引。


登录后可阅读本文完整内容。