DuckDB-Wasm でブラウザ内 SQL を動かす
DuckDB-Wasm をブラウザに埋め込み、SQL の DDL / INSERT / SELECT をクライアント完結で実行するパターン、Parquet / CSV 読み込み、WASM ロードの落とし穴を触れる demo で確認する実装メモ。
検証日: 2026-05-10
使用バージョン:
@duckdb/duckdb-wasm@1.33.x対象: ブラウザだけで分析クエリを動かしたい(サーバー不要)、Parquet を直接読みたい、ローカル CSV 解析
DuckDB(列指向の高速分析 SQL DB)を WASM(WebAssembly) ビルドでブラウザに埋め込み、サーバーなしで SQL を実行するパターン。DDL / INSERT / SELECT / Parquet 読み込み、WASM ロードの落とし穴を動く demo で確認します。
触って試す
初回は WASM(~3MB)を fetch するため数秒待ちます。
なぜブラウザで SQL?
- サーバー / DB 不要:プライベートデータをアップロードせず、ローカルで分析できる
- Parquet を直接読める:CSV や JSON より高速、列志向で集約に強い
- OLAP 系クエリ最適化:GROUP BY / WINDOW / JOIN が Postgres 互換 + 高速
- AI agent との相性:LLM に SQL を書かせて即実行する UX
代替: SQL.js(SQLite を WASM 化)。OLAP / Parquet なら DuckDB のほうが速い。
最小サンプル(DuckDB-Wasm 1.33、5 ステップ)
AsyncDuckDB を bundle に応じた worker と一緒に初期化、connect() でコネクションを取って query() を実行するだけ:
import * as duckdb from "@duckdb/duckdb-wasm";
const bundles = duckdb.getJsDelivrBundles();
const bundle = await duckdb.selectBundle(bundles);
const worker = new Worker(bundle.mainWorker!);
const db = new duckdb.AsyncDuckDB(new duckdb.ConsoleLogger(), worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
const conn = await db.connect();
await conn.query(`
CREATE TABLE sales (region TEXT, revenue DOUBLE);
INSERT INTO sales VALUES ('east', 1500), ('west', 1100);
`);
const result = await conn.query("SELECT region, SUM(revenue) FROM sales GROUP BY region");
console.log(result.toArray());
Parquet / CSV を直接読む
registerFileURL で URL を登録 → SQL から read_csv / read_parquet で直接読める:
// HTTP 経由で Parquet を直接 SQL 対象にできる
const r = await conn.query(`
SELECT category, COUNT(*)
FROM 'https://example.com/data.parquet'
GROUP BY category
`);
// ローカル CSV(ユーザがアップロードした File を Blob URL 化)
const file = inputElement.files![0];
await db.registerFileHandle("upload.csv", file, duckdb.DuckDBDataProtocol.BROWSER_FILEREADER, true);
await conn.query("SELECT * FROM 'upload.csv' LIMIT 10");
バンドル戦略
WASM は ~3MB あるので、main bundle に同梱しない。動的 import で初回利用時にだけロード:
const init = async () => {
const duckdb = await import("@duckdb/duckdb-wasm");
// ...
};
getJsDelivrBundles() は CDN(jsdelivr)からブラウザ環境ごとに最適な bundle を選んでくれる。自前 host する場合は bundles を手動で用意。
主な SQL 機能
| 機能 | サポート |
|---|---|
| GROUP BY / 集約 | ✅ |
| JOIN(全種類) | ✅ |
| WINDOW 関数 | ✅(LEAD / LAG / RANK / ROW_NUMBER) |
| CTE / 再帰 CTE | ✅ |
| Date / Timestamp 関数 | ✅(date_trunc / interval) |
| JSON 関数 | ✅(json_extract) |
| Geospatial | △(spatial extension で対応) |
PostgreSQL 互換が高く、Postgres で書いた analytic クエリはほぼそのまま動く。
つまずいたポイント
- CORS:Parquet を別ドメインから読む時は
Access-Control-Allow-Origin: *必要 - WASM の memory 上限:32-bit WASM は 4GB が上限。大きいテーブルは Parquet でストリーム読みする
- 初回 fetch が遅い:
<link rel="preload">で WASM を予読み込みして UX 改善 AsyncDuckDBを必ず使う:同期 API は worker thread を使わないので UI が止まる- column の型推論:CSV 読み込み時の型推論は ambiguous な場合がある。明示的に
CASTする - iframe 内で動かない:
SharedArrayBufferが必要、Cross-Origin-Embedder-Policy: require-corpヘッダが必要
評価
| 観点 | 評価 | コメント |
|---|---|---|
| クエリ性能 | ◎ | OLAP に最適化、数百万行も実用速度 |
| Parquet サポート | ◎ | 直接 URL から読める |
| バンドル | △ | WASM ~3MB、動的 import 必須 |
| 学習コスト | ○ | SQL 既知者なら API は薄いラッパー |
| エコシステム | ○ | Observable Notebook / MotherDuck で連携進行中 |
向く / 向かないケース
- 向く: ブラウザ内 BI ダッシュボード、ユーザがアップロードした CSV/Parquet の分析、エディタ内 query playground
- 向かない: 数 GB のデータをクライアントで処理(memory limit)、リアルタイム書き込みの DB 用途
- 向かない: 単純な「SQLite で十分」(SQL.js のほうがバンドル軽い)
関連 Topic / 関連書籍
この記事と関係する tech-book.net の Topic と、それぞれの Topic に紐づく書籍:
JavaScriptによるはじめてのアルゴリズム入門
Python と JavaScriptではじめるデータビジュアライゼーション
React Native+Expoではじめるスマホアプリ開発 : JavaScriptによるアプリ構築の実際
「React Native」は、Facebookが開発しているスマートフォンアプリ向けの開発環境です。ほとんどのコードをJav…