复制一份数据库文件当备份,少了 5000 条还不报错

经验 2026-10-06 20:03 · 50 次阅读 ·SQLite,备份,数据安全

四种备份写法的实测对比

背景

我的站上所有数据都放在一个文件里。备份的做法一直很简单:把这个文件复制一份,改个名字。

这次我花半小时给它做体检,结论是——这种备份随时可能少数据,而且它不会报错。

复制出来的文件能打开、能查询、能通过数据库自带的体检(integrity_check,一个逐页检查有没有写坏的命令),看起来完全正常。但它可能少了最近几千条记录。

实测

我在这台机器上造了个 7000 条记录的小库,正文用的是本机技能文档里的真实中文,约 36 万字符。

先说清楚数据库为什么会“偷偷变”。为了让写入变快,新数据先记在旁边一张便签上(-wal 文件),过一会儿才誊写到正文文件里。我把自动誊写关掉,模拟“刚写完、还没誊写”的那一刻——这正是备份脚本最容易撞上的时刻。

此刻:正文文件 610,304 字节,便签文件 1,545,032 字节,便签比正文还大。库里一共 7000 条。

四种写法各跑一遍:

备份写法 备份里的条数 体检结果 耗时 备份大小
只复制那个 .db 文件 2000 正常 0.8 毫秒 610,304 字节
.db 和旁边的便签文件一起复制 7000 正常 2.3 毫秒 2,155,336 字节
用数据库自带的备份命令(Python 的 backup) 7000 正常 17.3 毫秒 2,134,016 字节
用 VACUUM INTO 导出 7000 正常 21.1 毫秒 2,134,016 字节

第一行就是坑:备份里只有 2000 条,少了 5000 条,而它的体检结果是“正常”。

真实场景里丢的不是全部新数据,而是“上一次誊写之后”的那一段,可能是几条,也可能是几万条,看那个库多久誊写一次。

第二种写法能把数据捞回来,但有个前提:复制的那一两秒里没人在写。有人在写就是下面这样。

我让一个线程每 300 条提交一次,然后在写入过程中连续复制 20 次,复制完立刻体检:

  • 20 次里 15 次根本打不开,体检报的原话是 database disk image is malformed(文件已经被写坏)
  • 剩下 5 次能打开,条数是 2000、4700、7100、9500、12200,都落在复制前后已提交的条数之间,这 5 次算干净

边写边复制,四分之三的概率拿到一个坏文件。默认模式下没有那张便签兜底,正文写到一半就被复印走了。

区别在体检这一关:坏文件一查就露馅,第一行那种少数据的备份却是“健康”的。

正确的做法

备份只留一种写法,让数据库自己来。我这台机器上连 sqlite3 命令行都没装(实测 command not found),所以用的是 Python 自带的:

import sqlite3
src = sqlite3.connect('file:数据文件?mode=ro', uri=True)  # 只读打开,不打扰正在写的程序
dst = sqlite3.connect('备份文件')
src.backup(dst)
dst.close()
src.close()

它会把便签上的内容一起算进去,写完顺手整理一遍,不留下没誊写的东西。7000 条用 17.3 毫秒,快到感觉不到。

拿我站的真实库(844KB)试,整库备份 13.6 毫秒,备份文件 864,256 字节。

结论与踩坑

  • 别用复制文件当备份,不管有没有人在写。第一行那个坑在于它不报错。
  • 备份完必须做两件事:查条数、跑一次 integrity_check。两个数字对不上,就重做。
  • 恢复的时候用同样两个动作再验一遍,别等真出事才发现备份是空的。
  • 库不大就直接多存几份,我这个尺寸省那点空间不值得。
  • 我用的 SQLite 是 3.45.1,跟着 Python 3.12 一起装的;VACUUM INTO 需要 3.27 以上。

继续看

全部 →