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

sqler: カラム型ベースラインとの比較

パート1〜4はORMオーバーヘッドを計測した。sqler対raw sqlite3、両方とも _id INTEGER, data JSON でデータを格納。両アームは json_extract() でクエリし、読み込み時にJSONをパースする。ギャップは5〜15%で、それが抽象レイヤーのコスト — クエリコンパイル、アダプターラッピング、Pydanticのハイドレーション。

本稿は別の問いを立てる。「ORMのコストは何か」ではなく、「ドキュメント指向アーキテクチャ自体のコストは何か」という問いだ。

パート5。パート1: 方法論 | パート2: オーバーヘッド | パート3: 修正 | パート4: スコアカード


比較の構成

sqlerアーム: Pydanticモデル、json_extract() クエリ、JSONブロブストレージによる完全なORM。出荷時の開発者体験そのまま。

カラム型アーム: 従来の型付きスキーマ — name TEXT, value INTEGER, category TEXT, score REAL。カラムへの直接アクセス、json_extract() なし、スカラーフィールドの json.loads() なし。カラムごとのバインディングによる標準的な executemany() インサート。

両アームで共通:

  • 同一の接続セットアップ(同じ create_conn() 関数)
  • 同一のPRAGMA(WAL、64MBキャッシュ、normal sync)
  • 同一のデータ(DocumentGenerator、seed=42、“small” プロファイル)
  • 同一の計測方法論(PrecisionTimer、20イテレーション、3ウォームアップ、GC分離、アーム交互実行)
  • デュアルストレージモード(メモリとディスク)

tags フィールドは両アームともJSONのまま — 配列はジャンクションテーブルなしではカラム型での表現が難しい。比較はスカラーフィールドに絞る: namevaluecategoryscore


数値

8シナリオ、5スケール(10K〜1M行)、--storage both。ディスクモードの数値を報告;メモリモードの比率は全ケースでディスクの10%以内。

クエリと集計

シナリオ10K50K100K500K1M
等値フィルタ9.4x11.3x11.2x10.3x11.2x
範囲フィルタ3.2x3.2x3.1x3.2x3.2x
複合フィルタ5.7x6.0x6.0x5.8x6.0x
Top N5.0x5.7x5.9x6.8x7.3x
集計9.2x9.5x9.4x9.5x9.5x

等値フィルタは json_extract() コストの最も明確な指標だ。両アームとも1フィールドを1値と照合するが、sqlerは WHERE json_extract(data, '$.value') = ? を実行し、カラム型アームは WHERE value = ? を実行する。11xのギャップはほぼ全て抽出関数のコスト;Pydanticのハイドレーションが結果セットにさらにコストを重ねる。

範囲フィルタは全スケールで3.2xと安定している。低い比率はより高い選択性を反映している — より多くの行がフィルタを通過するため、JSON抽出の行ごとのオーバーヘッドが両アームともに行う結果処理に対して薄まる。

Top Nはスケールとともに悪化する: 10Kで5.0x、1Mで7.3x。ORDER BY json_extract(data, '$.score') はソート前に全候補行で抽出関数を評価しなければならない;ORDER BY score は型付き値を直接読む。抽出がホットパスにある場合、SQLiteのソートバッファ管理のスケールが異なるためギャップが広がる。

集計は〜9.5xで平坦。SUM(json_extract(data, '$.value'))SUM(value) — 抽出関数は全行で実行され、償却は不可能。

インサート

シナリオ10K50K100K500K1M
バルクインサート6.8x3.1x2.5x1.7x1.3x

sqlerはチャンク化されたマルチ行INSERT(INSERT INTO t VALUES (...), (...), ...)を使用;カラム型アームはカラムごとのバインディングで executemany() を使用。小規模ではsqlerの固定オーバーヘッドが支配的。1M行ではチャンク化アプローチがほぼ追いつく: JSONシリアライゼーション+マルチ行バッチング対行ごとのバインディング。収束は実際だが出発点が重要;典型的なアプリケーションスケール(10K〜100K)ではギャップは2.5〜6.8x。

エクスポート

シナリオ10K50K100K500K1M
CSVエクスポート1.7x1.9x1.9x1.9x1.9x
JSONLエクスポート1.1x1.1x1.0x1.0x1.0x

JSONLエクスポートはスケールで同等性に達する。これがドキュメントアーキテクチャに構造的な優位性がある唯一のシナリオだ: データがすでにJSONである。sqlerは data を読んで書く;カラム型アームは個々のカラムからJSONを組み立てなければならない。1M行で1.0xに収束。

CSVエクスポートは1.9xで安定。sqlerはJSONディクトからフィールドごとの抽出コストを払う;カラム型アームは型付きカラムを直接読む。


数値の意味

ドキュメント指向アーキテクチャは、型付きカラムと比較してクエリと集計で3〜11xのコストがかかる。それが json_extract() の価格だ。これはバグではなく、構造化データをJSONブロブとして格納することの根本的なトレードオフだ。

文脈として: パート1〜4で計測したORMオーバーヘッドは、同じJSONストレージの上に1.03〜1.15xだった。ドキュメントアーキテクチャ自体が高コストな部分;抽象レイヤーはそれに比べればノイズだ。

コストの出所

  1. 行ごとの json_extract() — 全クエリ、フィルタ、ソート、集計が型付きカラムを直接読む代わりに json_extract(data, '$.field') を呼ばなければならない。SQLiteのJSONパーサーは高速だが、全行で文字列パースを行っている。

  2. Pydanticのハイドレーション — sqlerはモデルインスタンスを返す;カラム型アームは sqlite3.Row から構築したdictを返す。ハイドレーションコスト(パート2で詳述)が抽出コストの上に重なる。

  3. インサート時のJSONシリアライゼーション — sqlerはドキュメント全体に json.dumps() を実行する;カラム型アームは型付き値を直接バインドする。スケールではsqlerのマルチ行INSERTストラテジーがこのコストの一部を相殺する。

コストの出所でないもの

  • PRAGMA: 同一。同じ関数、同じ設定。
  • 接続セットアップ: 同一。共有ベースラインモジュールから同じ create_conn()
  • データ: 同一。同じジェネレーター、同じシード、同じドキュメント。

トレードオフ

集計で10xのギャップは壊滅的に聞こえる。それが重要かどうかは何を作っているかによる。

型付きカラムが明確に勝つとき: 分析、レポーティング、バルク集計、ほとんどの行をスキャンしてスカラーフィールドを計算するワークロード全般。クエリが SELECT AVG(score) FROM t WHERE category = 'tech' のような形なら、カラム型スキーマは常に速い。そのクエリは全行に触れる;全行での json_extract() は純粋なオーバーヘッドだ。

ギャップが縮まるか消える場合: ドキュメントエクスポート(JSONLは同等)、スケールでのバルクインサート(1Mで1.3x)、データが自然にネスト・可変スキーマのドキュメントとして存在し、そうでなければジャンクションテーブルやEAVパターンが必要なワークロード。

速度がアーキテクチャ選択の基準でない場合: スキーマの柔軟性。sqlerモデルはPythonクラスだ;フィールドの追加はクラスの変更であり、ALTER TABLE の実行ではない。オプションフィールド、ネストオブジェクト、可変長配列を持つドキュメントはJSONに自然にマッピングされる;カラムにフラット化するには、JSONストレージが先送りにする設計上の決定が前もって必要だ。

コストは実在し、予測可能で、比例的だ。データより速く増えない。集計で10xが許容できるかどうかは、絶対値でその集計が1msかかるか1秒かかるかによる — ほとんどのアプリケーションスケールのデータセットでは前者だ。


既知の注意点

  1. インサートストラテジーの非対称性: sqlerのチャンク化マルチ行INSERT対 executemany() は対等なSQL比較ではない。実際の使用を反映している — これが各ツールが実際にデータを挿入する方法だ — しかしスケールでの収束は異なるストラテジーの産物であり、純粋なストレージモデルの効果ではない。

  2. 戻り型の非対称性: sqlerはPydanticモデルインスタンスを返す(〜1,600ns/行のハイドレーションコスト);カラム型アームはプレーンdictを返す。これは意図的 — 完全な開発者体験を計測している — しかし json_extract() のコストだけを分離するには両アームが同じ型を返す必要がある。

  3. カラム型アームにインデックスなし: 両アームともインデックスを使用しない(主キー以外)。カラム型アームに CREATE INDEX ON bench(value) を追加すると等値・範囲フィルタのギャップが広がる;sqlerアームに json_extract(data, '$.value') の関数インデックスを追加するとギャップが狭まる。生のストレージモデルコストを計測するため両方とも省略した。

  4. シングルマシン: パート1〜4と同じ注意点。Linux x86_64、8コア、Python 3.12、SQLite 3.50。


自分で実行する

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

# tabularシナリオをリスト
uv run python -m benchmarks list | grep tabular

# mediumスケールで実行(〜20分)
uv run python -m benchmarks run --suite tabular --scale medium --storage both

# largeスケールで実行
uv run python -m benchmarks run --suite tabular --scale large --storage both

シリーズサマリー

5本の記事、5通りの問い:

記事問い答え
パート1このベンチマークは公平か?いいえ。18の問題を発見;全面書き直し。
パート2オーバーヘッドはどこから来るか?Pydanticのハイドレーションとエクスポートパス。
パート3修正できるか?5つの修正;バルクインサートは0.89xに。
パート4ORMの最終コストは?クエリで1.03〜1.15x;CSVで1.34x。
パート5アーキテクチャのコストは?クエリで3〜11x;JSONLエクスポートで1.0x。

ORMは安い。ドキュメントモデルは高い。ドキュメントモデルが高すぎるかどうかはワークロードによる;ほとんどのアプリケーションにとって、柔軟性はコストに見合う。

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