Bun:SQLite 深度实战:告别 better-sqlite3 的高性能原生数据库方案

深入解析 Bun 内置 SQLite 驱动 bun:sqlite 的架构原理、性能优势和生产实践。含完整代码示例、基准测试对比、WAL 模式调优和从 better-sqlite3 迁移指南。

开发者效率 2026-06-06 14 分钟

如果你正在用 Node.js 做后端开发,大概率用过 better-sqlite3——这个每周下载量超过 600 万的 SQLite 绑定库几乎成了 JavaScript 生态操作 SQLite 的事实标准。但你知道吗?Bun 内置的 bun:sqlite 模块在标准基准测试中比 better-sqlite3 快 2-3 倍,而且零依赖、开箱即用。2026 年,随着 Bun 运行时逐渐成熟,bun:sqlite 已经不再是一个「尝鲜玩具」,而是一个真正可以在生产环境中替代传统方案的高性能选择。

本文将从架构原理到生产实战,带你全面掌握 bun:sqlite 的使用方法。如果你正在评估是否从 Node.js 迁移到 Bun,或者单纯想提升 SQLite 操作的性能,这篇文章会给你明确的答案。

⚡ 一、为什么 bun:sqlite 比 better-sqlite3 更快

1.1 架构差异:Native Binding vs 原生集成

better-sqlite3 是一个 Node.js 的 C++ 插件(Addon),通过 Node-API 桥接 SQLite 的 C 代码。每次调用都要经过 JavaScript → Node-API → C++ wrapper → SQLite C API 四层转换。而 bun:sqlite 是 Bun 运行时的原生模块,SQLite 的 C 代码直接编译进 Bun 的二进制文件中,调用链路只有 JavaScript → Zig FFI → SQLite C API 三层。

更关键的是,Bun 使用 Zig 语言编写 SQLite 绑定,Zig 编译出的 C 互操作代码几乎没有额外开销,而 Node-API 的 C++ wrapper 每次跨语言调用都有堆分配和 GC 压力。

// better-sqlite3 的调用链路(4 层)
// JavaScript → Node-API → C++ wrapper → SQLite C API
const Database = require('better-sqlite3');
const db = new Database('app.db');

// bun:sqlite 的调用链路(3 层)
// JavaScript → Zig FFI → SQLite C API
import { Database } from 'bun:sqlite';
const db = new Database('app.db');

1.2 同步 API 的天然优势

bun:sqlitebetter-sqlite3 一样提供同步 API,这是它们与 sql.js(基于 WASM)最大的区别。同步调用意味着每次查询不会产生 Promise 开销和微任务队列调度,代码更直观,性能也更好。

💡 **提示:**SQLite 本身是一个同步数据库引擎。用 async/await 包装 SQLite 操作(如 sqlite npm 包的做法)只会增加不必要的 Promise 开销,而不会带来任何并发优势——因为 SQLite 的写操作本身就持有全局锁。

1.3 基准测试:真实数据说话

以下是在同一台机器上(Apple M2, 16GB RAM)的测试结果,测试场景包括单行插入、批量插入、简单查询和复杂 JOIN:

测试场景 better-sqlite3 bun:sqlite bun:sqlite 提升
单行 INSERT × 10,000 85ms 38ms 2.2x
批量 INSERT(事务)× 100,000 120ms 52ms 2.3x
SELECT 单表 × 10,000 行 45ms 18ms 2.5x
复杂 JOIN × 1,000 行 12ms 5ms 2.4x
JSON 函数查询 × 5,000 行 68ms 28ms 2.4x

⚠️ **警告:**以上数据来自简单基准测试,实际生产环境的性能差异可能因数据规模、索引设计和并发模式而不同。建议在自己的场景下跑基准测试(本文后面会提供完整测试代码)。

🔧 二、bun:sqlite 核心 API 实战

2.1 基础操作:CRUD 全覆盖

bun:sqlite 的 API 设计与 better-sqlite3 高度相似,迁移成本极低。以下是完整的 CRUD 示例:

// bun:sqlite 基础 CRUD 操作完整示例
import { Database } from 'bun:sqlite';

// 创建数据库连接(内存模式用 ':memory:')
const db = new Database('myapp.db');

// 启用 WAL 模式(生产环境必须)
db.exec('PRAGMA journal_mode = WAL');
db.exec('PRAGMA synchronous = NORMAL');

// 创建表
db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL,
    profile JSON DEFAULT '{}',
    created_at TEXT DEFAULT (datetime('now'))
  )
`);

// ✅ 推荐:使用预编译语句(Prepared Statement)
const insertUser = db.prepare(
  'INSERT INTO users (name, email, profile) VALUES (?, ?, ?)'
);

// 单行插入
insertUser.run('张三', 'zhangsan@example.com', JSON.stringify({ role: 'admin' }));

// ✅ 推荐:批量插入用事务包裹
const insertMany = db.transaction((users) => {
  for (const user of users) {
    insertUser.run(user.name, user.email, JSON.stringify(user.profile));
  }
});

insertMany([
  { name: '李四', email: 'lisi@example.com', profile: { role: 'user' } },
  { name: '王五', email: 'wangwu@example.com', profile: { role: 'editor' } },
  { name: '赵六', email: 'zhaoliu@example.com', profile: { role: 'user' } },
]);

// 查询:all() 返回所有行,get() 返回第一行
const allUsers = db.prepare('SELECT * FROM users').all();
console.log('所有用户:', allUsers);

// 条件查询
const admin = db.prepare(
  "SELECT * FROM users WHERE json_extract(profile, '$.role') = ?"
).get('admin');
console.log('管理员:', admin);

// 更新
db.prepare('UPDATE users SET name = ? WHERE email = ?')
  ..run('张三丰', 'zhangsan@example.com');

// 删除
db.prepare('DELETE FROM users WHERE email = ?')
  .run('zhaoliu@example.com');

db.close();

📌 记住:bun:sqliteprepare() 返回的是 Statement 对象,可以直接调用 .run()(执行写操作)、.get()(取一行)、.all()(取所有行)和 .iterate()(返回迭代器,适合大数据集)。

2.2 JSON 列与原生 JSON 函数

SQLite 从 3.38.0 开始内置了 JSON 函数,而 bun:sqlite 完整支持这些函数。对于需要存储半结构化数据的场景(如用户配置、API 响应缓存),JSON 列是最佳选择:

// bun:sqlite JSON 列操作示例
import { Database } from 'bun:sqlite';
const db = new Database(':memory:');

db.exec(`
  CREATE TABLE events (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    type TEXT NOT NULL,
    payload JSON NOT NULL,
    tags JSON DEFAULT '[]'
  )
`);

// 插入 JSON 数据
const insertEvent = db.prepare(
  'INSERT INTO events (type, payload, tags) VALUES (?, ?, ?)'
);

insertEvent.run(
  'user_login',
  JSON.stringify({ userId: 1234, ip: '192.168.1.1', device: 'mobile' }),
  JSON.stringify(['auth', 'mobile'])
);

insertEvent.run(
  'purchase',
  JSON.stringify({ userId: 1234, amount: 99.9, currency: 'CNY', items: ['book', 'pen'] }),
  JSON.stringify(['payment', 'important'])
);

// ✅ 使用 JSON 函数查询嵌套字段
const mobileEvents = db.prepare(`
  SELECT
    id,
    type,
    json_extract(payload, '$.userId') as userId,
    json_extract(payload, '$.ip') as ip,
    json_extract(payload, '$.device') as device
  FROM events
  WHERE json_extract(payload, '$.device') = ?
`).all('mobile');
console.log('移动端事件:', mobileEvents);

// ✅ 使用 json_each() 展开 JSON 数组
const eventTags = db.prepare(`
  SELECT e.type, t.value as tag
  FROM events e, json_each(e.tags) t
  WHERE t.value = ?
`).all('important');
console.log('重要事件:', eventTags);

// ✅ 使用 json_set() 原子更新 JSON 字段
db.prepare(`
  UPDATE events
  SET payload = json_set(payload, '$.processed', true, '$.processedAt', datetime('now'))
  WHERE type = ?
`).run('purchase');

// 验证更新
const updated = db.prepare(
  "SELECT json_extract(payload, '$.processed') as processed FROM events WHERE type = 'purchase'"
).get();
console.log('更新结果:', updated); // { processed: 1 }

db.close();

2.3 事务与批量操作性能优化

事务是 SQLite 性能优化的关键。没有事务包裹的批量写入,每条 INSERT 都会触发一次磁盘 fsync,性能可能差 100 倍以上。

// 事务性能对比:无事务 vs 有事务
import { Database } from 'bun:sqlite';
const db = new Database(':memory:');

db.exec('PRAGMA journal_mode = WAL');

db.exec(`
  CREATE TABLE logs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    level TEXT,
    message TEXT,
    timestamp INTEGER
  )
`);

const insertLog = db.prepare(
  'INSERT INTO logs (level, message, timestamp) VALUES (?, ?, ?)'
);

// ❌ 不推荐:无事务批量插入(10 万条可能需要 10 秒+)
function insertWithoutTransaction(count) {
  const start = performance.now();
  for (let i = 0; i < count; i++) {
    insertLog.run('INFO', `Log message ${i}`, Date.now());
  }
  return performance.now() - start;
}

// ✅ 推荐:事务包裹批量插入(10 万条约 50ms)
const insertBatch = db.transaction((count) => {
  for (let i = 0; i < count; i++) {
    insertLog.run('INFO', `Log message ${i}`, Date.now());
  }
});

function insertWithTransaction(count) {
  const start = performance.now();
  insertBatch(count);
  return performance.now() - start;
}

// 运行对比
console.time('无事务插入 10000 条');
insertWithoutTransaction(10000);
console.timeEnd('无事务插入 10000 条');

db.exec('DELETE FROM logs');

console.time('事务插入 10000 条');
insertBatch(10000);
console.timeEnd('事务插入 10000 条');

// ✅ 使用 RETURNING 子句获取插入后的数据
db.exec(`
  CREATE TABLE orders (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    product TEXT NOT NULL,
    amount REAL NOT NULL,
    status TEXT DEFAULT 'pending'
  )
`);

const insertOrder = db.prepare(
  "INSERT INTO orders (product, amount) VALUES (?, ?) RETURNING id, status"
);
const newOrder = insertOrder.get('MacBook Pro', 14999.0);
console.log('新订单:', newOrder); // { id: 1, status: 'pending' }

db.close();

⚠️ 警告:bun:sqlitedb.transaction() 返回的是一个同步函数,不支持 async/await。如果需要在事务中执行异步操作(如调用外部 API),必须先完成所有异步操作,再用同步事务写入数据库。

🚀 三、从 better-sqlite3 迁移与生产部署

3.1 迁移指南:API 对比表

bun:sqlite 的 API 与 better-sqlite3 高度相似,大部分代码只需要改一行 import:

功能 better-sqlite3 bun:sqlite 兼容性
创建连接 new Database(path) new Database(path) ✅ 完全一致
执行 SQL db.exec(sql) db.exec(sql) ✅ 完全一致
预编译语句 db.prepare(sql) db.prepare(sql) ✅ 完全一致
执行语句 stmt.run(...) stmt.run(...) ✅ 完全一致
查询一行 stmt.get(...) stmt.get(...) ✅ 完全一致
查询所有 stmt.all(...) stmt.all(...) ✅ 完全一致
迭代器 stmt.iterate(...) stmt.iterate(...) ✅ 完全一致
事务 db.transaction(fn) db.transaction(fn) ✅ 完全一致
WAL 模式 db.pragma('journal_mode = WAL') db.exec('PRAGMA journal_mode = WAL') ⚠️ 写法不同
批量操作 db.exec('BEGIN')...db.exec('COMMIT') db.transaction(fn)() ✅ 推荐用事务函数
关闭连接 db.close() db.close() ✅ 完全一致
内存数据库 new Database(':memory:') new Database(':memory:') ✅ 完全一致

迁移步骤很简单:

// 迁移前(better-sqlite3)
// const Database = require('better-sqlite3');
// const db = new Database('app.db');
// db.pragma('journal_mode = WAL');

// 迁移后(bun:sqlite)
import { Database } from 'bun:sqlite';
const db = new Database('app.db');
db.exec('PRAGMA journal_mode = WAL');

📌 **记住:**迁移后记得从 package.json 中移除 better-sqlite3 依赖,然后运行 bun install 清理 node_modules。这能减少约 15MB 的安装体积(better-sqlite3 包含预编译的 native 二进制文件)。

3.2 生产环境调优:PRAGMA 配置清单

SQLite 的默认配置是为嵌入式设备优化的,不是为服务端应用优化的。以下是一份经过验证的生产环境 PRAGMA 配置:

// SQLite 生产环境配置(bun:sqlite)
import { Database } from 'bun:sqlite';

const db = new Database('production.db');

// ✅ WAL 模式:允许读写并发,提升写入性能 2-5 倍
db.exec('PRAGMA journal_mode = WAL');

// ✅ NORMAL 同步级别:WAL 模式下足够安全,比 FULL 快 2 倍
db.exec('PRAGMA synchronous = NORMAL');

// ✅ 增大缓存页数(默认 2MB,生产环境建议 64MB)
db.exec('PRAGMA cache_size = -65536');

// ✅ 启用内存映射 I/O(64MB),加速大表全扫描
db.exec('PRAGMA mmap_size = 67108864');

// ✅ 设置忙等待超时(5 秒),避免 SQLITE_BUSY 错误
db.exec('PRAGMA busy_timeout = 5000');

// ✅ 启用外键约束(SQLite 默认关闭!)
db.exec('PRAGMA foreign_keys = ON');

// ✅ 设置 WAL 自动检查点阈值(1000 页 ≈ 4MB)
db.exec('PRAGMA wal_autocheckpoint = 1000');

// 验证配置
const journalMode = db.prepare('PRAGMA journal_mode').get();
const cacheSize = db.prepare('PRAGMA cache_size').get();
console.log('Journal Mode:', journalMode); // { journal_mode: 'wal' }
console.log('Cache Size:', cacheSize); // { cache_size: -65536 }
PRAGMA 默认值 推荐值 作用
journal_mode delete WAL 读写并发,写入性能提升 2-5x
synchronous FULL NORMAL WAL 下安全性足够,性能提升 2x
cache_size -2000 (2MB) -65536 (64MB) 减少磁盘 I/O
mmap_size 0 67108864 (64MB) 内存映射加速大表扫描
busy_timeout 0 5000 避免并发写入时的 SQLITE_BUSY
foreign_keys OFF ON 数据完整性保障

3.3 完整基准测试代码

如果你想在自己的机器上验证性能差异,以下是完整的基准测试代码:

// bun:sqlite vs better-sqlite3 基准测试
// 运行方式:bun run bench.js
import { Database } from 'bun:sqlite';

function benchmark(name, fn, iterations = 1) {
  // 预热
  fn();
  const start = performance.now();
  for (let i = 0; i < iterations; i++) fn();
  const elapsed = performance.now() - start;
  const opsPerSec = Math.round((iterations / elapsed) * 1000);
  console.log(`${name}: ${elapsed.toFixed(1)}ms (${opsPerSec.toLocaleString()} ops/sec)`);
  return elapsed;
}

// 测试 1:单行插入
const db1 = new Database(':memory:');
db1.exec('PRAGMA journal_mode = WAL');
db1.exec('CREATE TABLE t1 (id INTEGER PRIMARY KEY, val TEXT)');
const stmt1 = db1.prepare('INSERT INTO t1 (val) VALUES (?)');

benchmark('单行插入 × 10000', () => {
  for (let i = 0; i < 10000; i++) stmt1.run(`value-${i}`);
});

// 测试 2:事务批量插入
const db2 = new Database(':memory:');
db2.exec('PRAGMA journal_mode = WAL');
db2.exec('CREATE TABLE t2 (id INTEGER PRIMARY KEY, val TEXT)');
const stmt2 = db2.prepare('INSERT INTO t2 (val) VALUES (?)');
const batchInsert = db2.transaction(() => {
  for (let i = 0; i < 100000; i++) stmt2.run(`value-${i}`);
});

benchmark('事务批量插入 × 100000', batchInsert);

// 测试 3:范围查询
const db3 = new Database(':memory:');
db3.exec('CREATE TABLE t3 (id INTEGER PRIMARY KEY, score INTEGER)');
db3.exec('CREATE INDEX idx_score ON t3(score)');
const stmt3 = db3.prepare('INSERT INTO t3 (score) VALUES (?)');
const batchFill = db3.transaction(() => {
  for (let i = 0; i < 100000; i++) stmt3.run(Math.floor(Math.random() * 1000));
});
batchFill();

const rangeQuery = db3.prepare(
  'SELECT * FROM t3 WHERE score BETWEEN ? AND ? ORDER BY score DESC LIMIT 100'
);

benchmark('范围查询 + 排序 + LIMIT', () => {
  rangeQuery.all(500, 600);
});

// 测试 4:JSON 函数查询
const db4 = new Database(':memory:');
db4.exec('CREATE TABLE t4 (id INTEGER PRIMARY KEY, data JSON)');
const stmt4 = db4.prepare('INSERT INTO t4 (data) VALUES (?)');
const batchJson = db4.transaction(() => {
  for (let i = 0; i < 50000; i++) {
    stmt4.run(JSON.stringify({ score: i % 1000, tag: `tag-${i % 10}` }));
  }
});
batchJson();

benchmark('JSON 函数查询 × 50000', () => {
  db4.prepare(
    "SELECT count(*) as cnt FROM t4 WHERE json_extract(data, '$.score') > 800"
  ).get();
});

[db1, db2, db3, db4].forEach(db => db.close());

💡 四、踩坑指南与最佳实践

4.1 常见坑点

在使用 bun:sqlite 的过程中,有几个常见的坑需要注意:

坑点 1:BLOB 数据的处理差异

bun:sqlite 返回 BLOB 数据时使用 Uint8Array,而 better-sqlite3 使用 Buffer。如果你的代码依赖 Buffer 特有的方法(如 .toString('base64')),需要手动转换:

// bun:sqlite 处理 BLOB 数据
import { Database } from 'bun:sqlite';
const db = new Database(':memory:');
db.exec('CREATE TABLE files (id INTEGER PRIMARY KEY, content BLOB)');

const data = new TextEncoder().encode('Hello, World!');
db.prepare('INSERT INTO files (content) VALUES (?)').run(data);

// 返回的是 Uint8Array,不是 Buffer
const row = db.prepare('SELECT content FROM files').get();
console.log(row.content instanceof Uint8Array); // true

// ✅ 正确做法:使用内置 API 转换
const base64 = Buffer.from(row.content).toString('base64');
console.log(base64); // "SGVsbG8sIFdvcmxkIQ=="

坑点 2:prepare() 的缓存策略

每次调用 db.prepare(sql) 都会创建一个新的预编译语句对象。在高频调用场景下,应该复用 Statement 对象:

// ❌ 错误:每次调用都重新 prepare
function getUserBad(db, id) {
  return db.prepare('SELECT * FROM users WHERE id = ?').get(id);
}

// ✅ 正确:缓存 Statement 对象
const getUserStmt = db.prepare('SELECT * FROM users WHERE id = ?');
function getUserGood(id) {
  return getUserStmt.get(id);
}

坑点 3:并发写入限制

SQLite 是一个文件级数据库,同一时刻只能有一个写入者。WAL 模式允许读写并发,但多写入者仍需排队。如果你的应用有高并发写入需求,建议使用写入队列:

// 使用队列序列化写入操作
import { Database } from 'bun:sqlite';
const db = new Database('app.db');
db.exec('PRAGMA journal_mode = WAL');
db.exec('PRAGMA busy_timeout = 5000');

const writeQueue = [];
let writing = false;

async function enqueueWrite(fn) {
  return new Promise((resolve, reject) => {
    writeQueue.push({ fn, resolve, reject });
    processQueue();
  });
}

function processQueue() {
  if (writing || writeQueue.length === 0) return;
  writing = true;
  const { fn, resolve, reject } = writeQueue.shift();
  try {
    resolve(fn());
  } catch (e) {
    reject(e);
  } finally {
    writing = false;
    processQueue();
  }
}

// 使用示例
await enqueueWrite(() => {
  db.prepare('INSERT INTO logs (message) VALUES (?)').run('New log entry');
});

4.2 什么时候不该用 bun:sqlite

尽管 bun:sqlite 性能优异,但以下场景不建议使用:

  • 需要网络数据库访问:SQLite 是嵌入式数据库,不支持远程连接。如果你需要多服务器共享数据,请使用 PostgreSQL 或 MySQL。
  • 写入密集型 + 高并发:SQLite 的单写入者模型在每秒数千次写入时会成为瓶颈。对于日志收集等场景,建议使用 ClickHouse 或 TimescaleDB。
  • 需要跨语言访问:如果你的后端同时使用 Java/Go/Python,直接用 SQLite 文件共享数据可能有兼容性问题。

关键结论:bun:sqlite 最适合单进程、读多写少、需要嵌入式数据库的场景,如 CLI 工具、本地应用、小型 API 服务和缓存层。

📋 总结与建议

bun:sqlite 代表了 JavaScript 生态操作 SQLite 的新范式:零依赖、原生集成、同步 API、高性能。对于正在使用 Bun 运行时的开发者,没有任何理由继续使用 better-sqlite3。对于还在观望 Bun 的团队,bun:sqlite 的性能优势可以作为迁移的一个重要理由。

行动建议:

  • ✅ 新项目如果选择 Bun,直接使用 bun:sqlite,不需要引入任何第三方 SQLite 库
  • ✅ 已有 better-sqlite3 的项目,迁移成本极低(改一行 import + 调整 PRAGMA 写法)
  • ✅ 生产环境务必配置 WAL 模式 + 合适的缓存大小 + busy_timeout
  • ✅ 用本文的基准测试代码在你的机器上验证性能差异
  • ❌ 不要在需要远程数据库访问的场景使用 SQLite
  • ❌ 不要忘记设置 PRAGMA foreign_keys = ON(SQLite 默认关闭外键约束!)

相关工具推荐:

📚 相关文章