Java で始める DuckDB


DuckDB とは

OLAP(オンライン分析処理)ワークロードに特化した組み込み SQLite といったイメージです。

  • Apache Parquet ファイルをストレージとして使用
  • ベクトル化されたクエリ処理エンジン(vectorized query processing engine)を使用し、大規模データセット分析に使われる
  • クエリにはSQLを使用
  • 外部依存関係がなく、C++11コンパイラだけでビルド可能
  • 従来のクライアントサーバーモデルではなく、ホストプロセス内で組み込み実行される

DuckDB には C/C++API に加え、各種言語向けのクライアントAPIが提供されています。 ここでは Java から DuckDB を利用してみます。


DuckDB JDBC

DuckDB を Java から利用する場合、JDBC が提供されているので、これを利用するだけです。

Kotlin DSL の場合、以下の依存を追加します。

dependencies {
    implementation("org.duckdb:duckdb_jdbc:1.1.3")
}

duckdb_jdbc には、各種の環境別の共有ライブラリが同梱されており、環境に応じた共有ライブラリをJNIでロードします。

ネイティブライブラリを JDBC 経由で操作する流れになります。


DuckDB 操作

他のデータベースと同様に、JDBC を操作するだけです。

接続文字列として jdbc:duckdb: とすると、インメモリ動作となります(終了時にデータは無くなります)。

jdbc:duckdb:/tmp/my_database のようにデータベースファイルのパスを付与することで、データは永続化されます。

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

public class App {
    public static void main(String[] args) throws Exception {
        Connection conn = DriverManager.getConnection("jdbc:duckdb:");

        Statement stmt = conn.createStatement();
        stmt.execute("CREATE TABLE items (item VARCHAR, value DECIMAL(10, 2), count INTEGER)");
        stmt.execute("INSERT INTO items VALUES ('jeans', 20.0, 1), ('hammer', 42.2, 2)");

        try (ResultSet rs = stmt.executeQuery("SELECT * FROM items")) {
            while (rs.next()) {
                System.out.println(rs.getString(1) + " " + rs.getInt(3));
            }
        }
        stmt.close();
    }
}

単なる JDBC 操作なので、特に注目すべき点もありませんが、実行すると以下のような出力が得られます。

jeans 1
hammer 2

DuckDB 特有の機能を利用する場合は、DuckDBConnection にキャストして利用することになります。

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


Batch Insert

大量データのインサートにはバッチインサート機能を使うことができます。

以下のようになります。

private void batchInsert(DuckDBConnection conn) throws Exception {
    try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO items 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();
    }
}


Appender

バルク挿入には、Appender を利用することで、より高いパフォーマンスを得ることができます。

private void append(DuckDBConnection conn) throws Exception {
    try (var appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "items")) {

        appender.beginRow();
        appender.append(1);
        appender.append(2);
        appender.append(3);
        appender.endRow();

        appender.beginRow();
        appender.append(4);
        appender.append(5);
        appender.append(6);
        appender.endRow();

    }
}


データのインポート

以下のように CSV ファイルからテーブルにコピーすることができます。

COPY tbl FROM 'test.csv.gz' (HEADER false);

ロードされた内容から直接テーブルを作成することもできます。

CREATE TABLE test AS SELECT * FROM 'test.csv';

Parquet ファイルや JSON ファイルについても同様に操作できます。

COPY tbl FROM 'test.parquet';
COPY tbl FROM 'test.json';