Menu

SQLite 常见错误:数据库锁定、只读、文件损坏等

在生产环境中真正会遇到的 SQLite 错误:database is locked、只读数据库、磁盘映像损坏、约束失败,以及对应的解决方法。

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

报错只是 SQLite 在告诉你一些事

SQLite 的错误信息很简短,有时还挺晦涩,但它们对应的底层问题其实只有少数几类。你在生产环境中遇到的大多数错误都可以归入五类:锁、权限、损坏、表结构不匹配和约束冲突。本页会逐一讲解每一类:什么情况会触发它,它到底意味着什么,以及怎么解决。

错误字符串会和数字错误码成对出现(扩展错误码还会更具体)。在日志里这两种形式你都会看到:

Error: database is locked          -- code 5 (SQLITE_BUSY)
Error: unable to open database     -- code 14 (SQLITE_CANTOPEN)
Error: attempt to write a readonly -- code 8 (SQLITE_READONLY)
Error: database disk image is      -- code 11 (SQLITE_CORRUPT)

知道错误码对搜索很有帮助:搜 SQLITE_BUSY 得到的结果比搜那句英文错误信息好得多。

database is locked(SQLITE_BUSY)

在任何从多个地方写入数据的应用中,这都是最常见的 SQLite 错误。SQLite 会把写操作串行化:同一时间只有一个连接能持有写锁。如果第二个写入者在忙等待超时内拿不到锁,就会得到这个错误。

三种解决办法,按效果从大到小排列:

对大多数工作负载来说,单靠 WAL 模式就能解决锁的问题。忙等待超时则是应对真实写冲突的安全网。除了设置之外,还要检查你的代码:如果程序在做网络 I/O 时事务一直开着,那么整个过程中写锁都会被占用。让事务保持简短,工作一完成就 COMMIT(或 ROLLBACK)。

unable to open database file(SQLITE_CANTOPEN)

SQLite 尝试打开文件,但操作系统拒绝了。95% 的情况下,问题出在文件路径或它所在的目录上:

-- Things to check:
-- 1. Does the path exist?            ls -l /path/to/db.sqlite
-- 2. Does the parent directory exist? SQLite creates the file
--    but not the directory above it.
-- 3. Does the user running your process have read+write
--    permission on the directory (not just the file)?
-- 4. Is the volume mounted, not full, and not read-only?

有一种不太明显的情况:SQLite 需要在数据库旁边创建附属文件(-journal、-wal、-shm)。如果文件本身可写而目录不可写,打开会成功,写入却会失败。一定要给目录授予写权限。

attempt to write a readonly database(SQLITE_READONLY)

这和上一个错误是近亲。文件能正常打开,但写入失败。按出现频率排列,原因有:

  • 系统用户对文件或其所在目录没有写权限。
  • 连接是以只读标志打开的(SQLITE_OPEN_READONLY,或 URI 中的 mode=ro)。
  • 卷以只读方式挂载(在 Docker 绑定挂载和一些云文件系统中很常见)。
  • 数据库位于网络文件系统上,而该文件系统不支持 SQLite 所需的锁机制。

修正权限或重新挂载卷即可。如果你在用 Docker,请确认绑定挂载没有加 :ro,并且容器用户拥有该目录。

database disk image is malformed(SQLITE_CORRUPT)

文件的字节已经不符合 SQLite 的格式。真正的原因通常来自运行环境:在没有正确 fsync 的文件系统上写入中途进程被强制结束、在有写入者活动时复制数据库、硬件故障,或者通过 Dropbox/iCloud 同步文件。

首先,确认损坏情况:

如果 integrity_check 返回 ok,说明数据库没问题,错误来自别处(通常是一个过期的连接)。如果它返回一串问题,就需要恢复数据了。

最干净的恢复方式是使用命令行工具的 .recover 命令,它会把能提取的数据都导出到一个新数据库中:

sqlite3 corrupt.db ".recover" | sqlite3 recovered.db
sqlite3 recovered.db "PRAGMA integrity_check;"

如果你有最近的备份,就直接从备份恢复:这样更快,也不用面对“大部分数据找回来了”这种说不清的结果。正确复制一个正在使用的数据库的方法请看备份与恢复那一页(提示:不是 cp)。

no such table 和 no such column

这两个错误的意思和字面上一模一样,但原因通常是以下两种之一:你连接的数据库和你以为的不是同一个,或者某次迁移没有执行。

检查应用的连接字符串:相对路径是相对于当前工作目录解析的,而终端、IDE 和生产进程的工作目录各不相同。内存数据库(:memory:)每次都是全新的,如果你期望数据能持久保存,就会在这里栽跟头。

标识符的引号也很重要。不加引号的名称不区分大小写,但 "User" 和 "user" 是两个不同的标识符。如果建表时给表名加了引号,之后就必须一直加引号。

约束冲突

SQLite 会拒绝违反约束的写入。错误信息会说明是哪一个约束:

每种失败在底层都对应不同的错误码(SQLITE_CONSTRAINT_UNIQUE、SQLITE_CONSTRAINT_CHECK、SQLITE_CONSTRAINT_NOTNULL)。解决办法几乎总在应用层:写入前先校验输入,或者用 INSERT ... ON CONFLICT 有意识地处理重复数据。

FOREIGN KEY constraint failed 值得单独说一下:在 SQLite 中,外键 默认是关闭的。如果不开启它,无效的引用会悄无声息地写进去,等你终于开启强制检查时才报错。每个连接都要设置这个 pragma:

cannot start a transaction within a transaction

你在一个事务已经打开的情况下又调用了 BEGIN。SQLite 不允许嵌套事务,但允许嵌套保存点,效果是一样的:

如果事务由你的 ORM 或框架管理,那很可能是你让它开启了两次事务。检查一下是否开启了自动提交,以及连接池是否复用了一个已经有未结束事务的连接。

disk I/O error(SQLITE_IOERR)

操作系统拒绝了一次读或写。可能是磁盘满了、网络文件系统出了点故障,或者文件在 SQLite 使用期间被删掉了。首先要检查的是 df -h。其次,看看数据库是否放在 NFS 或云同步文件夹这类不稳定的地方:SQLite 默认运行在 fsync 正常工作的本地 POSIX 文件系统上。如果没法把它挪走,就要接受数据损坏的风险会变高。

syntax error near "..."

SQLite 的解析器会告诉你是哪个词元让它困惑了。问题通常出在报错位置往前三行左右:少了一个逗号、未加引号的标识符和关键字冲突,或者字符串里的单引号需要转义(写 'it''s',而不是 'it's')。

处理用户输入时,用参数绑定(? 占位符),而不是用字符串拼接构造 SQL:这样既能避开一整类语法错误,又能同时防住 SQL 注入。

诊断清单

生产环境出问题时,按下面的顺序检查,一分钟之内就能覆盖大多数情况:

五个 pragma,五个答案。再结合失败查询返回的错误码,你就能知道问题属于哪一类,下一步该打开文档的哪一页。

课程总结

整个旅程到这里就结束了。你从 CREATE TABLE 出发,学过了连接、索引、事务、WAL 模式、备份,现在又了解了 SQLite 在真实环境中会遇到的各种故障。这些模式反复出现:保持事务简短、开启外键、使用 WAL 模式、定期备份,并重视 PRAGMA integrity_check。坚持这些习惯,SQLite 就能安安静静地运行好多年。

常见问题

SQLite 为什么提示“database is locked”?

另一个连接持有写锁,而你的连接等待超时了。常见的解决办法是:用 PRAGMA journal_mode=WAL 开启 WAL 模式,让读操作不再阻塞写操作;用 PRAGMA busy_timeout = 5000 调高忙等待超时;并确保事务及时提交,不要一直开着。

SQLite 中“attempt to write a readonly database”怎么解决?

这几乎总是文件系统权限问题,而不是 SQLite 本身的问题。运行进程的系统用户需要同时对数据库文件和它所在的目录有写权限(SQLite 会在该目录里创建 -journal 或 -wal 附属文件)。检查文件所有者、权限位,并确认卷没有以只读方式挂载。

“database disk image is malformed”是什么意思?

SQLite 读到的字节不符合它预期的格式,通常是文件损坏导致的,原因可能是进程被强制结束、磁盘故障,或者在文件打开期间复制了它。先运行 PRAGMA integrity_check 确认问题,然后在命令行工具里用 .recover 把能抢救的数据导出到一个新数据库。如果有备份,直接从备份恢复会更快。

为什么会出现“no such table”或“no such column”?

你连接的数据库文件和你以为的不是同一个,或者某次迁移没有执行。用 PRAGMA database_list 查看 SQLite 实际打开的文件路径,用 .schema tablename 查看真实的列。标识符拼写错误和大小写不一致也很常见:SQLite 对不加引号的名称不区分大小写,但对加了引号的名称区分。

Coddy 编程语言插图

和 Coddy 一起学编程

开始