第1章:SQLite核心原理与本地文件布局

1.1 数据库文件结构与页组织

SQLite 将整个数据库存储为单一磁盘文件,内部以固定大小的页(Page)为基本单位进行读写。文件布局遵循严格的分区结构,包含文件头、页内容以及可选的回滚日志或 WAL 文件。

关键术语

  • 页大小(Page Size):默认为 4096 字节,可在创建数据库时指定(512~65536,且为 2 的幂)。
  • 文件头(File Header):前 100 字节,包含魔数、页大小、版本、编码标志等元数据。
  • B 树(B-Tree):表数据和索引均采用 B+树组织,每个叶子页存储实际行记录或索引条目。
  • 空闲页链表(Freelist):回收已删除页释放的空间,避免文件膨胀。
  • 溢出页(Overflow Pages):当一行数据超过一个页的大小时,使用链表连接。

下图展示了标准 SQLite 数据库文件的物理页布局

SQLite 数据库文件页布局 SQLite 数据库文件结构 文件头 100 字节元数据 页 1 (根) B-Tree 表根节点 页 2 (叶子) 行记录 页 3 (索引) B-Tree 索引 空闲页 溢出页 超大行数据 更多数据页 常规页 WAL / 回滚日志 WAL: write-ahead-log Journal: rollback 页缓存 (Page Cache) LRU 内存池

1.2 页缓存机制(Page Cache)

SQLite 在内存中维护一个页缓存(Page Cache),用于缓冲最近访问的磁盘页,减少同步 I/O 次数。缓存大小通过 PRAGMA cache_size 控制,默认值为 2000 页(约 8 MiB,当页大小为 4096 时)。

配置示例

-- 设置缓存大小为 10000 页
PRAGMA cache_size = 10000;

-- 查看当前缓存页数
PRAGMA cache_size;

-- 设置页大小 8192(必须在创建数据库前设置)
PRAGMA page_size = 8192;
页缓存关键参数
参数 默认值 说明 推荐范围
cache_size2000 页内存中最多缓存多少页500~200000
page_size4096每个页的字节数1024~65536
cache_spillOFF当脏页过多是否主动刷出OFF(嵌入式推荐)
性能调优提示: 对于小内存设备(如 IoT),应将 cache_size 设为 500 页以下,避免内存浪费;但对大量随机读取场景,增大缓存可显著降低 I/O。

1.3 WAL 与日志模式选择

SQLite 支持两种事务日志模式:回滚日志(Rollback Journal) 和 预写日志(Write-Ahead Log, WAL)。模式选择直接影响并发性、写入性能与崩溃恢复。

核心差异对比

特性 回滚日志 (DELETE/TRUNCATE/PERSIST) WAL 模式
并发读读需要等待写完成读写可并行,读不阻塞写
写入性能每次提交需同步原始文件追加到 WAL 文件,顺序写更快
文件数量仅数据库文件(+ journal 临时文件)数据库文件 + .wal + .shm (共享内存)
崩溃恢复需回滚所有未完成事务恢复时回放 WAL 即可
典型场景低并发、嵌入式、写密集型高并发读、桌面/服务器应用

切换命令与诊断

-- 设置 WAL 模式
PRAGMA journal_mode = WAL;

-- 设置回滚日志模式(DELETE 模式为最兼容)
PRAGMA journal_mode = DELETE;

-- 查看当前日志模式
PRAGMA journal_mode;

-- 检查 WAL 检查点状态
PRAGMA wal_checkpoint(PASSIVE);
常见错误: 在嵌入式环境(如 RTOS、裸机)中,WAL 模式依赖 msync() 或 fsync() 的语义,部分文件系统可能不支持共享内存文件(.shm),导致 WAL 启用失败。建议在部署前运行 PRAGMA integrity_check 验证。

1.4 实现流程示例(C 代码 API)

以下代码演示了如何打开数据库、设置页大小、WAL 模式并执行一条 DDL 语句。

#include <sqlite3.h>
#include <stdio.h>

int main() {
    sqlite3 *db;
    char *err_msg = 0;

    // 打开数据库(如果不存在则创建)
    int rc = sqlite3_open("example.db", &db);
    if (rc != SQLITE_OK) {
        fprintf(stderr, "无法打开数据库: %s\n", sqlite3_errmsg(db));
        sqlite3_close(db);
        return 1;
    }

    // 设置页大小为 8192(必须在创建表之前设置)
    sqlite3_exec(db, "PRAGMA page_size = 8192;", 0, 0, &err_msg);
    // 启用 WAL 模式
    sqlite3_exec(db, "PRAGMA journal_mode = WAL;", 0, 0, &err_msg);
    // 创建表
    sqlite3_exec(db, "CREATE TABLE IF NOT EXISTS users(id INT, name TEXT);", 0, 0, &err_msg);

    if (err_msg) {
        fprintf(stderr, "SQL 错误: %s\n", err_msg);
        sqlite3_free(err_msg);
    }

    sqlite3_close(db);
    return 0;
}

1.5 调试与验证技巧

  1. 使用 sqlite3 命令行工具检查文件结构:
    sqlite3 example.db
    .header on
    PRAGMA page_size;
    PRAGMA cache_size;
    PRAGMA journal_mode;
  2. 使用 file 命令查看文件头魔数:
    xxd -l 16 example.db | head -n 1
    # 输出示例: 00000000: 5351 4c69 7465 2066 6f72 6d61 7420 3300  SQLite format 3.
  3. 使用 PRAGMA database_list 确认当前数据库文件路径。
  4. 写入大规模数据后,查看 WAL 文件大小:ls -lh example.db example.db-wal。

1.6 常见错误与稳定性优化

错误场景 原因 解决/避免
database is lockedWAL 模式下写事务未提交设置 busy_timeout;检查事务延迟
文件损坏写操作进行中意外掉电启用 PRAGMA synchronous = FULL
磁盘空间不足WAL 文件持续增长定期调用 PRAGMA wal_checkpoint(TRUNCATE)
性能下降页缓存过小导致频繁缺页增大 PRAGMA cache_size;使用 mmap 模式
稳定性建议: 在嵌入式设备上,推荐使用 PRAGMA synchronous = NORMAL 配合 PRAGMA journal_mode = DELETE,兼顾性能与可靠性。若需极致可靠性,可选择 PRAGMA synchronous = FULL 并用事务包装所有写操作。