首页 / Bun 入门教程 / SQLite 与数据库

Bun 入门教程

SQLite 与数据库

本教程共 34 篇 · 第 29 篇 · 更新于 2026-08-06

Bunbun:sqliteSQLiteDatabase事务WALORM

本节目标:

  • 用内置的 bun:sqlite 打开数据库、用 db.query() 准备语句并绑定参数。
  • 区分 .all() / .get() / .run() / .values() / .iterate() 的返回形态。
  • 理解事务、WAL 模式与 using 自动关闭连接。
  • .as(Class) 把查询结果直接映射成类实例,并知道 bigint / safeIntegers 的处理。
  • 厘清 bun:sqlite 是「驱动」而非「ORM」,以及它与 Drizzle/Prisma/Kysely 等生态的关系。

Bun 原生实现了一个高性能的 SQLite3 驱动,导入路径是内置模块 bun:sqlite,无需安装任何 npm 包即可使用。它的 API 是同步的、面向语句(statement-based),灵感来自 better-sqlite3。官方基准显示,读查询比 better-sqlite3 快约 3–6 倍,比 deno.land/x/sqlite 快约 8–9 倍。

29.1 打开数据库

import { Database } from "bun:sqlite";

const db = new Database("mydb.sqlite");        // 打开/创建文件数据库
const mem = new Database(":memory:");          // 内存数据库(同 new Database() / new Database(""))
const ro  = new Database("mydb.sqlite", { readonly: true }); // 只读
const created = new Database("mydb.sqlite", { create: true }); // 不存在则创建

Database.open(path)new Database(path) 的等价写法。此外还能直接从一段 Uint8Array 打开数据库——这种方式可读可写,但改动不会落盘,很适合拿线上库的副本做一次性分析:

import { Database } from "bun:sqlite";
import { readFileSync } from "node:fs";

const db = new Database(readFileSync("snapshot.sqlite"));
Note

也可以用 ESM 导入属性直接加载:import db from "./mydb.sqlite" with { type: "sqlite" };,等价于 new Database("./mydb.sqlite")

关闭连接:

db.close(false); // 允许已 prepare 的语句继续工作到被回收
db.close(true);  // 立即终结所有语句并释放连接,出错则抛异常

using 语法可在代码块结束时自动关闭(TS 5.2+):

{
  using db = new Database("mydb.sqlite");
  using query = db.query("select 'Hello world' as message;");
  console.log(query.get());
}

29.2 准备语句与参数绑定

db.query() 会编译并缓存一条 SQL(缓存的是编译后的字节码,不是结果)。同名 SQL 多次调用返回同一 Statement 对象,可安全地用不同参数反复执行。

const query = db.query("SELECT * FROM users WHERE id = ?1");
query.get(1); // ✓
query.get(2); // ✓ 参数每次重新绑定

参数支持位置参数?1)和命名参数$name / :name / @name):

const q1 = db.query("SELECT ?1, ?2;");
q1.all("hello", "goodbye");

const q2 = db.query("SELECT $msg;");
q2.all({ $msg: "Hello world" });
Tip

默认命名参数需要带 $/:/@ 前缀。想要不带前缀绑定(如 ORM 场景),可在 new Database(path, { strict: true }) 打开时开启 strict;开启后缺参数会报错,而不是静默忽略。

29.3 不同的执行方法

同一个 Statement 用不同方法执行,返回形态不同:

方法返回典型用途
.all(params)object[]取回多行
.get(params)首行对象,无结果时为 null取回首行
.run(params){ lastInsertRowid, changes }写操作 / 建表
.values(params)unknown[][]只要列的裸数组
.raw(params)Uint8Array[][]所有值统一按字节返回
.iterate(params)迭代器大数据集逐行处理
db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)");
db.run("INSERT INTO users (name) VALUES ($name)", { $name: "Ada" });

const all = db.query("SELECT * FROM users").all();     // [{id:1,name:'Ada'}]
const one = db.query("SELECT * FROM users WHERE id=$id").get({ $id: 1 });
const names = db.query("SELECT name FROM users").values(); // [['Ada']]

for (const row of db.query("SELECT * FROM users").iterate()) {
  console.log(row); // 逐行,不一次性载入内存
}

Statement 本身实现了迭代器协议,所以 .iterate() 可以省略,直接对语句对象 for...of

for (const row of db.query("SELECT * FROM users")) {
  console.log(row);
}
Note

.run() 返回 lastInsertRowidchanges,适合建表、插入、更新、删除等不需要结果集的操作。db.exec()db.run() 的别名,两者行为一致。

怎么选?记住一条经验:结果集有多大,就选多「懒」的方法。几十行用 .all() 最省事;几十万行用 .iterate(),内存占用是常数级;只关心是否存在用 .get(),SQLite 找到第一行就停,不会白扫剩下的数据。

29.4 事务

bun:sqlite 把事务包装成函数,连续多条语句要么全成功要么全失败:

const insert = db.query("INSERT INTO users (name) VALUES ($name)");
const tx = db.transaction((names: string[]) => {
  for (const n of names) insert.run({ $name: n });
});
tx(["Ada", "Linus", "Ken"]); // 作为一个事务提交

事务可像普通函数一样调用,也支持 tx.deferred() / .immediate() / .exclusive() 指定锁模式,以及 tx.active 判断是否在事务中。

包装成函数这个设计不只是语法糖。函数正常返回时 Bun 帮你 COMMIT,抛出异常时自动 ROLLBACK,你不需要写 try/catch 手动回滚:

const tx = db.transaction((n: number) => {
  insert.run({ $name: "temp" });
  if (n < 0) throw new Error("非法输入"); // 这一抛,上面那条插入也会被撤销
  return n;
});

顺带说个性能数字级别的差异:逐条 INSERT 时每条都要各自提交一次,磁盘同步开销是主要瓶颈;套进一个事务里批量提交,插入上万行的耗时通常能降一到两个数量级。往 SQLite 里灌数据,请务必包事务。

29.5 WAL 模式

SQLite 的写前日志(WAL)模式能显著提升「多读单写」场景的性能,官方推荐大多数应用开启:

db.run("PRAGMA journal_mode = WAL;");
Warning

在 macOS 上,Bun 使用系统提供的 SQLite(Apple 默认开启持久 WAL),-wal-shm 伴随文件在 close() 后仍会保留——这是预期行为,不是 bug。若要在所有平台都清理这些文件,可在关闭前关闭持久 WAL 并做一次截断检查点:

import { Database, constants } from "bun:sqlite";
const db = new Database("mydb.sqlite");
db.run("PRAGMA journal_mode = WAL;");
db.fileControl(constants.SQLITE_FCNTL_PERSIST_WAL, 0);
db.run("PRAGMA wal_checkpoint(TRUNCATE);");
db.close();

29.6 把结果映射成类:.as(Class)

无需 ORM 也能把行映射成类实例——类的 getter/方法会被挂上原型、可直接调用(但构造函数不会被调用,更像 Object.create):

class Movie {
  title: string;
  year: number;
  get isMarvel() { return this.title.includes("Marvel"); }
}

const query = db.query("SELECT title, year FROM movies").as(Movie);
console.log(query.all()[0].isMarvel); // 调用类方法

29.7 整数与 bigint

SQLite 支持 64 位整数,但 JS 的 number 只有 53 位精度。默认 bun:sqlite 把整数作为 number 返回(超出 53 位会被截断)。需要精确大整数时,开启 safeIntegers: true,整数将以 bigint 返回,并校验绑定值不超过 64 位:

import { Database } from "bun:sqlite";
const db = new Database(":memory:", { safeIntegers: true });
const r = db.query(`SELECT ${BigInt(Number.MAX_SAFE_INTEGER) + 102n} as max_int`).get();
console.log(r.max_int); // 9007199254741093n

29.8 其他实用能力

多语句执行

database.run() 支持在单次调用里写多条以分号分隔的 SQL,适合初始化脚本:

db.run(`
  CREATE TABLE IF NOT EXISTS a (id INTEGER PRIMARY KEY);
  CREATE TABLE IF NOT EXISTS b (id INTEGER PRIMARY KEY);
`);

query()prepare() 的分工

两者都会把 SQL 编译成 Statement,区别只有一个:db.query() 把编译结果缓存在 Database 实例上,同一条 SQL 再次调用直接复用;db.prepare() 不缓存,每次都重新编译。

// 固定 SQL,反复执行 —— 用 query(),享受缓存
const findUser = db.query("SELECT * FROM users WHERE id = ?");

// 动态拼出来的一次性 SQL —— 用 prepare(),别污染缓存
const stmt = db.prepare(`SELECT * FROM users ORDER BY ${safeColumn}`);

判断标准很简单:SQL 文本是不是有限的几种。是,就用 query();如果 SQL 会随参数变化产生无数种形态,用 prepare()

加载 SQLite 扩展

需要 FTS5、JSON1 之外的第三方扩展时,用 db.loadExtension()

db.loadExtension("vector0");
Warning

macOS 上 Bun 默认链接的是 Apple 自带的 SQLite(因为它带来约 50% 的性能提升),而 Apple 的构建禁用了扩展加载。要在 macOS 上加载扩展,得先用 Database.setCustomSQLite() 指定一个自编译的 SQLite 库。

序列化 / 反序列化数据库

const olddb = new Database("mydb.sqlite");
const contents = olddb.serialize();       // => Uint8Array
const newdb = Database.deserialize(contents);

这在把内存库导出、做快照或跨进程传递数据库内容时很有用。

事务模式与底层常量

事务支持 deferred / immediate / exclusive 三种锁模式(db.transaction(fn).immediate() 等)。底层能力还有 db.fileControl(constants.SQLITE_FCNTL_PERSIST_WAL, 0)(见 29.5 的 WAL 清理示例),以及 constants 里导出的 SQLite 常量,供需要贴近原生 API 的场景使用。

调试技巧

Statement 调用 .toString() 会打印展开参数后的完整 SQL;.finalize() 可显式释放语句(多数情况下交给 GC 即可)。query.all() / .get() 返回的对象键名就是列名——BLOB 列会自动转成 Uint8Array

29.9 错误处理与异常

bun:sqlite 的 API 是同步的,错误会直接以异常抛出,用普通 try/catch 即可捕获:

try {
  db.query("INSERT INTO users (name) VALUES ($name)").run({ $name: "Ada" });
} catch (e) {
  // 例如约束冲突、SQL 语法错误、数据库已关闭等
  console.error(e.message);
}

几个常见错误来源:

  • 参数缺失/多余:默认模式下缺失命名参数不会报错(按 NULL 处理),开启 strict: true 后才会抛错——这正是 strict 的价值。
  • 数据库已关闭:对 close() 后的 Statement 调用方法会抛 Database has closed(但 toString()finalize() 仍安全)。
  • 整数越界:开启 safeIntegers: true 后,绑定超过 64 位的 bigint 会抛 ... is out of range

因为操作是同步的,await 并不会让 SQL 变「异步」——它只是让你能在 async 函数里用 try/catch 统一兜住。若想并发执行多条独立写入,把它们放进同一个事务里(见 29.4)通常比分别 await 更快也更安全。

29.10 bun:sqlite 与 ORM 生态的关系

bun:sqlite驱动层(负责连接 SQLite、编译语句、执行、转换类型)。它仍然偏底层——没有迁移管理、模型定义、关联查询等高层抽象。

Note

当你需要类型安全的查询构建、表结构迁移、模型关联时,通常会叠一个 ORM/查询构建器,它们底层可以走 bun:sqlite

  • Kysely:可通过 kysely-bun-sqlite 这类适配器把 bun:sqlite 作为底层驱动。
  • Drizzle / Prisma:等同样支持 Bun,或提供基于 bun:sqlite 的驱动接入方式。
    简单脚本、CLI 工具、本地缓存、单文件嵌入式数据库,直接用 bun:sqlite 往往就足够了;大型应用再考虑加 ORM。

29.11 小结与常见坑

  • bun:sqlite 是内置模块,同步 API,无需 npm install
  • db.query() 准备语句并缓存字节码;一次性 SQL 用 db.prepare() 避免污染缓存。
  • 选对执行方法:取数据用 .all/.get,写操作用 .run,大数据集用 .iterate()
  • 生产建议开启 WAL;注意 macOS 的 -wal/-shm 文件默认持久。
  • 需要大整数精度时开 safeIntegers: true
  • 它是驱动,不是 ORM;复杂项目可叠加 Kysely/Drizzle/Prisma 等。
Tip

想快速调试某条语句?对 Statement 调用 toString() 会打印展开参数后的完整 SQL,例如 db.query("SELECT $p").toString() 在绑定 42 后输出 SELECT 42

29.12 几个高频疑问

问:既然是同步 API,会不会把事件循环卡住? 会。SQLite 的查询在主线程上跑,一条扫全表的慢查询就是实打实的阻塞。所以要建好索引,大结果集用 .iterate() 分批处理;真有重计算,考虑放进 Worker 里。反过来说,绝大多数嵌入式场景的查询都在微秒级,同步反而省掉了 Promise 的调度开销——这正是它比异步驱动快的原因之一。

问:多个进程同时写同一个 .sqlite 文件行不行? SQLite 允许多读单写。开启 WAL 后读写不再互相阻塞,但同一时刻仍然只能有一个写者,其他写者会拿到「database is locked」。高并发写入的场景,SQLite 本来就不是合适选型。

问:?$name 该用哪个? 参数少、顺序固定就用 ?1?2;字段一多,命名参数可读性明显更好,改 SQL 时也不会因为漏改一个位置而错位。建议再配上 strict: true,漏传参数会直接报错而不是悄悄按 NULL 处理。

问:为什么我拼进 SQL 的用户输入出问题了? 永远不要用模板字符串拼 SQL 值。db.query("SELECT * FROM users WHERE name = '" + name + "'") 就是标准的 SQL 注入写法。参数绑定不只是安全问题,绑定过的语句还能命中缓存,性能也更好。

问:内存数据库能持久化吗? 能。用 db.serialize() 拿到 Uint8Array,再 Bun.write() 落盘即可;下次用 Database.deserialize() 恢复。跑测试时常见的做法是:测试期间全在 :memory: 里跑,失败时把库序列化出来存成文件方便复现。