Go 服务器中的高性能 SQLite 读取

发布: (2025年12月16日 GMT+8 04:44)
6 min read
原文: Dev.to

Source: Dev.to

工作负载假设

这些建议基于以下前提:

  • 读取占主导(写入很少或离线)
  • 单个服务器进程拥有数据库
  • 多个 goroutine 并发发起 SELECT
  • 数据库大小基本能放入 RAM 或操作系统缓存
  • 对断电后的持久性 并非关键

如果这些假设发生变化,下面的一些权衡需要重新考虑。

1. 使用 WAL 模式(不可协商)

PRAGMA journal_mode = WAL;

为什么重要

  • 读取永不阻塞写入
  • 读取永不阻塞其他读取
  • 读取时不触碰主数据库文件
  • 页面从 WAL + DB 顺序读取

对于读取密集的工作负载,WAL 实际上把 SQLite 变成了 无锁读取引擎

2. 设置 synchronous = NORMAL

PRAGMA synchronous = NORMAL;

在 WAL 模式下这是最佳平衡点:

  • 事务仍保持原子性和一致性
  • 每次提交不再额外执行 fsync
  • 磁盘刷新次数数量级下降

对读取密集的系统来说,延迟和吞吐量比突发断电时的持久性更重要。

3. 大幅增加页面缓存

PRAGMA cache_size = -65536;  -- ~64 MiB per connection
  • 负值表示 千字节,而不是页面数
  • 缓存是每个连接独立的
  • 更大的缓存降低页面错误和 B‑tree 遍历开销

更大的缓存直接转化为更少的磁盘读取和更快的扫描。

4. 启用内存映射 I/O(巨大的收益)

PRAGMA mmap_size = 20000000000;  -- 20 GiB (or larger than DB)

内存映射 I/O 让 OS 页面缓存 完成大部分工作:

  • 每页不再调用 read() 系统调用
  • 内核自动进行预读(readahead)
  • 全表扫描速度显著提升

如果数据库能放入 RAM,这会把 SQLite 的读取速度提升到 接近内存速度
经验法则:mmap_size 设置为大于数据库文件的大小。

5. 将临时对象保存在内存中

PRAGMA temp_store = MEMORY;

避免以下操作产生磁盘 I/O:

  • 排序
  • GROUP BY
  • 临时索引
  • 子查询物化

对于分析型或扫描密集的查询,这可以消除一个隐蔽但代价高昂的瓶颈。

6. 使用独占锁(单进程优化)

PRAGMA locking_mode = EXCLUSIVE;

好处

  • 更少的文件系统锁/解锁系统调用
  • 每个查询的延迟略有下降
  • 无需共享内存锁的协调

仅在没有其他进程需要访问数据库时安全

7. 让 SQLite 使用工作线程

PRAGMA threads = 4;

启用:

  • 并行扫描
  • 更快的大型 SELECT
  • 在多核机器上更好的 CPU 利用率

与 Go 的 goroutine 并发配合得很好。

8. 像吞吐量取决于它一样建立索引(确实如此)

没有任何 PRAGMA 调优能拯救糟糕的查询。

指南

  • 为每个出现在 WHEREJOINORDER BY 中的列建立索引
  • 避免在大表上使用 SELECT *
  • 使用 EXPLAIN QUERY PLAN 检查执行计划

一个缺失的索引就能把 1 ms 的读取变成 200 ms 的全表扫描。

9. 保持查询规划器统计信息最新

PRAGMA optimize;

何时运行

  • 模式(schema)变更后
  • 大批量导入数据后
  • 对长连接定期执行

确保 SQLite 选择最快的访问路径。

10. 只读模式以获得额外的安全性和速度

PRAGMA query_only = ON;

好处

  • 防止意外写入
  • 跳过部分写入相关的安全检查
  • 在运营上更安全

当数据库在运行时真正不可变时使用。

推荐的基线配置

PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA cache_size = -65536;
PRAGMA mmap_size = 20000000000;
PRAGMA temp_store = MEMORY;
PRAGMA locking_mode = EXCLUSIVE;
PRAGMA threads = 4;
PRAGMA query_only = ON;

这些 每个连接执行一次journal_mode 除外,它是持久的)。

Go‑特定注意事项

  • 使用连接池(database/sql
  • 允许大量读取连接(在 WAL 下成本低)
  • 为热点路径复用预编译语句
  • 避免不必要的读取序列化

在正确配置后,SQLite 在并发读取方面的扩展性极佳。

最后总结

SQLite 并不慢。配置不当的 SQLite 才慢。

通过 WAL、内存映射 I/O、适当的缓存以及合理的持久性权衡,SQLite 完全可以在单文件上轻松提供 每秒数百到数千次读取,且只需极少的内存和运维复杂度。

如果你的工作负载以读取为主且部署简单,SQLite 仍是最高效的数据库之一。

FreeDevTools

了解更多: FreeDevTools

任何反馈或贡献者都欢迎!它是在线的、开源的,随时供任何人使用。

⭐ 在 GitHub 上给它加星: freedevtools

Back to Blog

相关文章

阅读更多 »

使用 pprof 进行 Go 性能分析

什么是 pprof?pprof 是 Go 的内置分析工具,允许您收集和分析应用程序的运行时数据,例如 CPU 使用率、内存分配……

对 Go 和 Rust 的热爱吐槽!

警告:吐槽! 引言 我已经厌倦了这玩意儿。每隔几周,就会有某个 Rustacean 爬进来,得意地说 “但你尝试过 fearless concurrency …”。