深入解析Doris ZoneMap索引机制
ZoneMapIndex 实现分析
zone_map_index.cpp
实现了 Apache Doris 中的 ZoneMap 索引机制。ZoneMap 是一种轻量级索引,用于记录数据区域(zone)的最小值、最大值以及 NULL 值分布信息,实现快速的数据过滤和查询优化。
核心数据结构
ZoneMap 结构体(
ZoneMap
):min_value/max_value
: 区域的最小/最大值has_null/has_not_null
: NULL 值分布标记pass_all
: 是否包含所有值的标记
ZoneMapPB protobuf(
gensrc\proto\segment_v2.protoZoneMapPB
):message ZoneMapPB {optional bytes min = 1; // 最小非NULL值optional bytes max = 2; // 最大非NULL值 optional bool has_null = 3; // 是否包含NULL值optional bool has_not_null = 4; // 是否包含非NULL值optional bool pass_all = 5; // 是否包含所有值 }
为什么
1. 查询性能优化
ZoneMap 索引的核心价值在于快速数据过滤。在执行查询时,可以通过比较查询条件与 ZoneMap 中的 min/max 值,快速判断某个数据区域是否可能包含目标数据:
- 区域裁剪(Zone Pruning):如果查询条件的值不在
[min, max]
范围内,则该区域不可能包含目标数据,可以直接跳过 - NULL 优化:通过
has_null/has_not_null
标记,可以快速处理 IS NULL/IS NOT NULL 查询
2. 存储效率
相比其他索引(如 B+ 树索引),ZoneMap 的存储开销极小:
- 每个区域只需存储 min/max 两个值和少量标记位
- 不需要维护复杂的树结构
- 内存占用低,适合大规模数据
3. 层级优化
实现了两级 ZoneMap 架构:
- 页面级 ZoneMap:每个数据页面的统计信息
- 段级 ZoneMap(
ZoneMapIndexPB
):整个段的聚合统计信息
这种设计支持多级过滤:先通过段级 ZoneMap 快速判断整个段是否可能包含目标数据,再通过页面级 ZoneMap 进行精确过滤。
怎么做
都是be\src\olap\rowset\segment_v2\zone_map_index.cpp 定义的
1. 写入流程(ZoneMapIndexWriter)
TypedZoneMapIndexWriter
实现了写入逻辑:
数据收集阶段
void add_values(const void* values, size_t count) override {if (count > 0) {_page_zone_map.has_not_null = true;}// 计算最小值和最大值auto [min, max] = std::minmax_element(vals, vals + count);if (unaligned_load<ValType>(min) < unaligned_load<ValType>(_page_zone_map.min_value)) {_field->type_info()->direct_copy_may_cut(_page_zone_map.min_value,reinterpret_cast<const void*>(min));}// 类似处理最大值...
}
页面刷新阶段(flush
)
Status flush() override {// 1. 更新段级ZoneMap(聚合统计)if (_field->compare(_segment_zone_map.min_value, _page_zone_map.min_value) > 0) {_field->type_info()->direct_copy_may_cut(_segment_zone_map.min_value,_page_zone_map.min_value);}// 2. 序列化页面ZoneMapZoneMapPB zone_map_pb;_page_zone_map.to_proto(&zone_map_pb, _field);// 3. 存储到IndexedColumn_values.push_back(std::move(serialized_zone_map));return Status::OK();
}
完成阶段(finish
)
Status finish(io::FileWriter* file_writer, ColumnIndexMetaPB* index_meta) override {// 1. 存储段级ZoneMap到元数据_segment_zone_map.to_proto(meta->mutable_segment_zone_map(), _field);// 2. 使用IndexedColumnWriter写入所有页面ZoneMapIndexedColumnWriter writer(options, type_info, file_writer);for (auto& value : _values) {RETURN_IF_ERROR(writer.add(&value_slice));}return writer.finish(meta->mutable_page_zone_maps());
}
2. 读取流程(ZoneMapIndexReader)
ZoneMapIndexReader
负责读取:
加载阶段(load
)
Status load(bool use_page_cache, bool kept_in_memory,OlapReaderStatistics* index_load_stats = nullptr) {return _load_once.call([this, ...] {return _load(use_page_cache, kept_in_memory, std::move(_page_zone_maps_meta), index_load_stats);});
}
实际加载逻辑(_load
)
Status _load(...) {// 1. 创建IndexedColumnReader读取ZoneMap数据IndexedColumnReader reader(_file_reader, *page_zone_maps_meta);RETURN_IF_ERROR(reader.load(...));// 2. 读取所有页面ZoneMap并解析for (int i = 0; i < reader.num_values(); ++i) {vectorized::MutableColumnPtr column = vectorized::ColumnString::create();RETURN_IF_ERROR(iter.seek_to_ordinal(i));RETURN_IF_ERROR(iter.next_batch(&num_read, column));// 解析protobuf格式的ZoneMapif (!_page_zone_maps[i].ParseFromArray(column->get_data_at(0).data,column->get_data_at(0).size)) {return Status::Corruption("Failed to parse zone map");}}
}
3. 类型系统支持
实现了模板化的 ZoneMap 索引,支持所有基本数据类型:
#define APPLY_FOR_PRIMITITYPE(M) \M(TYPE_TINYINT) \M(TYPE_SMALLINT) \M(TYPE_INT) \M(TYPE_BIGINT) \M(TYPE_LARGEINT) \M(TYPE_FLOAT) \M(TYPE_DOUBLE) \// ... 更多类型
通过模板特化(TypedZoneMapIndexWriter
)为每种类型提供最优的实现:
template <PrimitiveType Type>
class TypedZoneMapIndexWriter final : public ZoneMapIndexWriter {using ValType = PrimitiveTypeTraits<Type>::StorageFieldType;// 类型特定的优化实现
};
4. 存储格式优化
IndexedColumn 存储
页面级 ZoneMap 使用 IndexedColumnWriter
存储:
const auto* type_info = get_scalar_type_info<FieldType::OLAP_FIELD_TYPE_BITMAP>();
IndexedColumnWriterOptions options;
options.write_ordinal_index = true; // 支持按序号访问
options.write_value_index = false; // 不需要值索引
options.encoding = EncodingInfo::get_default_encoding(type_info, false);
options.compression = NO_COMPRESSION; // 当前不压缩
内存管理
使用 vectorized::Arena
进行内存管理:
vectorized::Arena _arena;
// min/max 值内存由 Arena 管理,避免频繁内存分配
_page_zone_map.min_value = _field->allocate_zone_map_value(_arena);
5. 性能优化策略
延迟加载(Lazy Loading)
DorisCallOnce<Status> _load_once;
// 确保ZoneMap只加载一次,且在实际需要时才加载
批处理优化
在 add_values
中,使用 std::minmax_element
一次性处理整个批次:
auto [min, max] = std::minmax_element(vals, vals + count);
总结
ZoneMapIndex 通过轻量级统计信息实现了高效的数据过滤,其核心优势在于:
- 存储高效:每个区域只需存储 min/max 和少量标记
- 查询快速:支持基于范围的快速区域裁剪
- 架构清晰:两级 ZoneMap 支持多级过滤优化
- 类型丰富:模板化设计支持所有基本数据类型
- 实现优化:延迟加载、批处理、内存池等性能优化
这种设计使得 Doris 能够在不显著增加存储开销的前提下,大幅提升范围查询和过滤查询的性能,是列式存储系统中索引设计的优秀实践。
页面级别的zone_map
页面级别的zone_map是指为每个数据页面(Data Page)单独维护的zone_map索引,它记录了该页面内数据的最小值、最大值、是否存在NULL值等统计信息。
页面级别的zone_map是Doris存储引擎中的一种细粒度索引机制,其核心特点包括:
- 层级结构:每个数据页面都有一个对应的ZoneMapPB结构
- 存储格式:通过
IndexedColumnMetaPB page_zone_maps
字段存储,使用IndexedColumn结构 - 数据内容:包含每个页面的min、max、has_null、has_not_null等统计信息
从protobuf定义可以看出:
message ZoneMapIndexPB {// required: segment-level zone mapoptional ZoneMapPB segment_zone_map = 1;// required: zone map for each data page is stored in an IndexedColumn with ordinal indexoptional IndexedColumnMetaPB page_zone_maps = 2;
}
为什么(Why)
页面级别zone_map的设计目的:
- 精确过滤:相比段级别的zone_map,页面级别提供更细粒度的数据过滤能力
- 减少I/O:可以精确判断哪些页面需要读取,避免不必要的磁盘I/O
- 查询优化:结合ordinal index,可以快速定位到包含目标数据的具体页面
怎么做(How)
1. 构建过程分析
从代码分析可以看出,页面级别zone_map的构建流程:
在segment_writer.cpp中:
// 创建列写入器时决定是否启用zone_map
opts.need_zone_map = column.is_key() || schema->keys_type() != KeysType::AGG_KEYS;
在zone_map_index.cpp中:
TypedZoneMapIndexWriter::add_values()
:为每个传入的值更新当前页面的统计信息TypedZoneMapIndexWriter::flush()
:页面完成时,将当前页面的zone_map写入IndexedColumnTypedZoneMapIndexWriter::finish()
:完成所有页面zone_map的构建
2. 存储结构分析
页面级别zone_map使用IndexedColumn结构存储,这意味着:
- 有序存储:每个页面的zone_map按页面顺序存储
- 索引支持:通过ordinal index可以快速定位到特定页面的zone_map
- 压缩编码:支持各种编码和压缩算法
3. 与IndexedColumnWriter的关系
虽然indexed_column_writer.cpp
本身不直接处理zone_map,但它提供了基础存储结构:
- IndexedColumnWriter负责构建有序的IndexedColumn
- ZoneMapIndexWriter利用IndexedColumnWriter来存储页面级别的zone_map数据
- 每个页面的zone_map作为单独的值添加到IndexedColumn中
4. 查询使用流程
查询时使用页面级别zone_map的流程:
- 段级别过滤:首先使用segment_zone_map进行粗粒度过滤
- 页面级别过滤:遍历page_zone_maps,找出可能包含目标数据的页面
- 精确读取:只读取通过过滤的页面,大幅减少I/O
总结
页面级别的zone_map是Doris存储引擎中细粒度索引的核心组件,它通过为每个数据页面维护独立的统计信息,实现了更精确的数据过滤和查询优化。其设计体现了空间换时间的经典思想,通过额外的存储开销换取显著的查询性能提升。