DuckDB-WasmとApache Arrow IPCをつなぐ|JSON変換なしでブラウザ集計

JSON変換なしで集計:DuckDB-WasmとApache Arrow IPC ブラウザデータ分析
カテゴリー
ブラウザデータ分析
公開日
2026.09.10

はじめに

分析用データを毎回JSONへ変換すると、列名と型の復元、文字列化、メモリ上の複製が必要になります。DuckDB-WasmはApache Arrowをネイティブな受け渡し形式として使い、Arrow Tableを直接挿入できます。

この記事では、JavaScriptで作ったArrow TableをDuckDBへ入れ、SQL集計し、結果を再びArrowとして読むところまで作ります。さらに、ネットワークから届くArrow IPCストリームの扱いも確認します。

準備

mkdir duckdb-arrow-demo
cd duckdb-arrow-demo
npm init -y
npm install @duckdb/duckdb-wasm@1.32.0 apache-arrow@17
npm install --save-dev vite

DuckDB-Wasmが依存するApache Arrowと同じメジャーバージョンを使います。公式ドキュメントは、メジャーが異なると挿入が失敗する場合があると注意しています。

DuckDB-Wasmを起動する

import * as duckdb from "@duckdb/duckdb-wasm";
import workerUrl from "@duckdb/duckdb-wasm/dist/duckdb-browser-mvp.worker.js?url";
import wasmUrl from "@duckdb/duckdb-wasm/dist/duckdb-mvp.wasm?url";

const worker = new Worker(workerUrl);
const logger = new duckdb.ConsoleLogger();
const db = new duckdb.AsyncDuckDB(logger, worker);
await db.instantiate(wasmUrl);
const conn = await db.connect();

この例は互換性重視のMVPバンドルを固定しています。実用ではselectBundle()を使い、端末に合うバンドルを選べます。

Arrow Tableを直接挿入する

import { tableFromArrays } from "apache-arrow";

const sales = tableFromArrays({
  category: ["書籍", "文房具", "書籍"],
  amount: Int32Array.from([1200, 500, 1800]),
});

await conn.insertArrowTable(sales, { name: "sales" });

const result = await conn.query(`
  SELECT category, count(*) AS count, sum(amount) AS total
  FROM sales
  GROUP BY category
  ORDER BY category
`);

console.table(result.toArray().map((row) => ({
  category: row.category,
  count: Number(row.count),
  total: Number(row.total),
})));

DuckDBのBIGINT相当はJavaScriptでbigintになることがあります。DOM表示やJSON化の前に、値の範囲を確認してNumberまたは文字列へ変換します。

Arrow IPCストリームを取り込む

サーバーがArrow IPC streaming formatを返す場合は、Responseのチャンクを順に渡します。

const response = await fetch("/api/sales.arrow-stream");
if (!response.ok || !response.body) {
  throw new Error(`Arrow取得失敗: HTTP ${response.status}`);
}

const reader = response.body.getReader();
const inserts = [];

while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  inserts.push(
    conn.insertArrowFromIPCStream(value, { name: "streamed_sales" }),
  );
}
await Promise.all(inserts);

Arrow IPC streaming formatの末尾には、送信側がストリームの一部としてEOSを含めます。受信側でEOSを無条件に追加すると、末尾にEOSがすでにある場合に空の別ストリームとして解釈され、失敗することがあります。HTTPのチャンク境界とArrowメッセージ境界は同じとは限らないため、受信したチャンクを独自に解析・加工せずAPIへ渡します。

型を先に確認する

const schema = await conn.query("DESCRIBE sales");
console.table(schema.toArray());

集計前に列名と型を確認し、必須列がなければ利用者へ伝えます。外部データからSQL文字列を組み立てる場合、値はパラメーター化し、テーブル名・列名は許可リストで選びます。

後片付け

await conn.close();
await db.terminate();
worker.terminate();

ページ内で何度も初期化するとWorkerとWasmメモリを増やすため、接続を再利用し、終了時に解放します。

動作確認

  • 日本語カテゴリを含む3行が挿入できる
  • 書籍2件・合計3000、文房具1件・合計500になる
  • Arrowのメジャーバージョンをずらした検証環境では明確に失敗を検知する
  • 途中で切れたIPCストリームを正常扱いしない
  • 必須列なし、空データ、不正な型を表示する
  • 接続とWorkerを終了できる

制約

Arrowは効率的な列形式ですが、ブラウザのメモリ上限をなくしません。受信データ、Wasmメモリ、結果表が同時に存在する可能性があります。巨大データでは、サーバー側集約、列の絞り込み、Parquetの範囲取得も比較します。

トラブル対処

  • Tableが作られない:apache-arrowとDuckDB-Wasm依存先のメジャー版を一致させます。
  • IPC読込が完了しない:送信側がEOSを含む正しいArrow IPCストリームを返しているか確認します。受信側でEOSを二重追加しません。
  • JSON化で例外:bigintを範囲に応じてNumberまたは文字列へ変換します。

まとめ

DuckDB-WasmとArrowを組み合わせると、分析データをJSONへ戻さずに挿入・集計できます。互換性のあるArrowメジャー版を固定し、EOS、型、BigInt、メモリ解放まで確認するのが安全な実装の要点です。

参考リンク

タイトルとURLをコピーしました