来源:https://duckdb.org/2026/08/21/chunked-query-results-java-driver

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

Geertjan Wielenga, Alex Kasko
2026-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 (ResultSet rs = stmt.executeQuery("SELECT a, b FROM measurements")) {
    while (rs.next()) {
        long a = rs.getLong(1);
        double b = rs.getDouble(2);
        // ...
    }
}

调用 next() 会推进游标,然后调用 get* 方法在当前原生数据块中定位该值,并将其转换为 Java 表示。对于某些非基本类型,这种转换每次取值都需要跨越 JNI 边界,而一些转换则更为复杂,例如根据调用者提供的 java.util.Calendar(JDBC 规范在此处仍强制要求使用的 java.time 之前的类)来获取时间戳。这些步骤单独来看并不昂贵,但当它们乘以数百万行和若干列时,开销就累积起来了。这正是向量化执行旨在避免的单值解释开销。

该循环还存在固定的开销。为了让单个连接能同时保持多个结果集打开,结果流式传输默认是关闭的,这意味着除非你设置 jdbc_stream_results(大多数用户从未设置过此选项),否则查询的整个结果都会被读入内存。驱动程序还会组装完整的结果元数据集。这每个结果只发生一次,而不是每行一次,因此与行循环相比成本很低,但也不是零开销,而且大多数查询从不查看它。

ResultSet 路径是你的 ORM 和生态系统中其他部分已经使用的标准,对于绝大多数查询(即返回数百或数千行的查询)来说,它是正确的选择。分块 API 适用于另一种情况:查询返回大量数据,并且你控制管道的两端,逐行读取没有任何优势。

使用 DuckDBChunkedResult 读取数据块

Java 驱动程序现在通过 C API 提供的 duckdb_fetch_chunk 相同机制,直接暴露引擎的原生数据块流。查询结果变成按需获取的数据块序列,你可以从每个块中批量读取列向量,就像引擎生成它们的方式一样,从而避免了 JDBC 规范要求的每行开销。

综合起来:

try (DuckDBConnection conn = DriverManager
        .getConnection("jdbc:duckdb:")
        .unwrap(DuckDBConnection.class);
     DuckDBPreparedStatement ps = conn.prepare(
         "SELECT l_orderkey, l_linenumber, l_shipmode "
             + "FROM 's3://my-bucket-name/lineitems.parquet'")) {

    try (DuckDBChunkedResult res = ps.query()) {

        // 前进到下一个数据块,成功时返回 true
        while (res.nextChunk()) {

            // 从结果中获取当前数据块
            DuckDBDataChunkReader chunk = res.chunk();

            // 向量通过基于 0 的列索引访问
            DuckDBReadableVector orderKeys = chunk.vector(0);
            DuckDBReadableVector lineNumbers = chunk.vector(1);
            DuckDBReadableVector shipModes = chunk.vector(2);

            // 使用适合其类型的 getter 读取每列
            for (long row = 0; row < chunk.rowCount(); row++) {
                long orderKey = orderKeys.getLong(row);
                int lineNumber = lineNumbers.getInt(row);
                String shipMode = shipModes.getString(row);
                System.out.println(orderKey + " " + lineNumber + " " + shipMode);
            }
        }
    }
}

这种方法有几个显著的特点:

  • 惰性(Laziness)nextChunk() 一次从引擎拉取一个数据块。完整结果永远不会在原生端或 Java 端物化,因此你可以流式处理远大于堆内存的结果,就像引擎本身生成它们的方式一样。

  • 列式访问(Columnar access)。在数据块内部,你按向量进行操作。如果你的目标也是列式的(例如 long[]、一个 Arrow VectorSchemaRoot 或一个 Parquet 写入器),你可以在紧凑的单态循环中复制值,而不是在每一行上切换列。

  • 足够的元数据以便分发(Enough metadata to dispatch)。每个结果仍然报告其列数和列类型,因此你可以为每个向量选择合适的 get* 调用,而无需在数据旁边携带单独的模式描述。

  • 如果你写过 UDF,这个 API 会很熟悉。数据块内容通过 DuckDBDataChunkReader API 访问,该 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-8 byte[] 形式读取 VARCHAR 值,这样文本密集型工作负载就可以跳过每个值都实例化一个 Java String 的步骤。

如果这些限制中任何一项影响了你关心的用例,请提交 issue。你的反馈将驱动下一次迭代的优先级。

结论

对于大多数应用程序来说,JDBC ResultSet 仍然是正确的默认选择。但引擎以列式数据块生成其结果,而 DuckDBChunkedResult 让你的 Java 代码能够以相同的形式读取它们:直接且惰性,在 JNI 边界几乎没有开销。

该特性由 duckdb-java#682 提供,可在 Maven Central 上当前的 duckdb_jdbc 发布版本中获取。在你最大的结果集上试用它,并告诉我们它的性能表现。欢迎提交 issue 和 pull request,DuckDB 团队也随时欢迎你在 DuckDB Discord 的 #java 频道讨论 Java 相关问题。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐