一句 SQL,让你的 Parquet 少读 90% 的文件
你有一张用户事件表,按时间顺序写入 Parquet。每次插入新批次,数据追加到新文件。
问题来了:当你 WHERE user_id='USR-1001' 的时候,查询引擎必须打开每一个文件——因为这个用户的数据零散地分布在整个时间线上,每个 Parquet 文件都可能包含它。
在大数据里这不是小事。100 个 Parquet 文件,每个 200MB,一次查询 20GB 的 I/O。你明明只想要 50 条记录。
BigQuery 多年前就用 Clustering 解决了这个问题。但这是 BigQuery 的付费功能,你用的是普通的 Parquet 数据湖。
DuckDB Labs 今年 4 月发布了 DuckLake v1.0,带来了一个叫 Sorted Tables 的功能。说白了就是 BigQuery Clustering 的开源复刻——而且比 BigQuery 更灵活。
● ● ●
它干了什么
一行 SQL:
ALTER TABLE events SET SORTED BY (user_id, ts ASC);
之后,所有写入该表的 INSERT、compaction、flush 操作都会物理排序数据,确保每个 Parquet 文件的 user_id 分布都是窄范围、不重叠的。
效果:
查询 WHERE user_id='USR-1001' 的时候,引擎只需要打开一个文件。Parquet 的 footer 里存了每个文件的 min/max 统计信息——排序后这些统计信息从"全量覆盖"变成了"各管一段",文件级剪枝正式起效。
打开文件后还有第二层优化:文件内部的行组(Row Group)也是排序的,不匹配的行组直接跳过。文件 + 行组,两层剪枝。
● ● ●
比 BigQuery 更灵活的地方
BigQuery Clustering 只能对列名排序,最多 4 列。
DuckLake 支持任意 SQL 表达式。
-- 按小时截断——天然的时间分区排序
ALTER TABLE events SET SORTED BY (date_trunc('hour', event_time) ASC);
-- 自定义 Macro
CREATE MACRO event_bucket(t) AS date_trunc('day', t);
ALTER TABLE events SET SORTED BY (event_bucket(event_time) ASC);
DuckLake 的 v1.0 发布公告明确写了:表达式排序可以用于 space-filling curve(空间填充曲线,如 Hilbert/Z-order)排序——把二维地理坐标映射到一维排序键,让空间查询同时受益于剪枝。这在 Iceberg 和 Delta Lake 里做不到。
● ● ●
一个小设计,体现实战感
高频写入场景里,排序会拖慢 INSERT。DuckLake 给了选择:
CALL my_ducklake.set_option('sort_on_insert', false, table_name => 'events');
关闭 insert 排序后:
- 超出 inlining 阈值的大量写入 → 仍然排序后写 Parquet
- 在 inlining 阈值内的小写入 → 暂存于元数据数据库,不排序,等
CHECKPOINT时批量排序
这个 sort_on_insert 开关与 Data Inlining(DuckLake 支持将小写入直接存在元数据数据库 SQLite/PostgreSQL 中,避免产生海量小文件)配合得很好。高频写入不卡,批量 flush 时统一排序。
● ● ●
与 Iceberg / Delta Lake 的对比
| DuckLake | Iceberg | Delta Lake | BigQuery | |
|---|---|---|---|---|
| **排序表达式** | 任意 SQL + Macro | 列名 + transform | 仅列名 | 列名 ≤4 |
| **空值控制** | NULLS FIRST/LAST | ✓ | 部分 | 默认 |
| **insert 排序可关** | ✓ | ✗ | ✗ | N/A |
| **元数据存储** | SQL 关系表 | JSON/Avro 文件 | JSON 事务日志 | 黑盒 |
真要论区别:别的格式也能排序,但 DuckLake 能对排序怎么排这件事,给到你 SQL 表达式级别的控制。换个角度看:排序键在 DuckLake 里是一条 SQL,存进数据库;在 Iceberg 里,它只是 JSON 里的一行属性。
● ● ●
什么场景该用
- 01高基数列上的点查询——user_id、device_id、transaction_id。排序后从全表扫描变成单文件命中
- 02时间范围的表达式排序——
date_trunc('day', ts)替代手动维护ds分区列 - 03Geo 数据——如果你有自己的 Hilbert/Z-order 实现,直接用表达式排序
不适合的场景:写入速度是绝对瓶颈,且你对读性能没要求——那就别排序,或者关掉 insert 排序,让 compaction 在处理。
● ● ●
两个注意
- 01不追溯。
SET SORTED BY之前的旧数据不会自动重排。要让历史数据也排序,需要执行一次完整的 compaction。 - 02v1.0 刚发布。生产案例还不多,但元数据可以迁出到任意 SQL 数据库,没有 lock-in 风险。DuckLake 是 MIT 许可。
Sorted Tables 代表了 DuckLake 从"又一个 table format"到"lakehouse 原生查询优化"的转变。它没有发明排序,但把排序这件事的表达力从列名推到了 SQL 表达式,解决了一个普遍痛:高基数点查询吃全表 I/O。
DuckLake 官方文档:ducklake.select/docs/stable/duckdb/advanced_features/sorted_tables.html
原文深度教程:thefulldatastack.substack.com/p/understanding-ducklakes-sorted-tables