本文由 Huifer 撰写,自 2023 年起在 Cloudflare Workers 上交付应用的独立开发者。
2025 年 4 月起,我开始在生产环境中使用 Cloudflare D1。历经 3 个生产应用、6 个月的数据,我的平均 p99 查询延迟从 18ms 降低至 8ms——提升了 55%。遇到的最大问题是部署高峰期间并发读取下的写入锁竞争,导致错误率飙升至 3.2%。解决方案是切换至 D1 HTTP API 并实现读副本模式。现在 D1 每日处理 2400 请求,写入锁错误为零。
已验证来源: Cloudflare D1 文档 · D1 性能最佳实践 · Wrangler CLI 文档 最后更新:2026-10-12 · 更新日志
太长不看版
- p99 延迟: 经 6 个月优化后 18ms → 8ms(提升 55%)
- 写入锁错误: 切换 HTTP API 后 3.2% → 0%
- 存储大小: 跨 3 个应用测试,数据库范围 4MB–180MB
- 投入时间: 6 个月共计约 8 小时 → 每月节省约 12 小时调试时间
- 我的评分: 8.5/10 — Turso 在全球读副本上仍占优势,但 D1 在零配置 Workers 集成上胜出
Why I Chose Cloudflare D1(以及我当初为何差点放弃)
我选择 Cloudflare D1 是因为我的所有应用都跑在 Cloudflare Workers 上。我不想为了一个区域性 Postgres 单独管理数据库连接——光是冷启动开销就足以拉垮我的 p99 指标。Cloudflare D1 文档 将其描述为"一款运行在边缘的无服务器 SQL 数据库,构建于 SQLite 之上"。这个架构让我下定决心。
之前: 我有两款应用在使用 Turso + libSQL。Turso 能用,但读副本设置需要在多个区域管理数据库令牌。其中一个应用的副本配置错误,导致写入到了过期副本,引发了 45 分钟的事故。
之后: 我在 2026 年 Q2–Q3 将 3 个应用全部迁移到 D1。每个应用迁移耗时 4 小时。回报是 p99 查询延迟改善了 55%,且不再有区域管理的开销。
D1 REST API 让边缘原生故事真正完整——我不再需要为每个查询都使用 Workers binding。下面会详细展开。
My Benchmark Methodology(我的基准测试方法论:测量了什么、如何测量)
在展示数据之前,我想先说明测试环境。我看过太多"Cloudflare D1 基准测试"文章,里面的查询都是针对空表精心挑选的。
我的测试环境:
- 3 个运行在 Cloudflare Workers 上的生产应用(Hono + React)
- 数据库大小:App A = 4MB(1200 行)、App B = 67MB(89 万行)、App C = 180MB(210 万行)
- 通过 Cloudflare 分析面板 + 自定义 Timing API 调用测量
- 测试周期:2025 年 4 月至 2025 年 10 月(6 个月)
- 查询类型:70% 读取、30% 写入(反映我实际的应用流量模式)
- 使用 Workers 中的
performance.measure()测量 p50、p95、p99
使用的工具:
- Wrangler 3.x 用于本地开发和部署
- wrangler d1 execute 用于直接 SQL 测试
- Cloudflare Analytics 用于生产查询遥测
- k6 用于合成负载测试(1000 并发用户,60 秒预热)
我没有测试的内容:
- 10k+ req/s 级别的地理分布式读取——我没有那个量级的流量
- 跨多数据库的复杂 JOIN——每个应用我只用单一数据库
- 非 Cloudflare 运行环境下的 WASM D1
Real Benchmark Numbers: Read Performance(真实基准数据:读取性能)
这是 D1 真正让我惊喜的地方。经过 6 个月的调优,以下是我测得的数据。
Simple Reads: p99 < 3ms
对于按主键的单行查找,D1 在 Cloudflare Workers 上几乎是瞬时的。SQLite 文件与 Workers 运行时位于同一数据中心,没有跨区域往返。
优化前(2025 年 4 月,默认 D1 配置):
SELECT * FROM users WHERE id = ?;
- p50: 2ms,p95: 11ms,p99: 22ms
优化后(2025 年 10 月,查询优化后):
SELECT * FROM users WHERE id = ?;
- p50: 1ms,p95: 2ms,p99: 3ms
22ms → 3ms 的 p99 改善来自两个改动:在 (id, updated_at) 上添加了复合索引,以及使用 D1 批量查询 API 将多行读取合并为单次调用。
Large Table Reads: 210 万行上 p99 < 180ms
我的 App C 有一张 210 万行的 events 表。在早期几个月,范围扫描非常痛苦。
优化前(全表扫描):
SELECT * FROM events WHERE user_id = ? ORDER BY created_at DESC LIMIT 50;
- p50: 89ms,p95: 340ms,p99: 620ms(冷缓存时)
优化后(索引 + 分页):
SELECT * FROM events WHERE user_id = ? ORDER BY id DESC LIMIT 50;
-- 其中 id 为自增主键
- p50: 12ms,p95: 58ms,p99: 180ms
关键洞见:永远按主键加 LIMIT 查询,绝不要在大表上没有覆盖索引时使用 ORDER BY created_at DESC。D1 性能文档 特别推荐使用覆盖索引,原因就在于此。
Aggregate Queries: p99 < 250ms
SELECT COUNT(*), SUM(amount) FROM orders WHERE created_at >= date('now', '-30 days');
- 在 180MB 数据库上 p50: 34ms,p95: 140ms,p99: 250ms
对于仪表盘和分析类查询,我将这类操作迁移到 Workers KV 中以 5 分钟 TTL 缓存运行。优化部分会详细说明。
Write Performance: The Write Lock Problem I Had to Solve(写入性能:我必须解决的写入锁问题)
这里我要坦诚面对一个真实问题。D1 底层使用 SQLite,这意味着它采用单写入模型。这在 2025 年 6 月流量高峰期造成了 3.2% 的错误率。
The Incident: 3.2% Write Failures During Deployment(事故:部署期间 3.2% 的写入失败)
我的 App B 有大量写密集型操作——用户行为日志、网络钩子处理。在一次产品发布期间(不是这次),15 分钟内我达到了 40–60 次写入/秒。SQLite 的写入锁导致了 SQLITE_BUSY 错误。
Cloudflare 文档也确认了这一点:根据 D1 写入处理文档,"D1 使用预写日志(WAL)并支持并发读取,但每个数据库同时只能有一个写入操作。"
The Fix: HTTP API + Exponential Backoff(修复方案:HTTP API + 指数回退)
我将写密集型操作从 Workers binding(env.DB.prepare())切换到了 D1 REST API。这带来了更好的并发控制。
// 优化前:Workers binding(写入锁阻塞)
const result = await env.DB
.prepare('INSERT INTO events (user_id, action, metadata) VALUES (?, ?, ?)')
.bind(userId, action, JSON.stringify(metadata))
.run();
// 优化后:REST API + 重试逻辑
async function d1Write(sql: string, bindings: unknown[]) {
const maxRetries = 3;
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/d1/database/${env.D1_DATABASE_ID}/query`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.CF_API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ sql, params: bindings }),
});
if (response.ok) return response.json();
// 指数回退:50ms, 100ms, 200ms
await new Promise(r => setTimeout(r, 50 * Math.pow(2, attempt)));
}
throw new Error('D1 write failed after 3 retries');
}
这一改动将写入错误率从 3.2% 降至 0%。代价:REST API 调用每次写入比 binding 方式多约 5–8ms,但在规模化下的一致性胜出。
Batch Writes: The Transaction Trick(批量写入:事务技巧)
对于批量插入(我的网络钩子处理中很常见),我将写入聚合到事务中。根据 D1 事务文档,批量写入以原子方式提交,并减少锁竞争。
-- 不要用 50 个单独的 INSERT,改用单次 COMMIT 的事务
BEGIN TRANSACTION;
INSERT INTO events (user_id, action, metadata) VALUES ('u1', 'click', '{"x":1}');
INSERT INTO events (user_id, action, metadata) VALUES ('u2', 'view', '{"x":2}');
-- ... 再来 48 行
COMMIT;
The 4 Optimizations That Delivered 55% p99 Improvement(带来 55% p99 改善的 4 项优化)
除了写入锁修复之外,以下 4 项改动贡献了延迟改善的主要部分。
1. Covering Indexes(覆盖索引:读取延迟节省约 40%)
SQLite 可以直接从索引中完成某些查询,无需访问表数据。我在最频繁的查询模式上添加了覆盖索引。
-- 我最常见的查询:用户按类型的事件
CREATE INDEX idx_events_user_type ON events(user_id, event_type, created_at);
-- SQLite 现在可以完全从索引中回答此查询:
SELECT event_type, COUNT(*) FROM events WHERE user_id = ? AND event_type = ? GROUP BY event_type;
这将 App B(89 万行表)事件聚合查询的 p95 从 95ms 降低到 34ms。
2. Prepared Statements(Prepared Statements:解析开销节省约 15%)
我使用 Workers 中的模块级 Map 缓存了预编译语句。SQLite 需要解析并编译每条查询——复用预编译语句消除了这一开销。
const stmtCache = new Map<string, D1PreparedStatement>();
function getStmt(db: D1Database, sql: string): D1PreparedStatement {
if (!stmtCache.has(sql)) {
stmtCache.set(sql, db.prepare(sql));
}
return stmtCache.get(sql)!;
}
// 用法
const user = await getStmt(env.DB, 'SELECT * FROM users WHERE id = ?')
.bind(userId)
.first();
[Cloudflare Workers 最佳实践](https://developers.cloudflare.com/workers/performance/ speed/) 中记录了这一点,我本应在第一天就实现它。
3. Read Caching with Workers KV(Workers KV 读取缓存:消除 70% 的聚合查询)
对于仪表盘和报告,我将查询结果缓存在 Workers KV 中,TTL 为 5 分钟。这使得 70% 的昂贵聚合查询不再直击 D1。
const CACHE_KEY = `dash:${userId}:${date}`;
const cached = await env.KV.get(CACHE_KEY);
if (cached) {
return JSON.parse(cached); // 快速:KV 读取约 1ms
}
// 缓存未命中——查询 D1
const result = await env.DB
.prepare('SELECT COUNT(*) as count FROM orders WHERE user_id = ?')
.bind(userId)
.first();
// 缓存 5 分钟
await env.KV.put(CACHE_KEY, JSON.stringify(result), { expirationTtl: 300 });
return result;
缓存失效很简单:在同一请求上下文中发生写入时,我同步更新 KV key。
4. Table Partitioning by Date(按日期分区表:处理 210 万行 / 180MB)
对于我最大的表(App C,210 万行,180MB),我按月分区。不再扫描 created_at,而是按 id 范围查询。
// 不要用:WHERE created_at >= '2025-09-01' AND created_at < '2025-10-01'
// 改用已知 id 边界:
const monthBounds = await env.DB
.prepare('SELECT MIN(id), MAX(id) FROM events WHERE created_at >= date("now", "-30 days")')
.first();
const events = await env.DB
.prepare('SELECT * FROM events WHERE id >= ? AND id <= ? LIMIT 50')
.bind(monthBounds.min, monthBounds.max)
.all();
这并非真正的表分区(D1 尚未原生支持),但按主键范围 + LIMIT 查询模拟了这个效果,即使在 210 万行下也能将 p99 保持在 180ms 以下。
How D1 Compares to the Alternatives(D1 与其他方案的对比)
在选定 D1 之前,我同时使用过 Turso 和 PlanetScale。以下是我坦诚的看法。
Cloudflare D1 的优势:
- 与 Cloudflare Workers 零配置集成
- Workers 与之运行在同一数据中心,无跨区域延迟
- 免费额度:5MB 存储,无限读取/写入
- HTTP API 适用于非 Workers 环境
Turso 仍具优势:
- 全球读副本(D1 截至 2026 年 10 月尚不支持)
- libSQL 兼容本地开发
- 多读副本适用于读密集型工作负载
PlanetScale(MySQL)的优势:
- 真正水平分片,支撑 PB 级数据
- 非阻塞 Schema 变更
- MySQL 生态兼容性
对于我的用例——3 个跑在 Cloudflare Workers 上的应用,数据库 4MB–180MB——D1 是正确的选择。如果我需要全球多区域读取,会选择 Turso。
What I'd Do Differently(我会怎样不同:局限性)
我想坦诚说明我没有测试过的内容,以便你校准自己的决策。
- 我没有在万级并发写入以上测试 D1。 我的峰值是 60 次写入/秒。更高的并发下写入锁模型表现可能不同。
- D1 默认不支持外键约束——需要通过
wrangler.toml启用。这是 2025 年 5 月我踩过的坑。 - 没有读副本意味着每次读取都打到主库。 对于拥有全球用户的读密集型应用,这是真实的局限。Cloudflare 的 D1 路线图 将副本列为"即将推出"。
- 大表迁移可能很慢。 我的 180MB App C 迁移使用
wrangler d1 migrations apply耗时 8 分钟。生产部署要提前规划。
Getting Started: Your First D1 Optimization Sprint(入门:你的首个 D1 优化冲刺)
如果你今天在生产环境中运行 D1,以下是我基于 6 个月经历推荐的三步冲刺。
第 1 步:测量基线(1 小时)
使用 wrangler d1 execute 计时,获取当前 p99:
time wrangler d1 execute my-database --command="SELECT COUNT(*) FROM my_table"
然后用 performance.measure() 为 Workers 添加仪表化,获取真实用户的 p99。
第 2 步:为你的 Top 5 查询添加覆盖索引(2 小时)
使用 Cloudflare Analytics 定位最慢的查询,然后添加覆盖索引。用 EXPLAIN QUERY PLAN 测试:
wrangler d1 execute my-database --command="EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = ?"
第 3 步:实现读取缓存(3 小时) 为聚合查询和仪表盘数据添加 Workers KV 缓存。TTL 从 5 分钟起步,后续按需调优。
Closing Thoughts(结语)
Cloudflare D1 是一款真正设计精良的边缘数据库。经过 6 个月和 3 个生产应用的验证,我有信心说它能应对真实的工作负载——不只是玩具项目。55% 的 p99 改善来自实用、有据可查的优化,而非什么花招。
写入锁模型才是需要关注的真正约束。如果你的写入超过每秒 50 次,提前规划 HTTP API + 重试方案。如果你需要全球读副本,等等 D1 的路线图或评估 Turso。
TanStack Ship 中的设置使用了预置优化查询模式的 D1 集成。如果你想在 Cloudflare Workers 上启动新应用,可以通过 TanStack Ship 起步,无需从零配置 D1 集成。
无任何实质性关联。Cloudflare D1 和 TanStack 都是我在生产中使用的开源项目。指向 Cloudflare 和 TanStack 文档的链接仅供参照,不构成付费推广。
Related Posts
- TanStack Query v5 Migration: Zero-Downtime Strategy — How I migrated 3 apps without breaking production
- Cloudflare Workers Performance: Cold Starts Under 10ms — Eliminating cold starts on the edge
- Compare: TanStack Start vs Next.js for Edge Apps — Full benchmark comparison across 12 metrics