SQLite
SQLite 不是缩小版 MySQL,也不是需要独立守护进程的数据库服务。它是一套链接进应用进程的关系型数据库引擎:应用通过 C API 或语言驱动提交 SQL,SQLite 在同一进程内完成解析、规划、字节码执行、页缓存、事务和文件读写。选对场景时,它把部署、网络和运维成本压到极低;选错场景时,单写者、单文件和宿主文件系统会成为无法靠调参消除的边界。
本文以 SQLite 核心与 CLI 3.53.4 作为原理和命令参考基线。发行版软件仓库、Python 标准库和语言驱动可能嵌入不同补丁版本,因此不能把包名或驱动版本等同于 SQLite 核心版本。每种运行时都必须执行 sqlite_version()、sqlite_source_id() 和编译选项探测;版本不在项目批准范围内时停止上线,而不是假设所有示例都运行在 3.53.4。
| 运行入口 | 本文依赖或参考 | 实际版本判定 |
|---|---|---|
| SQLite CLI | 核心 3.53.4 | SELECT sqlite_version(), sqlite_source_id() |
Python sqlite3 | 随 Python 运行时提供 | sqlite3.sqlite_version 与 SQL 探测 |
better-sqlite3 | 12.10.0,内嵌 SQLite 3.53.1 | 应用启动时执行 SQL 探测 |
| Xerial JDBC | 3.53.1.0,内嵌 SQLite 3.53.1 | 建立连接后执行 SQL 探测 |
SQLite 3.53.4 包含 WAL reset 相关损坏修复。使用内嵌 3.53.1 的 Node.js 或 JDBC 驱动时,应把版本差异纳入风险评估,限制高风险 WAL 场景,并在兼容驱动携带 3.53.4 或更高补丁后升级;不能用系统 CLI 已升级来证明应用内嵌核心也已升级。
一、是什么
1. SQLite 的准确定位
SQLite 是一个自包含、无服务器、零配置、事务型 SQL 数据库引擎。这里的“无服务器”是指没有独立数据库服务进程和网络协议,不是“运行在云函数里”。连接 SQLite 本质上是让当前应用进程打开一个普通数据库文件,并调用 SQLite 库完成读写。
这决定了它最适合设备本地数据、桌面应用文件、移动端数据、边缘节点、单机服务的低并发持久化、测试数据集和可搬运数据容器。官方的选型表述很直接:SQLite 主要替代的是应用自行维护的文件格式,而不是 PostgreSQL、MySQL 或 SQL Server 这样的集中式数据服务。
SQLite 支持 ACID 事务、SQL、索引、视图、触发器、公共表表达式、窗口函数、JSON 函数、全文检索扩展和在线备份接口,但它不提供账号体系、监听端口、复制集、自动故障转移、跨节点共识或集中连接治理。这些不是“少配了几个参数”,而是产品边界。
2. 官方资源、版本线和许可
| 资源 | 地址 | 用途 |
|---|---|---|
| 官网 | sqlite.org | 产品入口和最新稳定版本 |
| 文档 | SQLite Documentation | SQL、事务、文件格式和 API |
| 下载 | SQLite Download Page | CLI、源码、amalgamation 与摘要 |
| 发布记录 | Release History | 补丁修复和兼容变化 |
| 源码 | SQLite Fossil | 权威源码仓库 |
| Git 镜像 | sqlite/sqlite | 便于浏览的官方镜像 |
| 论坛 | SQLite Forum | 官方技术讨论 |
| 安全 | SQLite Security | 漏洞报告和安全边界 |
SQLite 核心源码属于公共领域。语言驱动并不自动继承这一许可,例如 Xerial SQLite JDBC 同时包含自己的许可证和本地库打包方式,Node.js 驱动也有独立许可证。制品审查必须同时检查 SQLite 核心版本、驱动版本、平台原生二进制和组织的软件供应链要求。
SQLite 使用 3.x.y 版本线。补丁版本可能修复错误结果、崩溃恢复或查询优化问题,不能因为数据库文件格式长期稳定就永远冻结旧运行时。升级时需要保留原文件备份,并用真实查询、迁移、恢复和异常断电场景做回归。
3. 一条 SQL 在进程内如何执行
sqlite3_prepare_v2() 把 SQL 编译为虚拟数据库引擎 VDBE 的字节码程序,sqlite3_step() 驱动字节码逐步执行,sqlite3_finalize() 释放语句资源。语言驱动虽然把这些调用包装成 execute()、run() 或 PreparedStatement,底层仍是准备、绑定、执行、读取结果和释放。
查询规划器根据表统计信息、索引、连接顺序和谓词选择访问路径。EXPLAIN QUERY PLAN 展示的是高层访问策略,EXPLAIN 展示的是 VDBE 指令。看到“使用索引”只能说明访问入口,不能证明返回行少、临时排序消失或总耗时稳定。
B-tree 层保存表和索引记录。普通 rowid 表以整数 rowid 作为表 B-tree 的键,二级索引保存索引列和定位行所需的信息。WITHOUT ROWID 表直接以主键组织 B-tree,适合主键较短且天然承担访问顺序的模型,但不是所有表都应机械切换。
Pager 以页为单位管理缓存、锁、事务和崩溃恢复。VFS 把打开、读取、写入、同步、锁和删除映射到宿主操作系统。SQLite 请求了 xSync 不等于硬件一定完成稳定落盘,最终耐久性仍依赖文件系统、挂载参数、虚拟化层和存储设备正确实现同步语义。
4. 数据库文件不是唯一运行时文件
主数据库通常是 app.db。rollback journal 模式可能出现 app.db-journal;WAL 模式会出现 app.db-wal 和 app.db-shm。-wal 保存尚未 checkpoint 回主文件的已提交页,-shm 保存 wal-index 和进程间协调信息。
不能在数据库活跃时只复制主文件,也不能因为 -wal 或 -shm “看起来很大”就直接删除。正确备份使用 CLI .backup、语言驱动的 Online Backup API、VACUUM INTO,或者在确认所有连接关闭后复制完整、稳定的数据库文件。
临时排序、索引构建和大型查询还可能使用临时目录。容量预算必须同时考虑主文件、WAL、rollback journal、临时空间、备份副本和宿主剩余空间。
5. 页、记录与类型系统
SQLite 的数据库文件由固定大小的页组成。PRAGMA page_size 查看页大小,PRAGMA page_count 查看页数,两者相乘得到主数据库文件的逻辑容量。PRAGMA freelist_count 反映文件内可复用空闲页,不代表操作系统已经回收磁盘空间。
SQLite 使用动态类型和类型亲和性。列声明会影响转换倾向,但普通表不会像严格类型数据库那样拒绝所有不匹配值。需要稳定接口契约时使用 STRICT 表,并配合 CHECK、NOT NULL、UNIQUE 和外键约束。金额优先存最小货币单位整数,时间优先固定为 UTC 文本或 Unix 时间整数,不使用二进制浮点保存财务金额。
INTEGER PRIMARY KEY 是 rowid 的别名。AUTOINCREMENT 额外保证已使用过的 rowid 不被再次自动分配,会增加 sqlite_sequence 写入和开销。业务只要求唯一递增主键时通常不需要 AUTOINCREMENT。
6. 事务、锁和隔离
SQLite 默认处于 autocommit。每条独立写语句都是一个事务;显式 BEGIN 把多条语句放进同一事务。
BEGIN DEFERRED 在第一次实际访问时再取得锁。事务先读后写时,可能因为已有其他写者而在升级阶段失败。BEGIN IMMEDIATE 一开始就尝试取得写事务资格,适合“确认接下来必然写入”的短事务,可以更早暴露竞争。BEGIN EXCLUSIVE 在 WAL 和 rollback journal 下的效果不同,不应作为通用性能技巧。
rollback journal 保存被覆盖前的旧页。提交完成后主文件成为新状态,异常恢复根据 journal 回滚。WAL 把新页追加到 -wal,读事务在自己的 end mark 上读取稳定快照,checkpoint 再把已提交页搬回主文件。WAL 允许读者与一个写者并行,但同一数据库文件仍然只能有一个写事务。
SQLite 默认提供串行化隔离。共享缓存配合 read_uncommitted 是特殊边界,不应作为业务并发方案。长读事务会阻止 checkpoint 越过其快照,长写事务会让其他写者持续等待。真正的优化是缩短事务和批量提交,而不是无限增加 busy_timeout。
7. 线程与连接边界
SQLite 的线程安全取决于编译选项和驱动。PRAGMA compile_options 中的 THREADSAFE 表示库的编译模式,不等于某个语言连接对象可以被任意线程并发使用。Python sqlite3、JDBC 和 Node.js 驱动各有自己的连接线程约束。
稳妥模型是一次业务操作独占一个连接,连接上显式设置外键、busy timeout 和安全 PRAGMA,事务结束后立即释放。不要让多个请求线程同时操作同一个连接,也不要在网络调用期间持有写事务。
二、为什么
1. 什么时候优先选择 SQLite
本地优先应用、桌面软件、移动端、边缘设备和嵌入式程序通常希望离线可用、部署简单、文件可搬运。SQLite 把数据库能力放进应用进程,没有端口、服务发现和独立账号系统,故障面更小。
单机 Web 服务也可以使用 SQLite,前提是写入短、写并发可排队、应用实例不直接跨多台机器共享同一文件。一个应用进程在 SQLite 前统一接收网络请求,与很多客户端通过网络文件系统直接打开数据库文件,是完全不同的架构。
SQLite 还适合作为测试数据库、数据转换中间层和应用文件格式。但如果生产使用的是 PostgreSQL 或 MySQL,不能默认 SQLite 测试已经覆盖目标数据库的锁、类型、DDL 和 SQL 方言语义。
2. 什么时候不应该使用 SQLite
多个应用实例需要同时写同一个数据库、需要数据库内建账号与审计、需要在线主从复制和自动切换、需要跨地域容灾、需要独立扩展计算与存储,或者写并发无法在单写者前排队时,应选择客户端/服务器数据库。
网络文件系统上的锁与同步语义可能不可靠。WAL 要求访问同一数据库的进程位于同一主机,不能把 app.db 放到 NFS、SMB 或对象存储挂载后让多台机器直接写。
当查询主要是大规模列式扫描和分析时,DuckDB 更合适;当数据是企业共享事实源、需要集中权限和高并发写时,PostgreSQL 或 MySQL 更合适;当数据是单个应用自己的本地状态时,SQLite 往往更简单。
| 判断维度 | SQLite | DuckDB | PostgreSQL / MySQL |
|---|---|---|---|
| 运行形态 | 进程内、文件型 OLTP | 进程内、列式 OLAP | 独立数据库服务 |
| 主要负载 | 本地事务、点查、小批量写 | 扫描、聚合、文件分析 | 共享业务数据、高并发事务 |
| 写并发 | 每个数据库文件一个写事务 | 进程内协调,不面向高并发 OLTP | 多连接并发写 |
| 权限边界 | 操作系统文件权限 | 操作系统文件权限 | 数据库账号、角色和网络边界 |
| 高可用 | 应用级备份、同步和恢复 | 应用级文件与数据湖方案 | 复制、选主、故障转移生态 |
| 扩展方式 | 垂直扩容、拆库或迁出 | 文件分区、对象存储、数据湖 | 主从、分片、集群或云服务 |
3. WAL 不是默认答案
WAL 通常提升读写并行度并减少随机写,但会增加 -wal、-shm、checkpoint 和长读快照管理。只读介质、多主机共享文件或不支持共享内存的文件系统不适合 WAL。
rollback journal 更简单,数据库关闭后通常只需要主文件,但读写阻塞更明显。选择依据是并发模型、文件系统、备份方式和恢复要求,而不是“WAL 更快”这一句结论。
4. synchronous 是 RPO 选择
synchronous=FULL 更强调断电后的提交耐久性。WAL 模式下 NORMAL 仍保护数据库一致性,但最近已提交事务在系统断电时可能丢失。OFF 会扩大损坏或数据丢失风险,只适合可以重建的临时数据。
业务必须先定义 RPO。不能接受已确认提交丢失时使用 FULL 并验证底层存储;允许重建少量最近数据时可以评估 WAL + NORMAL。性能压测必须使用与生产相同的同步策略,否则结果没有容量意义。
5. 容量上限不等于可运营容量
SQLite 的理论文件上限非常高,但单文件备份时间、恢复时间、文件系统限制、部署制品大小和运维窗口会更早成为约束。容量规划至少计算:
主文件峰值
+ WAL 或 rollback journal 峰值
+ 排序和索引构建临时空间
+ 一份在线备份
+ 一份隔离恢复副本
+ 文件系统安全余量如果文件已经大到无法在目标 RTO 内复制、校验和恢复,就应分库、归档或迁移到服务型数据库,而不是继续引用理论最大值。
三、怎么做
1. 在 Linux 安装并确认真实版本
Ubuntu 或 Debian 使用发行版包:
sudo apt update
sudo apt install -y sqlite3
sqlite3 --versionRocky Linux、AlmaLinux 或 RHEL 使用:
sudo dnf install -y sqlite
sqlite3 --version包管理器提供的是发行版维护版本,不一定等于官网最新稳定线。继续操作前确认运行时身份:
sqlite3 :memory: \
"SELECT sqlite_version(), sqlite_source_id();"
sqlite3 :memory: "PRAGMA compile_options;"sqlite_version() 应返回实际库版本。compile_options 用于确认 THREADSAFE、FTS5、RTREE 等能力;不要看到 CLI 版本后假设 Python、JDBC 或 Node 驱动链接的是同一库。
2. 用 Docker 建立可重复实验环境
SQLite 没有需要长期运行的官方数据库容器。实验容器的意义是固定 Linux 用户空间和 CLI,不是模拟数据库服务器。
创建 Dockerfile:
FROM debian:bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends sqlite3 ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /workspace
ENTRYPOINT ["sqlite3"]构建并查看版本:
docker build -t sqlite-lab:local .
mkdir -p data
docker run --rm sqlite-lab:local :memory: \
"SELECT sqlite_version();"创建宿主数据库文件:
docker run --rm \
--user "$(id -u):$(id -g)" \
-v "$PWD/data:/workspace/data" \
sqlite-lab:local \
data/app.db "PRAGMA journal_mode=WAL;"预期输出是 wal,并且 data/app.db 由当前宿主用户持有。容器无法写入时先检查挂载目录权限,不要改成 chmod 777。
3. 创建数据库、约束和索引
先定义路径并确认父目录:
export SQLITE_DB="$PWD/data/app.db"
mkdir -p "$(dirname "$SQLITE_DB")"
test -w "$(dirname "$SQLITE_DB")"
sqlite3 "$SQLITE_DB" "SELECT sqlite_version();"初始化 schema:
PRAGMA foreign_keys = ON;
PRAGMA journal_mode = WAL;
PRAGMA synchronous = FULL;
PRAGMA busy_timeout = 5000;
CREATE TABLE IF NOT EXISTS customer (
customer_id INTEGER PRIMARY KEY,
email TEXT NOT NULL COLLATE NOCASE UNIQUE,
display_name TEXT NOT NULL,
created_at TEXT NOT NULL
) STRICT;
CREATE TABLE IF NOT EXISTS purchase_order (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL
REFERENCES customer(customer_id),
status TEXT NOT NULL
CHECK (status IN ('CREATED', 'PAID', 'CANCELLED')),
amount_cent INTEGER NOT NULL CHECK (amount_cent >= 0),
created_at TEXT NOT NULL
) STRICT;
CREATE INDEX IF NOT EXISTS idx_order_customer_created
ON purchase_order(customer_id, created_at DESC);将上一段完整 SQL 保存为当前项目根目录的 schema.sql,先确认文件存在且非空,再通过 CLI 执行:
test -s schema.sql
sqlite3 -bail "$SQLITE_DB" < schema.sql
sqlite3 -header -column "$SQLITE_DB" ".schema"-bail 让脚本在第一条 SQL 错误时停止。.schema 应显示两张表、外键、检查约束和索引。每个新连接仍需显式设置 foreign_keys,不能只在建库连接设置一次。
4. 使用短事务完成业务写入
库存扣减或状态流转必须用事务保护条件判断。下面的事务只有在客户存在时才创建订单:
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000;
BEGIN IMMEDIATE;
INSERT INTO customer(email, display_name, created_at)
VALUES ('alice@example.test', 'Alice', 'T0')
ON CONFLICT(email) DO UPDATE
SET display_name = excluded.display_name;
INSERT INTO purchase_order(
customer_id, status, amount_cent, created_at
)
SELECT customer_id, 'CREATED', 12900, 'T0'
FROM customer
WHERE email = 'alice@example.test'
RETURNING order_id, status, amount_cent;
COMMIT;BEGIN IMMEDIATE 在业务动作开始时取得写事务资格。RETURNING 应返回一行;没有返回行时必须 ROLLBACK,不能继续提交依赖前序结果的写入。
验证:
sqlite3 -header -column "$SQLITE_DB" \
"SELECT order_id, status, amount_cent
FROM purchase_order
ORDER BY order_id DESC LIMIT 1;"5. 正确使用参数,不拼接 SQL
CLI 的 .parameter 适合人工实验:
sqlite3 -batch "$SQLITE_DB" <<'SQL'
.parameter init
.parameter set :email alice@example.test
SELECT customer_id, email
FROM customer
WHERE email = :email;
SQL应用代码必须使用驱动参数绑定。表名、列名和排序方向不能用普通参数占位;需要动态标识符时只能从代码白名单映射,不能直接拼接用户输入。
6. Python 完整接入
Python 的 sqlite3 属于标准库,但内嵌 SQLite 版本由 Python 构建决定。先确认解释器和真实核心身份;版本不在项目批准范围内时停止:
python3 --version
python3 - <<'PY'
import sqlite3
print(sqlite3.sqlite_version)
print(sqlite3.connect(':memory:').execute(
'SELECT sqlite_source_id()'
).fetchone()[0])
PYPython 标准库已经包含 sqlite3。下面的示例包含路径、连接基线、事务、参数绑定、异常回滚和关闭:
from pathlib import Path
import sqlite3
DB_PATH = Path("data/app.db").resolve()
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
def open_db() -> sqlite3.Connection:
conn = sqlite3.connect(DB_PATH, timeout=5.0)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = ON")
conn.execute("PRAGMA busy_timeout = 5000")
conn.execute("PRAGMA trusted_schema = OFF")
return conn
def create_order(email: str, amount_cent: int) -> int:
if amount_cent < 0:
raise ValueError("amount_cent must be non-negative")
conn = open_db()
try:
conn.execute("BEGIN IMMEDIATE")
customer = conn.execute(
"SELECT customer_id FROM customer WHERE email = ?",
(email,),
).fetchone()
if customer is None:
raise LookupError("customer does not exist")
row = conn.execute(
"""
INSERT INTO purchase_order(
customer_id, status, amount_cent, created_at
) VALUES (?, 'CREATED', ?, 'T0')
RETURNING order_id
""",
(customer["customer_id"], amount_cent),
).fetchone()
if row is None:
raise RuntimeError("insert returned no row")
conn.commit()
return int(row["order_id"])
except Exception:
conn.rollback()
raise
finally:
conn.close()
if __name__ == "__main__":
print({"order_id": create_order("alice@example.test", 12900)})运行:
python3 app.py
sqlite3 "$SQLITE_DB" \
"SELECT order_id, status FROM purchase_order;"连接在 finally 中关闭,任何异常都会先回滚。长生命周期服务仍应让一次业务工作单元独占连接,不能跨线程共享同一个连接对象。
7. Node.js 完整接入
先确认使用 Node.js 22 或 24 的受支持版本,并记录 npm 版本。better-sqlite3 12.10.0 不再为 Node.js 20 和 23 提供预构建二进制;如果安装退回本地编译,需要先安装 build-essential、python3 和对应 Node.js 头文件。
node --version
npm --version版本不符合项目批准范围时停止。然后创建项目并锁定依赖:
mkdir sqlite-node && cd sqlite-node
npm init -y
npm install better-sqlite3@12.10.0
cp ../schema.sql .
mkdir -p data
sqlite3 data/app.db < schema.sql
sqlite3 data/app.db \
"INSERT INTO customer(email, display_name, created_at)
VALUES ('alice@example.test', 'Alice', 'T0');"创建 app.mjs:
import Database from 'better-sqlite3';
import { mkdirSync } from 'node:fs';
import { resolve } from 'node:path';
mkdirSync('data', { recursive: true });
const db = new Database(resolve('data/app.db'), {
timeout: 5000,
});
const runtime = db.prepare(
'SELECT sqlite_version() AS version, sqlite_source_id() AS sourceId'
).get();
console.log(runtime);
db.pragma('foreign_keys = ON');
db.pragma('journal_mode = WAL');
db.pragma('synchronous = FULL');
db.pragma('trusted_schema = OFF');
const insertOrder = db.transaction((email, amountCent) => {
const customer = db.prepare(
'SELECT customer_id FROM customer WHERE email = ?'
).get(email);
if (!customer) {
throw new Error('customer does not exist');
}
return db.prepare(`
INSERT INTO purchase_order(
customer_id, status, amount_cent, created_at
) VALUES (?, 'CREATED', ?, 'T0')
RETURNING order_id
`).get(customer.customer_id, amountCent).order_id;
});
try {
console.log({
orderId: insertOrder.immediate('alice@example.test', 25900),
});
} finally {
db.close();
}运行:
node app.mjs
sqlite3 data/app.db \
"SELECT count(*) FROM purchase_order;"启动输出必须包含 SQLite 版本和 source id;它们应与批准清单一致。immediate() 使获取写锁发生在事务开始阶段;发生 SQLITE_BUSY 时必须重试整个事务,不能只重试其中一条 INSERT。better-sqlite3 是同步驱动,单次查询会占用当前 Node.js 线程。需要处理慢查询或大型导入时,应放到 Worker 或独立任务,不要让事件循环长期阻塞。
8. Java 与 JDBC 完整接入
先确认 JDK、编译器和 Maven 可用;版本不符合项目批准范围时停止:
java -version
javac -version
mvn -version创建独立项目目录,并复用已经验证的 schema:
cd ..
mkdir -p sqlite-java/src/main/java/example
cd sqlite-java
cp ../schema.sql .pom.xml 使用已验证的 Xerial 驱动版本:
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId>
<artifactId>sqlite-demo</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
</properties>
<dependencies>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.53.1.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.5.0</version>
</plugin>
</plugins>
</build>
</project>创建 src/main/java/example/App.java:
package example;
import org.sqlite.SQLiteConfig;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public final class App {
public static void main(String[] args) throws Exception {
Path db = Path.of("data", "app.db").toAbsolutePath();
Files.createDirectories(db.getParent());
SQLiteConfig config = new SQLiteConfig();
config.setTransactionMode(SQLiteConfig.TransactionMode.IMMEDIATE);
try (Connection conn = DriverManager.getConnection(
"jdbc:sqlite:" + db, config.toProperties())) {
try (var version = conn.createStatement();
var rs = version.executeQuery(
"SELECT sqlite_version(), sqlite_source_id()")) {
if (!rs.next()) {
throw new IllegalStateException("missing runtime identity");
}
System.out.println(rs.getString(1));
System.out.println(rs.getString(2));
}
try (var statement = conn.createStatement()) {
statement.execute("PRAGMA foreign_keys = ON");
statement.execute("PRAGMA busy_timeout = 5000");
statement.execute("PRAGMA trusted_schema = OFF");
}
conn.setAutoCommit(false);
try {
long customerId;
try (PreparedStatement select = conn.prepareStatement(
"SELECT customer_id FROM customer WHERE email = ?")) {
select.setString(1, "alice@example.test");
try (ResultSet rs = select.executeQuery()) {
if (!rs.next()) {
throw new IllegalStateException(
"customer does not exist");
}
customerId = rs.getLong(1);
}
}
try (PreparedStatement insert = conn.prepareStatement(
"""
INSERT INTO purchase_order(
customer_id, status, amount_cent, created_at
) VALUES (?, 'CREATED', ?, 'T0')
""")) {
insert.setLong(1, customerId);
insert.setLong(2, 39900);
if (insert.executeUpdate() != 1) {
throw new IllegalStateException(
"expected exactly one inserted row");
}
}
conn.commit();
} catch (Exception error) {
conn.rollback();
throw error;
}
}
}
}编译和运行:
mkdir -p data
sqlite3 data/app.db < schema.sql
sqlite3 data/app.db \
"INSERT INTO customer(email, display_name, created_at)
VALUES ('alice@example.test', 'Alice', 'T0');"
mvn -q package
mvn -q exec:java -Dexec.mainClass=example.AppJDBC URL 指向文件,不是远端地址。程序输出的 SQLite 版本和 source id 必须与批准清单一致。TransactionMode.IMMEDIATE 让写事务尽早竞争写锁;遇到 busy 或 locked 错误时,应用应从事务入口重新执行整段业务逻辑。连接池不能提升 SQLite 的单写者并发,反而可能制造更多竞争;池大小应从请求模型和短事务压测得出。
9. 用执行计划验证索引
查询:
EXPLAIN QUERY PLAN
SELECT order_id, status, amount_cent
FROM purchase_order
WHERE customer_id = 1
ORDER BY created_at DESC
LIMIT 20;执行:
sqlite3 "$SQLITE_DB" \
"EXPLAIN QUERY PLAN
SELECT order_id, status, amount_cent
FROM purchase_order
WHERE customer_id = 1
ORDER BY created_at DESC LIMIT 20;"期望看到 SEARCH purchase_order USING INDEX idx_order_customer_created。看到 SCAN purchase_order 表示全表扫描;看到 USE TEMP B-TREE FOR ORDER BY 表示排序未由索引顺序满足。
更新统计信息:
sqlite3 "$SQLITE_DB" "PRAGMA optimize;"
sqlite3 "$SQLITE_DB" \
"SELECT name FROM sqlite_schema
WHERE type='index' ORDER BY name;"PRAGMA optimize 是日常维护入口。索引不是越多越好,每个索引都会增加写放大、文件大小和迁移时间。
10. 批量写入和事务容量
逐行 autocommit 会为每行承担事务同步成本。批量导入应使用一个有上限的事务:
BEGIN IMMEDIATE;
INSERT INTO purchase_order(
customer_id, status, amount_cent, created_at
) VALUES (1, 'CREATED', 100, 'T0');
INSERT INTO purchase_order(
customer_id, status, amount_cent, created_at
) VALUES (1, 'CREATED', 200, 'T+1');
COMMIT;批次不能无限增大。事务持续时间、WAL 增长、其他写者等待时间和失败重试成本共同决定批量上限。用真实数据测量每批耗时,并确保事务期间不调用外部 HTTP、消息队列或人工审批。
11. WAL、checkpoint 和竞争实验
设置 WAL 基线:
sqlite3 "$SQLITE_DB" <<'SQL'
PRAGMA journal_mode = WAL;
PRAGMA synchronous = FULL;
PRAGMA wal_autocheckpoint = 1000;
PRAGMA busy_timeout = 5000;
SQL查看状态:
sqlite3 "$SQLITE_DB" \
"PRAGMA journal_mode;
PRAGMA synchronous;
PRAGMA wal_checkpoint(PASSIVE);"
stat -c '%n %s bytes' "$SQLITE_DB"*wal_checkpoint(PASSIVE) 返回 busy、log 和 checkpointed 三个数字。busy 非零或 log 长期增长而 checkpointed 不前进,通常表示长读事务、持续写入或 checkpoint 调度不足。
用两个终端观察单写者。终端 A:
sqlite3 "$SQLITE_DB"PRAGMA busy_timeout = 5000;
BEGIN IMMEDIATE;
UPDATE purchase_order
SET status = 'PAID'
WHERE order_id = 1;保持事务未提交。终端 B:
time sqlite3 "$SQLITE_DB" \
"PRAGMA busy_timeout=1000;
BEGIN IMMEDIATE;
UPDATE purchase_order SET status='CANCELLED'
WHERE order_id=1;
COMMIT;"终端 B 约等待一秒后应返回 database is locked。终端 A 执行 ROLLBACK; 后重试应成功。这个实验说明 busy timeout 只提供有限等待,不会把一个数据库文件变成多写者系统。
12. 容量、页和空闲空间
查看主文件逻辑容量:
sqlite3 -header -column "$SQLITE_DB" <<'SQL'
SELECT
(SELECT page_size FROM pragma_page_size) AS page_size,
(SELECT page_count FROM pragma_page_count) AS page_count,
(SELECT freelist_count FROM pragma_freelist_count) AS freelist_count;
SQL
du -h "$SQLITE_DB"
df -h "$(dirname "$SQLITE_DB")"大量删除后 freelist_count 增加但文件不缩小是正常行为,空闲页会被后续写入复用。确实需要压缩时,在维护窗口评估 VACUUM;它需要额外临时空间并重写整个数据库,不能在磁盘接近满时盲目执行。
增量回收需要建库前设置 auto_vacuum=INCREMENTAL,改变现有库通常需要 VACUUM 重建。不要在不了解文件增长模型时直接打开自动回收。
13. 在线备份、校验和隔离恢复
业务在线时优先使用 CLI .backup。每次任务创建新的私有目录,避免旧产物被误判为本次备份:
export BACKUP_ROOT="$PWD/backup"
mkdir -p "$BACKUP_ROOT"
chmod 0700 "$BACKUP_ROOT"
export BACKUP_DIR
BACKUP_DIR="$(mktemp -d "$BACKUP_ROOT/sqlite.XXXXXXXX")"
sqlite3 "$SQLITE_DB" ".backup '$BACKUP_DIR/app.db'" || {
rm -f "$BACKUP_DIR/app.db"
exit 1
}
test -s "$BACKUP_DIR/app.db"
(cd "$BACKUP_DIR" && sha256sum app.db > app.db.sha256)
sqlite3 -readonly "$BACKUP_DIR/app.db" \
"SELECT max(order_id), count(*), coalesce(sum(amount_cent), 0)
FROM purchase_order;" \
> "$BACKUP_DIR/business-baseline.txt"app.db 必须非空,摘要文件和业务基线必须同时生成。也可以生成压实后的数据库副本:
test ! -e "$BACKUP_DIR/app-vacuum.db"
sqlite3 "$SQLITE_DB" \
"VACUUM INTO '$BACKUP_DIR/app-vacuum.db';"
test -s "$BACKUP_DIR/app-vacuum.db"VACUUM INTO 生成经过重排和压实的 SQLite 数据库文件,不是 SQL 逻辑导出。目标文件必须不存在,并需要足够空间。.backup 和 Online Backup API 更适合分步复制活跃数据库。任何方法都要检查命令退出码和产物。
把备份复制到新的隔离恢复目录,并校验恢复副本而不是原备份:
export RESTORE_ROOT="$PWD/restore"
mkdir -p "$RESTORE_ROOT"
chmod 0700 "$RESTORE_ROOT"
export RESTORE_DIR
RESTORE_DIR="$(mktemp -d "$RESTORE_ROOT/sqlite.XXXXXXXX")"
cp "$BACKUP_DIR/app.db" "$RESTORE_DIR/app.db"
cp "$BACKUP_DIR/app.db.sha256" "$RESTORE_DIR/app.db.sha256"
cp "$BACKUP_DIR/business-baseline.txt" \
"$RESTORE_DIR/business-baseline.expected.txt"
(cd "$RESTORE_DIR" && sha256sum -c app.db.sha256)摘要输出必须是 app.db: OK。再执行数据库检查,并让恢复副本生成同一组业务基线:
sqlite3 -readonly "$RESTORE_DIR/app.db" <<'SQL'
PRAGMA integrity_check;
PRAGMA foreign_key_check;
SELECT max(order_id), count(*), coalesce(sum(amount_cent), 0)
FROM purchase_order;
SQL
sqlite3 -readonly "$RESTORE_DIR/app.db" \
"SELECT max(order_id), count(*), coalesce(sum(amount_cent), 0)
FROM purchase_order;" \
> "$RESTORE_DIR/business-baseline.actual.txt"
cmp -s "$RESTORE_DIR/business-baseline.expected.txt" \
"$RESTORE_DIR/business-baseline.actual.txt"integrity_check 必须只返回 ok,foreign_key_check 必须没有行。业务计数和聚合必须与备份时记录的基线一致。验证通过后再让应用连接恢复副本,不在原文件上直接试恢复。
关闭全部连接后复制主文件可以作为离线备份。WAL 活跃时不得只复制主文件;checkpoint 不是备份,-wal 变小也不代表已有异地恢复点。
14. schema 迁移与 user_version
应用启动时读取 PRAGMA user_version,只执行从当前版本到下一版本的迁移。不要使用“忽略重复列错误”代替迁移状态管理。
迁移示例:
PRAGMA foreign_keys = ON;
BEGIN IMMEDIATE;
ALTER TABLE purchase_order
ADD COLUMN note TEXT;
PRAGMA user_version = 2;
COMMIT;迁移后验证:
sqlite3 -header -column "$SQLITE_DB" \
"PRAGMA user_version;
PRAGMA table_info(purchase_order);
PRAGMA foreign_key_check;"SQLite 对部分复杂 DDL 需要“新表、复制数据、删除旧表、重命名”的重建流程。整个流程必须在事务中进行,并在提交前验证行数、关键聚合、外键和索引。迁移前先做可恢复备份,失败后回滚并继续使用原 schema。
15. 安全配置
SQLite 没有数据库账号,数据库文件和父目录权限就是第一安全边界:
sudo install -d -m 0750 -o app -g app /var/lib/myapp
sudo install -m 0640 -o app -g app \
data/app.db /var/lib/myapp/app.db
namei -l /var/lib/myapp/app.db应用使用专用 Linux 用户运行。备份目录应与在线文件分离,并拥有独立访问控制。不要把数据库放在 Web 静态目录、容器镜像层或任何可被下载的位置。
每个连接设置:
PRAGMA foreign_keys = ON;
PRAGMA trusted_schema = OFF;
PRAGMA busy_timeout = 5000;除非业务明确需要,不调用 enable_load_extension。导入外部 SQLite 文件前先当作不可信输入,在隔离环境检查文件类型、大小、integrity_check、foreign_key_check、schema 和业务约束,不直接让高权限服务打开用户上传文件。
16. 监控和日常运维
SQLite 没有服务端指标端点,监控来自应用、文件系统和业务探针。应用至少记录操作类型、耗时、返回码、受影响行数、事务回滚和 SQLITE_BUSY、SQLITE_FULL、SQLITE_CORRUPT 次数,但不得记录参数中的密码和隐私数据。
文件侧检查:
stat -c '%n %s bytes' "$SQLITE_DB"*
df -h "$(dirname "$SQLITE_DB")"
du -sh "$(dirname "$SQLITE_DB")"
lsof "$SQLITE_DB" "$SQLITE_DB-wal" 2>/dev/null数据库侧检查:
sqlite3 -readonly "$SQLITE_DB" <<'SQL'
PRAGMA quick_check;
PRAGMA page_count;
PRAGMA freelist_count;
SQL
sqlite3 "$SQLITE_DB" "PRAGMA wal_checkpoint(PASSIVE);"quick_check 适合高频低成本检查,integrity_check 用于备份验收、恢复和维护窗口。不要在高峰期频繁执行全库完整检查。
17. 性能优化顺序
先确认慢的是锁等待、全表扫描、临时排序、同步写、业务返回行过多还是宿主磁盘。第一证据来自应用耗时分段、EXPLAIN QUERY PLAN、驱动指标、文件容量和系统 I/O。
优化顺序通常是缩短事务、修正查询和索引、批量提交、更新统计、限制返回量,再评估 cache_size、mmap_size、temp_store 和 synchronous。直接设置 synchronous=OFF 或把所有临时数据放内存,可能只是把延迟问题变成数据风险或内存风险。
PRAGMA cache_size=-65536 表示大约 64 MiB 的建议缓存,不是硬限制。每个连接都可能持有自己的页缓存,连接数乘以缓存设置才是进程内存预算。
18. 升级与回退
升级前记录运行时身份:
sqlite3 "$SQLITE_DB" \
"SELECT sqlite_version(), sqlite_source_id();"
sqlite3 "$SQLITE_DB" "PRAGMA compile_options;" \
> sqlite-compile-options.txt完成在线备份和隔离恢复验证后,用新运行时打开备份副本,执行真实查询、迁移、写入、回滚、WAL checkpoint 和应用集成测试。确认通过后再切换应用制品。
回退应回退应用和驱动制品;如果新版本已经执行不可逆 schema 迁移,则必须使用迁移前备份或明确的反向迁移,不能只替换动态库。升级和回退都要验证 user_version、schema、完整性、外键和业务对账。
19. 清理实验环境
关闭所有 CLI 和应用连接后,在同一个命令块中校验全部绝对路径;任意变量未定义或任意路径不匹配时,命令立即停止,不执行删除:
: "${SQLITE_DB:?}" "${BACKUP_ROOT:?}" "${RESTORE_ROOT:?}"
actual_db="$(realpath -m "$SQLITE_DB")"
actual_backup="$(realpath -m "$BACKUP_ROOT")"
actual_restore="$(realpath -m "$RESTORE_ROOT")"
expected_db="$(realpath -m "$PWD/data/app.db")"
expected_backup="$(realpath -m "$PWD/backup")"
expected_restore="$(realpath -m "$PWD/restore")"
test "$actual_db" = "$expected_db" || exit 1
test "$actual_backup" = "$expected_backup" || exit 1
test "$actual_restore" = "$expected_restore" || exit 1
rm -f -- "$actual_db" "$actual_db-wal" "$actual_db-shm"
rm -rf -- "$actual_backup" "$actual_restore"
docker image rm sqlite-lab:local三个 test 都无输出且退出码为 0 才会继续删除。生产目录不使用这组清理命令。
四、问题处理
1. 无法打开数据库文件
现象
应用返回 unable to open database file,CLI 也无法连接目标路径。
影响
应用不能读写;如果驱动悄悄使用相对路径,还可能在另一个目录创建空数据库。
常见根因
父目录不存在、应用用户无权限、路径是目录、只读容器挂载、相对路径解析到了错误工作目录。
定位顺序
先打印绝对路径,再检查父目录、文件类型、逐级权限和挂载模式。
定位命令
readlink -f "$SQLITE_DB"
namei -l "$SQLITE_DB"
ls -ld "$(dirname "$SQLITE_DB")"
findmnt -T "$(dirname "$SQLITE_DB")"输出判断
父目录必须存在且应用用户具有搜索和写权限。findmnt 显示 ro 时不能创建 WAL、journal 或临时文件。
解决步骤
创建专用目录并赋予应用用户最小权限,应用配置改用绝对路径。容器同时检查宿主目录 UID/GID 与挂载的读写模式。
验证
sudo -u app sqlite3 "$SQLITE_DB" \
"BEGIN; CREATE TABLE IF NOT EXISTS health(id INTEGER); ROLLBACK;"命令退出码为零,并且没有在其他目录出现同名空数据库。
预防
启动日志输出规范化后的数据库路径、文件设备号和只读状态,但不要输出敏感业务内容。
2. database is locked 或 SQLITE_BUSY
现象
写入返回 database is locked 或 SQLITE_BUSY,延迟在 busy timeout 附近形成尖峰。
影响
事务失败、请求重试放大,队列堆积时可能拖垮应用。
常见根因
写事务过长、读事务长期不结束、连接泄漏、事务内调用外部服务、多个进程同时批量写。
定位顺序
先确认 journal mode,再找持有文件的进程和应用中的未结束事务,最后检查写事务耗时分布。
定位命令
sqlite3 "$SQLITE_DB" "PRAGMA journal_mode;"
lsof "$SQLITE_DB" "$SQLITE_DB-wal" 2>/dev/null
pidstat -d -p ALL 1 5输出判断
WAL 仍只有一个写事务。某进程长期持有文件并伴随应用事务耗时增长,说明需要修正事务生命周期,而不是继续增大 timeout。
解决步骤
回滚或结束泄漏事务,把外部调用移出事务,拆分超大批次。对确定会写的短事务使用 BEGIN IMMEDIATE,并设置有限 busy_timeout。
验证
重复并发写入,SQLITE_BUSY 率恢复到容量基线,写事务耗时不再持续增长。
预防
监控事务持续时间、busy 次数和写队列长度;为每次重试设置总时限和抖动,禁止无限重试。
3. WAL 文件持续增长
现象
app.db-wal 长期增长,checkpoint 后仍不下降。
影响
磁盘占用增加,恢复和备份耗时变长,磁盘满后写入失败。
常见根因
长读事务固定旧快照、持续写入超过 checkpoint 能力、自动 checkpoint 被关闭、异常连接未关闭。
定位顺序
读取 checkpoint 三元组,检查长查询和连接生命周期,再检查 WAL 与磁盘增长。
定位命令
sqlite3 "$SQLITE_DB" \
"PRAGMA wal_checkpoint(PASSIVE);"
stat -c '%n %s bytes' "$SQLITE_DB-wal"
lsof "$SQLITE_DB-wal" 2>/dev/null输出判断
busy 非零或 log 持续大于 checkpointed,说明 checkpoint 被读快照阻挡或追不上写入。文件存在本身不是异常。
解决步骤
结束长读事务,关闭泄漏连接,在低峰执行 PRAGMA wal_checkpoint(TRUNCATE);。先确认没有关键长事务,不能直接删除 WAL。
验证
sqlite3 "$SQLITE_DB" \
"PRAGMA wal_checkpoint(TRUNCATE);"
stat -c '%s' "$SQLITE_DB-wal"checkpoint 返回 busy 为零,WAL 大小回到稳定基线。
预防
限制流式查询持有连接的时间,监控 checkpoint 结果和 WAL 增速,避免在事务中等待客户端慢速消费。
4. 磁盘满或 SQLITE_FULL
现象
写入、索引创建、VACUUM 或备份返回 database or disk is full。
影响
当前事务失败;如果备份只生成半个文件而未检查退出码,恢复点不可用。
常见根因
主文件、WAL、临时排序、VACUUM 双份空间或备份占满文件系统;文件数量上限也可能耗尽。
定位顺序
同时检查容量、文件数量、主文件组和临时目录,不只看主文件。
定位命令
df -h "$(dirname "$SQLITE_DB")"
df -i "$(dirname "$SQLITE_DB")"
du -sh "$(dirname "$SQLITE_DB")"
stat -c '%n %s' "$SQLITE_DB"*输出判断
可用空间必须覆盖当前写入、WAL 峰值和维护操作额外副本。VACUUM 前空间不足时不能执行。
解决步骤
停止非必要写入,清理确认无引用的旧备份和临时文件,扩容文件系统。事务失败后由应用回滚并重新打开连接确认状态。
验证
sqlite3 "$SQLITE_DB" \
"PRAGMA quick_check;
BEGIN; CREATE TABLE IF NOT EXISTS disk_probe(id INTEGER); ROLLBACK;"quick check 返回 ok,探针事务成功。
预防
按主文件、WAL、临时空间、备份和恢复副本共同设置容量告警,维护操作前做空间预算。
5. 数据库变成只读
现象
读取正常,写入返回 attempt to write a readonly database。
影响
业务更新失败,但健康检查如果只执行查询可能仍显示正常。
常见根因
数据库或父目录权限变化、容器只读挂载、文件系统 remount 为只读、恢复文件所有者错误、连接使用了 mode=ro。
定位顺序
检查连接 URI、文件和父目录权限,再检查挂载状态与内核日志。
定位命令
namei -l "$SQLITE_DB"
findmnt -T "$SQLITE_DB"
journalctl -k -n 100 --no-pager输出判断
文件可写但父目录不可写时仍无法创建 journal、WAL 或临时文件。内核因 I/O 错误切只读时必须先处理存储故障。
解决步骤
修复正确用户和目录权限,恢复读写挂载。存储异常时停止写入并从已验证备份恢复,不在故障介质上反复尝试。
验证
执行一个可回滚写探针并确认退出码为零。
预防
健康检查同时包含只读查询和可回滚写探针,监控挂载状态与磁盘错误。
6. 外键没有生效
现象
子表出现不存在的父键,插入未报错,integrity_check 仍返回 ok。
影响
业务关系损坏,删除和迁移可能留下孤儿数据。
常见根因
连接没有设置 PRAGMA foreign_keys=ON,在事务内部才尝试开启,或连接池创建新连接后遗漏基线。
定位顺序
在发生问题的同一连接读取 foreign_keys,再执行专用外键检查。
定位命令
sqlite3 -header -column "$SQLITE_DB" \
"PRAGMA foreign_keys;
PRAGMA foreign_key_check;"输出判断
foreign_keys 必须为 1。foreign_key_check 返回任意一行都表示存在违反约束的数据;integrity_check 不检查外键。
解决步骤
在每个连接开始事务前开启外键,修复或隔离孤儿数据。不要简单删除子行掩盖业务事实,先确认权威数据来源。
验证
重新执行 foreign_key_check 应无行,并用反例插入确认驱动返回约束错误。
预防
把连接基线封装在唯一连接工厂中,集成测试同时验证正例和违反外键的反例。
7. 查询突然变慢
现象
数据增长后接口延迟上升,CPU 或磁盘读取增加。
影响
请求超时,长读事务还可能拖住 WAL checkpoint。
常见根因
全表扫描、索引顺序不匹配、临时排序、返回行过多、统计信息陈旧、函数包裹索引列。
定位顺序
先捕获真实 SQL 和参数规模,再看执行计划、返回行数和 I/O,不先改 PRAGMA。
定位命令
sqlite3 "$SQLITE_DB" \
"EXPLAIN QUERY PLAN
SELECT order_id, status
FROM purchase_order
WHERE customer_id=1
ORDER BY created_at DESC LIMIT 20;"
sqlite3 "$SQLITE_DB" "PRAGMA optimize;"输出判断
SCAN、USE TEMP B-TREE 或返回大量行是直接证据。SEARCH ... USING INDEX 仍需结合实际返回量判断。
解决步骤
重写谓词和分页方式,建立与过滤及排序顺序匹配的索引,限制返回列和行数。删除重复或无效索引前先检查其他查询。
验证
新旧计划对比后,用生产规模数据重复测量延迟、读取页和事务持续时间。
预防
对核心查询保存计划基线,数据分布和 schema 变化后重新执行 PRAGMA optimize 并复测。
8. 数据库损坏或 database disk image is malformed
现象
查询返回 database disk image is malformed、SQLITE_CORRUPT 或完整性检查报告页错误。
影响
部分或全部数据不可读取,继续写入可能扩大损失。
常见根因
存储或文件系统故障、错误复制活跃数据库、直接删除或替换 WAL、异常工具修改数据库、底层锁语义失效。
定位顺序
立即停止应用写入并关闭连接,保留主文件、WAL、SHM 和 journal,再在隔离路径执行只读检查。
定位命令
sudo systemctl stop myapp
mkdir -m 0700 sqlite-incident
cp -a "$SQLITE_DB"* sqlite-incident/
sqlite3 -readonly "sqlite-incident/$(basename "$SQLITE_DB")" \
"PRAGMA integrity_check;"
journalctl -k -n 200 --no-pager输出判断
只返回 ok 才表示内部结构通过。任何其他行都需要按损坏处理;内核 I/O 错误说明不能信任当前介质。
解决步骤
优先从最后一个已验证备份恢复并重放可确认的业务增量。.recover 只用于没有更好备份时尽力导出,不能保证所有约束和业务语义完整。
验证
恢复副本必须通过完整性、外键、schema 和业务对账,再由应用做读写回归。
预防
使用 Online Backup API 或 .backup,定期做隔离恢复演练,监控存储错误,禁止手工删除 WAL。
9. 打开了错误的数据库文件
现象
应用提示“表不存在”,但 CLI 查看预期文件时 schema 正常;目录中出现新的小型同名数据库。
影响
应用可能向空库写入数据,造成看似成功但业务数据分叉。
常见根因
相对路径随工作目录变化、容器挂载点错误、环境变量为空、测试和生产共用默认文件名。
定位顺序
同时输出应用工作目录、数据库绝对路径、文件大小和 PRAGMA database_list。
定位命令
pwd
readlink -f "$SQLITE_DB"
sqlite3 "$SQLITE_DB" "PRAGMA database_list;"
find . -name 'app.db' -printf '%p %s bytes\n'输出判断
database_list 的 main 路径必须等于配置解析后的绝对路径。多个同名文件说明路径治理失败。
解决步骤
配置改为经过校验的绝对路径,启动时确认父目录和预期 schema。未知空库先隔离,不直接删除。
验证
应用与 CLI 查询同一个业务哨兵行,文件大小和更新时间随一次受控写入同步变化。
预防
禁止生产默认相对路径,启动日志记录数据库绝对路径和 user_version。
10. 备份文件存在但无法恢复
现象
备份任务显示成功,恢复时却缺表、完整性失败或业务对账不一致。
影响
名义 RPO 没有真实恢复能力,事故时无法按计划恢复。
常见根因
活跃 WAL 模式下只复制主文件、没有检查备份命令退出码、产物被截断、恢复验证只检查文件存在。
定位顺序
确认备份方法、产物摘要、文件大小和数据库检查结果,再核对业务基线。
定位命令
(cd "$BACKUP_DIR" && sha256sum -c app.db.sha256)
cat "$BACKUP_DIR/business-baseline.txt"
sqlite3 -readonly "$BACKUP_DIR/app.db" \
"PRAGMA integrity_check;
PRAGMA foreign_key_check;
SELECT max(order_id), count(*), coalesce(sum(amount_cent), 0)
FROM purchase_order;"输出判断
摘要必须返回 app.db: OK,完整性必须为 ok,外键检查无行,订单高水位、计数和金额聚合必须与保存的业务基线一致。只有文件存在或只有摘要一致都不能证明业务可恢复。
解决步骤
废弃不完整产物,使用 .backup、Online Backup API 或 VACUUM INTO 重新生成,在隔离目录恢复并验证。
验证
让测试应用连接恢复副本,完成读、写、回滚和关键业务查询后再登记为可用恢复点。
预防
备份任务的成功条件绑定命令退出码、摘要、完整性、外键和业务对账;定期执行恢复演练。
11. schema 迁移停在中间状态
现象
应用启动时报缺列、重复列或 user_version 不匹配,不同实例看到不同 schema 假设。
影响
新旧代码都可能无法稳定运行,重试迁移还可能重复写数据。
常见根因
迁移未放在事务中、忽略 DDL 错误、先更新 user_version 后修改 schema、迁移期间进程被终止。
定位顺序
读取 user_version、sqlite_schema、目标表列和迁移日志,再判断事务是否已整体回滚。
定位命令
sqlite3 -header -column "$SQLITE_DB" \
"PRAGMA user_version;
PRAGMA table_info(purchase_order);
SELECT type, name FROM sqlite_schema ORDER BY type, name;"输出判断
user_version 必须与实际 schema 对应。版本已增加但列或索引缺失,说明迁移协议错误。
解决步骤
停止新版本写入,从迁移前备份恢复或执行经过验证的修复迁移。后续迁移按“BEGIN IMMEDIATE、DDL、数据校验、user_version、COMMIT”的顺序执行。
验证
在备份副本上从旧版本连续迁移到目标版本,重复启动不再执行已完成迁移,失败后原 schema 保持可用。
预防
迁移脚本纳入应用版本控制,每个版本只允许一条确定路径,并在发布前使用真实规模副本演练。
12. 类型或约束与预期不一致
现象
数字列出现文本、布尔值出现多种表示,排序或聚合结果异常。
影响
查询结果错误,跨语言驱动读取类型不一致,迁移到服务型数据库时失败。
常见根因
普通表的动态类型被误认为严格类型、金额使用 REAL、缺少 CHECK、应用绑定了错误参数类型。
定位顺序
查看建表 SQL 和异常值的 typeof(),再检查驱动绑定代码。
定位命令
sqlite3 -header -column "$SQLITE_DB" \
"SELECT amount_cent, typeof(amount_cent), count(*)
FROM purchase_order
GROUP BY amount_cent, typeof(amount_cent)
ORDER BY 3 DESC;"
sqlite3 "$SQLITE_DB" ".schema purchase_order"输出判断
amount_cent 应全部是 integer。出现 text 或 real 说明入口约束不足或旧数据已经漂移。
解决步骤
新表使用 STRICT 和 CHECK,应用按明确类型绑定参数。旧数据先隔离异常行,再通过受控表重建迁移。
验证
导入反例必须被约束拒绝,历史数据扫描不再出现错误 typeof()。
预防
schema 同时表达类型、范围和枚举约束,集成测试覆盖语言驱动的边界值。
13. 网络文件系统或多主机共享导致异常
现象
锁错误无规律出现、WAL 无法启用、数据库偶发损坏,问题只在共享存储环境复现。
影响
多个主机可能同时认为自己拥有写权限,破坏事务和文件锁假设。
常见根因
NFS、SMB 或对象存储挂载不完整实现 POSIX 锁与同步;多个容器调度到不同主机后直接共享数据库文件。
定位顺序
确认数据库文件所在文件系统、应用实例所在主机和 journal mode,再检查是否存在跨主机直接打开。
定位命令
findmnt -T "$SQLITE_DB"
stat -f -c '%T' "$SQLITE_DB"
hostname
sqlite3 "$SQLITE_DB" "PRAGMA journal_mode;"输出判断
数据库位于网络文件系统且有多个主机直接访问时,架构不满足 SQLite 文件锁边界。WAL 不能跨主机使用。
解决步骤
把数据库放回单主机本地可靠文件系统,由一个应用服务统一提供网络访问;需要多机数据库能力时迁移 PostgreSQL 或 MySQL。
验证
确认只有目标主机进程打开文件,完成并发读写、异常重启和恢复实验。
预防
部署配置将 SQLite 数据卷绑定到单节点,调度策略禁止多副本直接挂载同一数据库文件。
14. 扩展加载失败
现象
调用 FTS5、RTREE、JSON 或 load_extension 时提示函数、模块或共享库不存在。
影响
搜索、空间索引或业务查询无法执行;盲目开启动态扩展还会扩大代码执行风险。
常见根因
运行时没有编译对应能力、不同驱动捆绑不同 SQLite、扩展 ABI 不匹配、动态加载被安全策略关闭。
定位顺序
先读取运行时版本和 compile options,再区分内建能力与外部共享库。
定位命令
sqlite3 :memory: \
"SELECT sqlite_version();
PRAGMA compile_options;"
sqlite3 :memory: \
"SELECT json_valid('{\"ok\":true}');"输出判断
compile options 和实际函数探针共同决定能力。未知 PRAGMA 可能被静默忽略,因此不能只靠配置命令退出码判断。
解决步骤
选择包含所需扩展并经过测试的驱动制品,或在受控构建中静态编译扩展。非必要不启用任意动态扩展加载。
验证
对扩展执行最小正例和错误输入反例,并在应用使用的真实驱动中运行,而不是只测试系统 CLI。
预防
把 SQLite 版本、source id、compile options 和扩展探针写入制品验收,升级驱动时重新验证。
