
同一张表,同一份数据,同一台机器,五万行插进去,最慢的一组 10.015 秒,最快的一组 0.018 秒。差 556 倍,而两组之间的差别只有一个:提交的次数。
先摆数字。五万行订单记录写在同一块盘上,表结构完全相同,五组写法各自跑一次。
== SQLite 插入 50000 行,四种提交方式的实测 ==
sqlite3 版本 3.53.1
A 逐条 commit 耗时 10.015 秒 每秒 4992.6 行 产物 2.20 MiB
journal_mode(默认): delete
B 一个事务 耗时 0.034 秒 每秒 1449776.1 行 产物 2.20 MiB
journal_mode(WAL): wal synchronous: 2
C WAL + 一个事务 耗时 0.035 秒 每秒 1440590.1 行 产物 0.01 MiB
WAL 模式下的附属文件: wal.db 0.01 MiB, wal.db-shm 0.03 MiB, wal.db-wal 2.21 MiB
synchronous(NORMAL): 1
D WAL+NORMAL executemany 耗时 0.018 秒 每秒 2780526.0 行 产物 0.01 MiB
注意 D 用了 executemany,A/B/C 是逐行 execute
== 每 1000 行分一次事务(10 万行量级外推不测) ==
E 每 1000 行一次事务 耗时 0.042 秒 每秒 1194982.3 行 产物 2.20 MiB
A 组每插一行提交一次,每秒不到 5000 行。B 组把五万行包在一个事务里,耗时 0.034 秒。C 组换成 WAL 日志模式,同样是 0.035 秒。E 组每 1000 行提交一次,落在两者之间,因为一次提交能摊掉 1000 行。D 组 0.018 秒最快,但它用的是 executemany,和前面几组的逐行 execute 不是同一种调用方式,这组数字只能当参考,不能直接和 B 比。
那 10 秒花在哪
花在提交上。SQLite 默认的日志模式是 delete,每提交一次就要保证数据真的落到磁盘:先写日志并刷盘,然后改主库,最后删掉日志。这台机器上五万次提交摊下来,每次大约 0.2 毫秒,看着不多,乘上五万就是 10 秒。把提交减到一次,那套动作只做一遍,剩下四万九千九百九十九行都在内存里攒着,所以耗时会掉到两位数毫秒。
这也解释了为什么按行数分批的效果是连续的:一批 1000 行,提交次数降到 50 次,代价就按比例缩小。批量写入真正的优化点从来不是 SQL 写得多漂亮,是减少落盘次数。
WAL 在这组里没帮上忙
C 组和 B 组的耗时几乎相同,0.035 对 0.034。WAL 的价值在另一件事上:写的时候读的人不会被挡,读写可以并行。单线程、一次性包事务的场景里它换不来速度,这不是它坏,是用错了地方。
换来的东西还有代价,在主库文件之外多出的附属文件:
WAL 模式下的附属文件: wal.db 0.01 MiB, wal.db-shm 0.03 MiB, wal.db-wal 2.21 MiB
synchronous(NORMAL): 1
D WAL+NORMAL executemany 耗时 0.018 秒 每秒 2780526.0 行 产物 0.01 MiB
注意 D 用了 executemany,A/B/C 是逐行 execute
主库文件只有 0.01 MiB,因为数据还留在 -wal 里,等一次 checkpoint 才写回主库。看到「数据库文件几乎没变大」别急着高兴,去目录里看一眼那几个附属文件有多大。
行数对得上,产物大小不一样
== 校验四种写法的行数是否一致 ==
batched.db count = 50000
one_tx.db count = 50000
per_row.db count = 50000
wal.db count = 50000
wal_normal.db count = 50000
五个库的 count 都是五万,四种写法的数据没差别。
出问题的是产物大小这一栏会骗人。A、B、E 三组的库文件都是 2.20 MiB,C 组显示 0.01 MiB,看上去像 C 组省了空间。实际是 C 组的数据还在 -wal 里没落回主库。判断磁盘占用要连附属文件一起算。
落到代码里的几条
最常见的写法是循环里逐条 execute 然后立刻 conn.commit(),这也是 A 组。判断自己有没有踩上这个坑有个快办法:看导入耗时的量级,几十行数据要几百毫秒、几千行要几秒,基本就是逐条提交,这个量级的差异不是 SQL 语句优化能解释的。
修法是把整个循环包进 with conn:,或者手工 BEGIN / COMMIT,中间不提交。数据量大到内存放不下时再考虑按几千行一批提交,仍然比逐条提交快一到两个数量级。
日志模式这边我倾向于保持默认。WAL 留了连带要考虑的检查点与目录里的附属文件,除非确实存在边写边读的并发场景,否则换过去只会多一件事要维护。这一点上宁可保守。