executeBatch()` on a prepared INSERT runs s are ~1–2 orders of magnitude slower than the Appender
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 42/100
Research direction
Start with executeBatchedPreparedStatement() in DuckDBPreparedStatement.java around L727–749 and the result handling around L233–238. Trace execute_prepared_statement in src/jni/duckdb_java.cpp and PreparedStatement::Execute in src/main/prepared_statement.cpp, then compare the InternalAppender path. Done means the proposed batching and opt-in insert rewrite preserve JDBC errors and fallback behavior while removing avoidable per-row overhead.
Written by the indexing model from the issue text.
Description
PreparedStatement.executeBatch() is currently a convenience emulation: it executes every batched parameter row as an individual statement execution, each paying a JNI crossing, per-value parameter conversion, a full query lifecycle in the DuckDB core, and (on the way back) a freshly built result-set metadata object and DuckDBResultSet — per row. For plain parameter-only INSERTs, the same data written through DuckDBAppender bypasses all of it and is dramatically faster.
Analysis today
-
Java loop —
executeBatchedPreparedStatement()callsexecute()once per batched parameter row ([DuckDBPreparedStatement.java~L727–749]). The only batching benefit is that the loop is wrapped in a single transaction (startTransaction()), which saves the per-row commit/WAL flush. -
Per-row result machinery — every
execute()call then builds aDuckDBResultSetMetaData(JNI string arrays for column names/types viabuild_meta) and a newDuckDBResultSet, only to read the changed-rows count out of it ([DuckDBPreparedStatement.java~L233–238]). For a batch of N rows that is N metadata objects and N result sets of garbage whose only payload is along. -
Per-row JNI + parameter conversion —
_duckdb_jdbc_executeconverts each parameter individually viaGetObjectArrayElement+to_duckdb_value(an instanceof chain per value) before callingstmt->Execute(params)([src/jni/duckdb_java.cpp,execute_prepared_statement]). -
Full statement lifecycle in the core —
PreparedStatement::Executeis not a lightweight re-execute: it constructs anExecuteStatementand sends it throughClientContext::RunInternalStatement(duckdb core,src/main/prepared_statement.cpp), i.e. pending query, executor, pipeline scheduling,PhysicalInserton a 1-row chunk, and a materialized result — per row.
By contrast, DuckDBAppender writes values into a 2048-row data chunk through direct ByteBuffers and hands the whole chunk to the storage layer via duckdb_append_data_chunk — no planner, no executor, one JNI crossing per chunk instead of several per row. Constraints (NOT NULL, PK/unique via index append) are still enforced at flush, so correctness-wise the storage path is equivalent for plain inserts.
Proposed
-
Stop building metadata/result sets per row for
CHANGED_ROWSresults (semantics-preserving). When the statement returns a row count, return it natively as alongfrom the JNI call instead of constructingDuckDBResultSetMetaData+DuckDBResultSetper execution. This helps every update/insert, not just batches. -
Batch bind across JNI (semantics-preserving). Add a native entry point that carries all N parameter rows of a batch in one call and loops on the C++ side. This amortizes the JNI crossing and the per-value
GetObjectArrayElementround trips while keeping the executor path and exact JDBC error semantics. -
Opt-in rewrite of batched parameter-only INSERTs to the internal appender, behind a connection property (precedent: MySQL Connector/J
rewriteBatchedStatements=true). In the JNI layer, detect a bare parameter-only single-table INSERT; if it matches, drain the batch through anInternalAppenderchunk inside the surrounding transaction; on any deviation (cast required,ON CONFLICT/RETURNING, partial columns) fall back to the current path. Opt-in because failure attribution within a batch changes (chunk-level instead of statement-level).
- Dominant language
- C++
- Stars
- 127
- Forks
- 80
- Avg merge
- 13h 41m
- Merged PRs (30d)
- 44
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from duckdb/duckdb-java
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
duckdb/duckdb-java#832 · 2 comments ·
-
Native SIGSEGV in DuckDB JDBC when repeatedly executing queries with window functions and LIMIT 0 Open
Difficulty 4/5 3-5 days Newbie friendliness 48/100
duckdb/duckdb-java#871 · 4 comments ·
-
Difficulty 4/5 3-5 days Newbie friendliness 45/100
duckdb/duckdb-java#872 ·
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
duckdb/duckdb-java#837 ·
-
capi_v2
Difficulty 5/5 Over a week Newbie friendliness 35/100
duckdb/duckdb-java#806 · 1 comment ·
All issues in duckdb/duckdb-java
Similar issues
-
Difficulty 1/5 Under an hour Newbie friendliness 90/100
AXERA-TECH/ax-llm#77 ·
-
Difficulty 1/5 Under an hour Newbie friendliness 90/100
games-on-whales/wolf#509 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
-
bug-unconfirmed
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
NVIDIA/cuda-samples#453 ·