SQLite 3.49.1 の PRAGMA 変更は 1.6倍止まりだった
この記事の見出し
SQLite 3.49.1 で 100,000 行を INSERT すると、FULL+delete は534050 rows/s、OFF+memory は863750 rows/sだった。
差は1.6倍。synchronous=OFF と journal_mode=memory にすれば、もっと派手に伸びるだろうと思っていたので、この結果は少し拍子抜けだった。2026-06-09、2026年6月9日の夜に一度だけ通した測定で、Python は3.12.10、マシン種別は ARM64 と記録されている。
効きが小さく見えた理由は、たぶん入れ方にある。Python 標準の sqlite3 で executemany を使い、100,000 行をまとめて1トランザクションとしてコミットした。fsync の回数が少ない状態から始まっているので、synchronous を落としても余地が残りにくい(この条件を忘れると、PRAGMA の結論だけが独り歩きしてしまう)。では、設定差ではなく commit 単位が主役だったのか。
1トランザクションで入れた
試した設定は3通り。FULL+delete、NORMAL+wal、OFF+memory を並べ、INSERT 本体の秒数、INSERT の rows/s、インデックス作成、100件のポイント SELECT、最後のデータベースファイルサイズを見た。
import sqlite3
conn = sqlite3.connect(path)
conn.execute("PRAGMA synchronous=FULL")
conn.execute("PRAGMA journal_mode=delete")
conn.executemany("INSERT INTO t VALUES (?, ?)", rows)
conn.commit()
実際のスクリプト全体はここでは省く。この記事で見たいのは、Python のループや SQL 文の細部ではなく、synchronous と journal_mode の組み合わせでどのくらい変わるかだから。とはいえ、1トランザクションでまとめている、という条件だけはかなり効いている。
SELECT はキャッシュやページ配置の影響を受ける。ここだけを過大に読むと、見方がずれてしまう。今回の主役はあくまで INSERT で、SELECT や index_s は周辺の確認として見ている(commit 単位の影響を先に見るべきだった)。
INSERTの差は見えるが桁は変わらない
表の数値は、測ったものをそのまま置く。
| 設定 | insert_s | insert_rows_per_s | index_s | select100_ms | db_bytes |
|---|---|---|---|---|---|
| sync=FULL,journal=delete | 0.187秒 | 534050 rows/s | 0.054秒 | 11.21ms | 3743744 bytes |
| sync=NORMAL,journal=wal | 0.153秒 | 654606 rows/s | 0.038秒 | 1.13ms | 3743744 bytes |
| sync=OFF,journal=memory | 0.116秒 | 863750 rows/s | 0.065秒 | 7.86ms | 3743744 bytes |
一番遅い FULL+delete と一番速い OFF+memory を比べると、0.187秒から0.116秒へ縮んでいる。rows/s では534050 rows/sから863750 rows/sなので、体感としても速くなっている。けれど、桁が変わるような勝ち方には見えなかった。
NORMAL+wal は中間に収まった。0.153秒で654606 rows/sなので、FULL+delete よりは速い。OFF+memory との差もある。ただ、安全性をかなり削った設定にしたわりには、OFF+memory が圧勝した感じまでは届いていない(PRAGMA 名だけで結論を出すには弱い)。
ここで「SQLite の PRAGMA は効かない」と読むのは違うと思う。今回の条件では、すでに commit 回数を1回へ寄せている。1行ずつ autocommit で入れる形なら、FULL+delete はもっと苦しくなるはずだが、それはこの表の外に残っている。
SELECTの1.13msは主役にしない
100件のポイント SELECT は、FULL+delete が11.21ms、NORMAL+wal が1.13ms、OFF+memory が7.86msだった。ここはどの設定でもほぼ変わらないはず、と思っていたので、1.13msだけ低いのは目に留まる。
ただし、今回の比較では SELECT を勝ち負けの主役にしないほうがよい。SELECT 用のインデックスはそれぞれ作っているし、行数もクエリ数も同じ。journal_mode は主に書き込み時の安全性とログの持ち方に効く設定なので、ポイント SELECT の差をそのまま PRAGMA の効果として読むのは無理がある。
インデックス作成も同じように扱う。index_s は0.054秒、0.038秒、0.065秒で、こちらも大きな差というほどではなかった。NORMAL+wal が一番短く、OFF+memory が一番長い。INSERT の順位とそろっていないので、測定の揺れを含んだ値として見るのが自然だろう。短い処理はブレやすい。
memory journalでもdb_bytesは同じ
db_bytes は3設定とも3743744 bytesだった。実際に並べるまで、ここも少し勘違いしていた。journal_mode=memory と聞くと、何かがファイルに残らなくなってサイズも変わりそうに感じるので、この点は少し意外だった(ファイルサイズまで変わると思い込んでいた)。
実際には、memory journal はロールバックジャーナルをメモリに置く設定であって、メインのデータベースファイルをメモリ専用にする設定とは別物、という理解に直した。今回の db_bytes は最終的な .db ファイルのサイズなので、3通りで同じになるのも自然に見える。
この点を勘違いすると、journal_mode=memory を「ファイルを作らないモード」と読んでしまう。少なくとも今回の JSON では、最終ファイルサイズの差は出ていなかった。ここで変わるのは、メイン DB の容量ではなく、書き込み中のログの持ち方だと見たほうがいい。
見落としやすいのはcommit単位
今回の測り方で大きいのは、100,000 行を1トランザクションで入れていること。ここを忘れると、synchronous=OFF が思ったより効かない、という雑な結論だけが残ってしまう。
1行ずつ autocommit で INSERT するなら、たぶんかなり違う結果になる。コミットのたびに同期処理が走る形になり、FULL+delete はもっと苦しくなるはず。ただし、今回はそれを測っていないので、ここで新しい数字は書かない。次に測るなら、まずそこを分けてみたい。
この測定で言えるのは、executemany でまとめて入れる限り、PRAGMA を攻めても差は1.6倍程度だった、というところまで。SQLite 3.49.1 や Python 3.12.10 の一般的な上限を決めるには、回数も条件も不足している(memory journal の意味を少し取り違えていた)。
危険側へ倒す前に測る
この結果だけで synchronous=OFF を常用する気にはならなかった。OFF+memory は863750 rows/sまで出ているが、FULL+delete でも534050 rows/sは出ている。短いバッチなら十分に速い、と実際に試してみて感じた。
一時データを作って、壊れたら作り直せる処理なら OFF+memory を選ぶ余地はある。スクレイピング結果の中間テーブルや、テスト用のローカル DB なら向いている。一方で、消えると困る作業ログや手作業で集めたデータを入れるなら、FULL+delete か NORMAL+wal に寄せたい(短い一回測定なので順位を盛らないようにした)。
安全性を削った設定にしても、今回は派手な勝ち方をしなかった。SQLite が遅いか速いかという話ではなく、トランザクションの切り方が先に効いている。次は1行ずつ autocommit するケースと、複数回まわした中央値を測りたい。今回の1回ぶんだけで、PRAGMA の強弱を断定するのは危ない気がする。この点は失敗の記録でもある。なぜ1.6倍で止まったのかをもう少し分けるなら、次は commit 単位を変えて測ってみる必要がある(そこを見ないまま設定名だけで語ると、また外しそうだ)。