HN 日本語サマリー

← 一覧へ戻る
プログラミング

SQLiteのパフォーマンスをプリソートで向上させる

SQLite improving performance with pre-sort (andersmurphy.com)

43 pointsby tosh4 コメント

要約

この記事では、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