alitrack

DuckLake 的维护命令,有三个教程不说的坑

DuckLake 的维护命令,有三个教程不说的坑。这三个坑不是猜测,是我在本机把六个维护函数全部跑了一遍之后,亲手踩中的。

先把背景铺一下。DuckLake 把元数据放进 SQL 数据库(Postgres 或 SQLite),数据文件用 Parquet 存对象存储,2026 年 4 月出了 v1.0。它自带六个维护函数:flush_inlined_data、expire_snapshots、merge_adjacent_files、rewrite_data_files、cleanup_old_files、delete_orphaned_files。也可以一句 CHECKPOINT 全部跑完。

这六个函数怎么排维护策略,MotherDuck 合作的三部曲教程刚写完终章(2026-10-06),讲得清楚。我照着它的用例在本机复现了一遍,技术上全部成立:inlining 阈值 10 行实锤,flush 把 catalog 里的小插入落成 Parquet,DROP 加重建表之后旧版本文件留在磁盘上,dry_run 只列不动。教程本身没有错。

但它没写的部分,恰好是要在生产里疼的部分。

● ● ●

坑一:孤儿文件清理会漏检,刚出现的孤儿看不见

DuckLake 里的孤儿文件,指目录不再追踪、但还躺在存储里的数据文件。清理函数叫 ducklake_delete_orphaned_files,教程演示了 dry_run 参数,说先干跑看一眼有什么。

我照抄了它的调用,然后往数据目录里手工塞了一个复制的 Parquet 文件,制造一个真实的孤儿。干跑的结果是:空的。文件明明在那里,函数说没有孤儿。

翻函数签名才找到原因。这个函数有四个参数:catalog、dry_run、cleanup_all、older_than。older_than 是一个安全窗,不传的时候用保守的默认值,只有「在存储里躺过了安全窗」的孤儿才会被列出来。我塞的文件是新的,还在窗口里,所以看不见。显式传 older_than 加 now() 之后,孤儿立刻出现,可以删,删完磁盘文件数从 5 降到 4,功能本身没问题。

问题在运维语义上。如果你的巡检脚本照抄教程的调用形态,每次干跑都返回空,你会得出「我的 lake 很干净」的结论,而新产生的孤儿一直在漏。这个参数在官方文档的函数签名里有,在 MotherDuck 自己的 cookbook 模板里是一级配置项(叫 ORPHAN_OLDER_THAN,默认 7 天),唯独在教程正文里一次都没出现。

● ● ●

坑二:Python 侧缺一个依赖,维护脚本第一步就崩

教程的收尾方案是把清理命令交给 Python 调度,做成每天跑的 pipeline。我在本机用 py-duckdb 写复现脚本,第一个查询就崩了:

ModuleNotFoundError: No module named 'pytz'

只是查了一下快照表 ducklake_snapshots,不是什么冷门操作。快照时间戳列是 TIMESTAMP WITH TIME ZONE 类型,Python 客户端读这类列需要 pytz,维护相关函数的返回大量带这个类型。装上 pytz 之后继续跑,全部通过。

有意思的是,这个坑 MotherDuck 官方文档专门写过:cookbook 的注意事项里说快照过期读取时间戳列,模板自带 pytz 并固定时区,Flight 运行环境常常没有系统时区,需要 SET TimeZone='UTC',两边都缺就报 UnknownTimeZoneError。官方知道,模板处理了,教程没提。

● ● ●

坑三:GCS 用户跑不了清理,报错还没有解法

前两个坑是「不知道会踩」,第三个是「踩了爬不出来」。

ducklake 仓库的 issue #836(2026 年 3 月开出,到我核验时还是 open 状态):cleanup_old_files 和 delete_orphaned_files 两个函数在数据存 Google Cloud Storage 时直接失败。原因是这两个函数的删除走 S3 风格的批量删除接口(POST ?delete),而 GCS 的 S3 兼容层没有实现这个操作,返回 HTTP 400 NotImplemented。

后果在 issue 里写得很直白:待删文件在队列里无限累积,inlined 数据表永远不会被清。报告者给的建议修复(批量删除失败时退回逐个删)还没有合入。教程的用例建立在 S3 上,一个字没提 GCS。

顺带说一个已经修掉的兄弟坑。issue #1090:数据桶大到百万对象级时,孤儿清理函数会对整个桶发一次空前缀的 LIST,直接超时,SET http_timeout 也救不了,连 CHECKPOINT 里藏着的同一段逻辑都躲不开(设置了 auto_compact=false 也没用)。这个 4 月底的 issue 后来修了,但它留了一个提醒:大存储上的清理操作,行为和小 demo 是两回事。

● ● ●

教程为什么会有这三个坑

不是教程作者不老实。我核过这个系列:作者 Hoyt Emerson 是内容机构 Early Signal 的创始人,MotherDuck 在他的客户名单上,这篇终章在正文开头就写了 partnership 披露。技术内容逐条对官方文档核过,九条可核主张没有一条硬错误,是这个作者两篇受核文章里质量最高的一篇。

坑的真正来源是口径。作者的用例是每天批量重建一次表:没有流式小写入,所以不用 flush;没有小文件累积,所以不用 merge。六个维护函数他只用了三个,这在它的场景里完全成立。但「这三函数完成我们策略要求的一切」这句话,没有附带前提条件。读者如果拿它当通用清单,流式场景缺了 flush 和 merge,巡检场景漏了安全窗,Python 场景少了 pytz。

真正把三个坑都讲全的,反倒是 MotherDuck 自己的 cookbook(文档站里那篇 Keep DuckLake Reads Fast With Scheduled Maintenance):六步序列、每个阈值都是配置项、首跑强制干跑、注意事项里 pytz 和时区都点名了。想直接上手的读者,从 cookbook 起步比跟教程手搓更稳。

● ● ●

我拿到的三条实操结论

第一,清理巡检的调用形态要带上 older_than,并且知道自己在等多长的安全窗。默认窗口下「干跑返回空」不等于「没有孤儿」,只等于「没有过窗的孤儿」。急查可以显式传 older_than 等于当前时刻,但要明白这等于放弃了对在飞查询的保护。

第二,任何跑维护任务的 Python 环境,先装 pytz,再固定时区。这是官方模板已经交过学费的位置,没必要自己再交一次。

第三,选存储前看一眼 issue #836 的状态。GCS 用户目前没有官方清理路径,S3 和本地存储不受影响。等这个 issue 关了再考虑把数据放 GCS 不迟。

DuckLake 的维护函数面本身是齐的,六个函数加 CHECKPOINT 加 catalog 级自动过期选项(set_option 里有个 expire_older_than,可以让快照过期不依赖外部调度,教程同样没提)。缺的东西是把「机制文档、坑位文档、issue 边界」三层缝起来的那一篇。希望这篇补上了。

参考来源:

https://thefulldatastack.substack.com/p/ducklake-in-production-maintenance

https://ducklake.select/docs/stable/duckdb/maintenance/expire_snapshots

https://motherduck.com/docs/cookbook/flight-ducklake-maintenance/

https://github.com/duckdb/ducklake/issues/836

https://github.com/duckdb/ducklake/issues/1090

https://ducklake.select/2026/04/02/data-inlining-in-ducklake/