SQLiteが泣きたくなるほど遅い場合の対処法

# はじめに

恒例のお断りですが、この文章の内容は、筆者が所属している会社・団体とは一切関わりがありません。いわゆる「自主的な研究の成果の発表」というものです。

自分でちょっとしたプログラムを書いていて「ここはデータベースを使うと便利そう、でもPostgreSQLやMySQL/MariaDBを持ち出すまでもないよね」という時には、皆さん、SQLiteを使うのではないかと思います。で、ちょっと使ってみて、「うわっ、何これ。遅すぎ。やっぱりPostgreSQLとか本格的なの使わないと駄目か、、」と思ってしまった方、SQLiteの実力はそんなものではありません。

# トランザクションを使いましょう
はい、いきなり答です。トランザクションですよ、トランザクション。大事なことなので２回繰り返しました。

「トランザクションって何ですか」という人はそもそもこんな記事を読まないような気がしますが、例えばPostgreSQLのチュートリアルに[トランザクション](https://www.postgresql.jp/document/14/html/tutorial-transactions.html)という節がありますので、そちらで勉強してください。PostgreSQLの付属文書ではありますけど、そこに書いてある内容は一般的なものです。

で、具体的にどう使うかですが、データベースにデータを（たくさん）入れたい場合、以下のようなことをやるのではないでしょうか。
~~~
for (i = 0; i < データ数; i++)
   INSERT INTO my_table VALUE (データ_i, ...);
~~~
（※ 疑似コードです。）
上記のまとまりを一つのトランザクションにします。具体的には以下のようにBEGINとCOMMITで囲んでやります。
~~~
BEGIN TRANSACTION;

for (i = 0; i < データ数; i++)
   INSERT INTO my_table VALUE (データ_i, ...);

COMMIT TRANSACTION;
~~~
（※ もちろんこれも疑似コードです。）

こうすることでびっくりするくらい速くなります（速くなる場合があります）。

# 以下詳細
ここまでたどり着くまでちょっとした紆余曲折がありました。ネットで検索すると、「SQLiteを使うにはまずトランザクションから始める」みたいな書き方がしてあったり（最初に見つけたのはこんな感じのサイトでした。これはこれで正しいのだと思いますが、、、）、「トランザクションを使わないと遅いらしい」「100倍位差があるらしい」とか伝聞の形で書いてあってどこまで信用して良いものやら、、、、
ということで、意を決して「よし、それなら自分で確かめてやる」となるまで、SQLiteといろいろな単語を組み合わせた検索結果の屍の山が、、、ということで、参考になったサイトはあるはずなのですが、屍の山に埋もれてしまっているので、面倒なので発掘はしません。

## 実測結果
結果的に実行時間に近いものが計測できてしまったので、結果を書いておきます。どこまで影響するかは分かりませんが、実測した環境は以下の通りです。

- OS: FreeBSD
- CPU: Windows10は動くけどWindows11は非対応
- メモリ: 潤沢
- ストレージ: HDD
- SQLite 3.41.0
- 1593個のデータをINSERT

~~~
終了時刻 = 令和X年XX月XX日 X曜日 XX時21分03.091571秒
（トランザクション化していないINSERTを含む処理）
開始時刻 = 令和X年XX月XX日 X曜日 XX時21分46.627005秒
~~~
「トランザクション化していないINSERTを含む処理」には43.54秒掛かっていることが分かります[^start]。
~~~
終了時刻 = 令和X年YY月YY日 Y曜日 YY時30分22.871513秒
（トランザクション化したINSERTを含む処理）
開始時刻 = 令和X年YY月YY日 Y曜日 YY時30分23.327457秒
~~~
上に書いたような方法でINSERTをトランザクション化すると0.46秒になっています。なので、どこかに書いてあった「100倍位差があるらしい」というのも誇大広告ではなかったことが分かりました。（厳密には、それぞれ43.54秒、0.46秒掛かっている処理にはINSERT以外の処理も含まれています。ただし、違いはINSERTをトランザクション化するかしないかだけです。ですので、トランザクション化した部分に限れば100倍以上高速化しているのではないかと思います。）

INSERTの時に"Inserting ... "なんて表示させていたのですが、トランザクション化していない場合には、それはコンソールで表示が流れているのが見て分かりますよね、、、初期のMS-DOSでもあるまいし、今どきそんなプログラムは、、、

## 考察のようなもの
高速化の対価として何かあるのではないかというのが、気になるかもしれません。COMMITまで処理が進めば、結果は同じですから高速化で失うものは何もありません。差が出るのは、何回目かのINSERT処理で異常終了してしまった場合です。トランザクション化していない場合は、そこまでにINSERTが完了しているデータに関してはデータベースに登録されています。一方、トランザクション化した場合にはデータは一切登録されないということになると思います（0か100か、中途半端はない、というのがトランザクションの第一の意義と思いますので）。
ただ、「何回目かのINSERT処理で異常終了」というのはちょっと考えにくいです。だって、「自分でちょっとしたプログラムを書いていて『ここはデータベースを使うと便利そう』」と思ったということは、（おそらく機械的に）きれいなデータが大量に作成されているからのはずで、それが「何回目かのINSERT処理で異常終了」ということは、まずは自分のプログラムのバグの心配をするべきでしょう。
「いやいや、自分のプログラムは正常でも、PCそのものが突然終了することもあるよね」と言われれば否定はできません。ですが、SQLiteも含めて結局はそのPCの中での話ですから、「PCそのものが突然終了」したらお手上げでしょう。その場合でもデータの整合性を気にするのであれば、もうそれは完全に異次元の話ではないかと思います。

INSERT以外でも速くなるのか、というのも気になるかもしれません。大量の処理をまとめることができるのであれば、速くなるのではないかと思います。データベースにデータを登録する場合は似たような処理を数多く繰り返すということになりますので、これにぴったり当てはまるでしょう（つまり、INSERTの大活躍する場面ということになります）。あと、まとめられる大量の処理としては、「登録してあるデータ（のほとんど）を更新する」というのが思いつきますので、その場合にも当てはまるような気がします。確かめるまでの元気はありませんが。

## おまけ
そう言えば141回INSERTを繰り返すのが主な処理である別のプログラムも作っていたのでした。そちらの実行時間を計測すれば、より直接的な計測ができますね。

トランザクション化していない版
~~~
$ time ./b.out
（省略）

real	0m4.002s
user	0m0.805s
sys		0m0.033s
~~~

トランザクション化した版
~~~
$ time ./a.out
（省略）

real	0m0.743s
user	0m0.659s
sys		0m0.007s
~~~

データ数が（上のものに比べて）少ないためか、100倍とまでは行きませんでした。と言っても体感できるくらいには速くなってますが。

## さらにおまけ
[SQLiteのサイト](https://www.sqlite.org/lang_transaction.html)によれば
- "BEGIN TRANSACTION"は"BEGIN"だけでも良い（"TRANSACTION"は省略可能）
- "COMMIT TRANSACTION"は"COMMIT"だけでも良い（"TRANSACTION"は省略可能）
- "COMMIT"は"END"でも良い（"COMMIT TRANSACTION"を"END TRANSACTION"と書いても良い）

らしいです。
サイトにはもっと色々書いてありますが、たぶんここから先は沼です。興味のある方は独力で頑張ってください。

[^start]: え、「終了」と「開始」の言葉の使い方がおかしいのではないか、ですか。いえ、これで正しいのです。時間を測りたかったのは、INSERTを含む処理そのものではありませんので。「時間を測りたい処理をして、INSERT」というプログラムを繰り返し起動したので、結果的にINSERTの時間を測るのに近いことができてしまった、ということです。