SQLite并不是为超大并发写入场景设计的数据库,但很多桌面应用、移动端App甚至嵌入式系统都会用它存储数GB乃至数十GB的数据。理解它的最大容量与性能拐点,能帮你判断是否需要迁移到PostgreSQL或MySQL。先看下面这张图,它展示了SQLite在不同数据库大小下的B树深度变化。

SQLite的存储引擎与文件大小上限
SQLite把所有表、索引、视图和事务日志都放在一个独立的磁盘文件里。这个文件被划分为固定大小的页,页大小默认为4096字节,可以通过PRAGMA page_size调整,有效范围从512到65536字节。每一页要么属于表或索引的B树节点,要么属于空闲列表或溢出页。整个数据库的页数上限由4字节有符号整数决定,最大值大约是2147483646页。简单计算一下,默认页大小4096字节乘以最大页数21亿,得到约8TB的理论容量。如果把页大小设成65536字节,页数上限不变,理论容量就能达到约140TB。这就是网上流传的“SQLite最大140TB”说法的来源。
但理论值离实际可用很远。一方面,大多数文件系统对单文件大小有限制,比如FAT32最大单文件4GB,ext4最大单文件16TB,XFS可以支持到8EB,但很少有嵌入式设备的文件系统能轻松承载140TB的单个文件。另一方面,SQLite官方FAQ也指出,数据库文件超过1TB时,很多实用操作会变得极慢,甚至备份和恢复都成问题。实际上,当你把一个SQLite数据库从10GB增长到100GB,你会发现仅仅是打开数据库并执行SELECT count(*)就可能需要等待数秒。这暴露了B树索引在深层次下随机读的代价。
此外,页大小并非越大越好。较大的页能减少B树层级,但会放大写放大效应:即使只修改一行中的一个字段,SQLite也可能需要重写整个页。如果页大小64KB,写入一个小整数就要刷64KB数据到磁盘。因此对于频繁更新的表,较小的页大小反而更有利。创建数据库时就应该根据工作负载设置好PRAGMA page_size,因为一旦写入了数据,再修改页大小就需要执行VACUUM重建文件,代价很高。
数据库大小对查询性能的影响
SQLite的查询性能主要受B树深度和缓存命中率影响。每往下一层B树,就要多一次磁盘随机读。对于百万行级别的表,非叶子节点通常能全部缓存在内存里,因此点查延迟只有几微秒。但当数据库膨胀到几十GB,索引高度从3层变成5层甚至6层,每次二级索引查找都可能触发多次磁盘寻道。机械硬盘上单次随机读约10毫秒,6层深度意味着最坏情况60毫秒,比内存缓存命中时的0.1毫秒慢了600倍。即使使用SSD,随机读延迟也在0.1毫秒量级,6次就是0.6毫秒,对高频查询依然有明显影响。
另一个被忽视的因素是SQLite的SQLITE_DEFAULT_CACHE_SIZE默认值只有2000页,约8MB。这意味着无论你的机器有多少内存,SQLite默认只把最近使用的2000个页放在页缓存中。对于大数据库,这个缓存可能只覆盖了索引最顶层的几页,稍微深入一点的查询就会频繁触发磁盘读。可以通过PRAGMA cache_size把缓存调大,比如设置为1000000页(对应4GB内存),能极大改善大库的只读查询性能。但要注意,这并不会增加SQLite可用的最大内存总量,只是改变了它从操作系统申请页缓存的大小。
写入性能受数据库大小的影响更为剧烈。SQLite使用B树存储表数据,插入新行时如果页面已满,就会发生页分裂,需要分配新页并更新父节点。数据库越大,空闲页越分散,找到合适的页进行分裂就越慢。同时,未启用WAL模式时,每次写事务都要先写回滚日志,再修改数据库文件,整个过程是串行的。在一个100GB的数据库上执行批量插入,每秒可能只能写入几百行,而同样操作在1GB数据库上可以达到上万行。如果你的应用需要持续写入大库,务必使用PRAGMA journal_mode=WAL,它把新数据追加到单独的WAL文件,再周期性检查点合并,能减少大量随机写。
大数据库优化策略与最佳实践
面对已经膨胀到数十GB的SQLite库,最有效的办法不是继续调参数硬撑,而是从数据生命周期角度做拆分。常见做法是按时间或业务维度对表进行分区,旧数据定期归档到只读的SQLite文件或压缩存储,主库只保留最近几个月的数据。例如一个日志采集系统,每天生成一个独立的SQLite文件,查询时通过ATTACH DATABASE临时挂载,既控制了单文件大小,又保留了跨库查询能力。SQLite的ATTACH最多支持挂载10个附加数据库,对于大多数分析型查询足够了。
在单个大库内部,有几个PRAGMA参数值得花时间调整。除了前面提到的cache_size,page_size在建库时设定外,mmap_size也很有用。启用内存映射I/O后,SQLite可以直接通过指针读写文件,减少系统调用和用户态与内核态之间的数据拷贝。通常设置PRAGMA mmap_size=268435456(256MB)就能让热点页常驻进程地址空间。另一个容易被忽略的是PRAGMA optimize,它会在合适的时机更新查询规划器的统计信息,避免大库中因统计过期导致的错误执行计划。可以定期执行这个命令,或者用ANALYZE重建整个数据库的统计表。
最后,定期执行VACUUM能重建数据库文件,清理碎片并压缩空闲空间。但在大库上运行VACUUM会锁库较长时间,建议使用VACUUM INTO 'backup.db'创建一个紧凑的副本,再用副本替换原文件,这样对在线服务影响最小。如果数据已经超过100GB,且查询模式复杂,考虑切换到PostgreSQL或MySQL是更理性的选择。SQLite的优势在于零配置、嵌入式、事务完整,而不是无限扩展。把数据量控制在50GB以内,配合合理的缓存和WAL设置,SQLite依然能提供相当不错的性能。