SQLite因其零配置、单文件、嵌入式这些特性,成为桌面软件、移动应用和小型服务中广泛使用的数据库。但正因为它的架构和MySQL、PostgreSQL这类客户端/服务器数据库完全不同,很多从传统数据库转过来的开发者,一开始都会在锁冲突、并发、文件路径这些地方反复踩坑。这篇FAQ把日常提问频率最高的问题整理出来,逐条分析原因并给出解决办法。

database is locked 是怎么回事?
这是SQLite被问得最多的问题,没有之一。SQLite使用文件级锁来实现并发控制,当一个连接正在写数据库时,其他连接的写入请求会被阻塞。如果在超时时间内锁没有释放,就会抛出database is locked错误。默认的超时时间只有5秒左右,高峰期很容易触发。
遇到这个错误,第一步要排查的是代码里是否存在事务没有提交或回滚的情况。常见的坑是开启了事务后,某条分支抛出异常直接跳出了函数,事务一直挂着没关闭,锁就永远不释放。正确的做法是使用try-finally或者在支持的语言中使用自动提交事务的封装,确保任何路径下事务都会被关闭。
第二步是设置busy_timeout,让SQLite在锁被占用时自动等待重试,而不是立刻报错:
-- 设置等待时间为10秒(单位毫秒) PRAGMA busy_timeout = 10000;
开启WAL模式也能明显缓解锁冲突。默认的回滚日志模式下,读写互斥;而WAL模式下读和写可以并行,只有写与写之间才互斥,绝大多数读多写少的场景都能受益:
PRAGMA journal_mode = WAL;
需要提醒的是,WAL模式下会额外产生db-wal和db-shm两个文件,备份数据库时如果只拷贝db文件而忽略这两个文件,可能丢失最近未检查点的数据。备份时建议使用.backup命令或API,而不是直接复制文件。
多个线程或多个进程可以同时访问SQLite吗?
可以,但有前提条件。SQLite是线程安全的,前提是编译时开启了SQLITE_THREADSAFE选项(绝大多数发行版默认开启,串行模式)。同一进程内的多线程访问,推荐每个线程使用独立的连接,这也是官方文档明确建议的方式。虽然开启串行模式后多线程共用一个连接也不会崩溃,但性能和稳定性都不如每线程一个连接。
跨进程访问则要依赖操作系统的文件锁。如果多个进程频繁读写同一个数据库文件,锁冲突的概率会显著上升,此时WAL模式几乎是必选项。还要注意网络文件系统(如NFS、SMB共享目录)上的文件锁并不可靠,官方明确不建议把db文件放在网络挂载路径上,轻则锁失效,重则数据库损坏。
如果你的应用确实需要大量并发写入,比如每秒成百上千次插入,就要认真考虑SQLite是否合适。可以通过批量事务来缓解:把多次插入合并到一个事务里提交,写入吞吐量能提升几十倍,因为每次独立提交都会触发一次磁盘同步:
import sqlite3
conn = sqlite3.connect('app.db')
cursor = conn.cursor()
# 批量写入:一次事务提交1000条,远快于逐条自动提交
cursor.execute('BEGIN')
for i in range(1000):
cursor.execute('INSERT INTO logs (msg) VALUES (?)', (f'record {i}',))
conn.commit()
conn.close()自增主键总是从1开始吗?ROWID又是什么?
SQLite里没有真正的自增列。建表语句里的INTEGER PRIMARY KEY实际上是表的ROWID别名。每张SQLite表内部都有一个隐藏的64位ROWID,即使你没有声明主键它也存在。声明INTEGER PRIMARY KEY后,这一列就与ROWID合二为一,插入NULL时自动分配一个未使用的最大值加一。
这里有个容易混淆的点:AUTOINCREMENT关键字的作用和大多数人想的相反。不加AUTOINCREMENT时,新ROWID取当前表内最大ROWID加一,如果删掉了最后一行,ROWID可能被复用;加上AUTOINCREMENT后,SQLite会跟踪历史最大值,保证主键只增不减、永不复用,但会多维护一张sqlite_sequence表,插入性能略有下降。除非业务上依赖ID的单调递增性(比如同步逻辑),否则不建议加。
另外,主键列必须是INTEGER PRIMARY KEY这种精确写法,写成BIGINT PRIMARY KEY或INT PRIMARY KEY都不会被当作ROWID别名,插入时也就不会自动填值,需要手动指定,这是实际开发中经常踩的一个坑。
数据库文件损坏了怎么恢复?
损坏的常见原因包括:程序运行中断电或崩溃导致写入中断、多个进程在不支持锁的文件系统上并发写、以及写库过程中文件被外部程序修改。症状通常是打开时报database disk image is malformed。
恢复的第一步是尝试.recover命令(较新版本的sqlite3命令行工具自带),它会把能读出的数据导出成SQL脚本,比老的.dump容错性更好:
sqlite3 broken.db ".recover" > recovered.sql sqlite3 new.db < recovered.sql
如果命令行工具版本太旧不支持recover,可以用PRAGMA integrity_check;先定位损坏的页面,再用.dump尽量导出,遇到报错就跳过继续。平时最重要的预防措施是做好备份,并保持journal_mode为WAL和synchronous为FULL或NORMAL,不要为了性能随意把synchronous设成OFF,那样断电时损坏的概率会大幅上升。
查询变慢了,如何优化?
SQLite在数据量达到百万级后,如果查询明显变慢,第一件事是检查执行计划。在查询前加上EXPLAIN QUERY PLAN,看是否进行了全表扫描:
EXPLAIN QUERY PLAN SELECT * FROM orders WHERE user_id = 42;
如果结果里出现SCAN TABLE字样,说明没有用到索引,需要为过滤列建立索引。建立索引后再次检查,出现SEARCH TABLE ... USING INDEX才说明索引生效了。
几个实用的优化点:一是用ANALYZE更新统计信息,帮助查询优化器做出更好的选择;二是避免SELECT *,只取需要的列,覆盖索引可以直接从索引返回数据而不用回表;三是模糊查询注意LIKE的前导通配符%keyword无法使用普通索引,高频模糊搜索场景建议用FTS5全文索引扩展;四是定期执行VACUUM回收删除数据留下的空闲页,尤其是频繁增删的表,VACUUM之后文件体积和查询性能往往都有改善。
最后部署层面的问题也别忽略:Windows服务程序里用相对路径打开数据库,实际位置会取决于服务工作目录,很容易把db文件建到意料之外的地方;应用没有目标目录的写权限时,SQLite可能只读或直接报错。上线前务必确认路径是绝对路径且账户有读写权限,这些细节问题在FAQ中同样占了不少比例。