Duckdb存储结构

DuckDB 存储结构:从逻辑表到磁盘 Block

1. 核心结论

DuckDB 原生表的主线只有一条:

Table
  → Row Group
    → ColumnData
      → Column Segment
        → DuckDB Block
          → Buffer Manager
            → Vector / DataChunk

表按行划分为 Row Group;Row Group 内按列组织;每列拆成可独立压缩的 Column Segment;Segment 映射到固定大小的 DuckDB Block;查询时由 Buffer Manager 把 Block 读入内存并解码成 Vector。

这里最重要的两个边界是:

  • Column Segment:逻辑压缩与解码单元。
  • DuckDB Block:物理存储、I/O 和缓存单元。

ChatGPT Image 2026年7月19日 14_12_10

ChatGPT Image 2026年7月19日 13_59_41
ChatGPT Image 2026年7月19日 15_51_42

2. 示例表

全文使用同一张表说明:

CREATE TABLE events (
    tenant_id INTEGER,
    ts TIMESTAMP,
    payload VARCHAR
);

3. DuckDB 原生存储层

DuckDB 存储层可以用一句话概括:

表先按行切成 Row Group;每个 Row Group 内按列组织成 ColumnData;每列再切成若干 Column Segment;Segment 压缩后放进数据库文件的 Block;Block 由 Buffer Manager 读入内存。

Table
  └── Row Group
       ├── ColumnData A
       │    ├── Column Segment A0
       │    └── Column Segment A1
       └── ColumnData B
            ├── Column Segment B0
            ├── Column Segment B1
            └── Column Segment B2
                     ↓
              Block ID + Block Offset
                     ↓
             .duckdb 数据库文件
                     ↓
               Buffer Manager

Parquet:
逻辑表(Table)
  └── 映射为 Parquet File
       └── Row Group
            ├── Column Chunk A
            │    ├── Dictionary Page(可选)
            │    ├── Data Page A0
            │    └── Data Page A1
            └── Column Chunk B
                 ├── Dictionary Page(可选)
                 ├── Data Page B0
                 ├── Data Page B1
                 └── Data Page B2
                          ↓
                   文件偏移(file offsets)
                          ↓
                     .parquet 文件
                          ↓
              Reader / 文件系统 / OS Page Cache

需要特别注意:DuckDB 原生存储中通常不把物理单位称为传统数据库的“Page”,它的核心物理 I/O 单位叫 Block。如果你说的 page 是操作系统的 4 KiB 内存页,那么一个 DuckDB Block 通常会覆盖多个 OS page。在常见的 4 KiB OS Page 环境中,DuckDB 的 Block 边界是对齐的;但这不是对任意操作系统 Page 大小都成立的通用保证。
但parquet它也可能:
一个 Parquet Page 跨多个 OS Page;
一个 OS Page 同时包含两个相邻 Parquet Page 的字节;
Parquet Page 的开始位置不和 OS Page 对齐。

因为 Parquet Page 是可变长度的文件格式对象,而不是固定大小的物理 I/O 页。Parquet 的目标是紧凑存储和跨系统交换,因此 Page 通常在 Column Chunk 中连续排列,不按 4 KiB 等操作系统页面边界补齐。
Parquet 格式不要求 Page 与操作系统页面对齐。

flowchart TD T[Table: events] RG0[Row Group 0] RG1[Row Group 1] C1[tenant_id ColumnData] C2[ts ColumnData] C3[payload ColumnData] S10[Segment T0] S11[Segment T1] S20[Segment TS0] S30[Segment P0] S31[Segment P1] B42[Block 42] B43[Block 43] BM[Buffer Manager] V[Vector / DataChunk] T --> RG0 T --> RG1 RG0 --> C1 RG0 --> C2 RG0 --> C3 C1 --> S10 C1 --> S11 C2 --> S20 C3 --> S30 C3 --> S31 S10 --> B42 S11 --> B42 S20 --> B42 S30 --> B43 S31 --> B43 B42 --> BM B43 --> BM BM --> V

3.1 Row Group:一批连续行

Row Group 是表的水平分区。例如:

events
├── Row Group 0:行 0 ~ 122879
├── Row Group 1:行 122880 ~ 245759
└── ...

它的作用是:

  • 把大表分成可管理的行范围;
  • 保存统计信息,用于跳过不可能命中的数据;
  • 提供原生表并行扫描的重要边界;
  • 为列压缩提供组织范围。

Row Group 不是行存。 它只规定一批行号;内部仍然按列存储。

3.2 ColumnData:Row Group 中的一列

在一个 Row Group 内,每列对应一个 ColumnData

Row Group 0
├── tenant_id ColumnData
├── ts ColumnData
└── payload ColumnData

ColumnData 负责管理该列的:

  • Segment 集合;
  • 扫描位置;
  • Scan / Select / Skip
  • 统计信息和更新状态。

3.3 Column Segment:一列中的连续压缩片段

一列可以包含多个 Segment:

payload ColumnData
├── Segment P0
├── Segment P1
└── Segment P2

一个 Segment 通常包含:

start / count
compression
statistics
block_id
block_offset
segment_size

Segment 的行数并不固定。它取决于数据类型、数据宽度、压缩率和 Block 可用空间。因此宽字符串列通常比整数列产生更多 Segment。


4. Segment 与 Block 的对应关系

4.1 多个小 Segment 可以共享一个 Block

Block 42
┌──────────────────────────────┐
│ Segment T0    offset = 0     │
├──────────────────────────────┤
│ Segment T1    offset = 55 KB │
├──────────────────────────────┤
│ Segment TS0   offset = 105KB │
├──────────────────────────────┤
│ 剩余空间                     │
└──────────────────────────────┘

定位依靠:

block_id + block_offset

因此:

  • 相同 block_id、不同 block_offset:多个 Segment 共享一个 Block;
  • 大 Segment 可能接近独占一个 Block;
  • Constant Segment 可能无需普通数据 Block;
  • 长字符串或复杂类型可能另外引用溢出或子存储。

4.2 读取粒度与解码粒度不同

假设查询只需要 Block 42 中的 Segment T1:

逻辑解码:只解码 Segment T1
物理读取:通常加载整个 Block 42

因此:

DuckDB 可以只解码需要的 Segment,但当 Block 尚未在内存中时,物理上通常仍需读取该 Segment 所在的完整 Block。

这也是优化数据搬运时必须明确的边界。


5. Block 与 Buffer Manager

DuckDB Block 是 .duckdb 文件内部的固定大小物理单元,常见默认分配大小约为 256 KiB。

.duckdb 文件
├── 文件头 / 数据库元数据
├── Block 0
├── Block 1
├── Block 2
└── ...

Buffer Manager 负责:

  • 把 Block 从文件读入内存;
  • 缓存和淘汰 Block;
  • Pin 当前正在使用的 Block;
  • 执行软件预取;
  • 协调数据库页和查询内存。

不要混淆:

概念 含义
DuckDB Block DuckDB 自己的物理 I/O 与缓存单元
OS Page 操作系统内存页,常见为 4 KiB
DataChunk 查询执行中的内存批次,不是磁盘块

一个 256 KiB DuckDB Block 在常见 4 KiB 页系统上覆盖约 64 个 OS Page,但 Segment 在 Block 内未必按 OS Page 对齐。


6. 一次查询如何读取数据

查询:

SELECT payload
FROM events
WHERE tenant_id = 42;

读取路径:

flowchart TD A[检查 Row Group / Segment 统计] B[读取 tenant_id 所在 Block] C[解码 tenant_id Vector] D[生成 SelectionVector] E{是否有幸存行?} F[payload 执行 Skip] G[读取 payload 所在 Block] H[按 Select 或 Scan 解码 payload] I[输出 DataChunk] A --> B --> C --> D --> E E -- 否 --> F E -- 是 --> G --> H --> I

关键点:

  • 各列拥有独立扫描状态;
  • 过滤列可以先读取;
  • 没有幸存行时,载荷列可以 Skip
  • 有幸存行时,再对载荷列执行 SelectScan
  • 但物理读取的基本单位仍然是 Block。

DuckDB 原生存储与 Parquet 的区别

7. 两套层级

DuckDB 原生表
Table
  → Row Group
    → ColumnData
      → Column Segment
        → DuckDB Block
          → Buffer Manager

Parquet 文件
File
  → Row Group
    → Column Chunk
      → Dictionary Page / Data Page
        → 文件字节区间
          → Reader / OS Cache

8. 概念对应

DuckDB Parquet 说明
Table Parquet File 或文件集合 顶层数据对象,近似对应
Row Group Row Group 都表示一批行,最接近
ColumnData Column Chunk 都表示某 Row Group 中的一列
Column Segment Page 都是细粒度列数据单元,但不完全等价
DuckDB Block 无直接对应物 Parquet 格式内部没有固定 Block 层
Buffer Manager Reader + OS/对象存储缓存 属于读取实现,而非 Parquet 格式本身

最重要的一点是:

Parquet Page 不是 DuckDB Block。

Parquet 的 Page 是文件中的编码和压缩字节区间;它最终落在哪些文件系统块或 OS Page 上,由底层存储系统决定。


9. 压缩方式的区别

DuckDB:Segment 自适应压缩

Column Segment
  → Analyze 数据分布
  → 选择 Compression Function
  → Compress
  → 写入 DuckDB Block

特点:

  • 每个 Segment 可以选择不同算法;
  • 偏向快速解码和查询执行;
  • 与固定大小 Block 和 Buffer Manager 深度结合;
  • 读取时支持 Scan / Select / Skip

示例列可能采用:

可能的编码
tenant_id Bit Packing、FOR、RLE
ts FOR、Bit Packing
payload Dictionary、FSST、Zstd

Parquet:Page Encoding + Compression Codec

Column Chunk
  → 切分 Page
  → Page Encoding
  → 通用压缩 Codec
  → 顺序写入文件

特点:

  • Page 是主要编码与压缩边界;
  • Encoding 和通用 Codec 是两层;
  • 强调跨系统兼容、批量扫描和文件可移植性;
  • Footer 记录 Row Group 与 Column Chunk 的位置;
  • 可选 Page Index 支持更细粒度跳过。

10. 核心差异表

维度 DuckDB 原生存储 Parquet
定位 数据库内部格式 通用列式文件格式
核心层级 Row Group → ColumnData → Segment → Block Row Group → Column Chunk → Page
压缩单元 Column Segment Page
物理 I/O 单元 固定大小 DuckDB Block 由文件系统、对象存储和读取器决定
元数据入口 数据库内部元数据 文件尾 Footer
读取管理 Buffer Manager Parquet Reader + 底层缓存
主要目标 查询执行、更新、空间复用 交换、归档、共享、批量扫描

11. 最终心智模型

【DuckDB】
一批行:Row Group
一列:ColumnData
一段压缩数据:Column Segment
物理读写:Block
内存管理:Buffer Manager
执行输出:Vector / DataChunk

【Parquet】
一批行:Row Group
一列:Column Chunk
一段编码压缩数据:Page
定位入口:Footer
物理读写:由读取器和底层文件系统决定

DuckDB 是数据库内部存储:强调 Segment、固定 Block 和 Buffer Manager。Parquet 是交换型列式文件:强调 Column Chunk、Page 和 Footer。

posted @ 2026-07-19 14:52  aixueforever  阅读(68)  评论(0)    收藏  举报