DuckDB Java驱动支持分块查询结果,绕过JDBC逐行开销
DataHot 速览
DuckDB Java驱动1.5.3.0新增分块查询结果能力,可以惰性获取列式数据块,而不是通过JDBC ResultSet逐行、逐值读取。DuckDB引擎内部按2048行一批的列向量处理数据;JDBC自1997年设计为一次一行/一值,强制驱动把列式块切分成行,造成不必要的转换开销。新API让Java应用直接以列式块消费结果,适合回填数组、Arrow缓冲区或机器学习特征矩阵等场景。官方Java客户端文档也已全面更新,覆盖连接、查询、Arrow与流式结果等API。
为什么值得关注:Java开发者集成DuckDB时常受JDBC逐行转换瓶颈限制;该功能直接暴露列式块读取路径,对数据管道和特征工程场景有明确性能价值。
译文
AI 逐段翻译DuckDB Java 驱动程序中的分块查询结果
Geertjan Wielenga, Alex Kasko 2026-08-21 | 7 分钟
TL;DR: DuckDB Java 驱动程序现在可以将查询结果作为懒加载的列式数据块序列返回,避免了 JDBC 逐行 ResultSet 及其每个值的开销。
DuckDB 是一个列式、向量化的数据库。引擎中的每个操作符都处理数据块:列向量批次,每次 2,048 行。这是 DuckDB 快的重要原因:引擎将解释开销分摊到数千个值上,而不是每个值支付一次。
另一方面,JDBC 设计于 1997 年,基于一个非常不同的理念。ResultSet API 一次提供一行 (next()) 和一个值 (getInt(1), getString(2) 等等)。它是一个可靠、熟悉的 API,在 Java 生态系统中得到广泛支持,但它强制数据采用 DuckDB 在内部从未使用的形式。
DuckDB Java 驱动程序必须弥合这两者。它内嵌了原生 DuckDB 库,并通过 JNI 与之通信。当引擎已经产生 2,048 行的列式块时,JDBC 规范要求驱动程序在读取之前将其切分成行和单元格。如果您的应用程序要将这些值直接放回列式形式,例如数组、Arrow 缓冲区或机器学习特征矩阵,数据会被拆分成行,只是为了重新组装成列,两边都浪费了工作。
Java 驱动程序的 1.5.3.0 版本提供了一个替代方案。
提示 我们全面更新了Java 客户端文档,现在在专用页面中涵盖了驱动程序的完整 API 表面:定义连接, 运行查询,处理结果 (包括 Arrow 方法 和 流式结果), 导入数据 使用appender 和 批量写入器, 以及 定义函数。本文讨论的分块查询结果记录在分块结果 下。
逐行读取结果
一个典型的 JDBC 读取循环如下所示:
try(ResultSetrs=stmt.executeQuery("SELECT a, b FROM measurements")){while(rs.next()){longa=rs.getLong(1);doubleb=rs.getDouble(2);// ...}}调用next() 前进游标,然后get* 调用在当前原生块中定位值并将其转换为 Java 表示。对于某些非原始类型,该转换必须对每个值跨越 JNI 边界,并且一些转换更复杂,例如根据调用者提供的java.util.Calendar (JDBC 规范仍然要求的前java.time 类) 获取时间戳。这些步骤单独来看都不昂贵,但乘以数百万行再乘以几列,它们就累积起来。这正是向量化执行旨在避免的每个值的解释开销。
该循环也带有固定开销。为了单个连接可以同时保持多个结果集打开,结果流式处理 默认关闭,这意味着查询的整个结果被读入内存,除非您设置jdbc_stream_results,大多数用户从未设置的选项。驱动程序还组装完整的元数据。这每个结果发生一次,而不是每行一次,因此与行循环相比成本低,但并非免费,大多数查询从不查看它。
ResultSet 路径是您的 ORM 和生态系统其余部分已经使用的标准,对于绝大多数查询,即返回数百或数千行的查询,它是正确的选择。分块 API 用于另一种情况:查询返回path 是你的 ORM 和生态系统中其他部分已经使用的标准,对于绝大多数查询,即返回成百上千行的查询,它是正确的选择。分块 API 适用于另一种情况:查询返回大量数据,您控制管道的两端,逐行读取没有优势。
使用 DuckDBChunkedResult 读取分块
Java 驱动程序现在直接公开引擎的原生块流,通过 C APIC API 提供的相同机制作为 duckdb_fetch_chunk。查询结果成为懒加载的数据块序列,您从每个块中批量读取列向量,就像引擎产生它们一样,避免了 JDBC 规范要求的每行开销。
综合起来:
try(DuckDBConnectionconn=DriverManager.getConnection("jdbc:duckdb:").unwrap(DuckDBConnection.class);DuckDBPreparedStatementps=conn.prepare("SELECT l_orderkey, l_linenumber, l_shipmode "+"FROM 's3://my-bucket-name/lineitems.parquet'")){try(DuckDBChunkedResultres=ps.query()){// advance to the next chunk, returns true on successwhile(res.nextChunk()){// get the current chunk from the resultDuckDBDataChunkReaderchunk=res.chunk();// vectors are addressed by 0-based column indexDuckDBReadableVectororderKeys=chunk.vector(0);DuckDBReadableVectorlineNumbers=chunk.vector(1);DuckDBReadableVectorshipModes=chunk.vector(2);// read each column with the getter for its typefor(longrow=0;row<chunk.rowCount();row++){longorderKey=orderKeys.getLong(row);intlineNumber=lineNumbers.getInt(row);StringshipMode=shipModes.getString(row);System.out.println(orderKey+" "+lineNumber+" "+shipMode);}}}}这种方法有几个显著特性:
- 懒加载。
nextChunk()一次从引擎拉取一个块。完整结果永远不会在原生侧或 Java 侧物化,因此您可以流式处理比堆大得多的结果,就像引擎本身产生它们一样。 - 列式访问。 在块内,您逐个向量工作。如果您的目的地也是列式的,例如
long[], 一个 ArrowVectorSchemaRoot或 Parquet 写入器,您在紧凑的单态循环中复制值,而不是在每一行切换列。 - 足够的元数据以进行分发。 每个结果仍然报告其列数和列类型,因此您可以为每个向量选择正确的
get*调用,而无需在数据旁边携带单独的架构描述。 - 如果您编写过 UDF,这是一个熟悉的 API。 块内容通过相同的
DuckDBDataChunkReaderAPI 访问,该 API 是驱动程序的用户定义函数 用于读取输入向量的 API。因此,在 UDF 中读取函数参数的相同代码也可以在外部读取查询结果。 - 零基索引。 块列和行 是 0 基的,与 C API 和 UDF 接口匹配。
在向量内部,您可以自由地以适合您代码的方式迭代,包括 Java Stream。但是,对单个块进行并行 流通常没有帮助:2,048 个值足够小,fork/join 协调通常会花费超过节省的成本。如果您需要并行性,请在块之间应用,而不是在单个块内。
当前限制
这是 API 的第一个迭代,在采用之前您应该了解一些限制:
- 目前仅支持基本数据类型。 支持标量类型。复合类型 (
LIST,STRUCT) 尚不能通过分块接口读取。计划在未来的版本中支持它们。 - 仅限预准备语句。
query()目前存在于DuckDBPreparedStatement上,并且没有query(String)便利重载。预准备语句添加一行。 - 目前读取器表面较小。 读取器目前涵盖基本类型,未来版本计划增加更多。已经计划的一个新增功能是直接读取
VARCHAR值作为 UTF-8byte[],因此以文本为主的工作负载可以跳过为每个值实现 JavaString的过程。
如果这些阻碍了您关心的用例,请提出问题。您的反馈决定着下一迭代的优先级。
结论
JDBC ResultSet 仍然是大多数应用程序的正确默认选择。但引擎以列式块的形式生成结果,而 DuckDBChunkedResult 让您的 Java 代码以相同形式读取它们:直接且惰性,在 JNI 边界处几乎没有开销。
该功能由 duckdb-java#682 提供,并可在 Maven Central 上的当前 duckdb_jdbc 版本中获得。请在您最大的结果集上试试,并告诉我们性能如何。欢迎提出问题和 拉取请求,DuckDB 团队始终乐于在 #java 频道的 DuckDB Discord 上讨论 Java。
这篇内容对你有用吗?
反馈只用于改善内容筛选,不等同于收藏