- カテゴリー
- ブラウザデータ分析
- 公開日
- 2026.09.10
Contents
はじめに
分析用データを毎回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、メモリ解放まで確認するのが安全な実装の要点です。

