土法炼钢兴趣小组的算法知识备份

【SQLite 内核】完整性检查与损坏恢复:integrity_check 与修复边界

文章导航

分类入口
databasestorage
标签入口
#sqlite#integrity-check#quick-check#corruption#recover#dump#cksumvfs#pragma

源码下载

本文相关源码已整理,共 1 个文件。

打开下载目录 →

目录

第 7、8 篇钉住了两条自动恢复路径:hot rollback journal 回放、WAL 崩溃恢复,两者都不需要用户干预,官方文档也强调这一点——应用崩溃或断电后,“partially written transaction should be automatically rolled back the next time the database file is accessed”。但自动恢复只覆盖”最近一次事务是否完整落盘”这一个问题,不覆盖磁盘位翻转、错误工具误操作、文件系统 bug 造成的结构损坏。这些情况没有自动路径,只能靠 PRAGMA integrity_check 主动去查,查出问题后再靠 .recover 之类的工具去抢救。

常见误区是把”完整性检查通过”当成”数据绝对没问题”的证明,或者以为损坏恢复工具能把损坏前的数据原样找回来。本文只钉三件事:

  1. integrity_checkquick_check 到底检查什么、明确不检查什么,复杂度差在哪。
  2. 本机 3.53.2 上,一个完好数据库与一个被刻意翻转字节的数据库,integrity_check 的真实返回内容分别是什么;.recover.dump 在数据已经丢失的情况下各自的真实行为。
  3. 官方 How To Corrupt Your Database Files 列出的常见损坏来源如何对应第 2、7、8、9 篇已经拆过的文件格式与并发机制,以及修复工具的能力边界。

本文是「SQLite 内核」系列第 14 篇(共 17 篇)。→ 系列目录

篇目 核心内容
第 13 篇 · ATTACH / 多库边界 跨库事务与锁、原子性张力
第 14 篇 · 完整性检查与损坏恢复 integrity_check/quick_check、损坏来源、.recover 边界
第 15 篇 · 在线备份与 sqlite3_backup Hot copy API、与 cp 的语义差

版本锚定:官方 PRAGMA statements(sqlite.org/pragma.html,integrity_check/quick_check 章节)、How To Corrupt An SQLite Database File(sqlite.org/howtocorrupt.html)、Recovering Data From A Corrupt SQLite Database(sqlite.org/recovery.html)、The Checksum VFS Shim(sqlite.org/cksumvfs.html)。实测锚定本机 SQLite 3.53.2


一、integrity_checkquick_check:查什么、不查什么

官方 PRAGMA statements 文档对 integrity_check 的定义很具体:它做的是”数据库的低级格式与一致性检查”,查找:

检查全部通过时返回单行字符串 ok;有问题则每条问题各占一行,最多返回 N 条(默认 100 条,可传参调整)。文档同时明确写了两条边界:

PRAGMA integrity_check does not find FOREIGN KEY errors. Use the PRAGMA foreign_key_check command to find errors in FOREIGN KEY constraints.

See also the PRAGMA quick_check command which does most of the checking of PRAGMA integrity_check but runs much faster.

quick_check 跳过的是 UNIQUE 约束校验和”索引内容是否与表内容一致”这两项,因此复杂度从 integrity_check\(O(N \log N)\) 降到 \(O(N)\)\(N\) 为总行数)——省掉的正是需要排序/比对索引与表的那部分工作。这意味着 quick_check 通过不能替代”索引没有和表数据脱节”这条结论,只有 integrity_check 才检查这一点。

flowchart TD
  q["Suspect corruption?"] --> qc["PRAGMA quick_check<br/>O(N), skips UNIQUE + index-vs-table"]
  qc -->|"errors found"| stop1["Stop: database has structural problems"]
  qc -->|"ok"| ic["PRAGMA integrity_check<br/>O(N log N), full structural + index check"]
  ic -->|"errors found"| stop2["Stop: run .recover, do not keep writing"]
  ic -->|"ok"| fk["PRAGMA foreign_key_check<br/>FK constraints (separate from integrity_check)"]
  fk --> done["Structural + FK checks passed<br/>(not a content-checksum guarantee, see section 6)"]

需要强调:这两个 PRAGMA 检查的是结构一致性——B-Tree 的页链接、cell 边界、索引与表的对应关系是否自洽,不是”文件里存的每一个字节有没有被意外改动”。第六节会展开这条边界为什么容易被误解。


二、实测:完好库、人为损坏、修复工具的真实行为

环境:本机 sqlite3 3.53.2。复现脚本:reproduce/14-integrity.sh

2.1 完好库:两个 PRAGMA 都返回 ok

=== healthy database: integrity_check / quick_check ===
ok
ok

三行数据、两页大小的最小数据库,integrity_checkquick_check 都返回单行 ok,符合文档描述的”无错误”格式。

2.2 制造一次真实的结构损坏

不伪造输出,而是真的在文件里翻转字节:用 PRAGMA page_count/page_size 确认这个库是两页(页大小 4096 字节),hexdump page 2 后定位到页内偏移 0x0fee(文件绝对偏移 8174)开始的 18 字节正是三条表行记录(cell)在页尾的实际存储位置,对这 18 字节做异或翻转,不触碰文件头和 schema 页。结果:

=== corrupted copy: integrity_check ===
Error in 2nd command line argument: database disk image is malformed
*** in database main ***
Tree 2 page 2 cell 2: Extends off end of page
Tree 2 page 2 cell 1: Extends off end of page
Tree 2 page 2 cell 0: Extends off end of page

quick_check 在同一份文件上给出完全相同的三行错误——因为这次损坏影响的正是 cell 边界本身,属于 quick_check 也会检查的基础结构问题,不属于它跳过的 UNIQUE/索引一致性范畴。直接 SELECT * FROM t; 则直接报错退出,读不到任何一行:

=== corrupted copy: SELECT * FROM t ===
Error in 2nd command line argument: database disk image is malformed

2.3 .recover.dump 在数据已丢失时的真实差异

对这份损坏文件分别跑 .recover.dump,两者 exit code 都是 0,都不会因为部分数据不可读而报错退出——这是需要单独强调的一点,下面详细区分行为差异。

.recover 生成的 SQL:

.dbconfig defensive off
BEGIN;
PRAGMA writable_schema = on;
PRAGMA foreign_keys = off;
PRAGMA encoding = 'UTF-8';
PRAGMA page_size = '4096';
PRAGMA auto_vacuum = '0';
PRAGMA user_version = '0';
PRAGMA application_id = '0';
CREATE TABLE t(id INTEGER PRIMARY KEY, name TEXT);
PRAGMA writable_schema = off;
COMMIT;

把这份 SQL 重新加载进一个新数据库后:

-- rows recovered --
-- row count --
0
-- integrity_check of recovered database --
ok

.recover 只救回了表结构(CREATE TABLE t),三行数据一行都没有——因为我们翻转的正是这三行数据实际存储的字节,内容已经被真正破坏,不是”位置乱了但内容还在”,.recover 没有办法从物理上不存在的字节里生成数据。重新加载后的数据库本身是结构完好的(integrity_check 返回 ok),但这只能证明”新库自洽”,不能证明”数据完整”。

再看 .dump(对同一份损坏文件):

PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE t(id INTEGER PRIMARY KEY, name TEXT);
COMMIT;

.dump 同样只输出了表结构,没有任何一条 INSERT也没有打印任何错误或警告,退出码是 0,看起来像是”成功导出了一个空表”。这一点比 .recover 更值得警惕:.recover 至少是专门设计来在结构层面之下工作的抢救工具(底层用 sqlite_dbdata/sqlite_dbptr 虚表直接扫描裸页,不依赖正常的 B-Tree 遍历);.dump 走的是普通 SELECT 路径,一旦某张表的页读不出来,.dump 只是安静地跳过这部分内容,不会像交互式 SELECT * FROM t; 那样报错——如果有人把”跑一次 .dump 没报错”当作”这份数据库没问题”的信号,这次实测的结果就是一个反例。


三、常见损坏来源:官方文档与本系列已拆过的机制对应

官方 How To Corrupt An SQLite Database File 用一整篇文档列举了历史上真实出现过的损坏路径。挑四类和本系列直接相关的:

来源 官方文档原话(节选) 与本系列的关系
应用绕开 SQLite 直接写裸文件 “SQLite database files are ordinary disk files. That means that any process can open the file and overwrite it with garbage. There is nothing that the SQLite library can do to defend against this.”(§1) 第 2 篇的文件格式没有任何权限保护,谁都能用普通文件 I/O 改写
备份/热拷贝踩在事务中间 “Systems that run automatic backups in the background might try to make a backup copy of an SQLite database file while it is in the middle of a transaction. The backup copy then might contain some old and new content, and thus be corrupt.”(§1.2) 第 15 篇要展开的 cpsqlite3_backup 语义差,根源就是这一条
断电 + 关掉保护性 PRAGMA “Using PRAGMA journal_mode=OFF or PRAGMA journal_mode=MEMORY and taking an application crash in the middle of a write transaction.”(§7);“Setting PRAGMA synchronous=OFF can cause the database to go corrupt if there is an operating-system crash or power failure”(§7) 第 7 篇的 rollback journal、第 8 篇的 WAL 都依赖日志和 sync 语义;关掉 journal_mode/synchronous 等于主动拆掉这两篇讲的保护机制
网络文件系统锁语义不可靠 “SQLite depends on the underlying filesystem to do locking as the documentation says it will. But some filesystems contain bugs in their locking logic… This is especially true of network filesystems and NFS in particular.”(§2.1) 第 9 篇已经引用过这条,用来解释为什么官方建议不要在 NFS 上跑 SQLite——五态锁阶梯的正确性建立在”文件锁原语可靠”这个前提上

这四类损坏来源指向同一个结论:SQLite 对”正常路径内的崩溃”(进程崩溃、断电、期间没人乱动文件)有完整的自动恢复能力,但对”绕开正常路径”的操作没有任何防御——不是设计缺陷,官方原文就是这么写的:“There is nothing that the SQLite library can do to defend against this”。integrity_check 存在的意义,正是给这类”防御范围之外”的损坏提供一个事后能主动查出来的手段。

sequenceDiagram
  participant App as Application
  participant Pager as SQLite Pager
  participant Disk as Database file
  Note over App,Disk: Normal path: crash/power-loss mid-transaction
  App->>Pager: write via SQLite API
  Pager->>Disk: journal / WAL (per article 7-8)
  Note over Pager,Disk: crash here -> automatic recovery on next open
  Note over App,Disk: Bypass path: no automatic defense
  App->>Disk: raw file write / cp mid-transaction / NFS lock race
  Note over Disk: no journal/WAL entry protects this write
  Note over App: only PRAGMA integrity_check can catch it after the fact

四、修复边界:抢救,不是恢复

.recover(CLI 的 .recover 命令,底层是 recovery API:sqlite3_recover_init() + sqlite3_recover_step())的官方定位说得很直接:

The recovery API does as good of a job as it can at restoring a database, but the results will always be suspect.

文档列出的几种典型退化情况:

官方把这类工作定性为”salvage”(打捞),不是”restore”(还原):能救多少算多少,救回来的东西还需要人工核对和测试,不能直接当作恢复前的原始数据继续用。CLI 里 .recover 支持的几个参数(--ignore-freelist 避免把 freelist 里的旧内容误当作有效数据复活;--lost-and-found TABLE 给找不到归属表的内容一个落脚点;--no-rowids 控制 rowid 提取行为)本质上都是在”抢救更多内容”和”避免引入更多错误数据”之间做取舍,不存在一个默认参数组合能保证结果正确。

备份优先于修复:以上所有边界共同指向一个操作原则——integrity_check 发现问题后,第一反应应该是”停止继续写入、保留现场、检查有没有可用的备份”,而不是直接对生产文件跑 .recover 期待它把数据完整找回来。第 15 篇要讲的在线备份 API 正是用来在损坏发生前建立这个安全网,.recover 只是在没有备份时的最后手段。


五、学术谱系、工程间隙与开放问题

谱系integrity_check 检查的是应用层的 B-Tree 结构自洽性,和更底层的存储介质校验(文件系统 checksum、RAID 校验)是两个不同层次的防线,彼此不替代。SQLite 官方在这条边界之上又补了一层:The Checksum VFS Shim(cksumvfs,3.32.0 起随源码提供的可选扩展)在每个页末尾追加 8 字节校验和,专门用来”detect database corruption caused by random bit-flips in the mass storage device”——这恰好是 integrity_check 结构检查覆盖不到的那类损坏(内容被悄悄改了一个字节,但结构仍然自洽,见第六节误解 1)。三层检查(文件系统级、cksumvfs 页级、integrity_check 结构级)覆盖的问题域互不重叠,不能用其中一层的”通过”去代表另外两层。

工程间隙:文档反复强调”recovery API 的结果始终存疑”,这与很多用户期待的”有损坏恢复工具=数据保险”存在明显落差——本文第二节 .dump 在数据丢失时静默退出 0 的实测,正是这个落差在实际操作中最容易踩到的坑:工具没有义务在”部分内容读不出来”时报错,因为对 .dump/.recover 而言,“跳过读不出来的内容”本身就是设计好的正常行为,不是异常。

开放问题:官方文档没有给出”多久应该跑一次 integrity_check“的通用策略——integrity_check\(O(N \log N)\) 的全表扫描,对于移动端、边缘设备上体积不大的数据库可以频繁跑,但对几十 GB 的嵌入式数据库,全量检查的 I/O 与 CPU 成本、要不要在线跑(跑的时候和其他连接的锁交互如何)都没有官方给出的运维基准,需要应用自己按数据规模和访问模式决定检查节奏,这属于第 17 篇讨论选型运维成本时仍然开放的问题。


六、常见误解

  1. integrity_check 通过 = 数据绝对没被改动过。」 不对。integrity_check 验证的是 B-Tree 结构自洽——页链接、cell 边界、索引与表是否对应,不是逐字节校验内容。一次不破坏结构的字节翻转(比如把某个 TEXT 值中间的一个字符改掉)完全可能在 integrity_check 眼里”什么都没发生”。想检测这类静默位翻转需要额外加载 cksumvfs(见第五节),integrity_check 本身不提供这层保护。

  2. 「删掉 -wal 文件总是安全的,反正主文件才是真正的数据。」 第 8 篇已经说明 WAL 帧本身参与正常读路径,不只是恢复用的日志。官方 How To Corrupt-journal-wal 明确并列为同一类风险:热日志文件被移动、删除或改名之后,“automatic recovery will not work and the database may go corrupt”。只要上一次异常退出留下的是热 -wal(还没做完 checkpoint),删掉它就等于放弃了那批尚未落盘的已提交事务,具体后果取决于当时的 checkpoint 进度,不能一概而论为”安全”。

  3. .recover 或者 .dump 能把损坏前的数据完整找回来。」 见第二、四节:本文实测里 .recover.dump 都只救回了表结构,三行原始数据一行没有——因为损坏的字节就是数据本身所在的位置,工具没有办法凭空补出已经不存在的信息。官方文档把这类工具定性为”salvage”,明确列出内容丢失、旧数据复活、值被篡改、约束失效等多种退化模式,不是一个可以无脑信任的”一键恢复”。

  4. quick_check 通过就等于 integrity_check 通过,反正官方说’大部分检查都做了’。」 quick_check 明确跳过 UNIQUE 约束校验和”索引内容是否与表内容一致”这两项检查,复杂度也因此从 \(O(N \log N)\) 降到 \(O(N)\)。如果损坏恰好发生在索引与表数据的对应关系上(例如索引指向了一个已经不存在的 rowid),quick_check 可能不会发现,只有跑一次完整的 integrity_check 才能确认。


七、小结

  1. integrity_check 检查 B-Tree 结构自洽性(页链接、cell 边界、索引与表的对应关系),不检查 FOREIGN KEY,也不是内容级校验和;quick_check 跳过 UNIQUE 与索引一致性检查换取 \(O(N)\) 的速度,两者不能互相替代。本机 3.53.2 实测:完好库两个 PRAGMA 都返回 ok;对页内 cell 内容做字节级翻转后,两者都报出一致的”Extends off end of page”结构错误。
  2. 官方 How To Corrupt 列出的损坏来源里,绕开 SQLite 直写裸文件、事务中途热拷贝、断电叠加 journal_mode=OFF/synchronous=OFF、NFS 锁语义不可靠,分别对应第 2、7、8、9 篇已经拆过的文件格式与并发机制——SQLite 对”正常路径内的崩溃”有完整自动恢复,对”绕开正常路径”的操作没有任何防御,这是文档原话,不是设计缺陷。
  3. .recover.dump 在数据已经丢失的情况下都会静默跳过、exit code 为 0,不会主动报错——本文的实测复现了这一点:两者都只救回表结构,三行实际数据全部丢失。修复工具的定位是”尽力打捞”,不是”保证还原”;备份永远应该在损坏发生之前建好,这是第 15 篇的主题。

参考资料

规范与官方文档(A 级)

  1. SQLite Documentation, PRAGMA Statementsintegrity_check / quick_check 章节(sqlite.org/pragma.html)。
  2. SQLite Documentation, How To Corrupt An SQLite Database File(sqlite.org/howtocorrupt.html)。
  3. SQLite Documentation, Recovering Data From A Corrupt SQLite Database(sqlite.org/recovery.html),含 .recover CLI 用法与 Recovery API。
  4. SQLite Documentation, The Checksum VFS Shim(sqlite.org/cksumvfs.html)。

实验(A 级,本机实测)

  1. 本机 sqlite3 3.53.2:完好库 integrity_check/quick_check;对页内 cell 字节翻转后的结构错误报告;.recover.dump 在数据丢失场景下的真实退出码与输出对比。脚本 reproduce/14-integrity.sh,输出见第二节。

站内

  1. Rollback Journal 模式WAL 与 checkpoint——本文”断电 + 关闭保护性 PRAGMA”一节依赖的日志机制。
  2. 锁状态与 shared cache——NFS 锁语义不可靠的前序讨论。
  3. 单文件格式与页面头——本文字节级损坏实验定位 cell 位置所依赖的页面布局。
  4. 本系列 indexPLAN.md

上一篇ATTACH / 多库边界 下一篇在线备份与 sqlite3_backup

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-07-17 · database / storage

【SQLite 内核】单文件格式与页面头

拆解 SQLite 单文件格式:100 字节 database header 与 B-Tree 页面头的逐字段布局,用本机 3.53.2 CLI 实测 hexdump 核对 magic、page size、页类型标志,并钉住 file format 官方规范与 Bayer/McCreight 谱系。

2026-07-17 · database / storage

【SQLite 内核】Pager 与 Page Cache

拆解 Pager 作为 B-Tree 与操作系统文件之间的契约层:page cache 命中/未命中路径、脏页生命周期与提交前的可回滚保证,并与 PG shared_buffers、InnoDB Buffer Pool 的跨进程共享模型对照。


By .