6.1 Parquet 格式:内部结构详解
Apache Parquet 是列式存储格式,由 Twitter 和 Cloudera 联合开发,是大数据生态最重要的文件格式之一。理解其内部结构对于优化 DuckDB 查询至关重要。
- Row Group(行组) Parquet 文件被水平切分为若干行组,默认每组 ~128MB 的数据(约 100-200 万行)。行组是并行读取的基本单位,DuckDB 可以多线程并行处理不同行组。
- Column Chunk(列块) 行组内每列的数据存储在连续的 Column Chunk 中。查询只需要某几列时,只需读取对应列块,其他列块完全跳过,这是列存 I/O 效率高的根本原因。
- Page(页) Column Chunk 继续细分为 Page(默认 1MB)。Page 是压缩和编码的基本单位,也是索引统计的最细粒度。
- Footer(页脚) 文件末尾存储所有元数据:Schema 定义、行组位置、每列的统计信息(min/max/null count)。读取文件时先读 Footer,根据统计信息决定是否读取某个行组。
- Dictionary Encoding 对低基数列(如 status、category)使用字典编码:先建一个值字典,实际存储字典索引号而非原始字符串,大幅节省空间("active"/"inactive" 变成 0/1)。
- RLE / Bit Packing Run Length Encoding(游程编码)对连续相同值压缩极有效。Bit Packing 将整数压缩到最小位数(如 0-7 只需 3 bit)。两者组合是 Parquet 整数列的常用压缩方式。
压缩算法对比
| 压缩算法 | 压缩率 | 速度 | 推荐场景 |
|---|---|---|---|
| SNAPPY(默认) | 中 | 极快 | 实时读写,延迟敏感 |
| ZSTD | 高 | 快 | 推荐!兼顾压缩率和速度 |
| GZIP | 高 | 慢 | 归档存储,不频繁读 |
| LZ4 | 低 | 最快 | 临时文件、高频读写 |
| BROTLI | 最高 | 很慢 | 极少场景 |
6.2 为什么列式存储比行式快?
以一个 1亿行、10列的销售表为例,对比 SELECT SUM(amount) FROM orders WHERE category='Electronics':
行式存储(MySQL) 必须读取所有 10 列 × 1亿行 = 约 5GB 数据,仅仅是为了取其中的 amount 和 category 两列。内存带宽浪费 80%。
列式存储(DuckDB) 只读 category 列(过滤)+ amount 列(计算),共约 200MB。CPU 向量化连续处理同类型数据,SIMD 指令高效利用。速度快 10-25 倍。
列存的三大优势
- I/O 减少:只读需要的列,跳过其余列
- 压缩率高:同列数据类型相同,压缩效果极佳(5-10x)
- 向量化友好:连续内存中的同类型数据,CPU SIMD 指令可批量处理
6.3 写入 Parquet 文件
SQL-- 基础写法
COPY orders TO 'orders_export.parquet' (FORMAT PARQUET);
-- 从查询结果写入
COPY (
SELECT order_id, user_id, amount, order_date
FROM orders
WHERE order_date >= '2024-01-01'
) TO 'orders_2024.parquet' (FORMAT PARQUET);
-- 指定压缩算法(推荐 ZSTD)
COPY orders TO 'orders_zstd.parquet' (FORMAT PARQUET, COMPRESSION ZSTD);
-- 写入多个分区文件(按列值分区)
COPY orders TO 'output/' (FORMAT PARQUET, PARTITION_BY (year, month));
Python 中写入 Parquet
PYTHONimport duckdb
con = duckdb.connect()
# SQL 方式
con.execute("""
COPY (SELECT * FROM 'sales.csv' WHERE amount > 100)
TO 'sales_filtered.parquet'
(FORMAT PARQUET, COMPRESSION ZSTD)
""")
# 把 DataFrame 保存为 Parquet(通过 DuckDB)
import pandas as pd
df = pd.read_csv('data.csv')
duckdb.sql("COPY df TO 'data.parquet' (FORMAT PARQUET, COMPRESSION ZSTD)")
# 按月份分区写入
duckdb.sql("""
COPY (
SELECT *, year(order_date) as yr, month(order_date) as mo
FROM 'orders.csv'
)
TO 'partitioned_orders/'
(FORMAT PARQUET, PARTITION_BY (yr, mo), OVERWRITE_OR_IGNORE true)
""")
6.4 Parquet 元数据统计与 Zone Maps 剪枝
每个 Parquet 行组的每列都存储了 min/max/null_count 统计信息,DuckDB 在执行查询前会读取这些统计信息,直接跳过不满足条件的行组,这称为谓词下推(Predicate Pushdown)和 Zone Maps 剪枝。
SQL-- 查看 Parquet 文件的元数据统计
SELECT
row_group_id,
column_id,
column_name,
min_value,
max_value,
null_count
FROM parquet_metadata('orders.parquet');
-- 场景:查询 amount > 10000 的订单
-- DuckDB 扫描每个行组的 amount.max
-- 如果某行组的 max_amount < 10000,整个行组被跳过
-- 可能只扫描 5% 的数据!
SELECT order_id, amount
FROM 'orders.parquet'
WHERE amount > 10000; -- Zone Maps 自动剪枝
如何让 Zone Maps 更有效 将数据按查询的过滤列排序后写入 Parquet,可以最大化 Zone Maps 的剪枝效果。例如,如果你经常 WHERE order_date BETWEEN ... 过滤,就按 order_date 排序写入 Parquet,相同日期的数据会集中在同几个行组中,大部分行组可以直接跳过。
6.5 Delta Lake 与 Apache Iceberg 简介
Parquet 是文件格式,而 Delta Lake 和 Iceberg 是基于 Parquet 构建的开放表格式(Open Table Format),在 Parquet 之上增加了事务、版本控制、Schema 演化等功能。
| 特性 | 原始 Parquet | Delta Lake | Apache Iceberg |
|---|---|---|---|
| ACID 事务 | 无 | 有 | 有 |
| 时间旅行 | 无 | 有(按版本/时间) | 有(快照) |
| Schema 演化 | 无 | 有限 | 完整支持 |
| 增量读写 | 无 | 有 | 有 |
| DuckDB 支持 | 原生 | 通过 delta 扩展 | 通过 iceberg 扩展 |
| 创始公司 | — | Databricks | Netflix |
SQL-- DuckDB 读取 Delta Lake 表
INSTALL delta; LOAD delta;
SELECT * FROM delta_scan('s3://my-bucket/delta-table/');
-- DuckDB 读取 Apache Iceberg 表
INSTALL iceberg; LOAD iceberg;
SELECT * FROM iceberg_scan('s3://my-bucket/iceberg-table/');
6.6 实战:100GB 数据集本地分析
SQL-- 场景:100GB 的电商订单 Parquet 数据集
-- 按年月分区存储:data/year=2024/month=01/part-*.parquet
-- 1. 先查一下有多少数据
SELECT COUNT(*) FROM 'data/**/*.parquet';
-- 2. 利用分区剪枝:只查 2024 年 Q4 的数据
SELECT COUNT(*), SUM(amount)
FROM read_parquet('data/**/*.parquet', hive_partitioning=true)
WHERE year = 2024 AND month >= 10; -- 只扫描 3 个月的分区!
-- 3. 对大数据集做聚合分析(超内存自动溢写磁盘)
SET memory_limit = '8GB'; -- 限制内存使用
SELECT
year,
month,
product_category,
SUM(amount) AS revenue,
COUNT(*) AS orders,
AVG(amount) AS avg_order_value
FROM read_parquet('data/**/*.parquet', hive_partitioning=true)
GROUP BY ALL
ORDER BY year, month, revenue DESC;
-- 4. 将分析结果导出为小体积 Parquet
COPY (
SELECT year, month, product_category, SUM(amount) AS revenue
FROM read_parquet('data/**/*.parquet', hive_partitioning=true)
GROUP BY ALL
) TO 'summary.parquet' (FORMAT PARQUET, COMPRESSION ZSTD);
本章小结 Parquet 内部结构:行组 → 列块 → 页,Footer 存储元数据统计。列存三大优势:少 I/O、高压缩率、向量化友好。ZSTD 是写入 Parquet 的推荐压缩算法。Zone Maps 利用行组统计信息做谓词下推,按查询列预先排序数据可最大化剪枝效果。Delta Lake/Iceberg 是在 Parquet 上增加事务和版本控制的开放表格式。