侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 178 篇文章
  • 累计收到 7 条评论

五万行逐条提交 10 秒,包进一个事务 0.034 秒

2026-10-12 / 0 评论 / 9 阅读

五万行逐条提交 10 秒,包进一个事务 0.034 秒

同一张表,同一份数据,同一台机器,五万行插进去,最慢的一组 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 留了连带要考虑的检查点与目录里的附属文件,除非确实存在边写边读的并发场景,否则换过去只会多一件事要维护。这一点上宁可保守。

    🤞 分享