DuckDB存储页面布局:B+树索引与数据页的物理结构
【免费下载链接】duckdb 项目地址: https://gitcode.com/gh_mirrors/duc/duckdb
你是否曾好奇数据库如何高效管理海量数据?当执行SELECT * FROM table WHERE id=100时,DuckDB如何在毫秒级找到目标数据?本文将深入解析DuckDB的底层存储机制,揭示B+树索引与数据页的物理结构,让你彻底理解数据如何在磁盘上组织和访问。
存储系统概览
DuckDB采用自包含的单文件存储架构,所有数据和元信息都封装在一个文件中。这种设计简化了部署和迁移,但对存储效率提出了更高要求。存储系统的核心组件包括:
- 块管理器(Block Manager):负责磁盘空间分配与释放,管理数据块的生命周期
- 元数据管理器(Metadata Manager):维护数据字典、索引信息等关键元数据
- B+树索引:加速数据检索的核心数据结构
- 数据页:存储实际表数据的物理单元
核心代码定义可见 src/storage/checkpoint_manager.cpp,其中BlockManager类负责块分配验证:
void Storage::VerifyBlockAllocSize(const idx_t block_alloc_size) {
if (!IsPowerOfTwo(block_alloc_size)) {
throw InvalidInputException("the block size must be a power of two, got %llu", block_alloc_size);
}
if (block_alloc_size < MIN_BLOCK_ALLOC_SIZE) {
throw InvalidInputException(
"the block size must be greater or equal than the minimum block size of %llu, got %llu",
MIN_BLOCK_ALLOC_SIZE, block_alloc_size);
}
}
数据块(Block)基础结构
DuckDB将磁盘空间划分为固定大小的块(Block),作为存储分配的基本单位。块大小必须是2的幂,默认配置下最小为4KB,最大不超过2GB(32位整数限制)。
块分配验证机制
块管理器在创建块时执行严格验证,确保块大小符合系统要求:
- 必须是2的幂(便于地址计算和对齐)
- 最小4KB(
MIN_BLOCK_ALLOC_SIZE) - 最大不超过2GB(32位有符号整数限制)
相关实现见 src/storage/storage_info.cpp 第88-108行。
块的类型划分
DuckDB将块分为两类:
- 数据块:存储表数据和索引结构
- 元数据块:存储数据库字典、表结构等元信息
元数据管理器采用特殊编码存储指针,将64位指针拆分为56位块ID和8位块内索引:
MetaBlockPointer MetadataManager::GetDiskPointer(MetadataPointer pointer, uint32_t offset) {
idx_t block_pointer = idx_t(pointer.block_index);
block_pointer |= idx_t(pointer.index) << 56ULL;
return MetaBlockPointer(block_pointer, offset);
}
B+树索引结构
DuckDB使用B+树作为主要索引结构,优化范围查询和有序访问。与传统B树不同,B+树将所有数据存储在叶子节点,内部节点仅存储索引键,形成"索引-数据"分离的层次结构。
B+树节点布局
每个B+树节点对应一个物理块,节点结构包含:
- 节点头信息(类型、键数量、校验和等)
- 索引键数组
- 子节点指针/数据指针数组
虽然DuckDB源码中未直接提供BPlusTreeIndex实现,但可通过检查点逻辑推断索引存储方式。在 src/storage/checkpoint_manager.cpp 中,索引信息通过IndexStorageInfo结构体持久化:
// 索引存储信息序列化
void CheckpointWriter::WriteIndex(IndexCatalogEntry &index_catalog_entry, Serializer &serializer) {
serializer.WriteProperty(100, "index", &index_catalog_entry);
}
// 索引存储信息反序列化
void CheckpointReader::ReadIndex(CatalogTransaction transaction, Deserializer &deserializer) {
auto create_info = deserializer.ReadProperty<unique_ptr<CreateInfo>>(100, "index");
auto &info = create_info->Cast<CreateIndexInfo>();
auto root_block_pointer = deserializer.ReadPropertyWithDefault<BlockPointer>(101, "root_block_pointer", BlockPointer());
}
B+树操作特性
- 插入优化:当节点满时分裂为两个节点,保持树平衡
- 范围查询:叶子节点通过双向链表连接,支持高效范围扫描
- 聚簇索引:表数据按索引键顺序存储,进一步加速查询
数据页物理结构
DuckDB的数据页采用面向列的存储方式,即同一列的数据连续存储,这种设计显著提升了分析查询性能。
列存数据页布局
每个数据页包含一个或多个列段(Column Segment),结构如下:
┌─────────────────────────────────────────────┐
│ 页面头 (Page Header) │
├─────────────┬─────────────┬─────────────────┤
│ 列段1头信息 │ 列段1数据 │ │
├─────────────┼─────────────┤ │
│ 列段2头信息 │ 列段2数据 │ 未使用空间 │
├─────────────┼─────────────┤ │
│ ... │ ... │ │
└─────────────┴─────────────┴─────────────────┘
列段头信息包含数据类型、压缩算法、统计信息等元数据,实际数据采用自适应压缩存储。
部分块管理
对于小数据量的列段,DuckDB采用部分块(Partial Block)机制合并多个小列段到一个物理块,减少空间浪费:
// 部分块创建与管理
PartialBlockForCheckpoint::PartialBlockForCheckpoint(ColumnData &data, ColumnSegment &segment, PartialBlockState state,
BlockManager &block_manager)
: PartialBlock(state, block_manager, segment.block) {
AddSegmentToTail(data, segment, 0);
}
// 部分块合并
void PartialBlockForCheckpoint::Merge(PartialBlock &other_p, idx_t offset, idx_t other_size) {
auto &other = other_p.Cast<PartialBlockForCheckpoint>();
auto &buffer_manager = block_manager.buffer_manager;
auto old_handle = buffer_manager.Pin(other.block_handle);
auto new_handle = buffer_manager.Pin(block_handle);
memcpy(new_handle.Ptr() + offset, old_handle.Ptr(), other_size);
// 合并段信息...
}
相关实现见 src/storage/table/column_checkpoint_state.cpp。
数据压缩策略
DuckDB根据数据类型自动选择压缩算法:
- 数值类型:使用FSST或LZ4压缩
- 字符串类型:使用字典编码+LZ4压缩
- 常量列:特殊标记为常量块,不存储重复值
存储版本控制
DuckDB支持存储格式版本控制,确保不同版本间的兼容性。当前最新存储版本为64(对应v0.9.0及以上):
const uint64_t VERSION_NUMBER = 64;
// 存储版本映射表
static const StorageVersionInfo storage_version_info[] = {
{"v0.0.4", 1}, {"v0.1.0", 1}, ..., {"v0.9.0", 64}, {"v0.9.1", 64}, {"v0.9.2", 64},
{"v0.10.0", 64}, {"v0.10.1", 64}, {"v0.10.2", 64}, {nullptr, 0}
};
版本控制实现见 src/storage/storage_info.cpp。
总结与实践建议
DuckDB的存储系统通过精心设计的B+树索引和列式数据页,在单文件架构下实现了高效的数据管理。关键优化点包括:
- 块大小选择:根据工作负载选择合适的块大小(默认4KB)
- 索引设计:为频繁过滤的列创建B+树索引
- 数据压缩:利用DuckDB自动压缩特性,平衡存储效率和查询性能
通过理解这些底层机制,你可以更好地优化数据库设计和查询性能。下一篇我们将探讨DuckDB的事务日志(WAL)机制,敬请期待!
【免费下载链接】duckdb 项目地址: https://gitcode.com/gh_mirrors/duc/duckdb
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考




