核心结论DuckDB 原生表的主线只有一条Table→ Row Group→ ColumnData→ Column Segment→ DuckDB Block→ Buffer Manager→ Vector / DataChunk表按行划分为 Row GroupRow Group 内按列组织每列拆成可独立压缩的 Column SegmentSegment 映射到固定大小的 DuckDB Block查询时由 Buffer Manager 把 Block 读入内存并解码成 Vector。这里最重要的两个边界是Column Segment逻辑压缩与解码单元。DuckDB Block物理存储、I/O 和缓存单元。ChatGPT Image 2026年7月19日 14_12_10ChatGPT Image 2026年7月19日 13_59_41ChatGPT Image 2026年7月19日 15_51_42示例表全文使用同一张表说明CREATE TABLE events (tenant_id INTEGER,ts TIMESTAMP,payload VARCHAR);3. DuckDB 原生存储层DuckDB 存储层可以用一句话概括表先按行切成 Row Group每个 Row Group 内按列组织成 ColumnData每列再切成若干 Column SegmentSegment 压缩后放进数据库文件的 BlockBlock 由 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 ManagerParquet逻辑表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 与操作系统页面对齐。Table: eventsRow Group 0Row Group 1tenant_id ColumnDatats ColumnDatapayload ColumnDataSegment T0Segment T1Segment TS0Segment P0Segment P1Block 42Block 43Buffer ManagerVector / DataChunk3.1 Row Group一批连续行Row Group 是表的水平分区。例如events├── Row Group 0行 0 122879├── Row Group 1行 122880 245759└── …它的作用是把大表分成可管理的行范围保存统计信息用于跳过不可能命中的数据提供原生表并行扫描的重要边界为列压缩提供组织范围。Row Group 不是行存。 它只规定一批行号内部仍然按列存储。3.2 ColumnDataRow Group 中的一列在一个 Row Group 内每列对应一个 ColumnDataRow Group 0├── tenant_id ColumnData├── ts ColumnData└── payload ColumnDataColumnData 负责管理该列的Segment 集合扫描位置Scan / Select / Skip统计信息和更新状态。3.3 Column Segment一列中的连续压缩片段一列可以包含多个 Segmentpayload ColumnData├── Segment P0├── Segment P1└── Segment P2一个 Segment 通常包含start / countcompressionstatisticsblock_idblock_offsetsegment_sizeSegment 的行数并不固定。它取决于数据类型、数据宽度、压缩率和 Block 可用空间。因此宽字符串列通常比整数列产生更多 Segment。Segment 与 Block 的对应关系4.1 多个小 Segment 可以共享一个 BlockBlock 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 可能接近独占一个 BlockConstant Segment 可能无需普通数据 Block长字符串或复杂类型可能另外引用溢出或子存储。4.2 读取粒度与解码粒度不同假设查询只需要 Block 42 中的 Segment T1逻辑解码只解码 Segment T1物理读取通常加载整个 Block 42因此DuckDB 可以只解码需要的 Segment但当 Block 尚未在内存中时物理上通常仍需读取该 Segment 所在的完整 Block。这也是优化数据搬运时必须明确的边界。Block 与 Buffer ManagerDuckDB Block 是 .duckdb 文件内部的固定大小物理单元常见默认分配大小约为 256 KiB。.duckdb 文件├── 文件头 / 数据库元数据├── Block 0├── Block 1├── Block 2└── …Buffer Manager 负责把 Block 从文件读入内存缓存和淘汰 BlockPin 当前正在使用的 Block执行软件预取协调数据库页和查询内存。不要混淆概念 含义DuckDB Block DuckDB 自己的物理 I/O 与缓存单元OS Page 操作系统内存页常见为 4 KiBDataChunk 查询执行中的内存批次不是磁盘块一个 256 KiB DuckDB Block 在常见 4 KiB 页系统上覆盖约 64 个 OS Page但 Segment 在 Block 内未必按 OS Page 对齐。一次查询如何读取数据查询SELECT payloadFROM eventsWHERE tenant_id 42;读取路径否是检查 Row Group / Segment 统计读取 tenant_id 所在 Block解码 tenant_id Vector生成 SelectionVector是否有幸存行?payload 执行 Skip读取 payload 所在 Block按 Select 或 Scan 解码 payload输出 DataChunk关键点各列拥有独立扫描状态过滤列可以先读取没有幸存行时载荷列可以 Skip有幸存行时再对载荷列执行 Select 或 Scan但物理读取的基本单位仍然是 Block。回到顶部DuckDB 原生存储与 Parquet 的区别7. 两套层级DuckDB 原生表Table→ Row Group→ ColumnData→ Column Segment→ DuckDB Block→ Buffer ManagerParquet 文件File→ Row Group→ Column Chunk→ Dictionary Page / Data Page→ 文件字节区间→ Reader / OS Cache