SQLite 与数据库
本教程共 34 篇 · 第 29 篇 · 更新于 2026-08-06
本节目标:
- 用内置的
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()返回lastInsertRowid与changes,适合建表、插入、更新、删除等不需要结果集的操作。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");
WarningmacOS 上 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: 里跑,失败时把库序列化出来存成文件方便复现。