DuckDB 不存一行数据,268KB 文件当一个数据湖
一个 268KB 的 DuckDB 文件,不存一行数据,挂载进来却是一整套能直接查的表——把它传到对象存储上,分享数据集只需要发一个链接。官方博客前两天专门写了这个玩法,我完整跑了一遍,值得说给你听。
起因是个老痛点。你有一堆 Parquet 文件放在 S3 上,想分享给同事或者客户,能怎么办?发一串路径清单过去,对方把路径粘到 read_parquet() 里,自己猜每一列是什么意思。文件一分区、列名一改,清单作废,再来一轮。
DuckDB 团队前两天发的博客《A DuckDB Database with No Data in It》给了一个很干净的答案:把「表清单」本身做成一个数据库文件。
视图目录工作原理
● ● ●
视图只存查询语句,不存数据
DuckDB 的表把行存在数据库文件里;视图只存一段 SQL 文本,查询时现跑。如果视图里写的 FROM 指向的是远程 Parquet 文件,那这个数据库文件里就一行数据都没有——它变成了一个目录:一组起好名字、随时可查的关系,数据留在原地。
这个想法不是官方原创。2024 年 5 月,Nikolas Goebel 写过一篇《DuckDB Doesn't Need Data To Be a Database》,在 Hacker News 上拿了 406 分。他的说法是:现代数据库早就把「数据存在哪」和「数据怎么读」分开了,DuckDB 干脆可以当作数据云的浏览器,访问一个数据集靠的是 URL,不是本地拷贝。官方这次把它落成了正式文档。
● ● ●
实测:268KB 文件,挂下 38 万行
我照着官方流程在本机跑了一遍,环境是 v2.0.0 alpha。先建一个持久化数据库文件,在里面建一个指向远程文件的视图:
INSTALL httpfs;
ATTACH 'rail_catalog.duckdb' AS rail;
USE rail;
CREATE VIEW services AS
SELECT *
FROM 'https://blobs.duckdb.org/train_services.parquet';
这个数据集是荷兰铁路的列车停靠记录。文件本身 1.6MB、380,959 行,而只含这个视图的 rail_catalog.duckdb,量出来是 268KB。
换一个全新进程,以只读方式挂回来查:
LOAD httpfs;
ATTACH 'rail_catalog.duckdb' AS rail (READ_ONLY);
SELECT station_name, count(*) AS calls
FROM rail.services
GROUP BY station_name
ORDER BY calls DESC
LIMIT 3;
结果和官方博客分毫不差:Utrecht Centraal 7663 次,Amsterdam Centraal 7591 次,Zwolle 5013 次。数据一条不在文件里,查的时候现取。
● ● ●
挂到网上,别人拿一个链接就能用
发布方把 rail_catalog.duckdb 传到任何 DuckDB 读得到的地方——S3 桶或者 HTTPS 地址都行,跟 Parquet 文件放一个桶里最顺手。共享数据集从此变成共享一个 URL,对方不需要知道背后有几个文件、怎么分区的。
消费方一行挂载:
ATTACH 'https://example.com/rail_catalog.duckdb' AS rail (READ_ONLY);我在本地起了个 HTTP 服务模拟远端托管,挂载查询都通。试往只读库上写,会收到一句干脆的报错:
Invalid Input Error: Cannot execute statement of type "CREATE"
on database "rail" which is attached in read-only mode!
两个实测出来的细节,文档里没细说:
一是语法。1.x 时代 ATTACH ... AS x READ_ONLY 不带括号也能跑,v2.0 会直接报 Parser Error,必须写成 ATTACH '...' AS rail (READ_ONLY)。博客示例用的是新语法,老用户照旧习惯敲会懵一下。
二是托管服务要支持 HTTP Range 请求。我用 Python 起的简易服务不支持 Range,DuckDB 会警告「Falling back to full file download」然后整个文件拉下来。268KB 无所谓,catalog 长大了就是另一回事。S3 和正经 CDN 都支持 Range,自建服务要注意。
● ● ●
数据文件天天变,查询不用跟着改
这是我认为最值钱的部分。消费方查的是视图,发布方改的是文件,中间隔了一层。
我模拟了两种常见的变更。
第一种,单个 Parquet 胀到太大,发布方按年拆成目录,视图改成 read_parquet('s3://my-bucket/rail/services/year=/.parquet'),一行改动,消费方毫无感知。第二种更狠:新一批文件把 station_name 列改名成了 station,视图里用 * EXCLUDE (station), station AS station_name 映射回去,消费方手里的 SQL 一个字符不用动。
我把改名后的文件在本地复现了一遍,挂上新视图再查,Top 3 站点还是 7663、7591、5013。对消费方来说,目录是稳定的;对发布方来说,重分区、换桶、改 schema,都只是自己家里的事。
● ● ●
用之前想清楚的几件事
官方文档列了三条限制,都是实话:消费方必须同时能访问 catalog 文件和它引用的每一个数据源,内网数据配公网目录是行不通的;HTTPS 和 S3 连接是只读的,改视图只能改本地副本再传一次;查询性能取决于网络和文件布局,远程查询的那些优化技巧(该投影的投影、该分区的分区)在这里全部适用。
还有一条我的观察:这个模式适合「数据已经是 Parquet、消费方用 DuckDB」的场景。两边有一头不满足,目录做得再优雅也落不了地。
回到开头发路径清单的那个场景:现在你发出去的可以是一个链接。接收的人挂上就能查,你在桶里怎么折腾文件,他那边永远是同一套表。数据共享这件事,很多时候缺的不是带宽,是一个双方都认的稳定接口——几百 KB 的视图文件,恰好就是那个接口。
你手头有没有那种「路径清单发过去对方一脸懵」的数据集?如果有,花十分钟建个 catalog 试试,比改造存储结构便宜得多。
参考来源:
- 01
A DuckDB Database with No Data in It(DuckDB 官方博客,2026-10-07)
https://duckdb.org/2026/10/07/view-only-mode.html
- 01
Share a DuckDB Database That Only Stores Views(DuckDB 官方指南)
https://duckdb.org/docs/preview/guides/network_cloud_storage/duckdb_views_only
- 01
DuckDB Doesn't Need Data To Be a Database(Nikolas Goebel,2024-05-28)
https://www.nikolasgoebel.com/2024/05/28/duckdb-doesnt-need-data
- 01
Hacker News 讨论(406 分)
https://news.ycombinator.com/item?id=40509987