最近在整理一个内部工具的数据存储方案时,我重新把目光放回了 SQLite。说实话,过去很长一段时间,我和不少后端工程师一样,提起 SQLite 的第一反应是“这不是移动端、桌面工具或者小项目才用的数据库吗?”直到我认真看了一遍它的事务能力、WAL 模式、单文件数据格式,又注意到 Turso 这类团队围绕 libSQL 做的一系列分布式探索,才意识到一个被我忽略很久的事实:SQLite 不是不强,而是我们对它的认知,还停留在它最不起眼的那些使用场景上。
这篇文章不打算复述文档,而是想聊清楚一个判断:SQLite 真正的威力,不在某个极端性能数字,而在于它重新定义了“数据库”在一个应用里的存在方式。它可以是嵌入进程的文件,可以被复制、备份、分发,也可以通过 Turso 这类方案变成部署在边缘节点的数据库服务。这种灵活性,才是它比很多重型数据库更值得重新学一遍的原因。
1. SQLite 被低估,不是性能问题,而是认知问题
1.1 很多人觉得它是“玩具数据库”,是因为只见过它的玩具用法
我见过不少开发者对 SQLite 的刻板印象,基本来源于这几类场景:浏览器用它存本地数据,移动 App 用它做客户端缓存,某个小工具用单文件保存配置。这些场景都有一个共同特征:数据量不大,并发不高,不需要独立数据库服务。于是大家很容易得出一个结论——SQLite 只是一个“轻量级方案”,等系统变复杂,就一定要换 PostgreSQL 或 MySQL。
这个结论的问题在于,它把“SQLite 经常出现在轻量场景”误解成“SQLite 只能做轻量场景”。实际上,SQLite 的架构和 MySQL、PostgreSQL 有本质区别。后者是典型的服务器/客户端模型:一个独立数据库进程负责管理数据,应用通过网络协议连接它,连接需要账号、权限、端口,数据被保存在一个由数据库服务器管理的目录或存储引擎里。SQLite 完全不同,它是一个嵌入式数据库,数据库引擎直接链接进应用进程,应用读写的是一个普通磁盘文件。
这个架构上的差异,直接决定了我们看待它的方式。不是“弱”,而是“结构不同”。服务器型数据库的优势是集中管理、网络访问、多进程并发;SQLite 的优势是零配置、零网络、文件即数据库。这两者不是高下之分,而是适用模型不同。
在我自己的开发经验里,一件事让我对 SQLite 的印象彻底改观:有一个内部工具需要保存大约几万条任务记录,数据量不大,但需要事务、索引、条件查询。最初我用 PostgreSQL 起了一个 Docker 容器做存储,每次换环境都要重新配置数据卷和账号,颇为繁琐。后来同事建议直接改用 SQLite,我花了几分钟把代码里连接数据库那段逻辑换成了sqlite3.connect("tasks.db"),结果所有功能原样跑通,还少了一个需要持续维护的容器服务。
这个体验让我意识到:SQLite 不是做不到复杂查询,而是它的复杂度被隐藏在了“文件”这个极简概念里。
1.2 它真正改变的是数据库的“所有权模型”
服务器型数据库在工作中承担的角色,更像是托管服务。你需要考虑部署、启动、连接池、迁移、备份、监控、权限,这些工作本身会占用大量精力。SQLite 则不同,它把数据库引擎和你的应用打包在一起,数据就是一个文件,跟随你的项目走。备份可以是复制文件,迁移可以是替换文件,测试环境可以直接用一个临时文件。
这种“所有权模型”的变化,其实比性能数字更值得关注。
为什么?因为对绝大多数中小型应用和内部工具来说,真正的瓶颈从来不是查询速度,而是管理复杂度。很多团队里,数据库本身就是一个需要长期维护的组件。如果有一种方案能让“数据库”退化成项目里的一个文件,开发、测试、交付、部署的整个链条都会被简化。
当然,它也有代价。进程内数据库意味着数据库不再是一个独立的网络服务,你的应用没法像访问 PostgreSQL 那样,从多个进程、多台机器同时连接同一个库,然后进行高并发写入。写入锁机制决定了它的并发写入能力有上限。但这不等于“不可用”,只是要求我们根据场景来做选择。
换个视角看:服务器数据库把数据集中到一个“服务”里,SQLite 把数据分发到各个“进程”旁边。前者适合集中管控,后者适合边云协同、本地优先、边缘计算这类新场景。
2. 单跑通不算什么,真正麻烦的是批量、异常和长期维护
2.1 从“本地文件”到“生产数据”,中间还隔着几个工程问题
很多初学者在一个 Python 脚本里用sqlite3.connect("app.db")建好表、插入数据、查询成功,就认为 SQLite 已经学会了。这个阶段其实只完成了“最小可行验证”。单次跑通,只能说明流程没有断。真正决定它能不能放进生产环境的,是这些问题:
- 多进程同时读写同一个数据库文件时,到底会发生什么?
- 数据库连接异常中断后,文件会不会损坏?
- 需要从备份恢复时,单纯复制
.db文件是否足够? - 数据量从几万行涨到几千万行,索引和查询计划还合理吗?
- 如果项目上线后需要持续运维,有没有日志、监控和迁移手段?
这些都不是“SQLite 能不能做到”的问题,而是“你有没有按生产标准去使用它”的问题。
举个例子,WAL 模式。默认情况下,SQLite 的回滚日志模式可能会让并发读写互相堵塞。开启 WAL(Write-Ahead Logging)后,读写并发能力会有明显提升,这也是很多生产环境推荐开启的模式。但 WAL 模式不是没有代价,它会额外生成-wal和-shm文件。如果不了解这个机制,备份时只复制主文件,很可能丢掉最近未 checkpoint 的事务。
我在一个项目里就踩过类似的坑。当时直接写了个 cron 任务,把data.db当作普通文件复制到备份目录。后来数据库崩溃恢复时才发现,备份文件里缺少了最近一段时间的数据,原因正是 WAL 文件没有一起备份。从那以后,我养成了用sqlite3的.backup命令或在线备份 API 进行一致性备份的习惯。
2.2 它不是“不能做运维”,而是要用文件思维而不是服务思维做运维
传统的 PostgreSQL 运维里有几个固定动作:数据目录、日志、归档、复制、监控。SQLite 运维也有自己的固定动作,只是思路完全不同。
比如备份。冷备很简单,确保没有写入,然后复制文件。热备需要借助 SQLite 提供的在线备份机制,不能天真地边写边复制。恢复也不只是把文件放回去,还要检查主文件和-wal文件是否配对,是否需要先做一次 checkpoint。
迁移也一样。很多人的第一反应是“SQLite 没有像 Flyway 或 Alembic 那样的迁移管理机制”。实际上,SQLite 完全可以通过 Schema 版本表来管理迁移,也可以在连接后执行PRAGMA user_version来跟踪版本。你在 PostgreSQL 里养成的“迁移脚本 + 版本号”方法论,搬到 SQLite 依然有效。
长期维护还需要关注几点:
- 数据库膨胀:频繁更新和删除会产生空闲页,需要定期执行
VACUUM或自动auto_vacuum。 - 连接超时:多进程争用数据库锁时,
busy_timeout设置太短会导致操作提前失败。 - 数据库完整性:可以用
PRAGMA integrity_check做周期性检查。 - 查询计划:对于复杂 SQL,使用
EXPLAIN QUERY PLAN分析索引命中情况,和 PostgreSQL 的EXPLAIN逻辑类似。
很多人觉得 SQLite 不够“工程化”,其实不是它不能工程化,而是我们常常用“复制粘贴文件”的姿势,来对待一个需要按数据管理规范来用的引擎。
3. Turso 和 libSQL 在补哪块拼图:让 SQLite 重新回到网络世界
3.1 SQLite 长期以来缺席的那块拼图:远程访问与分布式同步
SQLite 作为嵌入式数据库有一个天然短板:它本身不是一个网络服务。如果你的应用部署在多台机器上,或者你的用户分布在不同区域,简单的单文件模型就不够用了。
传统数据库把数据集中在数据中心,应用通过 API 远程访问。SQLite 想让“文件”保留在应用本地,但应用又需要共享数据,这两者之间缺少一层同步和分发机制。这正是 Turso 这类项目在尝试补齐的部分。
Turso 的思路很直接:保留 SQLite 的开发体感,同时把同步、副本、托管这些服务端能力补上。它基于 SQLite 的一个分支 libSQL 来做扩展,把 SQLite 变成一个可以被托管的数据库平台,再通过嵌入式副本机制把数据分发到更靠近用户的位置。这样应用的读取操作可以在本地完成,写入则通过主库或集群来协调。
这个方案特别适合一类常见场景:读多写少、需要在多个地理位置提供低延迟读取、但又不想引入一套庞大分布式数据库系统的应用。内容站点的元数据、配置数据、用户个性化设置、边缘函数的数据缓存,都是这类场景。
3.2 分布式 SQLite 的几种不同尝试
围绕“让 SQLite 支持分布式运行”这件事,业界的尝试不止 Turso 一家。我了解到的方向大致有几类:
- 把 SQLite 作为主存储,再加一层同步层,多个节点可以拥有数据副本。
- 通过嵌入式副本,让每个节点都保有一份最近的数据,读取走本地,写入同步回主库。
- 在 libSQL 分支上扩展新特性,例如更完善的远程协议、更灵活的权限模型。
- 与无服务器平台结合,把数据库按“每个请求、每个用户、每个租户”粒度分发到边缘函数附近。
这些方向都说明一件事:SQLite 的“文件”属性,一旦加上网络同步,会变成一个很有意思的数据分发模型。它不再像一个独立的数据库服务器,而更像“数据集散中心”。
3.3 但这不等于所有人都应该立刻迁移过去
我对 Turso、libSQL 这类方案保持谨慎乐观。原因是:分布式数据库的复杂度不会消失,只会转移。传统集中式数据库把同步、冲突处理、一致性收敛放在一层;分布式 SQLite 方案把这些逻辑分布到各个节点和客户端。如果应用本身就是单机写入,多节点只读,这套模型会很舒服。但如果应用需要多节点同时写、需要强一致事务、需要复杂冲突解决,那就得认真评估它的成熟度。
从工程经验看,引入这类方案前最好先确认几个问题:
- 同步延迟是什么量级,能否满足业务需求?
- 主库故障时,写入行为的降级策略是什么?
- 不同副本之间出现冲突时,如何解决?
- 整个系统的可观测性工具链是否完善?
不要一上来就把核心生产库迁移到一个新模型上,可以先在一个边缘业务或内部工具上做灰度验证。
注意:如果你在评估 libSQL 或 Turso 时,发现团队的 SQLite 基础不够扎实,建议先在本地单机场景把事务、WAL、备份、迁移这些基本功练熟。分布式模型会放大基础概念的理解成本,而不是替代它。
4. 什么场景适合 SQLite,什么场景不适合:先看写入模式和部署位置
4.1 一个判断框架,三个问题
很多人的数据库选型还停留在“性能对比”上,比如“SQLite 每秒能写多少条,PostgreSQL 每秒能写多少条”。但对实际工程来说,更重要的不是跑分,而是匹配度。我会用一个更简单的问题集来判断:
第一个问题:你的应用是否需要多个进程同时写同一个数据库?
如果答案是否,SQLite 很可能合适。比如单机 Web 服务、桌面应用、CLI 工具、嵌入式设备、异步任务处理器里的状态存储,都只由一个进程写入。
如果答案是是,就需要小心。SQLite 支持多进程读写,但并发写入的锁竞争会更明显,性能上限也更低。更合适的选择可能是 PostgreSQL,或者考虑在 SQLite 之上增加一层同步调度。
第二个问题:你的数据是否需要跨区域、跨数据中心的低延迟访问?
如果数据只在一个区域,或者只在一个应用实例内部,SQLite 的单文件模型非常高效。但如果用户分布在不同地域,又希望每个地域都能快速读取数据,传统 SQLite 就不够了。这时可以考虑 Turso 这类带嵌入式副本的方案,或者干脆用全球多区域部署的托管数据库。
第三个问题:你的团队是否有数据库运维能力?
这听起来有点反直觉,因为 SQLite 的卖点是零运维。但真实情况是,团队如果没有认真理解事务、锁、备份、迁移,SQLite 反而容易被用得一团糟。而 PostgreSQL 这类系统虽然运维复杂,但周边工具链成熟,社区资料丰富,遇到问题更容易找到答案。
三个问题下来,往往能得出比较清晰的选择。
4.2 适合 SQLite 的场景:优先考虑独立进程与本地优先
从实际经验看,下面这几类场景很适合用 SQLite:
- 桌面应用或客户端应用的本地存储。
- 移动 App 的离线缓存。
- 内部工具和管理后台。
- 单机部署的 Web 应用(非多实例水平扩展)。
- CI/CD 流程里的数据存储。
- 数据分析或数据处理流水线中的中间状态。
- 边缘函数和嵌入式设备里的数据管理。
- 本地优先(local-first)应用的默认存储。
这些场景的共同特点是:应用本身就是数据的所有者,不需要外部进程频繁通过网络连入,数据规模通常可以从 GB 到几十 GB,查询模式相对清晰。
4.3 不适合 SQLite 的场景:多写节点是最大红线
反过来,这些场景要谨慎使用 SQLite:
- 高并发写入的 SaaS 应用,尤其是写入远多于读取的系统。
- 多实例部署到同一数据库文件(在共享磁盘上的跨机器访问)。
- 需要复杂多租户隔离、细粒度权限控制的系统。
- 需要跨区域强一致写入的事务型系统。
- 数据量增长极快,需要频繁水平扩容的系统。
- 依赖数据库提供高级高可用和自动故障转移机制的场景。
尤其要注意,很多人会把 SQLite 和 PostgreSQL 做“二选一”的对立。这不是好的选型逻辑。更合理的做法是:在一个系统内部针对不同模块选择不同数据库,这种多数据库架构在工程界已经很常见了。
4.4 即使选 SQLite,也要按生产标准配置
如果决定在项目里正式使用 SQLite,下面这些参数值得一开始就确认:
journal_mode=WAL:提高并发读写能力。synchronous=NORMAL:在 WAL 模式下兼顾安全与性能。busy_timeout:设置合理的锁等待时间,避免直接报错。foreign_keys=ON:SQLite 默认不开启外键约束,需要手动打开。
以下是一个常见的连接初始化示例:
import sqlite3 conn = sqlite3.connect("app.db") conn.execute("PRAGMA journal_mode=WAL;") conn.execute("PRAGMA synchronous=NORMAL;") conn.execute("PRAGMA busy_timeout=5000;") conn.execute("PRAGMA foreign_keys=ON;")这些配置不是魔法,但能避免很多新手容易撞上的坑。
5. 从尝鲜到落地:一条可以今天就执行的验证路径
5.1 工具链准备:CLI、图形客户端和一个顺手的数据浏览器
先用官方命令行工具sqlite3做快速验证。它几乎不需要安装,macOS、Linux 都自带或通过包管理器轻松获得。Windows 用户可以安装官方预编译二进制,也可以下载像 DB Browser for SQLite 这样的图形化客户端。
DB Browser for SQLite 的最大价值,是让你直观地看到表结构、数据分布、索引和查询结果。它对中文环境也很友好,很多教程里也把它作为入门工具。不过我的建议是,图形客户端可以辅助,但命令行一定要会用,因为服务器上没有图形界面。
常用命令不需要记住太多:
sqlite3 app.db ".tables" sqlite3 app.db ".schema tasks" sqlite3 app.db "SELECT * FROM tasks LIMIT 10;" sqlite3 app.db ".backup backup.db"这个.backup命令就是一致性备份的稳妥入口,比直接复制文件可靠得多。
5.2 最小验证:用一段代码跑通从建库到查询
如果你还在犹豫要不要在项目里用 SQLite,可以从一个 Python 脚本开始验证。Python 自带sqlite3模块,不需要额外装数据库服务。
import sqlite3 conn = sqlite3.connect("demo.db") cur = conn.cursor() cur.execute(""" CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY, title TEXT NOT NULL, done INTEGER DEFAULT 0 ) """) cur.execute("INSERT INTO tasks (title) VALUES (?)", ("写一篇关于 SQLite 的博客",)) conn.commit() rows = cur.execute("SELECT id, title, done FROM tasks").fetchall() print(rows) conn.close()这段代码完成后,你会在当前目录看到一个demo.db文件。用命令行打开它,你能看到刚才创建的表和数据。
这一步的收获不是“我会用 SQLite 存数据了”,而是建立起一个体感:数据库从“需要启动一个服务”变成了“连接一个文件”。
5.3 体感测试:WAL 模式到底改变了什么
想理解 WAL 的价值,可以做一个很简单的实验:在一个 Python 进程里开启 WAL 模式,然后并发地读写同一张表。你不需要写得特别复杂,关键是在有写入的情况下观察读取是否能够进行。
更实际的做法是,查看目录下的文件变化:
ls -la demo.db*开启 WAL 后,你会看到demo.db-wal和demo.db-shm文件。这两个文件不是多余的,它们分别保存着尚未合并的写入日志和共享内存索引。了解这一点,对日后做好备份非常重要。
如果备份时只复制demo.db,而不处理-wal文件,恢复后可能丢失最近的写入。稳妥的做法是使用.backup命令,或者在应用层调用 SQLite 的在线备份接口。
提醒:任何时候做恢复演练,都不要假设“文件复制过去就能用”,先在测试环境验证一次从备份恢复到启动查询的完整流程。
5.4 一个能帮你摸清边界的小实验
想验证 SQLite 在你预期负载下表现如何?不要只靠文档,真实验证才算数。你可以写一个小脚本,模拟多个线程同时写入数据库,观察锁冲突和耗时。
预期现象通常是这样的:开启busy_timeout后,部分写入会等待锁释放;没有设置时,会直接抛database is locked。这个实验能让你直观理解 SQLite 的并发写入上限,比读十篇性能分析都管用。
这个实验的价值,不在于得到一个“并发数超过多少就不行”的精确数字,而在于帮你建立排查直觉:当你以后遇到 SQLite 的锁相关报错,你会立刻想到锁等待、busy_timeout、写事务持有时间这些变量。
5.5 如果 SQLite 表现不如预期,按这个顺序排查
我在实际使用中遇到过不少问题,大部分并非 SQLite 本身缺陷,而是使用姿势不对。建议排查顺序如下:
- 看现象:是报错、卡顿、数据丢失,还是查询变慢?
- 看文件状态:
-wal、-shm文件是否存在?是否缺少它们? - 看连接模式:是否有多个进程、多线程在同时写同一个库?
- 看锁等待:
busy_timeout是否设置过小? - 看事务边界:写入事务是否长时间未提交,导致其它操作一直等待?
- 看查询计划:是否缺少索引,全表扫描导致性能问题?
- 看数据完整性:用
PRAGMA integrity_check做一次完整性检查。 - 看工具链边界:使用的 SQLite 版本、分支(如 libSQL)是否与项目依赖兼容。
这套顺序能覆盖大多数问题。最后再考虑“要不要换数据库”,因为很多情况下,换成 PostgreSQL 只会让问题从“文件锁”变成“连接数”或“权限配置”,并不自动解决本质。
5.6 想试 Turso / libSQL?先把回滚方案想好
如果被边缘部署、嵌入式副本这些概念吸引,想尝试 Turso 或 libSQL,我的建议是:先做小规模试点,不要直接把核心库迁过去。
具体可以这样安排:
- 先在本地跑通一个基于 libSQL 的最小应用,确认驱动和接口和上游 SQLite 的兼容性。
- 再通过云平台或自托管服务创建一个实例,跑一遍远程连接、同步、读取的流程。
- 把应用里数据访问层抽象出来,让底层可以从本地文件切换到远程连接,再切回来,这能显著降低风险。
- 对同步延迟、故障降级、数据冲突做一次真实演练,而不是只测正常路径。
这个过程中你会发现,SQLite 的“数据库即文件”哲学,和“数据库即服务”的托管哲学之间,需要你自己找到适合业务的那条线。
写在后面:重新学习 SQLite,其实是在重新思考“数据库应该长什么样”
如果把这次重新认识 SQLite 的收获沉淀成一句话,我会说:数据库选型不只是选引擎,更是在选一种所有权模型和工作流模型。
传统 PostgreSQL 代表了“集中式服务”模型:数据归一个中心管理,所有客户端通过协议访问它。SQLite 代表了“本地文件”模型:数据归应用本身所有,不依赖独立的数据库进程。Turso、libSQL 这类尝试则代表了“分布式本地优先”模型:保留文件的所有权体验,同时通过同步和副本重新接入网络世界。
这三种模型各有适用场景,不是越强越好,而是越匹配越好。
下一次当你需要给一个新应用选存储时,不要条件反射地启动一个数据库容器。先问自己:这个数据是单机所有,还是需要跨地域共享?写入是低频内部操作,还是高并发在线请求?有没有一个场景,可以让数据跟随应用走,而不是让应用绕着数据库转?
在这些问题没有想清楚之前,SQLite 至少是值得你认真尝试的选项。它可能不会给你带来极致性能,但会给你一种久违的自由:数据库不再是需要精心伺候的服务,而是你项目里一个可以被理解、被复制、被验证的文件。