---
title: "SQLite 的 WAL 一直变大：用两个连接找出长读事务"
date: 2026-11-13
slug: sqlite-wal-checkpoint-long-reader
canonical: https://kernelranch.com/blog/2026/11/sqlite-wal-checkpoint-long-reader
markdown_url: https://kernelranch.com/blog/2026/11/sqlite-wal-checkpoint-long-reader.md
tags: ["sqlite", "wal", "checkpoint"]
---SQLite 应用的写入还在成功，旁边的 -wal 文件却持续增长。先检查是否有长读事务。WAL 模式允许读写同时进行，但读者可能继续依赖旧快照，使 checkpoint 无法完成回收。

## 1. 保持快照，再尝试截断 WAL

保存为 wal_lab.py，运行 python wal_lab.py。两个连接打开同一个临时文件，写连接关闭自动 checkpoint，让对比可重复。

```python
import sqlite3
from contextlib import closing
from pathlib import Path
from tempfile import TemporaryDirectory

with TemporaryDirectory() as tmp:
    path = Path(tmp) / "lab.db"
    with closing(sqlite3.connect(path, isolation_level=None, timeout=0)) as writer:
        assert writer.execute("PRAGMA journal_mode=WAL").fetchone() == ("wal",)
        writer.execute("PRAGMA wal_autocheckpoint=0")
        writer.execute("CREATE TABLE events (id INTEGER PRIMARY KEY, body BLOB)")
        writer.execute("INSERT INTO events VALUES (1, zeroblob(1024))")
        writer.execute("PRAGMA wal_checkpoint(TRUNCATE)")

        with closing(sqlite3.connect(path, isolation_level=None)) as reader:
            reader.execute("BEGIN")
            assert reader.execute("SELECT count(*) FROM events").fetchone() == (1,)
            for _ in range(30):
                writer.execute("INSERT INTO events(body) VALUES (zeroblob(1024))")
            assert reader.execute("SELECT count(*) FROM events").fetchone() == (1,)
            blocked = writer.execute("PRAGMA wal_checkpoint(TRUNCATE)").fetchone()
            wal = path.with_name(path.name + "-wal")
            assert blocked[0] == 1 and wal.stat().st_size > 0
            print("reader snapshot: 1 row; writer committed: 31 rows")
            print("checkpoint busy:", blocked[0])

            reader.execute("COMMIT")
            done = writer.execute("PRAGMA wal_checkpoint(TRUNCATE)").fetchone()
            assert done == (0, 0, 0) and wal.stat().st_size == 0
            print("after reader commit:", done, "wal_bytes:", wal.stat().st_size)
            assert reader.execute("SELECT count(*) FROM events").fetchone() == (31,)
```

本地实测输出如下。

```text
reader snapshot: 1 row; writer committed: 31 rows
checkpoint busy: 1
after reader commit: (0, 0, 0) wal_bytes: 0
```

## 2. 生产环境先看 checkpoint 进度

PRAGMA wal_checkpoint(PASSIVE) 返回三个整数，依次是忙状态、WAL 中的页数和已 checkpoint 的页数。PASSIVE 会尝试推进 checkpoint，跳过需要等待的部分，所以第一列为 0 时还应比较后两列。对于持续写入的库，连续采样比单次文件大小更容易判断是否积压。

检查读取端有没有把事务带进网络调用、数据导出或等待用户输入的逻辑。一个游标或事务应该覆盖必要的读操作，然后及时结束。如果导出必须拿到同一快照，预留其 WAL 增长空间，并观察导出结束后 checkpoint 是否追上。

TRUNCATE 会尝试完成 checkpoint 并截断文件，可能等待其他连接。演练的 timeout=0 让它遇到冲突立即返回。线上先看忙状态、活跃事务和 I/O，再选择是否执行。手工删除 WAL 文件会绕过数据库恢复流程，应始终让 SQLite 管理相关文件。

## 小结

WAL 大小持续增长时，对照 checkpoint 进度和最长读事务。结束读事务后再次观察，确认积压可以回收。

## 数据来源与检索时间

检索时间 2026-11-13。

| 来源 | URL | 对应内容 |
|---|---|---|
| SQLite WAL | https://www.sqlite.org/wal.html | 并发读写、读快照与 checkpoint 饥饿 |
| SQLite PRAGMA | https://www.sqlite.org/pragma.html#pragma_wal_checkpoint | checkpoint 模式与返回字段 |

## 核验说明

- 本地 SQLite 3.45.1 用两个连接验证长读快照与截断恢复。实验关闭了自动 checkpoint，并使用 0 秒写连接等待时间。
