
数据库这件小事
独立开发者最头疼的事之一就是数据库。
租个 RDS?最便宜的 MySQL 也要 $10/月起,还没算存储和流量。自己装?要操心备份、升级、主从复制、连接池……Supabase 和 PlanetScale 很好,但免费套餐一过期就开始烧钱。
D1 是 Cloudflare 的 Serverless 数据库,基于 SQLite。免费 5GB 存储、每天 500 万次读、10 万次写,永久免费。付费后 $0.001/百万次读,存储 $0.75/GB-月,而且出口流量完全免费。
对独立开发者来说,这意味着你可以在 $0 的成本下跑一个正经的数据库,等到真正有用户了再按量付费。
- • 免费额度大:5GB 存储 + 500 万行读/天,永久免费
- • SQLite 兼容:标准 SQL 语法,迁移零成本
- • 全球读副本:自动跨区域复制,延迟低
- • 零出口费:数据传出完全免费
- • 与 Workers 原生集成:一个 Binding 搞定,不需要连接字符串

D1 是什么?
D1 是一个 Serverless SQL 数据库,底层基于 SQLite。你可以把它理解为"不用运维的 SQLite"——你写标准的 SQL,D1 帮你处理存储、备份、复制、扩展。
和传统数据库最大的区别是:没有连接池的概念。D1 通过 Workers Binding 直接调用,每次查询都是一个独立的请求,不需要维护数据库连接。这意味着你不用担心连接数耗尽、连接泄漏这些问题。
D1 的架构:
用户请求 ↓┌─────────────┐│ Worker │ ← 你的业务逻辑└─────────────┘ ↓ (Binding)┌─────────────┐│ D1 │ ← Serverless 数据库│ ┌───────┐ ││ │ Primary│ │ ← 写入节点│ └───┬───┘ ││ ↓ ││ ┌─────────┐ ││ │Replicas │ │ ← 全球只读副本(ENAM, WNAM, WEUR, EEUR, APAC)│ └─────────┘ │└─────────────┘写入流程:Worker 通过 Binding 发起写请求 → D1 Primary 节点处理 → 返回结果。写入总是走 Primary 节点,保证强一致性。
读取流程:Worker 发起读请求 → D1 根据 Session 配置路由到最近的 Replica → 返回结果。使用 Sessions API 可以保证读到最新写入的数据(Sequential Consistency)。
D1 vs 其他数据库:到底差在哪?
| 对比项 | Cloudflare D1 | Supabase | PlanetScale | Neon |
|---|---|---|---|---|
| 免费存储 | 5GB | 500MB | 5GB | 512MB |
| 免费读操作 | 500 万行/天 | 无限(有限制) | 10 亿行/月 | 191 小时/月 |
| 免费写操作 | 10 万行/天 | 无限(有限制) | 5000 万行/月 | 191 小时/月 |
| 出口费 | 免费 | 5GB 后收费 | 免费 | 免费 |
| 数据库类型 | SQLite | PostgreSQL | MySQL(兼容) | PostgreSQL |
| 全球分布 | 自动读副本 | 单区域 | 多区域 | 多区域 |
| Workers 集成 | 原生 Binding | 需连接字符串 | 需连接字符串 | 需连接字符串 |
| 付费起步 | $5/月(Workers) | $25/月 | $29/月 | $19/月 |
关键差异:
- 1. 免费额度最大方。D1 的 5GB 免费存储是 Supabase 的 10 倍、Neon 的 10 倍。读写操作的免费额度也远超同行。
- 2. 零出口费。D1 的数据传出完全免费,而 Supabase 超出 5GB 后要收 $0.09/GB。
- 3. SQLite vs PostgreSQL/MySQL。D1 用的是 SQLite 语法,对独立开发者来说更轻量。不需要管理用户、权限、Schema 这些复杂的东西。如果你的场景不需要存储过程、触发器、JSONB 高级查询,SQLite 完全够用。
- 4. 原生 Workers 集成。不需要连接字符串、不需要连接池、不需要 VPC。一个 Binding 直接调用,延迟最低。
- 5. 全球读副本免费。D1 的全球读副本不额外收费,而 PlanetScale 的多区域要额外付费。
免费额度 & 定价
免费额度(Workers Free 计划)
| 项目 | 免费额度 |
|---|---|
| 存储 | 5 GB |
| 读操作 | 500 万行/天 |
| 写操作 | 10 万行/天 |
| 数据库数量 | 10 个 |
| 单库最大容量 | 500 MB |
| Time Travel(PITR) | 7 天 |
| 单次 Worker 查询数 | 50 次 |
付费价格(Workers Paid,$5/月基础费)
| 项目 | 包含额度 | 超出价格 |
|---|---|---|
| 存储 | 5 GB | $0.75/GB-月 |
| 读操作 | 250 亿行/月 | $0.001/百万行 |
| 写操作 | 5000 万行/月 | $1.00/百万行 |
| 数据库数量 | 50,000 个 | 可申请增加 |
| 单库最大容量 | 10 GB | — |
| Time Travel(PITR) | 30 天 | — |
| 单次 Worker 查询数 | 1000 次 | — |
| 出口流量 | 免费 | 免费 |
关键点
- • 读写按行计费:不是按查询次数,是按扫描/影响的行数。一个
SELECT * FROM users如果返回 100 行,就算 100 行读。 - • 写入比读取贵 1000 倍:写操作 $1.00/百万行 vs 读操作 $0.001/百万行,所以要控制写入频率。
- • 免费额度永久有效:不像 Supabase 的免费额度 12 个月后过期。
- • 存储按月计费:不是按天,是按整月的平均使用量。

5 分钟快速开始
方式一:Dashboard 创建
- 1. 登录 Cloudflare Dashboard[1]
- 2. 左侧菜单选择 Storage & databases > D1
- 3. 点击 Create database
- 4. 输入数据库名称,选择地区
- 5. 完成
方式二:Wrangler CLI
# 安装 wrangler(如果没有)npm install -g wrangler# 登录wrangler login# 创建数据库wrangler d1 create my-database# 输出类似:# ✅ Successfully created DB 'my-database'# [[d1_databases]]# binding = "DB"# database_name = "my-database"# database_id = "xxxx-xxxx-xxxx"# 执行 SQLwrangler d1 execute my-database --command "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)"# 执行 SQL 文件wrangler d1 execute my-database --file=./schema.sql# 查询数据wrangler d1 execute my-database --command "SELECT * FROM users"方式三:在 Dashboard 中直接操作
创建数据库后,可以在 Dashboard 的 D1 页面直接执行 SQL、查看数据、导出备份。适合快速调试。

Workers 集成
D1 和 Workers 是天然搭档。通过 Binding,你可以在 Worker 中直接执行 SQL,零配置、低延迟。
配置 Binding
在 wrangler.toml 中添加:
[[d1_databases]]binding = "DB"database_name = "my-database"database_id = "xxxx-xxxx-xxxx"或在 wrangler.jsonc 中:
{ "d1_databases": [ { "binding": "DB", "database_name": "my-database", "database_id": "xxxx-xxxx-xxxx" } ]}基础用法
interface Env { DB: D1Database;}export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { const url = new URL(request.url); // 查询所有用户 if (request.method === "GET" && url.pathname === "/api/users") { const { results } = await env.DB.prepare( "SELECT * FROM users ORDER BY id DESC" ).all(); return Response.json(results); } // 查询单个用户 if (request.method === "GET" && url.pathname.startsWith("/api/users/")) { const id = url.pathname.slice(12); const user = await env.DB.prepare( "SELECT * FROM users WHERE id = ?" ).bind(id).first(); if (!user) return new Response("Not Found", { status: 404 }); return Response.json(user); } // 创建用户 if (request.method === "POST" && url.pathname === "/api/users") { const { name, email } = await request.json() as { name: string; email: string }; const { success, meta } = await env.DB.prepare( "INSERT INTO users (name, email) VALUES (?, ?)" ).bind(name, email).run(); return Response.json({ success, id: meta.last_row_id }); } // 更新用户 if (request.method === "PUT" && url.pathname.startsWith("/api/users/")) { const id = url.pathname.slice(12); const { name, email } = await request.json() as { name: string; email: string }; await env.DB.prepare( "UPDATE users SET name = ?, email = ? WHERE id = ?" ).bind(name, email, id).run(); return Response.json({ success: true }); } // 删除用户 if (request.method === "DELETE" && url.pathname.startsWith("/api/users/")) { const id = url.pathname.slice(12); await env.DB.prepare("DELETE FROM users WHERE id = ?").bind(id).run(); return Response.json({ success: true }); } return new Response("Not Found", { status: 404 }); },}; Batch 批量操作
一次 Worker 调用中执行多条 SQL,减少往返次数:
const results = await env.DB.batch([ env.DB.prepare("INSERT INTO users (name) VALUES (?)").bind("Alice"), env.DB.prepare("INSERT INTO users (name) VALUES (?)").bind("Bob"), env.DB.prepare("INSERT INTO logs (action) VALUES (?)").bind("created_users"),]);TypeScript 类型支持
D1 支持泛型,查询结果可以自动推导类型:
interface User { id: number; name: string; email: string; created_at: string;}const { results } = await env.DB.prepare( "SELECT * FROM users WHERE id = ?").bind(id).all();// results 的类型是 User[] 
实战场景
场景 1:用户系统
一个完整的用户注册、登录、信息管理:
interface Env { DB: D1Database;}export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { const url = new URL(request.url); // 注册 if (request.method === "POST" && url.pathname === "/api/register") { const { username, email, password } = await request.json() as { username: string; email: string; password: string; }; // 检查是否已存在 const existing = await env.DB.prepare( "SELECT id FROM users WHERE email = ?" ).bind(email).first(); if (existing) { return Response.json({ error: "Email already exists" }, { status: 409 }); } // 哈希密码(实际项目中用 bcrypt) const passwordHash = await hashPassword(password); const { meta } = await env.DB.prepare( "INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)" ).bind(username, email, passwordHash).run(); return Response.json({ success: true, id: meta.last_row_id }); } // 登录 if (request.method === "POST" && url.pathname === "/api/login") { const { email, password } = await request.json() as { email: string; password: string; }; const user = await env.DB.prepare( "SELECT id, username, password_hash FROM users WHERE email = ?" ).bind(email).first(); if (!user || !await verifyPassword(password, user.password_hash as string)) { return Response.json({ error: "Invalid credentials" }, { status: 401 }); } // 生成 token(实际项目中用 JWT) const token = await generateToken(user.id as number); return Response.json({ token, user: { id: user.id, username: user.username } }); } // 获取用户信息 if (request.method === "GET" && url.pathname === "/api/me") { const userId = await authenticate(request); if (!userId) return new Response("Unauthorized", { status: 401 }); const user = await env.DB.prepare( "SELECT id, username, email, created_at FROM users WHERE id = ?" ).bind(userId).first(); return Response.json(user); } return new Response("Not Found", { status: 404 }); },}; 对应的 Schema:
CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL, email TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now')));CREATE INDEX idx_users_email ON users(email);场景 2:博客系统
文章 + 评论 + 标签,典型的 CMS 场景:
interface Env { DB: D1Database;}export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { const url = new URL(request.url); // 发布文章 if (request.method === "POST" && url.pathname === "/api/posts") { const { title, content, tags } = await request.json() as { title: string; content: string; tags: string[]; }; const { meta } = await env.DB.prepare( "INSERT INTO posts (title, content, status) VALUES (?, ?, 'published')" ).bind(title, content).run(); const postId = meta.last_row_id; // 批量插入标签 if (tags.length > 0) { const stmts = tags.map((tag) => env.DB.prepare( "INSERT INTO post_tags (post_id, tag) VALUES (?, ?)" ).bind(postId, tag) ); await env.DB.batch(stmts); } return Response.json({ success: true, id: postId }); } // 获取文章列表(带分页) if (request.method === "GET" && url.pathname === "/api/posts") { const page = parseInt(url.searchParams.get("page") || "1"); const limit = parseInt(url.searchParams.get("limit") || "10"); const offset = (page - 1) * limit; const { results } = await env.DB.prepare( "SELECT id, title, created_at FROM posts WHERE status = 'published' ORDER BY created_at DESC LIMIT ? OFFSET ?" ).bind(limit, offset).all(); const total = await env.DB.prepare( "SELECT COUNT(*) as count FROM posts WHERE status = 'published'" ).first(); return Response.json({ posts: results, pagination: { page, limit, total: total?.count }, }); } // 获取文章详情(带标签) if (request.method === "GET" && url.pathname.startsWith("/api/posts/")) { const id = url.pathname.slice(11); const post = await env.DB.prepare( "SELECT * FROM posts WHERE id = ?" ).bind(id).first(); if (!post) return new Response("Not Found", { status: 404 }); const { results: tags } = await env.DB.prepare( "SELECT tag FROM post_tags WHERE post_id = ?" ).bind(id).all(); return Response.json({ ...post, tags: tags.map((t) => t.tag) }); } return new Response("Not Found", { status: 404 }); },}; 对应的 Schema:
CREATE TABLE posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT NOT NULL, status TEXT DEFAULT 'draft', created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')));CREATE TABLE post_tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, post_id INTEGER NOT NULL, tag TEXT NOT NULL, FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE);CREATE INDEX idx_posts_status ON posts(status);CREATE INDEX idx_post_tags_post_id ON post_tags(post_id);CREATE INDEX idx_post_tags_tag ON post_tags(tag);场景 3:Session 管理
D1 特别适合存储用户会话,配合 Workers 实现无状态认证:
interface Env { DB: D1Database;}export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { // 创建 Session async function createSession(userId: number): Promise { const token = crypto.randomUUID(); const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); await env.DB.prepare( "INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, ?)" ).bind(token, userId, expiresAt).run(); return token; } // 验证 Session async function getSession(token: string) { return await env.DB.prepare( "SELECT user_id, expires_at FROM sessions WHERE token = ? AND expires_at > datetime('now')" ).bind(token).first(); } // 清理过期 Session async function cleanExpiredSessions() { await env.DB.prepare( "DELETE FROM sessions WHERE expires_at < datetime('now')" ).run(); } // ... 处理请求 },}; 成本计算器
案例 1:个人博客
- • 100 篇文章,每篇 2000 字
- • 每天 5000 次页面访问
| 项目 | 用量 | 费用 |
|---|---|---|
| 存储 | 50MB | $0(免费额度内) |
| 读操作 | 50 万行/天 | $0(免费额度内) |
| 写操作 | 500 行/天 | $0(免费额度内) |
| 总计 | $0/月 |
案例 2:SaaS 后台
- • 1 万用户,每人 10 条记录
- • 每天 10 万次 API 请求
| 项目 | 用量 | 费用 |
|---|---|---|
| 存储 | 500MB | $0(免费额度内) |
| 读操作 | 200 万行/天 | $0(免费额度内) |
| 写操作 | 5 万行/天 | $0(免费额度内) |
| 总计 | $0/月 |
案例 3:中型应用
- • 10 万用户,每人 100 条记录
- • 每天 100 万次 API 请求
| 项目 | 用量 | 费用 |
|---|---|---|
| 存储 | 5GB | $0(免费额度内) |
| 读操作 | 5000 万行/月 | $0(免费额度内) |
| 写操作 | 100 万行/月 | $0(免费额度内) |
| 总计 | $0/月 |
对比 Supabase:同样场景,Supabase 免费套餐(500MB)早就超了,Pro 计划 $25/月起。
最佳实践 & 避坑指南
1. 用 Batch 减少往返
一次 Worker 调用中执行多条 SQL,用 DB.batch() 而不是多次 prepare().run():
// 好:一次 batchawait env.DB.batch([ env.DB.prepare("INSERT INTO users (name) VALUES (?)").bind("Alice"), env.DB.prepare("INSERT INTO logs (action) VALUES (?)").bind("user_created"),]);// 不好:多次调用await env.DB.prepare("INSERT INTO users (name) VALUES (?)").bind("Alice").run();await env.DB.prepare("INSERT INTO logs (action) VALUES (?)").bind("user_created").run();2. 用 .first() 只取一条
如果你只需要一条结果,用 .first() 而不是 .all():
// 好:只取一条const user = await env.DB.prepare( "SELECT * FROM users WHERE id = ?").bind(id).first();// 不好:取所有再取第一条const { results } = await env.DB.prepare( "SELECT * FROM users WHERE id = ?").bind(id).all();const user = results[0];3. 用 .raw() 减少序列化开销
如果你需要高性能,用 .raw() 获取原始数组而不是对象:
// 原始数组:[["Alice", "alice@example.com"], ["Bob", "bob@example.com"]]const rows = await env.DB.prepare("SELECT name, email FROM users").raw();4. 注意行数计费方式
D1 按扫描/影响的行数计费,不是按查询次数。一个全表扫描会消耗所有行的读额度:
// 好:带索引的精确查询await env.DB.prepare("SELECT * FROM users WHERE email = ?").bind(email).first();// 只消耗 1 行读// 不好:全表扫描await env.DB.prepare("SELECT * FROM users").all();// 消耗所有行的读5. 合理设计 Schema
- • 为常用查询字段添加索引
- • 避免在 D1 中存储大文件(最大 2MB/行),文件存 R2
- • 用
INTEGER而不是TEXT存储时间戳,查询更快
-- 好:索引 + 合理类型CREATE INDEX idx_posts_created_at ON posts(created_at);-- 不好:无索引 + TEXT 时间-- 每次 ORDER BY created_at 都是全表扫描6. 用 Sessions API 保证一致性
D1 有全球读副本,但读副本有延迟。如果你先写后读,要用 Sessions API:
// 创建 session,保证读到最新数据const session = env.DB.withSession("first-primary");// 写入await session.prepare("INSERT INTO users (name) VALUES (?)").bind("Alice").run();// 读取(保证读到刚才的写入)const user = await session.prepare("SELECT * FROM users WHERE name = ?").bind("Alice").first();7. 避坑
- • 不要在 D1 中存储文件:大文件(图片、视频)存 R2,D1 只存元数据
- • 注意免费额度的每日限制:500 万行读/天、10 万行写/天,超出会报错
- • 单库最大容量:免费 500MB,付费 10GB,超出需要拆分
- • D1 是单线程:每秒约 1000 次查询(假设每次 1ms),高并发场景要考虑缓存
- • 不要用 D1 做实时计数器:写入频率太高会快速消耗额度,用 KV 或 Durable Objects
相关资源
https://developers.cloudflare.com/d1/
引用链接
[1] Cloudflare Dashboard: https://dash.cloudflare.com/