十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

DeepSeek总结的DuckDB Java 驱动程序中的分块查询结果

DeepSeek总结的DuckDB Java 驱动程序中的分块查询结果 来源https://duckdb.org/2026/08/21/chunked-query-results-java-driverDuckDB Java 驱动程序中的分块查询结果Geertjan Wielenga, Alex Kasko2026-08-21 | 7 分钟阅读TL;DR:DuckDB Java 驱动程序现在可以将查询结果作为按需获取的列式数据块序列返回避免了 JDBC 逐行处理 ResultSet 及其单值开销。DuckDB 是一个列式、向量化的数据库。引擎内部的每个运算符都处理数据块data chunk即每次处理 2048 行的列向量批次。这是 DuckDB 性能卓越的重要原因之一引擎将解释开销分摊到数千个值上而不是为每个值单独支付一次开销。另一方面JDBC 设计于 1997 年其核心思想截然不同。ResultSet API 一次只返回一行通过next()一次只返回一个值通过getInt(1)、getString(2)等。它是一个稳定、熟悉的 API在整个 Java 生态系统中得到支持但它强制要求数据采用 DuckDB 内部从未使用过的形式。DuckDB Java 驱动程序必须在这两者之间架起桥梁。它嵌入了原生 DuckDB 库并通过 JNI 与之通信。当引擎已经生成了一个包含 2048 行的列式数据块时JDBC 规范要求驱动程序在你读取它之前将其切分为行和单元格。如果你的应用程序打算将这些值直接放回列式形式例如数组、Arrow 缓冲区或机器学习特征矩阵那么数据先被拆分成行然后又重新组装成列这浪费了双方的工作。Java 驱动程序的 1.5.3.0 版本提供了一种替代方案。提示我们已经全面更新了 Java 客户端文档现在通过专用页面涵盖了驱动程序的完整 API 接口定义连接、运行查询、处理结果包括 Arrow 方法和流式结果、使用 Appender 和 Batch Writer 导入数据以及定义函数。本文讨论的分块查询结果在“分块结果”Chunked Results部分有详细说明。逐行读取结果典型的 JDBC 读取循环如下所示try(ResultSetrsstmt.executeQuery(SELECT a, b FROM measurements)){while(rs.next()){longars.getLong(1);doublebrs.getDouble(2);// ...}}调用next()会推进游标然后调用get*方法在当前原生数据块中定位该值并将其转换为 Java 表示。对于某些非基本类型这种转换每次取值都需要跨越 JNI 边界而一些转换则更为复杂例如根据调用者提供的java.util.CalendarJDBC 规范在此处仍强制要求使用的java.time之前的类来获取时间戳。这些步骤单独来看并不昂贵但当它们乘以数百万行和若干列时开销就累积起来了。这正是向量化执行旨在避免的单值解释开销。该循环还存在固定的开销。为了让单个连接能同时保持多个结果集打开结果流式传输默认是关闭的这意味着除非你设置jdbc_stream_results大多数用户从未设置过此选项否则查询的整个结果都会被读入内存。驱动程序还会组装完整的结果元数据集。这每个结果只发生一次而不是每行一次因此与行循环相比成本很低但也不是零开销而且大多数查询从不查看它。ResultSet 路径是你的 ORM 和生态系统中其他部分已经使用的标准对于绝大多数查询即返回数百或数千行的查询来说它是正确的选择。分块 API 适用于另一种情况查询返回大量数据并且你控制管道的两端逐行读取没有任何优势。使用 DuckDBChunkedResult 读取数据块Java 驱动程序现在通过 C API 提供的duckdb_fetch_chunk相同机制直接暴露引擎的原生数据块流。查询结果变成按需获取的数据块序列你可以从每个块中批量读取列向量就像引擎生成它们的方式一样从而避免了 JDBC 规范要求的每行开销。综合起来try(DuckDBConnectionconnDriverManager.getConnection(jdbc:duckdb:).unwrap(DuckDBConnection.class);DuckDBPreparedStatementpsconn.prepare(SELECT l_orderkey, l_linenumber, l_shipmode FROM s3://my-bucket-name/lineitems.parquet)){try(DuckDBChunkedResultresps.query()){// 前进到下一个数据块成功时返回 truewhile(res.nextChunk()){// 从结果中获取当前数据块DuckDBDataChunkReaderchunkres.chunk();// 向量通过基于 0 的列索引访问DuckDBReadableVectororderKeyschunk.vector(0);DuckDBReadableVectorlineNumberschunk.vector(1);DuckDBReadableVectorshipModeschunk.vector(2);// 使用适合其类型的 getter 读取每列for(longrow0;rowchunk.rowCount();row){longorderKeyorderKeys.getLong(row);intlineNumberlineNumbers.getInt(row);StringshipModeshipModes.getString(row);System.out.println(orderKey lineNumber shipMode);}}}}这种方法有几个显著的特点惰性Laziness。nextChunk()一次从引擎拉取一个数据块。完整结果永远不会在原生端或 Java 端物化因此你可以流式处理远大于堆内存的结果就像引擎本身生成它们的方式一样。列式访问Columnar access。在数据块内部你按向量进行操作。如果你的目标也是列式的例如long[]、一个Arrow VectorSchemaRoot或一个 Parquet 写入器你可以在紧凑的单态循环中复制值而不是在每一行上切换列。足够的元数据以便分发Enough metadata to dispatch。每个结果仍然报告其列数和列类型因此你可以为每个向量选择合适的get*调用而无需在数据旁边携带单独的模式描述。如果你写过 UDF这个 API 会很熟悉。数据块内容通过DuckDBDataChunkReaderAPI 访问该 API 与驱动程序用于用户自定义函数读取输入向量的 API 相同。因此在 UDF 内部读取函数参数的相同代码也可以在 UDF 外部读取查询结果。基于 0 的索引Zero-based indexing。数据块的列和行都从 0 开始与 C API 和 UDF 接口匹配。在向量内部你可以自由选择适合你代码的迭代方式包括 Java Stream。不过对单个数据块使用并行流通常没有帮助2048 个值太小了fork/join 协调的开销通常超过其带来的收益。如果你需要并行性应该跨数据块应用而不是在单个数据块内部。当前限制这是该 API 的首次迭代在采用之前你应该了解以下几点限制目前仅支持基本数据类型。标量类型已受支持。复合类型LIST、STRUCT尚无法通过分块接口读取。对其的支持计划在未来版本中提供。仅支持预编译语句PreparedStatement。query()方法目前存在于DuckDBPreparedStatement上还没有方便的query(String)重载。先准备语句只需增加一行代码。目前读取器覆盖范围较小。当前读取器覆盖了基本类型未来版本计划扩大覆盖范围。一个已计划的功能是直接以 UTF-8byte[]形式读取 VARCHAR 值这样文本密集型工作负载就可以跳过每个值都实例化一个 Java String 的步骤。如果这些限制中任何一项影响了你关心的用例请提交 issue。你的反馈将驱动下一次迭代的优先级。结论对于大多数应用程序来说JDBC ResultSet 仍然是正确的默认选择。但引擎以列式数据块生成其结果而DuckDBChunkedResult让你的 Java 代码能够以相同的形式读取它们直接且惰性在 JNI 边界几乎没有开销。该特性由 duckdb-java#682 提供可在 Maven Central 上当前的duckdb_jdbc发布版本中获取。在你最大的结果集上试用它并告诉我们它的性能表现。欢迎提交 issue 和 pull requestDuckDB 团队也随时欢迎你在 DuckDB Discord 的 #java 频道讨论 Java 相关问题。
返回列表