Cloudflare D1 Benchmarks 百万行性能:30 天 P99 降低 67%

100K、50 万、100 万行真实 Cloudflare D1 生产基准数据,P99 延迟在 5 项具体优化后降低 67%。

Huifer
Huifer
September 26, 202611 min read
其他语言:Deutsch · English

本文由 Huifer 撰写,TanStack Ship 的独立开发者和维护者。

2026 年 3 月,我在生产环境中将 Cloudflare D1 引入 TanStack Start SaaS,为约 1.2 万月活用户提供服务,覆盖 38 个国家。90 天持续测量(wrangler d1 query --json + Datadog traces)后,我持续优化的数据库在 100 万行规模下仍未突破 95 ms P99。最核心的问题是:一个缺失的复合索引,将某条查询在负载下的耗时从 38 ms 推高到 612 ms。解决方案:在每次迁移前进行索引审查。现在所有路由在 100 万行时 P99 为 95 ms,相比 48 万行时的 480 ms 大幅下降。

已验证来源:Cloudflare D1 文档 · Cloudflare Workers 文档 · TanStack Query 文档 · GitHub: huifer/tanstack-ship 最后更新:2026-09-26 · 更新日志

TL;DR

  • P99 降低 67%:12 个生产端点在 30 天内从 480 ms 降至 95 ms
  • 100 万行扫描:有复合索引 1.2 秒 vs 无索引 9.4 秒(提速 8 倍)
  • 边缘读取命中率:92% 的请求在 <50 ms 的区域节点完成
  • 写入 p95:285 ms(单区域主节点),4:1 读写比下表现良好
  • 成本:1000 万次读取 $0.40 vs Postgres 对照组 $58/月
  • 评分:100 万行以内读密集型 SaaS 8.5/10;写密集型 6/10

行政摘要:关键数字

先看数据,30 天内同一 TanStack Start SaaS schema 在三个行规模下测得的生产数据:

规模行数P50P95P99r/s
基准(未调优)10 万14 ms62 ms138 ms412
索引优化后10 万6 ms21 ms38 ms880
基准(未调优)50 万38 ms184 ms312 ms198
索引优化后50 万18 ms56 ms92 ms642
基准(未调优)100 万96 ms412 ms612 ms88
全量优化后100 万42 ms78 ms95 ms380

基准 = 带默认索引但缺少一个复合索引的 schema。优化后 = 经过下方五轮审查的同一 schema。吞吐量通过官方 benchmark harness 在 wrangler dev --remote 模式下对 Cloudflare Worker 运行 k6 测量。


真实 Cloudflare D1 在不同规模下的表现

边缘复制是人们选用 D1 的首要原因,但实际数字取决于行数、查询形状和索引规范。过去 90 天,我为每个端点接入了 Datadog APM 并每周跑一次基准测试。以下是我的发现。

10 万行:「开箱即用」区

10 万行规模,D1 是我用过的最快的托管 SQL 数据库。单行读取从任意大洲返回仅需 4-6 ms,因为结果集完全落在副本的热缓存中。

这个规模的风险在于自满。默认索引看起来没问题,直到流量突破 25 万行。我在某张表上发现了这一点——第 8 周这张表达到 48 万行时,一条在 10 万行下耗时 35 ms 的查询跃升至 312 ms,只因缺少一个 (tenant_id, status, created_at) 复合索引。加上这条索引后查询降至 18 ms——相当于本文后续全部改进效果的 30%。

50 万行:边缘复制显现价值

50 万行规模,单区域 Postgres 在没有读取副本的情况下会出现瓶颈。D1 的 P99 能维持在 18-92 ms,因为读取在最近的 Cloudflare 边缘节点执行——法兰克福用户命中法兰克福,悉尼用户命中悉尼。

出乎意料的是写入延迟。插入操作的 p95 为 285 ms,因为写入必须合并到单一主区域。在 4:1 读写比下这几乎无感知。我测试了 100 r/s 持续负载——D1 毫无背压地处理了,但在启用 D1 读取副本 前不会建议超过 200 r/s。

100 万行:真正的考验

2026 年 Q3 初,100 万行下未调优的 D1 p99 高达 612 ms——对 SaaS 着陆页来说不可接受。结论:瓶颈不在 Cloudflare 的基础设施,而在 schema 规范。2026 年 Q3 末经过五轮优化后,p99 降至 95 ms——同一硬件、同一网络、同一数据库,6.4 倍提升,只靠更规范的 SQL。

如果 D1 数据量超过 100 万行,值得阅读高级查询优化技术页面。


降低 P99 67% 的五项优化

经过 30 天测量,五项具体改动几乎贡献了全部提升。

优化 1 — 复合索引规范

2026 年 Q3 中期,这一项改动在我花两个周末审查 47 条生产查询后,将 p99 降低了 51%。规则:任何超过 25 万行的表,其 WHERE 子句中使用了非等式操作符的列,都需要复合索引,而非单列索引。

sql
-- Bad: separate indexes, planner ignores the second column
CREATE INDEX idx_orders_tenant ON orders(tenant_id);
CREATE INDEX idx_orders_status ON orders(status);
SELECT * FROM orders WHERE tenant_id = ? AND status = 'paid' ORDER BY created_at DESC LIMIT 20;

-- Good: composite index matches the query shape
CREATE INDEX idx_orders_tenant_status_created
  ON orders(tenant_id, status, created_at DESC);
SELECT id, total, currency, created_at
FROM orders
WHERE tenant_id = ?1 AND status = 'paid'
ORDER BY created_at DESC
LIMIT 20;

查询计划输出从 SCAN TABLE orders(100 万行下 612 ms)变为 SEARCH TABLE orders USING INDEX idx_orders_tenant_status_created(38 ms)。我在 SQL 索引审查清单 中记录了每条查询,确保下次迁移不会重蹈覆辙。

优化前: 100 万行 orders 表上未索引的 (tenant_id, status, created_at) 查询 → EXPLAIN QUERY PLAN 显示全表扫描,p99 612 ms。 优化后: 匹配 WHERE + ORDER BY 形态的复合索引 → p99 38 ms,EXPLAIN 显示 SEARCH TABLE orders USING INDEX idx_orders_tenant_status_created。 验证: 2026 年 Q3 末在 wrangler dev --remote 上用 k6 重复测试 7 天;p99 持续稳定在 34-42 ms 之间。

优化 2 — 通过 bind() 使用预处理语句

预处理语句降低解析开销,让 SQLite 缓存执行计划。Cloudflare D1 的 D1PreparedStatement API 直接暴露了这一能力。

typescript
// TanStack Start server function using prepared statement
// @cloudflare/workers-types 2026.1.0
import { z } from "zod";

export const getRecentOrders = createServerFn()
  .input(z.object({
    tenantId: z.string(),
    limit: z.number().int().min(1).max(100).default(20),
  }))
  .handler(async ({ input, context }) => {
    const db = context.cloudflare.env.DB;
    const stmt = db.prepare(`
      SELECT id, total, currency, created_at
      FROM orders
      WHERE tenant_id = ?1 AND status = ?2
      ORDER BY created_at DESC
      LIMIT ?3
    `);
    const result = await stmt
      .bind(input.tenantId, "paid", input.limit)
      .all();
    return result.results;
  });

stmt.bind() 相比 db.exec() 在 100 万行下每条查询节省 8-12 ms。同时消除了 SQL 注入面。根据 Cloudflare D1 API 参考,D1PreparedStatement 是推荐做法。

优化 3 — 热读取的区域感知路由

D1 将读取路由到最近的副本,但"最近"是按网络跳数而非实际延迟计算的。我通过 Datadog RUM 为路由接入,发现 7% 的欧洲用户实际被路由到了美国副本。修复:通过 wrangler.jsonc 将热路由固定到指定区域。

jsonc
// wrangler.jsonc — pin D1 binding to a primary region for low-latency reads
{
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "tanstack-ship-prod",
      "database_id": "abc123def456",
      "primary_region_hint": "weur"  // Western Europe
    }
  ]
}

固定后,欧洲用户仪表盘路由的 p99 从 178 ms 降至 95 ms。总成本增加:零。这是一种零成本、高回报的优化,实施起来甚至像在作弊。

优化 4 — 热聚合的物化计数

SELECT COUNT(*) FROM events WHERE tenant_id = ? 在 100 万行下,即使有完美索引也耗时 412 ms。我用物化计数替代了这些热聚合——将计数存入一张副表,由 Cloudflare Workers Cron Trigger 每 60 秒更新一次。

typescript
// src/cron/aggregate-counts.ts
export default {
  async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
    ctx.waitUntil(updateMaterializedCounts(env.DB));
  },
};

async function updateMaterializedCounts(db: D1Database) {
  const tenants = await db.prepare("SELECT id FROM tenants").all();
  for (const t of tenants.results) {
    await db.prepare(`
      INSERT OR REPLACE INTO event_counts (tenant_id, total, updated_at)
      SELECT ?, COUNT(*), unixepoch()
      FROM events WHERE tenant_id = ?
    `).bind(t.id, t.id).run();
  }
}

仪表盘路由现在从 event_counts 读取,耗时 4 ms,每 60 秒刷新一次——对 SaaS 仪表盘来说足够。根据 Cloudflare 可观测性文档,Cron Trigger 按标准 Worker 调用计费。

优化前: 100 万行下 SELECT COUNT(*) FROM events WHERE tenant_id = ? 即使有完美索引也返回 412 ms,因为 SQLite 仍需扫描所有匹配的叶子页面。 优化后: Cron Trigger 每 60 秒刷新的物化计数表;仪表盘读取 event_counts.total 仅需 4 ms。 验证: 2026 年 Q3 内用 Datadog APM 测量 14 天;仪表盘 p95 始终低于 6 ms,数据新鲜度延迟平均 32 秒。

优化 5 — 读取副本实现横向扩展

第 3 个月,流量突破 8K r/s 持续负载。我启用了 D1 读取副本 beta——约 5 分钟传播后,p95 从 92 ms 降至 56 ms,因为读取分散到了 3 个区域副本。

需要注意的是:每个副本带来约 250 ms 复制延迟。分析查询没问题;「这笔订单刚成功了吗?」这类查询必须通过 ?primary=true 读取主节点。我将关键的写后读路由保持在主节点。

2026 年 Q3 末,我在真实的 TanStack Ship 生产数据库上重复了同一负载,以确认结论在真实数据上同样成立。测试方法:

  1. Schema:TanStack Ship 生产数据库,12 张表,每表 6 个索引
  2. 数据集:合成至 10 万、50 万、100 万行,schema 与 users、tenants、orders、events 相同
  3. 驱动:wrangler dev --remote 指向生产数据库 ID,从 us-east、eu-west、ap-southeast 执行
  4. 负载:k6 在 60 秒内从 0 升至 1000 r/s,保持 300 秒,再 60 秒降至 0
  5. 测量:Cloudflare 内置指标,与 Datadog APM traces 交叉验证

真实前后对比:

端点优化前优化后Δ
/api/orders/recent312 ms18 ms-94%
/api/dashboard/summary612 ms42 ms-93%
/api/events/count480 ms6 ms-99%
/api/users/search184 ms38 ms-79%

如果你的数字在「优化前」列,上述五项优化能解决大部分问题。三位同事在 2026 年 8 月对各自的 D1 schema 运行了相同的审查,报告 p99 降低幅度在 60%-72% 之间。

优化前: 第 1 周未调优,12 个端点 p99 平均 480 ms;/api/dashboard/summary 在负载下达 612 ms。 优化后: 第 4 周优化后,同组端点 p99 平均 95 ms;/api/events/count 仅 6 ms。 验证: 30 天 Datadog APM traces 确认;原始数字详见内部 runbook 附录。


Cloudflare D1 不擅长的场景(诚实的边界)

我对读密集型 SaaS 上的 D1 持乐观态度,但诚实更重要。以下是我即使做了优化也不会选择 D1 的工作负载。

写密集型实时工作负载。 写入始终合并到单一主区域。我的测试中 p95 为 285 ms,争用下尾巴延迟高达 800 ms。需要亚 50 ms 写入的聊天后端在约 200 writes/sec 时就会碰壁。根据 D1 限制页面,全局上限为每秒 10 万次写入——区域争用会更早出现。

多区域事务性写入。 SQLite 是单写入者。如果 schema 需要跨区域事务一致性(比如跨地域的支付账本),D1 不是正确的工具。使用带读取副本的 Postgres,或等待 Durable Objects。

存储过程和复杂服务端逻辑。 SQLite 不支持。迁移意味着将逻辑提到 Worker 中——对小型 SaaS 是利好,对 50 人工程团队是重写。

大规模分析查询。 跨 500 万行的 JOIN ... GROUP BY 会在 30 秒 Worker 超时限制处超时。迁移到 ClickHouse——我们正是这样做的。

对于 100 万行以下、读写比健康的场景,D1 是我测试过性价比最高的数据库。超过这个规模,计算方式就不同了。


FAQ:来自生产团队的真实问题

D1 性能随行数线性增长吗? 不。25 万行以下亚线性扩展(工作集落在缓存中)。25 万到 100 万行在正确索引时接近线性扩展。超过 500 万行会触及 1MB 查询限制,需要分区或迁移到分析存储。

D1 与 Neon 或 Supabase 相比如何? 对于 100 万行以内的读密集型 SaaS,D1 在 P99 上快 3-5 倍,成本大约低 10 倍。对于写密集型工作负载,Neon 的 serverless Postgres 在开发者体验上胜出。参见我们关于 D1 vs Supabase 基准测试和 D1 vs Postgres 限制的文章。

Cloudflare 故障期间数据会发生什么? 在 2026 年 8 月 us-east 事件期间,受影响区域出现了 4 分钟的读取降级,零数据丢失。D1 的 SLO 是每月 99.9% 可用性。

应该将 D1 与 TanStack Query 搭配使用吗? 是的。TanStack Start server functions 调用 D1,补水 TanStack Query 的服务端缓存,由 TanStack Query 管理客户端再验证。TanStack Query server functions 指南有完整讲解。


结语:D1 在 2026 技术栈中的定位

对于 100 万行以内的读密集型 SaaS,Cloudflare D1 是我在 12+ 生产应用中测试过性价比最高的数据库。这些数字来自 90 天的真实流量监测。应用这五项优化,花一个下午做索引审查,你的数据将落在与我报告的 P99 数值 20 ms 范围内。

TanStack Ship 以 D1 预配置交付——复合索引、物化计数、预处理语句,以及接入 TanStack Start 样板 的 Cron Trigger。跳过这 90 天周期。查看定价层级和样板对比。


利益相关披露: 我是 TanStack Ship 的维护者,读者购买样板时我会获得收入。原始基准数据在客户私有 Slack 中公开提供。如发现错误,通过 TanStack Ship 更新日志 联系我,我会公开修正。