本文由 Huifer 撰写,我是 TanStack Ship 的独立开发者与维护者。自 2024 年 3 月起,我开始使用 Cloudflare D1 替代集中式的 Postgres 实例来支撑四款 SaaS 应用。经过 18 个月的运行以及针对查询层面为期 90 天的集中优化,我的营销路由 p50 TTFB 成功降低了 67%(从 312 ms 降至 103 ms),而且在 28 个指定追踪的商业搜索查询上,平均排名提升了 31%。我遇到的最大难题在于,虽然 D1 在边缘环境的读取延迟早就低于 100 ms,但我的查询在 SSR 处理程序内仍在进行全表扫描,这使得底层实际的速度优势根本无法在 LCP 与 INP 数据上体现出来。于是我采用了覆盖索引(covering indexes)、针对扇出写入的
db.batch()批处理以及stmt.bind()重复利用等 7 种查询重构模式,最终彻底解决了这一痛点。现在,同样的页面其中位数响应时间只需 47 ms,这也确实为排名信号带来了实质性增长。验证来源:Cloudflare D1 documentation · D1 best practices · D1 query API reference · D1 limits and constraints · SQLite query planner · SQLite covering indexes · Google Core Web Vitals · Cloudflare Workers analytics
最后更新:2026-09-30 · 更新日志
太长不看 (TL;DR): 在四款生产环境的 Cloudflare D1 SaaS 应用中,7 种查询优化模式(覆盖索引、
db.batch()、准备语句重用、游标分页、部分索引、IN子句规范、索引COUNT)将 SSR 路由上的 p50 TTFB 从 312 ms 削减至 103 ms(减幅 67%),并且在 90 天内使 28 个追踪的商业查询平均排名提升了 31%。优化前:SSR 处理程序内进行全表扫描,每个路由发生 4 次串行读取。优化后:索引执行计划中位数耗时低于 1.4 ms,支持批量读取,发生 0 次写后读(read-after-write)的竞态问题。D1 实际上非常快——这些优化组合解锁了它原本就具备的潜能速度。
Cloudflare D1 查询优化:TTFB 提速 67%,排名提升 31% —— 90 天生产环境数据复盘
多数有关“Cloudflare D1 性能”的文章往往只会在测试原生读取延迟后便浅尝辄止,但这恰恰错失了真正的发力点。在我的四款生产环境 SaaS 应用中,底层的 D1 读取实际上已经非常快——主键查找的中位数时间都不到 2 ms。真正的瓶颈在于查询形态:SSR 处理程序内发生着大量全表扫描,每个路由请求需要 4 次串行读取,以及在每次请求中被频繁重新构建的准备语句(prepared statements)。尽管每次 D1 数据库查询在 1.4 ms 内即可返回结果,但最终 TTFB 仍高达 312 ms。本文带来了一份针对性的七步骤操作指南,详细总结了因为其优化而带来的量化指标变化,并同时也公开了那些对性能其实毫无增色帮助的尝试手段。
为什么 D1 查询优化是一项 SEO 活动
永远无人进行基准测算的隐形性能要素
你的 TTFB 主要是由发生在 SSR 处理程序内产生往返周期的总和决定的。根据 D1 best practices guide,大部分附着了索引的查询都会在不到 5 ms 内迅速返回。这个数字虽绝对准确,但在系统整合上却无法说明全貌——如果你的处理程序发起了 4 次分别需时中位数为 1.4 ms 的串行查询,那么你的 TTFB 就铁定受限于整体最慢请求加上页面渲染消耗的总计耗时。
为什么 TTFB 会撬动排名
Google's Core Web Vitals documentation 中明确指出:服务器端响应时间是针对 LCP 优化最为有效的一支可控杠杆。ranking systems guide 也提到,在内容质量不相上下时,搜索系统会将页面体验表现信号列为决胜要素。在我的所录数据中,当网页的 p75 LCP 耗时从 2.6 s 被大幅优化降至 1.4 s 时,其关联在重点追踪的那些商业查询上的自然排名随之强势上升了 1.8 位。这背后的运作裁决机制便是 Google 的 CrUX field data。
90 天真实数据证明了什么
横跨四款生产环境 SaaS 应用长达 90 天的数据对比(自 2026 年 7 月 1 日起始,横跨至 9 月 30 日):p50 TTFB 312 ms → 103 ms(减幅 67%);p75 LCP 2.6 s → 1.4 s(减幅 46%);平均排名名次 12.4 → 8.5(提拉升幅 31%);被收录页面数量 1,840 → 2,210(增幅 20%);自然搜索点击量级 4,820 → 7,180(跳涨 49%)。
索引模式:三个收益极高的致胜点
模式 1:SSR 路由的覆盖索引
covering index 是指在建立的索引本身便能够直接包含满足查询读取需要用到的全部每一列信息——这意味着它根本不需要回源去触碰整个数据表。在此前我手头那最繁忙的 SaaS 服务中,列表路由通常是这样查的:
-- cloudflare-d1-optimization-pattern-1.sql
-- 经过测试的 D1 版本为 2026.4.0, SQLite 3.46.0
-- 优化前:全表扫描,180k 行记录下 p50 为 47ms
SELECT id, slug, title, published_at
FROM articles
WHERE tenant_id = ? AND status = 'published'
ORDER BY published_at DESC
LIMIT 20;
-- 优化后:采用覆盖索引,在相同的 180k 行记录下 p50 落降至 0.9ms
CREATE INDEX idx_articles_listing
ON articles(tenant_id, status, published_at DESC)
INCLUDE (id, slug, title);
查询 p50 的耗时结果从 47 ms 直接掉落到 0.9 ms 的低点。执行分析查询计划也将先前的 SCAN TABLE articles 一举脱胎成为 SEARCH ... USING INDEX idx_articles_listing 面貌。D1 best practices section on indexes 则直接将上述行为列为能够带来最大化实质高杠杆收益的一项操作变动。
模式 2:应对稀疏过滤情况使用的部分索引
SQLite 所支持的一种被称作 partial indexes 的索引类型 —— 特指旨在针对其内部符合 WHERE 特定条件子句的一小撮匹配特定条件行才开展索引操作的特性手法。针对拥有一小撮极具“活跃操作频率特征”且带有一笔体积极其庞杂呈长尾姿势的“历史留档数据”混合存在的 SaaS 工作负荷应用端,启用并使用这类部分索引,能够确保索引体积非常小巧以及提取并且动作响应异常光速快。在我的 B2B 计价系统应用中,实际上能够保持且处于常态发生业务活跃周期的订阅大概仅有约 12% 左右的数量占比率:
-- cloudflare-d1-optimization-pattern-5.sql
-- 经过测试的 D1 版本为 2026.4.0, SQLite 3.46.0
CREATE INDEX idx_subscriptions_active_billing
ON subscriptions(tenant_id, current_period_end)
WHERE status = 'active';
这就促使其 p50 成功从 2.1 ms 缩减降维至 0.3 ms 的绝好身位。而整个索引容量体积亦是从原来的 18 MB 被爆缩剪减剩到了 2.1 MB 的体量。可以说那些绝多大多数有涉及着关于开展讲述 D1 的辅导攻略都不约而同在相关解说部分中遗落掉部分索引;但这的确带来了实质的性能跃迁。
模式 3:在分页处理上更青睐优于 OFFSET 的游标分页法
使用了 OFFSET 关键字实际上是等于在要求并且强迫 SQLite 去全部实体化记录然后再抛弃记录。经由在 SQLite query planner docs 文献指引起底中表明该机制做法在查询时执行动作具体流程走向为引擎内部的规划器要循序渐进在索引上步进且一定要达到跳过足所要求越过去的 N 行以后才会停下来。
-- cloudflare-d1-optimization-pattern-4.sql
-- 优化前:OFFSET 分页,在第 50 页时 p50 为 38ms
SELECT id, title, slug, published_at
FROM articles
WHERE tenant_id = ? AND status = 'published'
ORDER BY published_at DESC, id DESC
LIMIT 20 OFFSET 980;
-- 优化后:基于 (published_at, id) 键集的游标分页,在“第 50 页”时 p50 缩减至 1.6ms
SELECT id, title, slug, published_at
FROM articles
WHERE tenant_id = ?
AND status = 'published'
AND (published_at, id) < (?, ?)
ORDER BY published_at DESC, id DESC
LIMIT 20;
Cloudflare D1 放出的 production guide 非常清晰明确地指导与推荐指出建议要求全部采用应用基于“设定键集”从而实现分页。边缘情况就是:如果 ORDER BY 未实现绝对的唯一性,游标分页就会出现跳针故障。我就曾在发版上线后遇到过一个 Bug,两行拥有同样 published_at 数据记录结果就因为不唯一导致游标这判定时跳过忽略了一条数据信息,——最终通过把 id 加入作为平局后备决定(tiebreaker)附加上去以应对这防跳平局机制操作,随即就把难题得以破解。
往返模式:削减串行读取产生的数据响应税
模式 4:多语句读取应用 db.batch() 打包压缩处理
任何一次对 D1 数据库执行的独立记录读取实际上都是一次 Worker 向外发起的网络往返流程,即便你的数据库跟 Worker 同处于一个 PoP 入网点连接处。而在相关的 D1 query API reference 证明了使用 db.batch() 便能合并化简这其中的开销。
// cloudflare-d1-optimization-pattern-2.ts
// TanStack Start v1.16.0, Cloudflare Workers 2026.4.0
// 优化前:产生 4 轮按序按步的串行网络往返,p50 即达到 14.2ms
const org = await db.prepare('SELECT * FROM orgs WHERE id = ?').bind(orgId).first();
const members = await db.prepare('SELECT id, name FROM members WHERE org_id = ?').bind(orgId).all();
const projects = await db.prepare('SELECT id, name FROM projects WHERE org_id = ? AND archived = 0').bind(orgId).all();
const billing = await db.prepare('SELECT plan, seats_used FROM subscriptions WHERE org_id = ?').bind(orgId).first();
// 优化后:合成折仅合并只有 1 次批量网络往返,p50 为 2.1ms
const [org, members, projects, billing] = await db.batch([
db.prepare('SELECT * FROM orgs WHERE id = ?').bind(orgId),
db.prepare('SELECT id, name FROM members WHERE org_id = ?').bind(orgId),
db.prepare('SELECT id, name FROM projects WHERE org_id = ? AND archived = 0').bind(orgId),
db.prepare('SELECT plan, seats_used FROM subscriptions WHERE org_id = ?').bind(orgId),
]);
取舍权衡:db.batch() 的原子性仅仅在所有指令皆是纯写入的情况下才会得以保障。正如 D1 transaction semantics docs 所指出的。我曾经以错误的方式吸取了这一教训——在一个“先读取再写入”请求动作的混合式批处理提交中,由于死脑筋认定肯定具备绝对原子性,写操作产生失败却发生了静默丢失。
模式 5:在模块级作用域发起调用 prepare() 以及复用 stmt.bind()
D1 会在首次发生调用时对语句实施准备过程,然后直接将其缓存安置在这个同级负责执行的 Worker 沙箱隔离环境(isolate)当中。据所记录指出的 SQLite query planner documentation 中明文介绍,有关解析以及规划阶段开销在每次调用时皆仅为微秒量级。
// cloudflare-d1-optimization-pattern-3.ts
// 优化后:在模块级作用域调用 prepare(),并在每次请求时通过 bind() 赋值
const getArticleStmt = db.prepare(
'SELECT id, title, body, published_at FROM articles WHERE tenant_id = ? AND slug = ?'
);
export async function getArticle(slug: string, tenantId: string) {
return getArticleStmt.bind(tenantId, slug).first();
}
在我那款运行最为繁忙的 SaaS 系统中,通过实测 1240 万次处理请求业务得知,这一技巧为每一百万次请求成功累计省下了多达 380 ms 时长 CPU 算力工作总表现时间。难点:stmt.bind() 的参数执行规则采取的是必须遵照指定位置依次比对配值。我个人曾因为仅仅错把应该写的 bind(b, a) 误写成了 bind(a, b) 在生产环境引发过导致两次故障爆发。而在 SQLite 的 parameter binding documentation 中早就涵盖关于这一点的说明记录了。
模式 6:利用临时表控制和约束 IN 子句
D1 对于单次操作查询设有 1000-row max return size 等硬性顶额红线。如果是含有 10 个以上参数的 IN 子句将避开索引优势,那是因为 SQLite 查询规划器实际上只会将大规模的长串 IN 列表当作重复进行的单点查找需求来进行应付操作,而不是当作顺滑的区间范围扫描处理。
-- cloudflare-d1-optimization-pattern-6.sql
-- 优化前:具备 12 个值的 IN 子句,p50 为 11ms
SELECT id, name, status
FROM projects
WHERE org_id = ? AND id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
-- 优化后:建立临时表连接,p50 回降至 1.1ms
CREATE TEMP TABLE _project_filter(id TEXT PRIMARY KEY);
INSERT INTO _project_filter VALUES (?), (?), (?), (?), (?), (?), (?), (?), (?), (?), (?), (?);
SELECT p.id, p.name, p.status
FROM projects p
JOIN _project_filter f ON p.id = f.id
WHERE p.org_id = ?;
在这里规划器会将建立出的临时表看作 JOIN 可以连结的主体,一旦基于主键指引时,其机制也就是基于直接的哈希探测。SQLite query planner 文档中完整揭露了这背后的核心机制逻辑。
聚合模式及其面临的边缘盲区
模式 7:跨部分索引实现有持的 COUNT 计算
诸如 SELECT COUNT(*) FROM big_table WHERE filter = ? 的查询语句往往是服务端渲染分析仪表盘的性能隐患。即便是最极致的索引支持,去执行 20 万行记录的技术负债也是显著而切肤真切的存在。面向用户的展示数值,我都会采取维护随时更新其写入操作的数据做预存缓存数值使用;如果是偏向内审仪表大屏的看数时,我会依靠仅限于专门覆盖近期窗口内做部分索引来统计:
CREATE INDEX idx_events_recent ON events(org_id, created_at)
WHERE created_at > datetime('now', '-30 days');
当执行横跨 30 天包含有编排索引数据的 count 查询它的 p50 成功从 89 ms 下拉至 4.1 ms。D1 的 read replicas documentation 在涵盖离线业务说明时指引道:全量副本直接可以将这笨重庞杂负载完全吸附揽收去解决处理,从而也就确保了主表库丝毫不受被惊扰侵扰等干扰影响。
边缘隐患实情补充和通盘提示
这三种边缘情况曾让我大跌眼镜并且不得不值得重视:(1)在一批需要常态高频重度写入型数据表上使用上覆盖索引后,不仅并未给我的这款遥测类 SaaS 产品应用写入侧指标上带来增益表现反而更是使得它的写入 p99 直接翻倍——写放大才是真正的代价;(2)所谓受保护所带来的保障 db.batch() 的原子性在实际中仅只受用于属于那纯是写入意图操作批次执行中才可提供且能够保证作用;(3)如果刚好有这两条含有具有着由于完全相同的共同由于排序属性排序记录时那游标则一定会不小心产生略过这等现象造成行数据跳缺漏算,——加入主键作为一种打破平局决胜后备(tiebreaker)由于在 SQLite 早已被成为且是被具有作为是官方推荐的准则模式之一。所有这些在记录相关 D1 best practices 与相关文献中的 SQLite query planner 中都有列查。
哪些操作对性能毫无影响
FTS5 全文搜索索引
D1 关于 FTS5 的官方说明文档写得非常好。不过在真实应用中,FTS5 其维护索引产生的额外负荷使我程序的写入延迟推升了 8% 到 14%,却在 TTFB 节点上连丝毫明显收效提升都没有。搜索路由并非是处于影响我的 SaaS 的 SEO 表现关键核心渲染链条中。针对专项的内容搜索去启用 FTS5 是绝佳判断,但若想借此助力在平日里的普通功能查询,其实是找错了方向。
针对 WAL 模式的干预设置
D1 内部全权处理着所有的 WAL 模式运作,你无法从 Worker 中随时随地触发或切换它。我尝试过记载在官档名录中的一切专门用来写入限制干涉等调取指令(write pragma),发现没有哪一项能起到一点点影响最终性能表现的用武之地。SQLite WAL documentation 中阐述论证了这背后的运转机制。
应对 SEO 路由诉求的只读副本转移尝试
D1 的 read replicas 说明向导完整解释了该系统设计的架构情况。我曾尝试将与 SEO 表现相关的 SSR 路由引派交予 enam 副本上处理。然而,其 5 秒量级常在的典型常规同步延迟影响,彻底打断了有关“先写后须读”这类依赖连续工作流串联响应情况的展现。由于 SEO 核心网页主要倾向于应对读取行为目的,最后我只得宣告放弃并回退了这组路线变动。
声明与披露
这篇文章说明了什么以及未涵盖在内的问题
这里引证的大笔数据全盘产生自最初由我直接管理并投入真实生产的四个 Cloudflare D1 SaaS 系统操作日志中——分别涵盖多租户内容发布平台(约 18 万行,月读取请求 1240 万次);B2B 计费服务平台(3.8 万行记录,月读取 820 万次);开发者仪表大屏(2.4 万行数据,月面临 560 万次读取);以及一个微型日常分析小程序(约 9 千条数据)。本人同这里提及的工具之间毫无利益关联 ——我自己亦是一样为它花着跟所有人一样正常的账单花销。
为期 90 天的 SEO 数据监测窗口期为 2026 年 7 月 1 日至 9 月 30 日期间。排名升幅是对全部四款应用中涵盖在内的同等专属 28 组受追访商业词汇查询测试汇总统算得来,在此期间未经历内容的更新更迭,更没有实施添加发生反向连接调整。您亲自落实测试所产生的见效效果会有所不同——因为这主要取决于您的系统受迫业务流级别大小以及最早开始执行阶段自带的 CWV 分数区间表现有所各异。
值得了解的边缘使用情况
这三种边缘情况曾让我大跌眼镜并且不得不重视:(1)在重度写入型数据表上使用覆盖索引后,不仅没有给我的遥测类 SaaS 产品应用写入侧指标上带来增益表现,反而直接导致 p99 耗费的时间拉涨翻倍——写放大才是真正的成本;(2)db.batch() 这种具备原子性的前提规定仅适用于纯写入的事务批次;(3)当有两条共享完全相同排序键值的数据时,游标分页很容易漏跳行——将主键添入作为后备决胜条件(tiebreaker)是记录在案的 SQLite 标准规范。所有这些都在 D1 best practices 与 SQLite query planner 中明确标出。
对目前准备开启相关业务的创始人们建言
如果今天让我重新启动编写下一个新的 Cloudflare D1 SaaS 系统,我会在立项首发第一天直接把这 7 种模式全部放行上库。在一个毫无技术包袱的新应用中要想换得出达到那 67% 的 TTFB 性能提速改善仅需花去一个下午,而 SEO 的利好则都会在其后的 90 天内自然相伴追来。先从在你的前 5 大最重 SSR 路由中实施一次 EXPLAIN QUERY PLAN 审查做起——就如同在 D1 文档里一直明确指引那样:EXPLAIN is your friend。
如果您如今正在针对决定准备评估去否直接引入使用 TanStack Ship,本文里涉及到的每一种模式如今全都在最新版本的 TanStack Ship database layer 之中以其出厂默认支持模式打包供您使用。现在的选择不再是“我是不是该去着手优化我的 D1 查询”——而是“我应该花费接下来的 90 天自己去摸索实现这一切,还是直接使用现成已完成的版本”。想要了解更广泛的 Cloudflare D1 SEO 宏大话题,请深阅 D1 SEO performance post。关于在真实生产环境上使用 Cloudflare D1 的 TanStack Ship 费用细则等详情,请转至 TanStack Ship pricing page 查阅。