⌘K ctrl+k
Search Shortcut cmd + k | ctrl + k
Import Data

Overview

The JDBC driver offers two ways to load data into DuckDB in bulk: the high-performance Appender and a JDBC batch writer. This page describes both and when to use each.

Appender

The Appender is available in the DuckDB JDBC driver via the org.duckdb.DuckDBAppender class. An appender is created from a DuckDBConnection with the createAppender() method, passing the table name and, optionally, the schema and catalog it is applied to. The Appender is flushed when the close() method is called.

Example:

import java.sql.DriverManager;
import org.duckdb.DuckDBConnection;

DuckDBConnection conn = (DuckDBConnection) DriverManager.getConnection("jdbc:duckdb:");
try (var stmt = conn.createStatement()) {
    stmt.execute("CREATE TABLE tbl (x INTEGER, y DOUBLE, s VARCHAR)");

    // using try-with-resources to automatically close the appender at the end of the scope
    try (var appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "tbl")) {
        appender.beginRow();
        appender.append(10);
        appender.append(3.2);
        appender.append("hello");
        appender.endRow();
        appender.beginRow();
        appender.append(20);
        appender.append(-8.1);
        appender.append("world");
        appender.endRow();
    }
}

An appender can also be created with createAppender(tableName) for the default schema, or with createAppender(catalogName, schemaName, tableName) to target a specific catalog. Buffered rows are written to the table when the appender is closed, or earlier by calling flush(). Within a row, appendNull() writes a NULL and appendDefault() writes the column's DEFAULT value.

Nested and collection types are appended within a row as well. For a STRUCT column, call beginStruct(), append each field value in declaration order, then endStruct(). For a UNION column, call beginUnion(tag) — where tag is the name of the target UNION member — append that member's value, then endUnion(). For a LIST or ARRAY column, pass a Java array as a single value, optionally with a boolean null mask where true marks the corresponding element as NULL:

try (var appender = conn.createAppender("list_tbl")) {
    appender.beginRow();
    appender.append(new int[] {1, 2, 3});               // a LIST value
    appender.append(new int[] {4, 5}, new boolean[] {false, true}); // second element is NULL
    appender.endRow();
}

Maximizing Appender Performance

The Appender is already the fastest way to bulk-load data, but a few properties of how it works determine its throughput:

  • Row batching is fixed and internal. The Appender writes rows in fixed-size batches of 2,048 rows, one DuckDB vector, and flushes a batch automatically as soon as it fills. There is no configurable flush count, so adding an application-level batch size on top of the Appender does not improve performance.
  • Prefer primitive columns over complex types. Nested types such as STRUCT, LIST, and UNION cost more per value than flat, primitive columns. A schema built from primitive columns appends fastest.
  • Avoid allocating a Java object per row. Boxing values or constructing a new object for every row adds allocation and garbage-collection pressure. Prefer the primitive append() overloads and reuse buffers where you can.
  • Keep strings short where possible. DuckDB stores strings of up to 12 bytes inline; longer strings are stored out of line and add a small overhead per value.

Batch Writer

The DuckDB JDBC driver offers batch write functionality. The batch writer supports prepared statements to mitigate the overhead of query parsing.

The preferred method for bulk inserts is to use the Appender due to its higher performance. However, when using the Appender is not possible, the batch writer is available as an alternative.

Batch Writer with Prepared Statements

Prepared statements are the recommended way to use the batch writer, because the statement is parsed once and reused for every row. Bind each row's parameters, call addBatch() to queue the row, and call executeBatch() to write all queued rows in a single operation.

import java.sql.DriverManager;
import java.sql.PreparedStatement;
import org.duckdb.DuckDBConnection;

DuckDBConnection conn = (DuckDBConnection) DriverManager.getConnection("jdbc:duckdb:");
PreparedStatement stmt = conn.prepareStatement("INSERT INTO test (x, y, z) VALUES (?, ?, ?);");

stmt.setObject(1, 1);
stmt.setObject(2, 2);
stmt.setObject(3, 3);
stmt.addBatch();

stmt.setObject(1, 4);
stmt.setObject(2, 5);
stmt.setObject(3, 6);
stmt.addBatch();

stmt.executeBatch();
stmt.close();

Batch Writer with Vanilla Statements

The batch writer also accepts plain SQL statements that carry their values inline, without parameters. Queue each statement with addBatch(String) and submit them together with executeBatch(). This avoids parameter binding but does not benefit from the reuse that prepared statements provide.

import java.sql.DriverManager;
import java.sql.Statement;
import org.duckdb.DuckDBConnection;

DuckDBConnection conn = (DuckDBConnection) DriverManager.getConnection("jdbc:duckdb:");
Statement stmt = conn.createStatement();

stmt.execute("CREATE TABLE test (x INTEGER, y INTEGER, z INTEGER)");

stmt.addBatch("INSERT INTO test (x, y, z) VALUES (1, 2, 3);");
stmt.addBatch("INSERT INTO test (x, y, z) VALUES (4, 5, 6);");

stmt.executeBatch();
stmt.close();

Further Reading

  • Appender — the engine-level Appender interface that the Java DuckDBAppender wraps.
  • Data Import — DuckDB's other bulk-loading options, including reading directly from CSV, Parquet, and JSON files.
  • Run Queries — sending the INSERT and CREATE TABLE statements the batch writer builds on.
  • Define Connections — obtaining the DuckDBConnection an Appender is created from.
© 2026 DuckDB Foundation, Amsterdam NL
DuckDB Home Code of Conduct Trademark Use Blog