跳到主内容

SQLite 做本地数据仓库够用吗:100万行和1亿行到底差在哪,我跑了实测

够用,但有边界。我在这台机器上实打实灌了 100 万行数据,SQLite 装得下也查得动,单个条件查询从 106 毫秒压到了 0.02 毫秒。代价也很直白:两个索引把数据文件从 38.4 MB 撑到 79.5 MB,索引本体比原始数据还大;一次全表聚合跑了 746 毫秒;两个连接同时写,第二个直接抛 database is locked。下面是我跑出来的原始数字,以及每种情况该怎么办。

准备:先把这次实测的环境交代清楚

本机是 Windows,用 Python 3.13.14 自带的 sqlite3 模块,底层库版本 3.53.1。数据表故意做得很朴素,四列:一个自增 id、一个文本 key、一个浮点 value、一个日期字符串。灌 1,000,000 行,其中 key 只有 5 万个不同取值,用来模拟真实业务里那种「重复度高、需要按 key 检索」的场景。

有一点必须先说清楚:这 100 万行是脚本生成的合成数据,适合测「结构行为」——索引有多大、扫描要多久、什么时候会报锁,但不适合拿它的压缩率或 I/O 曲线去推算你的业务。

步骤一:先量清楚「到底能装多少」

第一步别急着写业务代码,先问两个 PRAGMA:

PRAGMA page_size;        -- 本机返回 4096
PRAGMA max_page_count;   -- 本机返回 4294967294

两个相乘就是你这个库的容量天花板:4096 × 4294967294 = 17,592,186,036,224 字节,也就是 16 TiB / 约 17.6 TB。这是默认配置下新建出来的库能涨到的上限,不需要任何额外设置。

接着我去翻了官方文档 sqlite.org/whentouse.html(页面标注最后更新于 2025-05-31),上面写的是 281 terabytes(2^48 字节 / 256 TiB)。两个数字不冲突:官方那个是理论天花板,需要你把 page_size 手动调到 64KB 才够得着;我默认建出来的库只有 17.6 TB。

这里有个坑值得单独说。之前写 AI Agent 那篇文章时我提过一句:涉及外部项目的关键字段,务必对着原文逐条核一遍,别信二手测评的结论。这条放在 SQLite 上完全成立——中文社区里流传最广的那个「SQLite 上限 140TB」是更早的口径,官方页面早就改成 281 TB 了。你要写进方案书,就自己打开那一页看一眼,几十秒的事。

步骤二:索引怎么建,以及它会贵得超出预期

我原本以为索引最多占数据的一个零头,结果不是。

先看没索引时的样子,一条按 key 的条件聚合:

SELECT count(*), avg(v) FROM t WHERE k='kw049999';
-- EXPLAIN QUERY PLAN: SCAN t
-- 耗时 106.6 ms

SCAN t 三个字说明它在全表扫。建索引:

CREATE INDEX idx_k ON t(k);
-- 建索引耗时 0.66 s,文件 38.4 MB → 54.6 MB

同一个查询立刻降到 0.02 ms,快了大约六千倍,EXPLAIN 也变成了 SEARCH t USING INDEX idx_k (k=?)。到这一步都很符合直觉。

翻车在下一步。为了让 avg(v) 不用回表,我又加了个覆盖索引:

CREATE INDEX idx_kv ON t(k, v);
-- 建索引耗时 0.99 s,文件 54.6 MB → 79.5 MB

查询确实再快一点点,但文件已经从最初的 38.4 MB 涨到 79.5 MB。摊开算:数据本体 38.4 MB,两个索引占了 41.1 MB,比数据本身还大。原因也不难猜,覆盖索引把 v 的值在每一条 key 条目里都复制了一份。

我后来总结的做法是,主表只放需要被检索和被聚合的窄列,详情类的大字段单独一张表,或者干脆落文件。这跟我们采集工具导出时「只导出结构化数据、不含详情大字段」是同一个思路——先把经常用到的那层压实,剩下的另说。

顺带一个正向结论:100 万行建索引只要 0.66 秒,相对 1.39 秒的灌入时间可以忽略。先灌数据、再建索引,别在空表上先建。

步骤三:真正会劝退你的两种场景

第一个是并发写。我开了两个连接,第一个 BEGIN IMMEDIATE 之后不提交,第二个再去写:

sqlite3.OperationalError: database is locked

官方文档的原话是:SQLite 支持「同时读取的人数不限,但任何一个瞬间只允许一个写者」。很多人把 WAL 模式当成解药,得区分清楚——WAL 解决的是「读和写互不阻塞」,它没有让多个连接同时写变成可能。实测也印证了这点:我这边的对照组里,单连接批量灌 100 万行,默认日志模式 1.39 秒,切成 WAL 反而 1.52 秒。WAL 的主场是多进程同时读写,单条写入流水线不是它的赛道,开了未必变快。

再说个我没找到捷径的地方。同样的 100 万行按日期分 28 组做 GROUP BY,跑了 746.7 ms,加任何索引都救不回来——它本来就要遍历每一行。这才是 SQLite 当数据仓库用最痛的点:点查快得像内存操作,聚合报表跟真正的列存差着量级。我的办法是把聚合结果预先算好写进结果表,查询时读结果表。这跟把每次设计取舍都写进开发文档是同一种思路:把不确定的东西固化下来,别让它每次重算。

结果校验:什么条件下我建议立刻换库

继续用 SQLite 没问题,如果全部满足:单机本地读写、数据量控制在 1 TB 以内(官方原话 less than a terabyte)、写操作能被一个进程顺序吃掉、查询以点查和时间范围扫描为主。该换客户端/服务器架构,只要碰到任意一条:数据挂在 NFS/SMB 上被多台机器直接读写;多个进程排不了队地同时写;需要 GB 级聚合秒级返回。

官方那张决策清单里最实用的一问是:数据和你的程序之间隔着网络吗?隔了就别用 SQLite——官方把关系引擎称为「带宽削减过滤器」,它的设计前提就是引擎和磁盘在同一台物理机上。

最后提醒一句:决定这类改造能不能跑通的,往往是某个开关级别的细节,而不是宏大的架构决策。就像做链接检测时一个 convert_charrefs 参数没设对,带 & 的 URL 就必然探测失败。SQLite 这边对应的是那几个 PRAGMA,它们不会让方案显得更有技术含量,但决定它半夜会不会挂。

常见问题

SQLite 到底能撑多少数据量不卡?

容量和流畅是两件事。容量上默认就能到约 17.6 TB;但「不卡」的边界由访问模式决定——点查百万行级别几乎无感,全表聚合百万行就要 700 毫秒往上。数据量只是门槛低,查询模式才是真门槛。

加了 WAL 是不是就不会报 database is locked 了?

不会。实测两个连接争写的场景,该报还是报。WAL 让读写不再互相阻塞,单次只有一个写者这条限制从头到尾没变。真要减少冲突,要么给写操作加带退避的重试,要么把写收拢到一个进程里去排队。

索引是不是越多越好?

本次实测给了个很直观的反例:两个索引让文件从 38.4 MB 涨到 79.5 MB,索引部分比数据还重。建之前用 EXPLAIN QUERY PLAN 确认它真的会被用到,覆盖索引尤其谨慎,它把列值在每个条目里又存了一遍。

那我该什么时候换到 PostgreSQL?

碰到这三条中的任意一条就该换了:数据要通过共享文件系统被多台机器同时读写;写无法排队、必须并行;聚合查询要求亚秒返回。三条都没碰到,换过去大概率只是多了一套要维护的服务。

这些数字能直接套到我的项目上吗?

不能直接套。我用的是脚本生成的四列合成数据,测的是结构行为。你的字段更长或含大文本时,索引膨胀会更明显;磁盘是机械盘时,写入那一组差距会被放大。拿这三个 PRAGMA 和你的真实表跑一遍,比抄我的数字有用。

相关阅读

分享到:

本文链接:https://www.biyeyuanma.cn/post/313.html

服务热线

加我微信

加我微信