alitrack

一句 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 分布都是窄范围、不重叠的。

效果:

Image

查询 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 的对比

DuckLakeIcebergDelta LakeBigQuery
**排序表达式**任意 SQL + Macro列名 + transform仅列名列名 ≤4
**空值控制**NULLS FIRST/LAST✓部分默认
**insert 排序可关**✓✗✗N/A
**元数据存储**SQL 关系表JSON/Avro 文件JSON 事务日志黑盒

真要论区别:别的格式也能排序,但 DuckLake 能对排序怎么排这件事,给到你 SQL 表达式级别的控制。换个角度看:排序键在 DuckLake 里是一条 SQL,存进数据库;在 Iceberg 里,它只是 JSON 里的一行属性。


● ● ●

什么场景该用

  1. 01高基数列上的点查询——user_id、device_id、transaction_id。排序后从全表扫描变成单文件命中
  2. 02时间范围的表达式排序——date_trunc('day', ts) 替代手动维护 ds 分区列
  3. 03Geo 数据——如果你有自己的 Hilbert/Z-order 实现,直接用表达式排序

不适合的场景:写入速度是绝对瓶颈,且你对读性能没要求——那就别排序,或者关掉 insert 排序,让 compaction 在处理。


● ● ●

两个注意

  1. 01不追溯。SET SORTED BY 之前的旧数据不会自动重排。要让历史数据也排序,需要执行一次完整的 compaction。
  2. 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