記事をどれだけ読んだかを示します

sqler: 最終スコアカード

パート1で18件の公正性問題を発見し方法論を書き直した。パート2でオーバーヘッドの原因をPydanticに特定しエクスポートを修正した。パート3で5つのマイルストーンにわたり4つのボトルネックを修正した。この記事でフルスイートを最後に一度実行する。

パート4/5。パート1: 方法論 | パート2: オーバーヘッド | パート3: 修正 | パート5: カラムナーベースライン


実行内容

24シナリオ、各20イテレーション、3ウォームアップ、--storage both、アーム交互実行、GC分離。4スケール:

スケール計測件数実行時間
Medium (50K)435~18分
Large (100K)432~40分
Xlarge (500K)429~3時間
Xxlarge (1M)429~6.5時間
合計1,725~10.5時間

最適化前の検証と合わせると: シリーズ全体で3,288件の計測。

環境: Python 3.12.11 | sqler 1.2026.3.5 | SQLite 3.50.4 | Linux x86_64(8コア)


スコアカード

最終スコアカード

1M行、ディスクモード:

カテゴリ比率評価
バルクインサート0.89x生のsqliteより速い
Export JSON0.97xパリティ
JSONL(IDなし)0.98xパリティ
FTSランク1.00x完全パリティ
バックアップ/リストア1.01x透過的
FTS再構築1.03xほぼパリティ
any_where1.04xほぼパリティ
Export JSONL1.06xほぼパリティ
クエリ1.03〜1.15x安定したオーバーヘッド
集計1.06〜1.13x安定したオーバーヘッド
JSON操作1.07〜1.14x安定したオーバーヘッド
Export CSV1.34x縮小不能

CSVエクスポートを除いて全て≤1.15x。クエリレイヤーは5〜12%のオーバーヘッドを加える。これはクエリのコンパイル、アダプターレイヤー、パート1で記録されたrow factoryアーティファクトのコストだ。いずれもスケールで増大しない — パーセンテージ自体よりその事実の方が重要だ。


全最適化が保持される

スケール横断検証のヘッドライン: どのスケールでもリグレッションなし。

最適化50K100K500K1M修正前
バルクインサート(M-3)0.96x0.91x0.91x0.89x1.87〜1.92x
any_where(M-2)1.02x1.03x1.05x1.04x1.47〜1.51x
FTS再構築(M-1)1.04x1.02x1.07x1.03x3.78〜4.65x
FTSランク(M-4)0.98x1.01x1.00x1.00x500K以上で1.50x
ハイドレーション(M-5)5.15x4.62x5.01x4.97x(新機能)

バルクインサートはスケールが大きくなるほど効率が上がる: 50Kで0.96x → 1Mで0.89x。マルチ行INSERTパターンのパーサー節約は行数が増えるほど複利になる。50回のSQL呼び出しと100万回の個別文では話が違う。

FTSランクの方がより重要な検証だ。1M行で: 1,037ms対1,033ms。これは以前唯一スケールとともに悪化していたシナリオ(50Kの0.95xから1Mの1.50xへ)で、パート3のシングルJOIN修正がリグレッションを完全に解消した。


スケールを横断した比率の安定性

比率の安定性

比率はフラットな線だ。バルクインサートはスケールでわずかに改善し、他は全て計測ノイズの範囲内で保持される。これはオーバーヘッドが比例的であることを意味する: 行ごとのコストであり、アルゴリズム的なものではない。ORMは基礎となるSQL作業より速く増大する複雑性を導入していない。

クエリ比率についての注記: パート1のv1.2データはメモリモードでクエリが0.95〜0.97xを示しており、sqlerが生のsqlite3より速いように見えた。最終スイートはディスクモードを計測しており、パート1で記録されたrow factoryアーティファクトがもはや真のオーバーヘッドを隠せない。修正後の数値は1.03〜1.15x。これが最適化パスで導入されたリグレッションではなく、クエリコンパイルとアダプターラッピングの実際のコストだ。

詳細内訳(ディスクモード)

クエリ:

種類50K100K500K1M
等値フィルタ(インデックスなし)1.13x1.21x1.24x1.14x
範囲50%選択率1.15x1.15x1.15x1.11x
複合5述語1.03x1.06x1.04x1.08x
Top-N(1000)1.12x1.11x1.12x1.11x

インデックスなし等値フィルタが最大の分散を示す(スケールで1.13〜1.24x)。より多くの述語を持つ複合クエリはオーバーヘッドが小さい。SQLiteがsqlerの固定per-queryコストに対して条件評価に多くの時間を費やすからだ。

JSON操作:

種類50K100K500K1M
配列contains1.05x1.05x1.05x1.09x
配列isin1.01x1.03x1.02x1.07x
ネストフィールド(深さ3)1.13x1.12x1.12x1.14x

深さ3のネストフィールドアクセスは一貫して~13%のオーバーヘッド。フラットな配列操作はパリティに近い。

集計:

種類50K100K500K1M
sum1.17x1.15x1.17x1.13x
avg1.19x1.13x1.17x1.13x
min1.08x1.16x1.15x1.09x
max1.11x1.07x1.12x1.06x

集計はスケールとともにパリティに収束する。1M行では全集計が1.13x未満。


1M行での絶対ウォールクロック時間

比率は割合を示す。絶対値は気にする必要があるかを示す。

操作sqlersqliteオーバーヘッド
FTS再構築32.3s31.4s+0.9s
バルクインサート7.0s7.9s−0.9s
any_where6.5s6.2s+0.3s
Export CSV10.3s7.7s+2.6s
複合5述語クエリ2.4s2.2s+0.2s
FTSランク1.04s1.03s+0.01s
等値フィルタ(インデックスなし)1.2s1.1s+0.1s
バックアップ614ms610ms+4ms

最大の絶対ペナルティはCSVエクスポート: 1M行でフィールドごとの抽出オーバーヘッドによる2.6秒の追加コスト。他は全て100万行で1秒未満の追加コスト。数百〜数千行を返すクエリ(実際のほとんどのワークロード)では、オーバーヘッドは1桁ミリ秒だ。

バルクインサートはexecutemany()の代わりにチャンク化マルチ行INSERTを使うことで1Mで0.9秒節約する。


縮小不能なもの

CSVエクスポート(1.34x): JSONdictからのフィールドごとの抽出と、フィールドごとの_serialize_value()。両アームともJSONをパースする。sqlerのオーバーヘッドはCSVフォーマットが要求するdict-to-row変換だ。C levelのJSON-to-CSVコンバーターで縮小できるかもしれないが、Pythonでは構造的な問題だ。

クエリオーバーヘッド(5〜12%): クエリのコンパイル、アダプターラッピング、パート1で記録されたrow factoryアーティファクト。1M行で5述語の複合クエリでは、絶対オーバーヘッドは200ms。インデックス付きルックアップとTop-Nクエリでは絶対オーバーヘッドは10ms未満。10msが問題かどうかはユースケース次第だ。

集計オーバーヘッド(6〜13%): クエリオーバーヘッドと同じ原因。1M行で最も高価な集計(sum)は1.13x、約100ms追加。


msgspecリードパス

M-5でSQLerMsgspecModelSQLerLiteModelのオプトイン代替として追加した。ハイドレーションが重要なリード重視ワークロード向け:

パスLiteデフォルト比
SQLerLiteModel付きqueryset.all()1.0x(ベースライン)
SQLerMsgspecModel付きqueryset.all()1.46x速い
queryset.as_dicts()~2.8x速い

as_dicts()パスはモデルインスタンスが不要な場合に最速の生dictを返す。msgspecパスは完全な型安全性をdataclassの1.46倍の速度で提供する。詳細はパート3で。

ハイドレーションの再現性(4回の独立実行)

実行コンテキスト純粋validateエンドツーエンドall()(ディスク)
Mediumスイート5.15x1.47x
Largeスイート4.62x1.43x
Xlargeスイート5.01x1.46x
Xxlargeスイート4.97x1.43x

ハイドレーションベンチマークはスケールパラメータに関わらず固定50K行を使う。各スケールのフルスイートの一部として実行することで4回の独立した計測が得られる。比率は高い再現性を示す。


既知の注意点

  1. Row factoryアーティファクト(3〜6%): ベースラインは文字列キーアクセスでsqlite3.Rowを使い、sqlerは整数インデックスアクセスを使う。これにより一貫してsqlerが読み取りで~5%速く見えるが、実際にはそうではない。パート1で記録済み。

  2. 単一マシン: 全計測が1台のマシン(Linux x86_64、8コア)で実施。異なるハードウェア、OS、Pythonバージョンでは比率が変わる可能性がある。

  3. update/deleteベンチマークなし: スイートはinsertとqueryを広範囲に計測しているが、スケールでのupdate()delete()はベンチマークしていない。

  4. 同時書き込み競合なし: 楽観的ロックは正確性についてテストされているが、競合下でのスループットはテストしていない。

  5. 100K CSVの異常値: 100Kの1回でCSVエクスポートが0.32xを示した(sqliteベースラインが期待される~716msの代わりに2,979msかかった)。環境的な外れ値で他のスケールでは再現不可。トレンド分析から除外。


自分で実行する

# ベンチマーク依存関係を含めてインストール
uv sync --all-groups

# mediumスケールで実行(~20分、メモリ + ディスク)
uv run python -m benchmarks run --scale medium --storage both

# largeスケールで実行(~40分)
uv run python -m benchmarks run --scale large --storage both

# 最新結果からチャート生成
uv run --group benchmarks python -m benchmarks plot

# 全シナリオをリスト
uv run python -m benchmarks list

ベンチマークスイートはPRAGMAの一致、アーム交互実行、GC分離を自動で適用する。結果はbenchmarks/results/にJSONで保存される。


この旅の全体像

このシリーズは同じストーリーを語る22のベンチマークから始まった: sqlerは速い。そのストーリーは間違っていた。正確には、誰も問わなかった質問への答えだった。「sqlerはベースラインより良い設定を持っていると速いか?」は有用な問いではない。

敵対的な監査で4ラウンドにわたり23件の公正性問題が発見された。公正な書き直しで全ての数値が悪化した。最適化パスで5つの実際のボトルネックを修正した。そのうち2つはコードのバグではなくベンチマークのバグだった。

最終的な数値: バルクインサート0.89x(生より速い)、FTSランク1.00x(完全パリティ)、他は全て≤1.15x、1.34xの縮小不能なギャップが1つ。50K、100K、500K、1M行で、1,725件の計測と10.5時間の実行時間をかけて確認。

これらの数値が十分かどうかは何を作っているかによる。ほとんどのワークロード(数千行をクエリしてエクスポートする類)では、オーバーヘッドは絶対値で10ms未満だ。100万行のバルク操作では、sqlerはナイーブなexecutemany()アプローチと同等かそれより速い。ORMレイヤーは無料ではないが、コストは安定していて予測可能で、その下にあるSQL作業に比べて小さい。

最初から読む: パート1: 方法論