プログラミング
SQLiteのパフォーマンスをプリソートで向上させる
SQLite improving performance with pre-sort (andersmurphy.com)
要約
この記事では、SQLiteへのランダムなデータ挿入がUUID4と同様に遅くなる問題に対し、プリソート(事前ソート)がどのようにパフォーマンスを向上させるかを探ります。特に、セッショントークンなどに使われる予測不可能なランダムなIDを挿入する際に、事前にデータをソートすることで、挿入速度が2〜3倍に改善されることを実証しています。
全文翻訳
SQLiteのパフォーマンスをプリソートで向上させる
2026年6月7日
前回の記事では、UUID4のランダム性が挿入速度に大きな影響を与えることと、UUID7がその問題をどのように解決できるかを示しました。しかし、ランダムな特性を持つ他のデータについてはどうでしょうか?UUID7で問題を解決できない場合はどうすればよいでしょうか?
ランダムなデータ
この記事で説明されているのと同様に、SecureRandomによって生成された160ビット(20バイト)のランダムな値を使用します。なぜでしょうか?セッショントークンなどでは、UUID7を使いたくないからです(情報漏洩の可能性、ユースケースに対して十分なエントロピーがない可能性など)。これは、プライマリキーがランダム(またはより具体的には順不同)な任意のデータを表すこともあります。
それらを生成するコードは次のとおりです。
```clojure
(defn random-unguessable-uid [] (let [buffer (byte-array 20)] (.nextBytes secure-random buffer)))
```
これがどのように機能するか見てみましょう。
```clojure
(d/q writer ["CREATE TABLE IF NOT EXISTS event(id BLOB PRIMARY KEY, data BLOB) WITHOUT ROWID"])
(dotimes [_ 10] (time (d/with-write-tx [db writer] (dotimes [_ 1000000] (d/q db ["INSERT INTO event (id, data) values (?, ?)" (random-unguessable-id) data])))))
```
結果:
```
total rows time in ms
1000000 2478
2000000 4927
3000000 6262
4000000 7195
5000000 8257
6000000 8704
7000000 9244
8000000 9771
9000000 10387
10000000 11103
```
1秒あたり約10万回の挿入です。UUID4と同様に、動作は遅いです。
プリソート
B+ツリーの決定的な特徴は、それが順序付けられていることです。シーケンシャルな書き込みは高速です。ランダムなデータは、ページのスラッシング、ページ分割、ツリーの再バランスを引き起こし、これに反します。これは良い状況ではありません。しかし、私たちはすでにデータをバッチ処理しています。では、挿入前にソートしたらどうなるでしょうか?
まず、ランダムなIDを高速に比較する方法が必要です。20バイトなので、それらを反復処理したくありません。代わりに、最初の8バイトだけを取り出して、それをlongに変換します。十分なソートを得るために、バイト配列全体を比較する必要はないでしょう。重要なのは、この比較がSQLiteと一致するように符号なしであることです。2つのBLOB値が比較されると、結果はmemcmp()を使用して決定されます。注:これはランダムなデータをソートする最も速い方法ではないかもしれません。私はインターネット接続なしでブログ記事を書いています(気が散りすぎるので)。検索も最近はひどいです。だから、ほとんどはdash(zealはLinux版)を使ったあいまいなテキスト検索で昔ながらのオフラインドキュメントで作業しています。Java/Clojureでバイト配列をソートするより速い/より良い方法を知っていたら教えてください!
```clojure
(defn bytes->long [^bytes bytes] (-> (ByteBuffer/wrap bytes 0 8) (ByteBuffer/.getLong 0)))
(defn byte-compare
"Compares the first 8 most significant bytes of a byte array. Big Endian (matches SQLites blob sort)."
[a b]
(Long/compareUnsigned (bytes->long a) (bytes->long b)))
```
これがどのように機能するか見てみましょう。
```clojure
(d/q writer ["CREATE TABLE IF NOT EXISTS event(id BLOB PRIMARY KEY, data BLOB) WITHOUT ROWID"])
(dotimes [_ 10] (time (d/with-write-tx [db writer] (->> (repeatedly 1000000 random-unguessable-id) (sort byte-compare) (run! (fn [id] (d/q db ["INSERT INTO event (it, data) values (?, ?)" id data])))))))
```
結果:
```
total rows time in ms
1000000 1987
2000000 2251
3000000 2296
4000000 2614
5000000 2687
6000000 3244
7000000 3118
8000000 3311
9000000 3485
10000000 3835
```
面白いですね!オーバーヘッドにもかかわらず、バッチをソートすることでパフォーマンスが約2〜3倍向上します。
結論
この投稿が、バッチ処理されたデータが、順不同のデータを扱う際にプリソートのような有用な最適化をどのように可能にするかを示すのに役立つことを願っています。
完全なベンチマークコードはこちらで入手できます。
ドラフトを読んでフィードバックをくれたDatastar discordの皆さん、ありがとうございました。議論hackernewsreddit