6.1 列式布局、Parquet 与编码
行式和列式布局决定查询要读取、解码哪些字节。分析查询只要宽表中的两列,行式页仍可能带上许多无关数据;列式布局能分开读取列,并利用相似值做编码。本课从文件结构检查这种节省来自哪里,避免把“少读了数据”算成“压缩了文件”。
本课目标
- 比较 row-oriented 与 column-oriented layout;
- 解释 Parquet file、row group、column chunk 与 page;
- 区分 encoding 与 compression;
- 理解 dictionary、RLE/bit-packing 与 delta encoding 的适用条件;
- 说明 nested data 为什么需要 definition/repetition levels。
Row layout 的优势与成本
行式 page 把一行的 attributes 放在相近位置:
[id=1, type=device, value=120000, name=...]
[id=2, type=spare_part, value=95000, name=...]适合:
- primary-key lookup 后返回大部分 row;
- insert/update 一个完整 row;
- transaction/MVCC 以 row version 为核心;
- narrow operational queries。
分析 query:
SELECT item_type_id, SUM(value_cents)
FROM catalog_items
WHERE value_cents >= 50000
GROUP BY item_type_id;row store 扫描时通常必须读取包含整行的 pages,即使 executor 只投影两列。但不应说“必然把其余 20 列全部解码进对象”:engine 可以只解析所需 fields,out-of-line values 也可能不读取。主要浪费常在 page bytes/cache density,而非所有 columns 一定完整 materialize。
index-only scan、vertical partitioning 和 columnar extension 还能改变对比。
Column layout
列式组织在一批 rows 内分别保存 columns:
item_id: [1, 2, 3, ...]
item_type_id: [1, 2, 1, ...]
value_cents: [120000, 95000, 180000, ...]
name: [...]优势:
- projection 只读取需要的 column chunks;
- 同 type/semantic values 相邻,利于 specialized encoding/compression;
- tight arrays 适合 SIMD/vectorized operators;
- min/max、dictionary、Bloom 等 metadata 可支持 skipping。
代价:
- reconstruct wide rows 需要对齐多个 column vectors;
- small random row update 与 version maintenance 更复杂;
- many tiny writes 往往需要 delta store、buffering 或 immutable parts;
- selective lookup 需要 index/primary-key structure 定位 granule。
现代系统常混合布局:row-oriented write buffer + columnar main store、columnar segment + delete bitmap、OLTP primary + analytical replica。不要只凭产品标签判断实际 path。
Parquet file layout
Parquet 是 self-describing columnar file format,不是 database engine。概念结构:
PAR1
Row Group 0:
Column Chunk A -> data/dictionary pages
Column Chunk B -> data/dictionary pages
Row Group 1:
Column Chunk A -> ...
Column Chunk B -> ...
File Metadata
metadata length
PAR1Row group
一批 rows 的 horizontal partition。每个 row group 包含这些 rows 的各 column chunk。row-group size 影响:
- parallel scheduling;
- metadata pruning 粒度;
- compression/dictionary scope;
- memory needed to write/read;
- object-store request pattern。
不存在跨 workload 固定的“128 MB–1 GB 正确值”。要结合 filesystem/object size、query selectivity、column width 与 writer memory 测量。
Column chunk
某 row group 中某 leaf column 的数据,由连续 pages 构成。file metadata 记录 column-chunk locations 和统计等信息,reader 先读 footer/metadata,再选择所需 chunks。
Page
column chunk 内的 encoding/compression unit。dictionary page(若有)与 data pages 带各自 headers。optional page index 能帮助兼容 reader 跳过 pages;没有 page index 或 reader 不使用时,不能声称 page-level pruning 必然发生。
“page 是最小物理 I/O 单元”也过度绝对。reader/object store 可以按 byte range 合并读取多个 pages,压缩块和 filesystem block 也会影响实际 I/O。
官方格式:Apache Parquet File Format。
Encoding 与 compression 不同
- encoding 利用 value structure,把 logical values 转成更紧凑/可处理 representation;
- compression codec 在 encoded bytes 上进一步压缩,例如 Snappy、ZSTD;
- encryption/checksum 又是不同职责。
同一 column chunk 可以部分 dictionary encoded、部分采用其他 encoding,具体由 writer 与 format rules 决定。
Dictionary encoding
dictionary: 0=device, 1=spare_part, 2=consumable
ids: 0, 0, 1, 0, 2, 1, ...适合低/中 cardinality 或重复值较多的 column。收益取决于 dictionary bytes、ID bit width、cache 和 data distribution。cardinality 太高时,dictionary 可膨胀或 writer fallback 到 plain encoding。
不能说“Parquet 对 string 默认总会用 dictionary”或给一个跨实现固定 threshold。writer option、版本、page/chunk distribution 都会改变选择。
RLE 与 bit packing
Run-length encoding 把连续相同值编码为 (value, run_length):
1,1,1,1,2,2,3 -> (1×4),(2×2),(3×1)若 values 不形成 runs,单纯 RLE 可能变大。Parquet 的 RLE/bit-packed hybrid 可按 runs 与 packed groups 表示 small integers/dictionary IDs;不要把它等同于只会压连续重复值的教科书 RLE。
sorting/clustering 能增加 runs、缩小 min/max ranges,却也会影响 ingest、其他 query dimensions 与 distributed skew。
Delta encoding
monotonic/locally correlated integers:
100, 103, 108, 115
base=100, deltas=3,5,7small deltas 可用更少 bits/varints。timestamp、sorted ID 常受益;随机 values 或 abrupt jumps 收益较小。字符串还可做 delta byte-array/prefix encoding。
NULL 与 nested data
columnar file 必须保留 row alignment。不同格式表示 NULL 的方法不同:Arrow 内存数组常用 validity bitmap,Parquet 使用 definition levels;两者都需要保留逻辑行位置,而不是为每个 NULL 写完整 value。
List/struct/map 会让“第 i 个 column value 对应第 i 行”变复杂。Dremel-style repetition/definition levels 描述:
- nested path 到哪一层 defined;
- repeated field 的 value 属于当前还是新 parent record。
schema evolution 还要处理 field identity、nullable/default、type widening 和 reader/writer compatibility。仅凭同名 field 合并 nested schemas 可能产生错误。
ORC 的相似与不同
ORC 也使用 columnar stripes、streams、indexes/statistics 与 footer/postscript。它和 Parquet 目标相似,但 physical encoding、metadata 和 reader ecosystem 不同。不能把 ORC “stripe index”说成 Parquet 完全没有同类 metadata;Parquet 也有 row-group statistics、optional column/page index 和 Bloom filter。
选格式应比较 engine integration、schema evolution、predicate support、codec、nested types、metadata size 和 object-store behavior,而不是只看理论 compression ratio。
Compression ratio 如何报告
encoded_compressed_file_bytes / logical_uncompressed_input_bytes按这里的定义,假设 100 MB 输入写成 25 MB 文件,比例为 0.25,越小表示越省空间;有些工具报告其倒数 4:1,图表必须注明方向。
必须说明 input representation:CSV text、Arrow arrays、database rows 的 baseline 不同。column projection 减少 query 读取 bytes,不减少已经写好的 full Parquet file size,不能把“未读取 18 列”算作 storage compression 来源。
报告同时包含:
- file bytes;
- selected-column bytes read;
- decompressed bytes;
- encode/decode CPU;
- writer memory;
- query wall time;
- row-group/page pruning。
本课小结
列式布局的核心收益是少读 columns、同类值高效编码和 vector-friendly memory。Parquet 用 row group、column chunk、page 与 footer metadata 把这些能力装进文件,但 skipping 与性能仍依赖 writer layout 和 reader implementation。下一课进入扫描执行,区分 projection、pruning、predicate evaluation 与 late materialization。