alitrack

MongoDB 搬进 DuckDB,Java 三种写法差 2.5 倍

一家做漏洞数据的公司,MongoDB 里的分析页面越跑越慢,加索引、改结构折腾了好几个月,某些查询还是卡在几十秒。他们的决定是:把一年的数据搬进 DuckDB,跑在同一台服务器上。

最后上线的那版代码跑 1 分 27 秒,而被否掉的社区扩展只要 55 秒。这多出来的 32 秒,买的是升级自由。

这个故事来自 10 月 5 日 DuckDB 官方博客的客座文章,作者 Geertjan Wielenga 和 Alex Kasko。数据集和代码全部公开,数字都可以自己复跑一遍。

● ● ●

目标定得很死

需求拆开看很具体:所有分析查询 1 秒内出结果,导出 5 万条记录到 Excel 不超过 10 秒,超出的任务放后台跑。DuckDB 必须和 MongoDB 待在同一台服务器上,资源有限,要免费开源。

作者放了一份脱敏数据集:485,168 条记录,24 个字段。坑在两个 VARCHAR 列上——description 中位数 5,000 字符,references 中位数 2,500 字符,最长的单条 32,000 字符。这种长文本列让很多操作天然变贵,后面的性能数字都建立在这个前提下。

● ● ●

四条路,一个比一个快

先说最省事的。mongo 社区扩展三行 SQL 就能把数据拉进来:

INSTALL mongo FROM community;
ATTACH 'host=localhost port=27017 database=db1' AS m (TYPE MONGO);
CREATE OR REPLACE TABLE m.vuln1 AS FROM 'vulnerability_sample.csv.zst';

样本数据 55 秒导完。进来之后 GROUP BY、COUNT 类查询全部低于 100 毫秒,5 万条导出不到 5 秒,排序后文件还小了一截。看起来该收工了。

但作者否掉了它。理由是社区扩展没有「future-proof」保证:DuckDB 迭代很快,哪天升级主程序,扩展没跟上编不过去,就被钉死在旧版本上。自己维护一个 C++ fork 又不现实,而且他们的数据管道全在 Java 生态里,希望这套导入代码能复用到其他 Java 数据源上。

于是开始用纯 Java 重写,基准就是那 55 秒。

第一版:JDBC executeBatch。 最直觉的写法,按天分批从 Mongo 拉数据,PreparedStatement 逐行 setString,攒够一批 executeBatch。8 个线程并行跑完 3 分 37 秒。慢的根源在 DuckDB 的 JDBC 层没有原生批量插入——executeBatch 是 Java 侧的循环,批里每一行都是一次独立的数据库级 INSERT,逐行开销全吃进去了。

第二版:Appender 接口。 换成 Connection#createAppender 拿到的 DuckDBAppender,beginRow、append、endRow,凑满 2,048 行的一个 Data Chunk 后一次性刷进 DuckDB。这已经是真批量了,总耗时降到 2 分 02 秒。但在真实数据规模上还是不够看。

第三版:Java 表函数。 作者跑去读 mongo 扩展的源码,发现它内部走的是用户定义表函数,于是照方抓药:注册一个 vuln_import 表函数,让 DuckDB 自己来「读」Mongo。每个线程的 apply 调用往输出里写最多 2,048 行,队列空了返回 0 表示读完。两个开关决定成败——init 里 setMaxThreads(8) 声明支持并发;SQL 侧 SET preserve_insertion_order = FALSE 放开乱序,否则 DuckDB 会为了保序把 apply 锁成单线程。

这一版 1 分 27 秒跑完,是所有 Java 写法里开销最低的。生产环境里因为磁盘限制,作者用了非并行变体,把 ORDER BY 合进同一条 CREATE TABLE,最终导数速度追平了 mongo 扩展。

四种导入方案耗时对比

四种导入方案耗时对比

导入方案
样本耗时
一句话点评
mongo 社区扩展
55 秒
最快,但要押注扩展跟得上主程序升级
JDBC executeBatch
3 分 37 秒
逐行 INSERT,最慢的 Java 写法
Java Appender
2 分 02 秒
真批量,2048 行一刷
Java 表函数
1 分 27 秒
8 线程并行,开销最低

● ● ●

这 32 秒买到了什么

把四行数字放在一起看,工程判断的部分比跑分的部分更有意思。55 秒的方案依赖别人维护的 C++ 扩展,1 分 27 秒的方案是自家团队看得懂、改得动的 Java 代码,还能拆出去对接别的数据源。慢一半多的时间,换来的是 DuckDB 出新版本时可以随时跟着升。

回头看那个卡在几十秒的分析页面:一年的数据进了 DuckDB 文件之后,GROUP BY 降到 100 毫秒以内,5 万条 Excel 导出 5 秒收工。MongoDB 继续干它擅长的事,分析负载整个搬走,服务器还是那一台。

如果你的团队也在 Java 栈里,数据源头五花八门,表函数这条路的完整代码在 staticlibs/duckdb_java_data_import 仓库里,DuckDB 8 月那篇 Java 表函数详解讲 API,两份对照着看就能抄作业。

三个问题挑一个聊聊:你的报表库和分析库分开吗?迁数据的时候踩过什么坑?如果社区扩展哪天断更了,你现在的管道扛得住吗?

● ● ●

参考来源

  1. 01
    Importing Data using Java Table Functions(DuckDB 官方博客,2026-10-05)https://duckdb.org/2026/10/05/import-data-with-java.html
  2. 02
    样例代码仓库 https://github.com/staticlibs/duckdb_java_data_import
  3. 03
    DuckDB Table Functions in Java(2026-08-25)https://duckdb.org/2026/08/25/table-functions-in-java
  4. 04
    Java Appender 官方文档 https://duckdb.org/docs/current/clients/java/data_import#appender
  5. 05
    Benchmarking DuckDB From Java: Fast INSERT, UPDATE, and DELETE https://sqg.dev/blog/java-duckdb-benchmark/