# SQLite到底能不能支撑一个小型网站？

URL: https://caijiao.org/posts/123
Source: docs/posts/123.md
Description: 开了 WAL 和 busy_timeout 之后，SQLite 的读几乎不成为瓶颈，真正的天花板是写入并发——写操作始终是单写者。每天五千次写只占用锁不到百分之一，Cakedesk 把它装进用户本机，一次性 €69 卖了 239 份。

先给配置，因为绝大多数"SQLite 不行"的抱怨，最后都能追溯到这两行没设：

```sql
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
```

默认的回滚日志模式下，写操作会锁住整个库，读者全部被挡在外面。切到 WAL（Write-Ahead Logging）之后，读不阻塞写、写不阻塞读，这是完全不同的两种表现。

`busy_timeout` 解决的是另一件事：拿不到写锁时，默认行为是立刻抛 `SQLITE_BUSY`；设了 5000 毫秒，它会先等 5 秒再放弃。对小型站来说，这一等就把绝大多数"并发报错"消掉了。

能。但要清楚它卡在哪儿。

## 天花板不是数据量，是写入并发

SQLite 常被误以为"只能撑小数据量"，这是个误会。官方 limits 页面给的单个数据库文件上限是 281 TB，实际项目里没有谁先撞到这条线。

真正的约束在别处：**写操作始终只有一个写者**。所有 INSERT、UPDATE、DELETE 串行执行，一次一个。WAL 让读可以并发，写不行。

换算成可感知的量级：读请求随便几千 QPS，只要库文件在 SSD 上、命中页缓存，几乎没有瓶颈。写请求一旦形成持续排队——比如每秒几十次以上的写入且每次事务较长——才开始出问题。

小型站的写入量通常远低于这个数。一个工具站可能一天几百次写，一个带用户系统的 SaaS 可能每分钟几十次。这两个量级 SQLite 都能扛。

## 读多写少是它的主场，而小网站大多如此

Inch Calculator 2026 年 6 月的估算访问量是 459 万。这类站的请求构成极其偏读：用户打开页面、看结果、走，写入几乎为零（最多记一条统计）。

![Inch Calculator 官网首页截图](/images/2026-09/inchcalculator.com.png)

*Inch Calculator 首页写着 3500+ 个免费计算器，按汽车、建筑、烹饪等场景分卡陈列。*

Homewyse 是另一个例子，42.885 万访问，做的是装修报价——材料、人工、报价拆开算。这类计算型页面每次请求最多读几次配置表，不存在写压力。

对这种负载，SQLite 反而比独立数据库服务更合适：没有网络往返、没有连接池、没有独立进程要监控。一次查询的耗时就是一次本地函数调用。

## Cakedesk 把它推到了极端：数据库在用户电脑上

Max Schmitt 做的 Cakedesk 是个桌面端发票应用，Electron 加 React 加 Node.js，数据全部存在用户本机（SQLite 或等价的本地文件存储）。

一次性 €69，没有订阅。2024 年卖出约 170 份，2025 年卖出 239 份新授权，毛收入约 16000 欧元。它连"服务端数据库"这个概念都不存在——每个用户一个库，天然隔离，天然没有并发问题。

这个架构的代价也很明确：没有服务端就做不了跨用户的功能，多设备同步、团队协作都别想。但它证明了另一件事——**对很多小产品来说，"多个小库"比"一个大库"简单得多**。

## 算一笔具体的容量账

拿一个真实量级估算：一个小型站每天 5000 次写操作，平均分散在 8 个小时里，约合每秒 0.17 次。一次写事务在本地 SSD 上通常几毫秒完成。

也就是说，单写者那把锁的占用率不到百分之一。这个距离"写锁顶住"还差着两个数量级。

读的那一侧更宽松。WAL 模式下读者完全不被写阻塞，只要库文件在 SSD 上、热点页在缓存里，几千 QPS 是轻松的。Inch Calculator 2026 年 6 月 459 万访问、Homewyse 42.885 万访问，落在数据库上的读压力对本地文件来说毫无压力。

另外要注意读的性能和数据量的关系不大，和页缓存命中率关系很大——库文件比可用内存大很多之后，随机读会明显变慢。这时候加内存比换数据库划算。

真正会出问题的场景是另一个：一个长事务持锁不放——比如批量导入几万条数据没分批——这时所有其他写都会被挡住。解决办法不是换数据库，是把大事务拆成小批。

## 什么时候必须换掉它

四个信号，出现任意一个就该认真考虑独立数据库服务：

一是出现多机写入。两台以上服务器要写同一个库时，单文件方案失效——除非你把写全部收拢到一台机器上，但那台就变成了单点。

二是写并发持续顶住写锁。`busy_timeout` 已经设了但日志里仍频繁出现 `SQLITE_BUSY`，说明写入量超过了单写者的能力。

三是需要复杂的在线 DDL。SQLite 的 ALTER TABLE 支持有限，大改表结构时要走"建新表、搬数据、改表名"这套流程。

四是需要细粒度权限和审计。多个人要连同一个库、各自只看一部分数据时，独立数据库服务的账号体系是必需的。

在此之前换，等于提前背上一份没有回报的运维工作。

## 备份比想象的简单

一个文件的好处在这里彻底兑现。热备份一条命令：

```sql
VACUUM INTO 'backup-2026-09.db';
```

它在库正常服务的同时产出一个紧凑的完整副本。定时跑一次、把文件传到对象存储，一套备份方案就成了。

如果要求更细的恢复粒度（比如能回到任意一秒），可以上 Litestream 这类工具，把 WAL 的变更持续复制出去。部署时把库放在持久卷上，[Docker](/docker/) 的 volume 配置要单独写清楚——容器重建时数据不能跟着没了，这是最容易出事的一处。

## 默认从它起步，把换掉的条件写成指标

新项目默认从 [SQLite](/sqlite/) 起步，开 WAL 和 busy_timeout，配好 `VACUUM INTO` 的定时备份。把"什么时候换"写成可观测的指标——比如写事务的等待时间——而不是凭感觉判断。

Data Fetcher 是这条路走通的例子：Andy Cloak 一个人在伦敦运营，Airtable 扩展，600 个付费客户、MRR 23000 美元。一人公司的产品规模可以远超"小项目"这三个字，而底层的存储方案往往比你以为的保守得多。你的站现在每秒有几次写？
