Menu

SQLite 备份与恢复:.backup 与 VACUUM INTO 教程

如何安全地备份和恢复 SQLite 数据库:.backup 命令、VACUUM INTO、在线备份 API,以及为什么直接复制文件是错误的做法。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

为什么不能直接用 cp 复制文件

SQLite 数据库是单个文件,所以很容易想到直接复制文件来备份。有时这样可行,但很多时候不行。

可能出两种问题:

  • 你复制时,另一个连接正在写入。目标文件里就会留下一个只应用了一半的事务,打开时就是损坏的。
  • 数据库处于 WAL 模式(大多数现代应用的默认模式)。最近的更改保存在单独的 database.db-wal 文件中。只复制主文件,你就会悄无声息地丢失数据。

SQLite 为此提供了正规的工具。它们能稳妥地处理锁、WAL 中的内容和并发写入。请用它们,而不是 cp。

.backup 命令

在命令行工具中备份数据库,最快的方法是 .backup 点命令:

sqlite3 app.db
sqlite> .backup backup.db
sqlite> .quit

它把 app.db 的完整副本写入 backup.db。即使有其他进程正在读写数据库,它也能正常工作:备份 API 不会一次加一把大锁,而是加一系列小锁,增量地复制页,并重新复制在复制过程中被修改的页。

输出是一个完全可用的 SQLite 数据库,像打开其他数据库一样打开它:

sqlite3 backup.db
sqlite> .tables

你也可以用一条 shell 命令完成整个操作,大多数 cron 任务最后都是这个样子:

sqlite3 app.db ".backup '/var/backups/app-$(date +%Y%m%d).db'"

一个文件进,一个文件出。没有导出再导入的来回,也不需要解析 SQL,只是在存储层复制页。

用 VACUUM INTO 生成压缩副本

VACUUM INTO 是一个相关但不同的工具。它把一份全新构建的数据库副本写入新文件:

结果是同一个逻辑数据库,但从头重写过:每一页都排列紧凑,没有碎片,也没有删除行后留下的空闲页。这让备份文件尽可能小。

该选哪个:

  • .backup:日常的、频繁的备份。速度更快,能很好地配合并发写入,逐字节一致。
  • VACUUM INTO:定期快照,同时想得到一个整洁、体积最小的文件。因为要重写所有内容,所以速度较慢,而且整个过程中会对源数据库加写锁。

两者都会生成一个可以立即打开的有效 .db 文件。

在应用代码中使用在线备份 API

在应用里,你不会去调用 sqlite3 命令行。你会使用驱动提供的在线备份 API。在 Python 标准库的 sqlite3 中,它是 Connection.backup:

import sqlite3

source = sqlite3.connect("app.db")
dest = sqlite3.connect("backup.db")

with dest:
    source.backup(dest)

source.close()
dest.close()

backup 方法把页从 source 复制到 dest,其他连接可以照常工作。你还可以传入 pages= 分块复制,传入 progress= 接收回调。对于大型数据库,想限制复制速度或显示进度时,这很有用。

其他语言的大多数驱动都以类似的名称提供同一套 C API(sqlite3_backup_init、_step、_finish)。流程总是一样的:打开源数据库,打开目标数据库,逐步复制页,结束。

数据库使用中的备份

这正是 SQLite 默默出彩的地方。.backup 和在线备份 API 都是为热备份设计的:源数据库在整个过程中可以一直处于打开和使用状态。

实际发生的过程:

  1. 备份获取一个共享锁,开始复制页。
  2. 如果写入方修改了一个还没被复制的页,备份会察觉到并重新读取它。
  3. 当每一页都一致时,复制就完成了。

你不需要停掉应用、断开连接或安排停机时间。在繁忙的数据库上,备份可能需要多几轮才能收敛,但它一定会收敛。最终得到的目标文件代表某一时刻的一致快照。

有一点需要知道:如果你使用 WAL 模式,请偶尔运行一次 PRAGMA wal_checkpoint(TRUNCATE);,避免 WAL 文件无限增长。备份本身会正确处理 WAL,这只是 WAL 的日常维护。

从备份恢复

恢复 SQLite 数据库出奇地简单,这正是它的好处。备份文件本身就是一个数据库。要使用它,直接打开就行:

sqlite3 backup.db
sqlite> SELECT COUNT(*) FROM notes;

要覆盖恢复一个正在使用的数据库,比如在数据丢失后进行恢复,安全的步骤是:

  1. 停止所有打开了该数据库的进程。
  2. 删除现有的 app.db、app.db-wal 和 app.db-shm 文件。旧数据库残留的 WAL/SHM 文件和恢复后的主文件放在一起,会让 SQLite 出错。
  3. 把备份复制到位:cp backup.db app.db。
  4. 重新启动应用。

-wal 和 -shm 文件很关键。如果跳过第 2 步,SQLite 可能会在恢复后的主文件上应用一个过时的 WAL,结果要么是数据损坏,要么是数据被奇怪地混在一起。

在命令行工具中,还有一个 .restore 命令,它是 .backup 的镜像:

sqlite3 app.db
sqlite> .restore backup.db
sqlite> .quit

它用 backup.db 的内容覆盖当前连接的数据库的内容。它反向使用同一套在线备份 API。

.dump 是另一种工具

在较老的教程里你会看到 .dump。它不是同一意义上的备份,它生成的是一个包含 CREATE 和 INSERT 语句的 SQL 文本文件:

sqlite3 app.db .dump > app.sql

恢复时,重新执行这些 SQL:

sqlite3 new.db < app.sql

它适合在 SQLite 版本之间迁移、在 git 中比较表结构差异,或者把数据迁移到其他数据库引擎。它比 .backup 更慢、文件更大,而且可能丢失一些信息(自定义排序规则、生成列和某些 pragma 可能需要额外处理)。要真正备份一个正在使用的数据库,优先用 .backup 或 VACUUM INTO。

一套合理的备份方案

对大多数应用来说,下面的组合效果很好:

  • 定时运行 .backup:每小时、每天,取决于你能承受丢失多少数据。开销小、速度快、可热备份。
  • 每周用 VACUUM INTO 备份到另一个路径。它能发现差异,给你一份压缩过的快照,还走了一条不同的代码路径。
  • 保留策略:保留最近 N 份每日备份和最近 M 份每周备份。SQLite 数据库压缩率很高,所以之后再执行 gzip backup.db 很值得。
  • 偶尔恢复一份备份,并对它运行几条查询。没有测试过的备份只是一种希望,算不上备份。
# Daily, in cron:
sqlite3 /var/lib/app/app.db ".backup '/var/backups/app-$(date +%F).db'"
gzip "/var/backups/app-$(date +%F).db"

# Weekly:
sqlite3 /var/lib/app/app.db "VACUUM INTO '/var/backups/app-weekly-$(date +%F).db'"

这两条命令在应用处理请求时运行都是安全的。

下一步:PRAGMA 设置

备份是一个运维问题,调整运行时行为是另一个。SQLite 通过 PRAGMA 语句暴露它的各种开关:日志模式、同步级别、缓存大小、外键约束检查。下一页会逐一讲解值得了解的那些。

常见问题

如何备份 SQLite 数据库?

在命令行工具中,连接到源数据库后运行 .backup path/to/backup.db。在应用代码中,使用在线备份 API(C 语言中是 sqlite3_backup_init,或者你所用语言驱动中的对应方法)。即使有其他连接正在写入,两种方式都能生成一份一致的副本。

可以直接复制 .db 文件作为备份吗?

只有在你确定没有任何进程以写入方式打开数据库时才可以。否则你可能在事务进行到一半时复制文件,得到一份损坏的备份,或者漏掉还在 WAL 文件中的数据。请改用 .backup 或 VACUUM INTO,它们会正确处理锁和 WAL 中的内容。

.backup 和 VACUUM INTO 有什么区别?

.backup 使用在线备份 API,生成逐字节一致的副本,包括未使用的页。VACUUM INTO 'file.db' 写出一份重新压缩过的副本:更小、没有碎片,但它会重写每一页。日常备份用 .backup,同时想回收空间时用 VACUUM INTO。

如何从备份文件恢复 SQLite 数据库?

如果备份是一个 .db 文件,直接打开它就行,SQLite 数据库就是单个文件。要覆盖恢复一个现有的数据库,先停止应用,替换文件(并删除残留的 -wal/-shm 文件),然后重新打开。在命令行工具中,你也可以连接到一个新数据库后运行 .restore path/to/backup.db。

Coddy 编程语言插图

和 Coddy 一起学编程

开始