DuckDB 一招鲜:用 read_duckdb() 批量读取多个 .duckdb 数据库文件

学会用 DuckDB 的 read_duckdb() 函数配合 glob 模式批量读取多个 .duckdb 文件,无需 ATTACH,告别手工拼接 UNION ALL 的痛苦。

痛点:批量读取 .duckdb 文件太繁琐

假设你在做一个月度数据分析项目,有 100 个按月份分片的 .duckdb 数据库文件,每个文件里都有一个名为 events 的表。现在你需要把它们合并在一起做汇总分析。

传统做法是什么?

ATTACH 'data/2024-01.duckdb' AS db1;
ATTACH 'data/2024-02.duckdb' AS db2;
ATTACH 'data/2024-03.duckdb' AS db3;
-- ... 还有 97 个文件要手动写
SELECT * FROM db1.events
UNION ALL
SELECT * FROM db2.events
UNION ALL
SELECT * FROM db3.events;
-- ... 97 个 UNION ALL

100 个文件 → 200 行 SQL。这不仅痛苦,而且无法动态扩展——每个月新增文件都要改代码。

一招解决:read_duckdb() + Globbing

DuckDB v1.5.0 引入了 read_duckdb() 表函数,它可以直接读取 .duckdb 文件中的表,无需 ATTACH

SELECT * FROM read_duckdb('data/*.duckdb');

就这么一行。支持 glob 通配符,自动扫描所有匹配的文件,自动合并结果。

更实用的写法:指定具体表

如果每个 .duckdb 文件里有多个表,你可以用 table_name 参数精确指定:

SELECT * FROM read_duckdb(
    'data/*.duckdb',
    table_name => 'events'
);

自动追踪数据来源

想知道每行数据来自哪个文件?read_duckdb() 会自动添加 _source_file 列,方便溯源:

SELECT 
    _source_file,
    COUNT(*) AS event_count,
    SUM(amount) AS total_amount
FROM read_duckdb('data/2024-*.duckdb', table_name => 'events')
GROUP BY _source_file
ORDER BY event_count DESC;

输出示例:

     _source_file      | event_count | total_amount
-----------------------+-------------+--------------
 data/2024-03.duckdb   |     152340  |    4892341.50
 data/2024-01.duckdb   |     148920  |    4723102.80
 data/2024-02.duckdb   |     151200  |    4812456.20

Python 中使用

在 Python 中也一样简洁:

import duckdb

# 一行代码读取所有 .duckdb 文件的 events 表
df = duckdb.query("""
    SELECT * FROM read_duckdb(
        '/data/monthly_reports/*.duckdb',
        table_name => 'events'
    )
""").df()

print(f"共读取 {len(df)} 行数据")

效果量化

维度ATTACH + UNION ALLread_duckdb()
代码行数200+ 行(100 个文件)3 行
新增文件适配手动修改 SQL零改动,自动生效
可读性❌ 难以维护✅ 一目了然
错误率高(容易漏写/拼错)极低
执行时间相近(底层优化一致)相近

核心收益:将 200+ 行手工 SQL 压缩到 3 行声明式查询,且新增文件零成本。

进阶用法

嵌套读取:从 read_duckdb 的结果再做聚合

SELECT 
    DATE_TRUNC('month', event_time) AS month,
    COUNT(*) AS total_events,
    AVG(amount) AS avg_amount
FROM read_duckdb('data/2024-*.duckdb', table_name => 'events')
GROUP BY month
ORDER BY month;

配合 FILTER 做数据质量检查

SELECT 
    _source_file,
    COUNT(*) FILTER (WHERE amount IS NULL) AS null_amounts,
    COUNT(*) FILTER (WHERE event_type = '') AS empty_types
FROM read_duckdb('data/*.duckdb', table_name => 'events')
GROUP BY _source_file
HAVING null_amounts > 0 OR empty_types > 0;

这个查询可以在 10 毫秒内找出所有存在数据质量问题的文件——比逐文件打开检查快几个数量级。

递归子目录

SELECT * FROM read_duckdb('data/**/*.duckdb', table_name => 'events');

** 支持递归匹配子目录,适合层级存储的场景。

延伸思考

read_duckdb() 的设计哲学是 “让 DuckDB 能读写自己的文件格式”。这带来了几个有趣的场景:

  1. 分布式数据处理:每个节点本地写一个 .duckdb 文件,汇总时用 read_duckdb('output/*.duckdb') 一键合并。
  2. 备份即数据:你的 .duckdb 数据库文件本身就是备份,随时可以用 read_duckdb() 读取任意时间点的数据。
  3. 插件化架构:微服务各自维护独立的 .duckdb 文件,跨服务分析时直接 glob 读取,无需共享数据库实例。

这个功能在 DuckDB v1.5.0 引入,支持 late materialization 和 filter pushdown,意味着即使读取几百个文件,优化器也会把过滤条件推到每个文件中执行,只读取真正需要的数据


📬 Subscribe to DuckDB Lab for weekly practical tips that you can use immediately. No fluff, just actionable DuckDB tricks.

📺 Watch video tutorials → DuckDB Lab YouTube

Subscribe for more DuckDB & AI automation tutorials

使用 Hugo 构建
主题 StackJimmy 设计