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

前十天的课程已经完成了两件很重要的事。第一件是建立 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 测试证明优化结果。
本日学习目标
- 理解 Data Skipping 在 Doris 查询加速中的位置;
- 区分自动维护索引、手工创建索引和历史索引能力;
- 掌握 Prefix Index 的 Sort Key、36 字节与最左前缀规则;
- 掌握 ZoneMap 的 Min、Max、Null 裁剪机制;
- 掌握 BloomFilter 的概率判断、适用操作符和限制;
- 掌握 NGram BloomFilter 对
LIKE '%pattern%'的加速方法; - 掌握 Inverted Index 的结构化过滤与全文检索能力;
- 理解 Bitmap Index、BITMAP 数据类型和 Delete Bitmap 的区别;
- 建立索引选型、创建、构建、验证、上线和复审流程;
- 完成一份可进入生产评审的索引设计与验证报告。
版本口径
截至 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 搭建的目标版本环境中确认。
一、索引的共同目标:尽可能少读数据

一条分析查询的成本可以粗略拆成下面几层:
- FE 接收 SQL,完成解析、绑定、改写和物理计划生成;
- BE 打开目标 Tablet、Rowset 和 Segment;
- 存储层读取、解压、解码列数据;
- 执行层完成过滤、聚合、排序、Join 和 TopN;
- 多个节点通过 Exchange 交换中间结果;
- 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 索引全景:自动维护、手工创建与历史能力

当前 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 决定最强的内建定位路径

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'
只过滤 service 或 user_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 由系统自动维护。每个 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 同时包含 SUCCESS、FAILED 和 CANCELLED,状态条件无法排除很多 Page。上游按状态成批写入,或排序键让相同状态聚集时,裁剪能力会增强。
3. 常见误区
- 查询范围本身覆盖大部分数据,ZoneMap 过滤少属于正常现象;
- 每个 Page 的 Min/Max 很宽,增加同类范围条件也难以改善;
- 只看到表上存在 ZoneMap,并不能推导出固定加速倍数;
- 分区裁剪已经把数据缩到很小时,ZoneMap 的增量收益可能有限。
Profile 字段会随版本演进,课程站需要维护版本映射。判断逻辑保持一致:观察 Segment、Page 和 Rows 的过滤量,确认实际读取是否显著下降。
五、BloomFilter:高基数等值查询的概率型 Skip Index

BloomFilter 使用位数组和多组 Hash 函数保存集合信息。写入时,每个值经过多次 Hash,相关位置被设置为 1;查询时再计算相同位置:
- 任意位置为 0,目标值一定不存在;
- 所有位置为 1,目标值可能存在。
第二类判断存在误判。误判会让 Doris 多读一个 Page,不会把真实命中数据过滤掉,因此结果正确性不受影响。
1. 适用条件
BloomFilter 适合:
column = value;column IN (...);- 高基数、重复率低的列;
- 查询值在绝大多数 Page 中都不存在。
常见字段有 user_id、order_id、trace_id、设备 ID 和 UUID。
低基数列通常收益有限。性别、布尔状态等字段很容易出现在每个 Page,BloomFilter 无法排除数据块。
2. 当前 4.x 的关键限制
官方文档列出的限制包括:
- 只加速
=和IN; !=、NOT IN、>、<和范围条件无法使用;TINYINT、FLOAT、DOUBLE列不支持创建;- 从 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%'

普通 BloomFilter 保存完整列值,无法直接判断一个长字符串是否包含某个字符片段。NGram BloomFilter 会先把字符串切成连续 N 个字符的片段,再把这些片段写入 BloomFilter。
字符串 DorisDB 在 gram_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。业务需要先定义“字符包含”还是“词项检索”,再选择索引。
