duckdb-wasm — 把分析数据库塞进浏览器标签页
已复核duckdb-wasm 把 DuckDB 的分析引擎编译成 WebAssembly,让浏览器或 Node 在进程内跑 SQL。日常类比:以前要请远端仓库管理员查账,现在把账本和计算器一起放进标签页——而且读远程 Parquet 时可以只抽需要的字节范围。
你写:
const bundle = await duckdb.selectBundle(duckdb.getJsDelivrBundles());const db = new duckdb.AsyncDuckDB(new duckdb.ConsoleLogger(), new Worker(bundle.mainWorker));await db.instantiate(bundle.mainModule, bundle.pthreadWorker);const conn = await db.connect();const table = await conn.query("select 1 as one");固定源码里,查询在 Worker 中执行,结果以 Arrow IPC 缓冲回到主线程,再被收成 arrow.Table。运行时依赖 apache-arrow。
不理解 duckdb-wasm,下面这些事都没法解释:
- 为什么同一套 JS API 要准备 mvp / eh / coi 三份 WASM
- 为什么远程文件打开会先发带
Range的同步 XHR - 为什么 OPFS 持久化走
db.open({ path: 'opfs://...' }),而不是只靠一条ATTACH - 为什么 JS 标量 UDF 挂在同步
DuckDBConnection上,异步连接没有同名方法
主链可以拆成五步:
-
选 bundle:
selectBundle()探测 WASM exception / SIMD / threads 与crossOriginIsolated。有异常处理就优先 eh;同时满足线程和跨源隔离且调用方提供了 coi 才用 coi。getJsDelivrBundles()只给出 mvp 与 eh,注释写明 coi 仍需显式 opt-in。 -
Worker 实例化:
AsyncDuckDB.instantiate(mainModule, pthreadWorker)把模块 URL 发给 Worker;浏览器绑定优先WebAssembly.instantiateStreaming,失败再退到数组缓冲。 -
异步消息:主线程用递增
messageId把CONNECT/RUN_QUERY等任务postMessage到 Worker,再用pendingRequests对回包。查询结果先从 WASM heapcopyBuffer拷出,再传回主线程。 -
HTTP / S3 文件:
BROWSER_RUNTIME用同步 XHR 探活与读区。注释写明 BLOB/HTTP 读取必须走 Range;allow_full_http_reads默认true,服务器不支持 206 时可以整文件回退。 -
OPFS:
open({ path: 'opfs://...' })时 Worker 先prepareDBFileHandle,给库文件和.wal创建FileSystemSyncAccessHandle,并打开useDirectIO。SQL 文本里的opfs://还可按opfs.fileHandling自动登记。
案例 1:按平台选 bundle 再查远程 Parquet
Section titled “案例 1:按平台选 bundle 再查远程 Parquet”import * as duckdb from "@duckdb/duckdb-wasm";
const bundle = await duckdb.selectBundle(duckdb.getJsDelivrBundles());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();const table = await conn.query( "select count(*) as n from 'https://example.com/lineitem.parquet'");console.log(table.toArray());这条路径会走 HTTP 协议与 Range 探测。示例 URL 只说明 API,本文没有实际下载该文件。
案例 2:用 OPFS 打开可写库
Section titled “案例 2:用 OPFS 打开可写库”await db.open({ path: "opfs://notes.db", accessMode: duckdb.DuckDBAccessMode.READ_WRITE});const conn = await db.connect();await conn.query("create table notes(id integer, body varchar)");await conn.query("insert into notes values (1, 'first note')");await conn.query("checkpoint");固定测试用的是 open({ path: 'opfs://test.db' }),关连接后再次 open 同一路径可以读回表。createSyncAccessHandle 依赖当前浏览器对 OPFS 同步句柄的限制,不能把任意静态托管环境都写成“关掉标签页数据一定还在”。
案例 3:同步连接上的标量 UDF
Section titled “案例 3:同步连接上的标量 UDF”import * as arrow from "apache-arrow";
conn.createScalarFunction("upper_js", new arrow.Utf8(), (s) => String(s ?? "").toUpperCase());const table = conn.query("select upper_js('hello') as v");createScalarFunction 只出现在同步 DuckDBConnection。AsyncDuckDBConnection 提供 query / send / prepare / Arrow 插入,没有同名 UDF 方法。
-
jsDelivr helper 不含 coi:需要 pthread 时必须自己提供
bundles.coi,并满足跨源隔离。maximumThreads也注明依赖SharedArrayBuffer。 -
WASM MIME 与 streaming:浏览器绑定先走
instantiateStreaming。自托管若没返回application/wasm,会落到 XHR 回退;回退失败才是“一直 loading”。 -
Range 失败不等于立刻放弃:默认允许 full HTTP read。代理若忽略
Range回 200,打开阶段可能把整文件读进 WASM heap。 -
异步 API 不是同步 API 的薄包装:
query()把完整 IPC 文件式缓冲收成 Table;send()才是流式AsyncRecordBatchStreamReader。大结果会经过 heap 拷贝和postMessage。 -
包版本 provenance 分裂:GitHub release
v1.33.0指向本提交,但仓内packages/duckdb-wasm/package.json仍写1.11.0;npm 把同一gitHead发成1.32.1-dev1.0,没有1.33.0包。后继1.33.1-dev*不在本文范围内。
适用 vs 不适用场景
Section titled “适用 vs 不适用场景”适用:
- 浏览器里对 Parquet / CSV / JSON 做 ad-hoc 聚合,结果用 Arrow 交给图表
- 需要按平台能力换 wasm 变体,而不是手写一套 SQL 引擎
- 用 OPFS 做可恢复的本地分析库,并且能接受同步文件句柄的环境约束
不适用:
- 高并发写入或多标签页抢同一 OPFS 句柄
- 必须在
AsyncDuckDBConnection上注册 JS UDF - 把未发布的 npm
latest(本稿检索时是1.33.1-dev*)当成 GitHub release - OLTP 记事本 / 购物车 → sqlite 的行存嵌入式模型更贴
固定版本边界
Section titled “固定版本边界”- 本文绑定
duckdb/duckdb-wasm@fa1d47b38...,对应 GitHub releasev1.33.0。 - 该提交的 npm 映射是
@duckdb/duckdb-wasm@1.32.1-dev1.0,不是1.33.0;仓内 package version 仍为1.11.0。 - 浏览器默认入口是
dist/duckdb-browser.mjs;Node 入口是dist/duckdb-node.cjs。同步 API 在./blocking。 - 核心依赖
apache-arrow@^17.0.0。C++ 侧通过 submodule 编进 DuckDB,并默认带json与core_functions扩展。 - 本文未实例化 WASM、未发 Range 请求、未跑 karma/jasmine,状态保持
UNVERIFIED。
- 浏览器分析库的合同在 Worker 边界——主线程看到的是消息和 Arrow 表,不是 C++ 执行器本身。
- 能力探测决定下载哪份机器码——eh / coi 不是别名,缺特征就回落到 mvp。
- 远程列存能成立,是因为运行时先问 Range——格式和 HTTP 语义绑在一起。
- 发布标签、仓内 version 与 npm dist-tag 可能对不齐——只能绑 commit,不能猜包名。
getJsDelivrBundles()在支持线程的浏览器里会自动返回 coi 吗?AsyncDuckDBConnection上能否调用createScalarFunction?- 远程文件的服务器忽略
Range并回 200 时,默认会失败还是整文件回退?
检查点:
- 不会。helper 只提供 mvp/eh;coi 必须调用方显式传入。
- 不能。该方法只在同步
DuckDBConnection上。 - 默认
allow_full_http_reads为 true,打开阶段可以整文件回退。
- 启动说明:DuckDB-Wasm launch post
- 固定源码:duckdb/duckdb-wasm —— 本文绑定提交
fa1d47b38ed0821cecab0bdc331c48abd0f2cc65 - 论文:DuckDB-Wasm: Fast Analytical Processing for the Web
- duckdb —— 同一引擎的本地/服务端形态
- sqlite —— 浏览器 SQL 的行存对照
- duckdb —— WASM 包装的本体引擎
- sqlite —— OLTP / 行存嵌入式对照
- clickhouse —— 只能在 server 跑的列存对照
- vite —— 文档给出的 worker / wasm URL 接入方式之一
- postgresql —— 行存客户端协议对照,见 postgres-js