运行时包管理器打包器测试运行器代码检查格式化指南参考博客反馈

SQLite

Bun 内置了高性能的 SQLite3 驱动。

Bun 原生实现了高性能的 SQLite3 驱动。要使用它,请从内置的 bun:sqlite 模块导入。

db.ts
import { Database } from "bun:sqlite";

const db = new Database(":memory:");
const query = db.query("select 'Hello world' as message;");
query.get();
Text
{ message: "Hello world" }

API 是同步的,并且速度很快。感谢 better-sqlite3 及其贡献者为 bun:sqlite 的 API 提供了启发。

功能包括:

  • 事务
  • 参数(命名参数和位置参数)
  • 预编译语句
  • 数据类型转换(BLOB 转换为 Uint8Array)
  • 无需 ORM 即可将查询结果映射到类 - query.as(MyClass)
  • 在 JavaScript 的所有 SQLite 驱动中拥有最快的性能
  • 支持 bigint
  • 多查询语句(例如 SELECT 1; SELECT 2;)可在一次调用 database.run(query) 中执行

bun:sqlite 在读取查询上比 better-sqlite3 快约 3-6 倍,比 deno.land/x/sqlite 快约 8-9 倍。各驱动均基于 Northwind Traders 数据集进行基准测试。查看并运行基准测试源码。

Bun、better-sqlite3 与 deno.land/x/sqlite 的 SQLite 基准测试


数据库

打开或创建 SQLite3 数据库:

db.ts
import { Database } from "bun:sqlite";

const db = new Database("mydb.sqlite");

打开内存数据库:

db.ts
import { Database } from "bun:sqlite";

const db = new Database(":memory:");
// 以下方式效果相同
const db = new Database();
const db = new Database("");

以只读模式打开:

db.ts
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite", { readonly: true });

如果文件不存在则创建数据库:

db.ts
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite", { create: true });

严格模式

默认情况下,bun:sqlite 绑定参数时需要包含 $、: 或 @ 前缀,且如果缺少参数不会抛出错误。

如果希望缺少参数时抛出错误,并且允许绑定时不使用前缀,可以在 Database 构造函数中设置 strict: true:

db.ts
import { Database } from "bun:sqlite";

const strict = new Database(":memory:", { strict: true });

// 因拼写错误会抛出错误:
const query = strict.query("SELECT $message;").all({ messag: "Hello world" });

const notStrict = new Database(":memory:");
// 不会抛出错误:
notStrict.query("SELECT $message;").all({ messag: "Hello world" });

通过 ES 模块导入属性加载

你还可以使用导入属性加载数据库。

db.ts
import db from "./mydb.sqlite" with { type: "sqlite" };

console.log(db.query("select * from users LIMIT 1").get());

此用法等同于:

db.ts
import { Database } from "bun:sqlite";
const db = new Database("./mydb.sqlite");

.close(throwOnError: boolean = false)

要关闭数据库连接,同时让通过 .prepare() 创建的语句继续工作,直到它们被终结或垃圾回收,请调用 .close(false):

db.ts
const db = new Database();
// ... 执行操作
db.close(false);

Database 拥有通过 .query() 创建的语句,并会立即将它们终结。最后一个未完成的 .prepare() 语句被终结后,Bun 会释放底层连接(以及数据库文件句柄)。

要终结所有未完成的语句、立即释放连接,并在 SQLite 报告关闭错误时抛出异常,请调用 .close(true):

db.ts
const db = new Database();
// ... 执行操作
db.close(true);

使用被 close() 终结的语句会抛出 Database has closed。有两个例外:toString() 会返回空字符串,finalize() 仍可安全调用。

using 语句

using 语句会在代码块退出时关闭数据库连接。

db.ts
import { Database } from "bun:sqlite";

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

.serialize()

bun:sqlite 支持 SQLite 内置的数据库序列化和反序列化机制,详见 serialize 和 deserialize。

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

内部调用了 sqlite3_serialize。

.query()

在 Database 实例上使用 db.query() 方法来预处理 SQL 查询。该方法返回一个缓存在 Database 实例上的 Statement 实例。查询不会被执行。

db.ts
const query = db.query(`select "Hello world" as message`);

WAL 模式

SQLite 支持预写式日志模式(WAL),它能显著提升性能,尤其是在有多个并发读取者和单个写入者时。我们建议大多数应用启用 WAL 模式。

启用 WAL 模式,请在程序开始时执行此 pragma 查询:

db.ts
db.run("PRAGMA journal_mode = WAL;");
什么是 WAL 模式?

在 WAL 模式下,对数据库的写入会直接写入一个名为“WAL 文件”(-wal)的独立文件。SQLite 还会创建一个共享内存索引文件(-shm)以协调读取。之后,SQLite 会将 WAL 文件中的内容整合到主数据库文件中。可以把 WAL 文件看作待处理写入的缓冲区。有关更详细的概述,请参阅 SQLite 文档。

WAL 附属文件清理

使用基于文件的数据库启用 WAL 模式时,SQLite 会在数据库旁创建两个附属文件:预写式日志(-wal)和共享内存索引(-shm)。SQLite 是否会在 .close() 后自动删除这些文件,取决于你的平台:

  • macOS:Bun 使用系统提供的 SQLite,Apple 构建时启用了持久化 WAL。-wal 和 -shm 文件在关闭后会保留。这不是 bug——这是 Apple 配置系统 SQLite 的方式。
  • Linux 和 Windows:Bun 静态链接了自己的 SQLite 构建版本,该版本遵循上游默认设置。没有其他连接处于打开状态时,SQLite 通常会在关闭后删除附属文件。

若要确保在所有平台上都清理附属文件,请在关闭前禁用 WAL 持久化并运行截断检查点:

db.ts
import { Database, constants } from "bun:sqlite";

const db = new Database("mydb.sqlite");
db.run("PRAGMA journal_mode = WAL;");

// ... 使用数据库 ...

// 禁用持久化 WAL(macOS 上需要)
db.fileControl(constants.SQLITE_FCNTL_PERSIST_WAL, 0);
// 执行检查点并截断 WAL 文件
db.run("PRAGMA wal_checkpoint(TRUNCATE);");
db.close();
// 仅保留 mydb.sqlite —— 没有 -wal 或 -shm 文件

语句

Statement 是一个_预编译查询_,也就是说,它已经被解析并编译成高效的二进制形式。它可以被执行多次。

通过 Database 实例的 .query 方法创建:

db.ts
const query = db.query(`select "Hello world" as message`);

查询中可以包含参数,既可以是数字(例如 ?1),也可以是命名参数($param、:param 或 @param)。

db.ts
const query = db.query(`SELECT ?1, ?2;`);
const query = db.query(`SELECT $param1, $param2;`);

执行查询时,将值绑定到这些参数。你可以通过几种不同的方法执行 Statement,每种方法都会以不同形式返回结果。

绑定参数值

通过向 .all()、.get()、.run() 或 .values() 方法传入对象绑定参数值。

db.ts
const query = db.query(`select $message;`);
query.all({ $message: "Hello world" });

也可以使用位置参数绑定:

db.ts
const query = db.query(`select ?1;`);
query.all("Hello world");

strict: true 允许无前缀绑定

默认绑定命名参数时需要包含 $、: 或 @ 前缀。启用 Database 构造函数中的 strict 选项可允许不带前缀绑定:

db.ts
import { Database } from "bun:sqlite";

const db = new Database(":memory:", {
  // 绑定时省略前缀
  strict: true, // [!code ++]
});

const query = db.query(`select $message;`);

// strict: true
query.all({ message: "Hello world" });

// strict: false
// query.all({ $message: "Hello world" });

.all()

使用 .all() 执行查询,返回所有结果数组。

db.ts
const query = db.query(`select $message;`);
query.all({ $message: "Hello world" });
Text
[{ $message: "Hello world" }]

内部调用 sqlite3_reset,并重复调用 sqlite3_step 直到返回 SQLITE_DONE。

.get()

使用 .get() 执行查询,并获取第一条结果对象。

db.ts
const query = db.query(`select $message;`);
query.get({ $message: "Hello world" });
Text
{ $message: "Hello world" }

内部会先调用 sqlite3_reset,随后重复调用 sqlite3_step,直到它不再返回 SQLITE_ROW。如果查询没有返回任何行,结果为 null。

.run()

使用 .run() 运行查询,并返回一个包含执行元数据的对象。这对于修改架构的查询(例如 CREATE TABLE)或批量写入操作非常有用。

db.ts
const query = db.query(`create table foo (bar text);`);
query.run();
Text
{
  lastInsertRowid: 0,
  changes: 0,
}

内部调用 sqlite3_reset 并调用一次 sqlite3_step,不遍历所有结果行,提升性能。

lastInsertRowid 属性是最后插入数据库的行的 ID。changes 属性是查询影响的行数。

.as(Class) - 将结果映射为类实例

使用 .as(Class) 运行查询,并将结果作为类的实例返回。类的方法、获取器和设置器在每一行上都可用。

db.ts
class Movie {
  title: string;
  year: number;

  get isMarvel() {
    return this.title.includes("Marvel");
  }
}

const query = db.query("SELECT title, year FROM movies").as(Movie);
const movies = query.all();
const first = query.get();

console.log(movies[0].isMarvel);
console.log(first.isMarvel);
Text
true
true

作为一项性能优化,Bun 不会调用类构造函数或运行默认初始化器,并且无法访问私有字段。这更像是 Object.create,而不是 new:Bun 会将类的原型指定给对象,因此其方法、获取器和设置器都能正常工作。

Bun 会将数据库列设为类实例上的属性。

.iterate() (@@iterator)

使用 .iterate() 运行查询并逐条返回结果。适合处理巨大结果集,避免一次性加载大量数据。

db.ts
const query = db.query("SELECT * FROM foo");
for (const row of query.iterate()) {
  console.log(row);
}

也可以使用 @@iterator 协议:

db.ts
const query = db.query("SELECT * FROM foo");
for (const row of query) {
  console.log(row);
}

.values()

使用 .values() 执行查询,返回结果的二维数组。

db.ts
const query = db.query(`select $message;`);

query.values({ $message: "Hello world" });
query.values(2);
Text
[ [ "Hello world" ] ]
[ [ 2 ] ]

内部调用 sqlite3_reset,并重复调用 sqlite3_step 直到返回 SQLITE_DONE。

.finalize()

使用 .finalize() 销毁 Statement 并释放与其关联的所有资源。完成销毁后,Statement 无法再次执行。通常情况下,垃圾回收器会替你完成此操作,但在对性能敏感的应用中,显式销毁可能很有用。

db.ts
const query = db.query("SELECT title, year FROM movies");
const movies = query.all();
query.finalize();

.toString()

对 Statement 调用 toString() 会打印出代入参数后的完整 SQL 语句,方便调试。

db.ts
import { Database } from "bun:sqlite";

// 设置
const query = db.query("SELECT $param;");

console.log(query.toString()); // => "SELECT NULL"

query.run(42);
console.log(query.toString()); // => "SELECT 42"

query.run(365);
console.log(query.toString()); // => "SELECT 365"

内部调用 sqlite3_expanded_sql,参数使用最近绑定的值展开。

参数

查询支持数字参数(?1)或命名参数($param、:param、@param)。执行查询时绑定参数值:

query.ts
const query = db.query("SELECT * FROM foo WHERE bar = $bar");
const results = query.all({
  $bar: "bar",
});
Text
[{ "bar": "bar" }]

数字(位置)参数同样支持:

db.ts
const query = db.query("SELECT ?1, ?2");
const results = query.all("hello", "goodbye");
Text
[
  {
    "?1": "hello",
    "?2": "goodbye",
  },
];

整数

SQLite 支持有符号 64 位整数,但 JavaScript 只支持有符号 53 位整数或使用 bigint 表示的任意精度整数。

所有位置都支持输入 bigint,但默认情况下,bun:sqlite 会将整数作为 number 类型返回。如果需要处理大于 2^53 的整数,请在创建 Database 实例时将 safeIntegers 选项设为 true。该选项还会验证传递给 bun:sqlite 的 bigint 值是否超出 64 位范围。

safeIntegers: true

当 safeIntegers 为 true 时,bun:sqlite 将整数返回为 bigint 类型:

db.ts
import { Database } from "bun:sqlite";

const db = new Database(":memory:", { safeIntegers: true });
const query = db.query(`SELECT ${BigInt(Number.MAX_SAFE_INTEGER) + 102n} as max_int`);
const result = query.get();

console.log(result.max_int);
Text
9007199254741093n

当 safeIntegers 为 true 时,如果绑定参数中的 bigint 值超过 64 位,bun:sqlite 会抛出错误:

db.ts
import { Database } from "bun:sqlite";

const db = new Database(":memory:", { safeIntegers: true });
db.run("CREATE TABLE test (id INTEGER PRIMARY KEY, value INTEGER)");

const query = db.query("INSERT INTO test (value) VALUES ($value)");

try {
  query.run({ $value: BigInt(Number.MAX_SAFE_INTEGER) ** 2n });
} catch (e) {
  console.log(e.message);
}
Text
BigInt value '81129638414606663681390495662081' is out of range

safeIntegers: false(默认)

当 safeIntegers 为 false 时,bun:sqlite 会将整数作为 number 类型返回,并将超出 53 位的整数四舍五入到最接近的可表示 number:

db.ts
import { Database } from "bun:sqlite";

const db = new Database(":memory:", { safeIntegers: false });
const query = db.query(`SELECT ${BigInt(Number.MAX_SAFE_INTEGER) + 102n} as max_int`);
const result = query.get();
console.log(result.max_int);
Text
9007199254741092

事务

事务以_原子方式_执行多个查询:要么全部成功,要么一个也不成功。使用 db.transaction() 方法创建事务:

db.ts
const insertCat = db.prepare("INSERT INTO cats (name) VALUES ($name)");
const insertCats = db.transaction(cats => {
  for (const cat of cats) insertCat.run(cat);
});

目前还没有插入任何猫。db.transaction() 返回一个新函数(insertCats),该函数会_包装_执行查询的函数。

要执行事务,请调用此函数。事务函数会将其参数传递给被包装的函数,并返回被包装函数的返回值。被包装的函数还可以访问执行事务时定义的 this 上下文。

db.ts
const insert = db.prepare("INSERT INTO cats (name) VALUES ($name)");
const insertCats = db.transaction(cats => {
  for (const cat of cats) insert.run(cat);
  return cats.length;
});

const count = insertCats([{ $name: "Keanu" }, { $name: "Salem" }, { $name: "Crookshanks" }]);

console.log(`Inserted ${count} cats`);

调用 insertCats 时,驱动会自动开始事务,并在被包装的函数返回时提交事务。如果抛出异常,驱动会回滚事务。异常会照常传播;驱动不会捕获它。

事务函数还提供 deferred、immediate、exclusive 等版本:

TypeScript
insertCats(cats); // 使用 "BEGIN"
insertCats.deferred(cats); // 使用 "BEGIN DEFERRED"
insertCats.immediate(cats); // 使用 "BEGIN IMMEDIATE"
insertCats.exclusive(cats); // 使用 "BEGIN EXCLUSIVE"

.loadExtension()

要加载 SQLite 扩展,请在 Database 实例上调用 .loadExtension(name):

db.ts
import { Database } from "bun:sqlite";

const db = new Database();
db.loadExtension("myext");

.fileControl(cmd: number, value: any)

要使用高级的 sqlite3_file_control API,请在 Database 实例上调用 .fileControl(cmd, value)。实际示例请参见 WAL 附属文件清理。

db.ts
import { Database, constants } from "bun:sqlite";

const db = new Database();
// 确保 WAL 模式不是持久的
// 这可以防止数据库关闭后 WAL 文件残留
db.fileControl(constants.SQLITE_FCNTL_PERSIST_WAL, 0);

value 可为以下类型:

  • number
  • TypedArray
  • undefined 或 null

参考

类型参考
class Database {
  constructor(
    filename: string,
    options?:
      | number
      | {
          readonly?: boolean;
          create?: boolean;
          readwrite?: boolean;
          safeIntegers?: boolean;
          strict?: boolean;
        },
  );

  query<ReturnType, ParamsType>(sql: string): Statement<ReturnType, ParamsType>;
  prepare<ReturnType, ParamsType>(sql: string): Statement<ReturnType, ParamsType>;
  run(sql: string, params?: SQLQueryBindings): { lastInsertRowid: number; changes: number };
  exec = this.run;

  transaction(insideTransaction: (...args: any) => void): CallableFunction & {
    deferred: (...args: any) => void;
    immediate: (...args: any) => void;
    exclusive: (...args: any) => void;
  };

  close(throwOnError?: boolean): void;
}

class Statement<ReturnType, ParamsType> {
  all(...params: ParamsType[]): ReturnType[];
  get(...params: ParamsType[]): ReturnType | null;
  run(...params: ParamsType[]): {
    lastInsertRowid: number;
    changes: number;
  };
  values(...params: ParamsType[]): unknown[][];

  finalize(): void; // 销毁语句,释放资源
  toString(): string; // 序列化为 SQL

  columnNames: string[]; // 结果列名
  columnTypes: string[]; // 基于首行实际值的类型(需先调用 .get()/.all())
  declaredTypes: (string | null)[]; // CREATE TABLE 模式中的类型(需先调用 .get()/.all())
  paramsCount: number; // 语句期望参数数量
  native: any; // 语句的底层对象

  as<T>(Class: new (...args: any[]) => T): Statement<T, ParamsType>;
}

type SQLQueryBindings =
  | string
  | bigint
  | TypedArray
  | number
  | boolean
  | null
  | Record<string, string | bigint | TypedArray | number | boolean | null>;

数据类型

JavaScript 类型SQLite 类型
stringTEXT
numberINTEGER 或 DECIMAL
booleanINTEGER (1 或 0)
Uint8ArrayBLOB
BufferBLOB
bigintINTEGER
nullNULL