Chapter 03

D1:全球边缘 SQLite

D1 是 Cloudflare 托管的 libSQL,一主多副本。写集中在主区域,读打到离用户最近的副本——对多读少写的业务,简单到让人怀疑是不是错过了什么。

创建数据库

wrangler d1 create my-db
# 输出:
#   database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
#   把下面这段 copy 到 wrangler.toml

# wrangler.toml:
# [[d1_databases]]
# binding = "DB"
# database_name = "my-db"
# database_id = "xxxxxxxx-..."

迁移与 schema

用 D1 自带的 migrations 目录:

wrangler d1 migrations create my-db init

# 生成 migrations/0001_init.sql,编辑它
-- migrations/0001_init.sql
CREATE TABLE users (
  id INTEGER PRIMARY KEY,
  email TEXT UNIQUE NOT NULL,
  name TEXT NOT NULL,
  created_at INTEGER NOT NULL DEFAULT (unixepoch())
);

CREATE INDEX idx_users_email ON users(email);
# 应用到本地 miniflare
wrangler d1 migrations apply my-db --local

# 应用到生产
wrangler d1 migrations apply my-db --remote

在 Worker 里查询

type Bindings = {
  DB: D1Database;
};

app.get('/users/:id', async (c) => {
  const id = c.req.param('id');
  const user = await c.env.DB
    .prepare('SELECT * FROM users WHERE id = ?')
    .bind(id)
    .first();
  if (!user) return c.text('not found', 404);
  return c.json(user);
});

D1 原生 API 方法:

方法返回适合
.first()单行 or null按 id 查一条
.all(){results: [...]}列表查询
.run(){success, meta}INSERT / UPDATE / DELETE
.raw()二维数组大结果流式处理

批处理与事务

D1 的事务通过 batch——一次原子提交多条语句:

const [r1, r2] = await c.env.DB.batch([
  c.env.DB.prepare('INSERT INTO users (email, name) VALUES (?, ?)').bind(email, name),
  c.env.DB.prepare('INSERT INTO audit (action) VALUES (?)').bind('user.create'),
]);
D1 没有传统 BEGIN/COMMIT
在同一个请求内交错多次查询和插入,中间不是事务——它们是独立的 RPC。想要原子性,把所有语句凑成一个 batch() 调用。

用 Drizzle ORM

原生 prepare 太裸。Drizzle 在 Workers 上有原生 D1 adapter,类型安全、迁移、studio 全齐:

npm i drizzle-orm
npm i -D drizzle-kit
// src/schema.ts
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';

export const users = sqliteTable('users', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  email: text('email').notNull().unique(),
  name: text('name').notNull(),
  createdAt: integer('created_at').notNull(),
});
// src/index.ts
import { drizzle } from 'drizzle-orm/d1';
import { eq } from 'drizzle-orm';
import { users } from './schema';

app.get('/users/:id', async (c) => {
  const db = drizzle(c.env.DB);
  const row = await db.select().from(users)
    .where(eq(users.id, Number(c.req.param('id'))))
    .get();
  return row ? c.json(row) : c.text('nope', 404);
});

只读副本:Sessions API

D1 2025 加入了读副本,默认关闭,开启后 Cloudflare 在多区域自动保留副本。使用时需要 Sessions API 来管理"读自己写"一致性:

const session = c.env.DB.withSession('first-primary');

// 写会走主区域
await session.prepare('INSERT INTO users ...').run();

// 后续读在同一个 session 内能看到这次写
const r = await session.prepare('SELECT * FROM users').all();

// 把 bookmark 序列化传给下个请求,维持读自己写
const bookmark = session.getBookmark();
return c.json({ data: r.results, bookmark });

典型玩法:把 bookmark 塞进响应 header,前端下次带回来,后端 withSession(bookmark)——用户自己的写一定能看到,其他用户的写异步传播。

命令行:本地 dev 与手工查询

# 直接跑 SQL
wrangler d1 execute my-db --local --command="SELECT COUNT(*) FROM users"

# 导入 SQL 文件
wrangler d1 execute my-db --remote --file=./seed.sql

# 导出整个 db
wrangler d1 export my-db --output=backup.sql

容量与限制

维度FreePaid
单库大小500MB10GB
库数量1050000+
行读/天500 万250 亿
行写/天10 万5000 万
单次查询返回最多 100MB

何时不要用 D1

高写并发
D1 写集中在主区域,几千 QPS 写入会瓶颈。考虑拆分多库(per-tenant 一库,D1 支持几万库),或者用 Postgres + Hyperdrive。
需要复杂分析查询
SQLite 没窗口函数外的一些分析能力,单库上限 10GB。数据仓库用 Analytics Engine 或外部 BigQuery。
强一致跨区
多副本最终一致,不保证每个 PoP 立刻看到最新写。要强一致跨区域,用 Durable Objects。

本章小结