Cloud Spannerクエリオプティマイザーの深層:CBOと分散実行プランの完全解剖
テックリードの私だ。本日は、Cloud Spannerの心臓部であり、数千ノード規模の分散ストレージからミリ秒単位でデータを釣り上げる「クエリオプティマイザーの内部構造」について話をしよう。
世間では「Spannerはリレーショナルデータベースのスケールアウト版だから、適当にSQLを書いても勝手に速くしてくれる」という誤解がまかり通っている。しかし、現実の現場で大規模なシステムを構築し、数億〜数千億行のデータを扱うとき、オプティマイザーの挙動を理解していないエンジニアが書いたクエリは、いとも簡単につまらないフルスキャン(全件走査)を引き起こし、CPU使用率を天井知らずに跳ね上げる。
今日は、Spannerのコストベースオプティマイザー(CBO)が内部で何をやっているのか、統計情報がどう分散実行プランを支配するのか、そして我々エンジニアが「どう設計し、どうSQLを書くべきか」をロジカルに伝授する。
—
1. Cloud Spannerオプティマイザーの全体像
Cloud Spannerのクエリ処理パイプラインは、大きく分けて以下の4つのフェーズで構成されている。
1. パース (Parsing & Semantic Analysis): SQL文を抽象構文木(AST)に変換し、スキーマ定義と照合する。
2. 書き換え (Query Rewriting): サブクエリのフラット化、冗長なJOINの排除、プレディケード(条件句)のプッシュダウンなど、論理的な最適化を行う。
3. コストベース最適化 (Cost-Based Optimization: CBO): 複数の物理実行プランを生成し、統計情報(Statistics)をもとに推定コストを算出して最小のものを選択する。
4. 分散実行 (Distributed Execution): 選択されたプランをRPCとスプリット(Split)単位の並列処理に変換し、マルチリージョン/マルチノードで実行する。
この中で、エンジニアが最も意識すべきは 「3. CBOと統計情報」 およびそれによって生成される 「4. 分散実行プラン」 だ。
—
2. コストベースオプティマイザー(CBO)と統計情報の真実
CBOの性能は、「統計情報の鮮度と精度」に完全に依存している。統計情報が狂っていれば、世界最高峰のオプティマイザーであっても、目隠しで爆弾処理をするような間違ったプランを選択する。
統計情報の自動収集と内部メカニズム
Spannerのバックグラウンドプロセスは、テーブルやインデックスのデータ分布を定期的にサンプリングし、以下の統計情報を自動収集している。
- 行数 (Row Count)
- データサイズ (Byte Size)
- 列ごとの値のカーディナリティ (Distinct Value Count)
- ヒストグラム (Value Distribution Histograms): 特定の範囲にデータがどれくらい偏っているかを示す分布図。
これがなぜ重要か? 例えば、`WHERE status = ‘ACTIVE’` という条件があったとする。
- `status` の大半が `ACTIVE`(カーディナリティが低い)場合:インデックスを使うよりフルスキャンした方が速い。
- `status` の大半が `INACTIVE` で `ACTIVE` は全体の1%未満(カーディナリティが高い)場合:セカンダリインデックスを使ったルックアップが圧倒的に速い。
CBOはこの判断を、テーブルの行数やヒストグラムの統計情報に基づいて数学的に行っている。
⚠️ 現場の罠:古い統計情報による「プラン劣化」
データが一気にバッチ挿入されたり、大規模な削除が行われた直後は、統計情報が追いついていないことがある。この状態で複雑なJOINを含むクエリを実行すると、オプティマイザーは古い統計情報をベースにコスト計算を行い、最悪の分散実行プラン(Nested Loopの多用など)を選択してしまう。
これを防ぐため、大規模なデータ移行やバッチ処理の後には、必要に応じて統計情報の更新(またはデフォルトの自動更新の挙動の把握)を意識することがシニアエンジニアの嗜みだ。また、オプティマイザーバージョン(`optimizer_version`)を固定している場合、Spannerのエンジンアップデートによる自動的な性能向上を取り逃がしている可能性があるため、定期的なバージョンの見直しが必要となる。
—
3. 分散実行プランの生成と「スプリット」の概念
Spannerの真骨頂は、データが主キーの範囲ごとに「スプリット(Split)」という単位に分割され、世界中のノードに分散配置されている点にある。
オプティマイザーが生成する実行プランは、単一ノードの上で動くものではない。「どのスプリットに対して、どの計算を分散してプッシュダウン(Push-down)するか」を表現したツリー構造になっている。
分散プランの主要なオペレータ
1. Distributed Cross Apply / Distributed Union:
複数スプリットに対する並列フェッチや、親テーブルの各行に対して子テーブルを効率的に引くための分散結合オペレータ。
2. Distributed Group By (Partial & Final):
データ量が多い集計クエリ(`GROUP BY`)において、まずは各スプリット(ローカル)で部分集計(Partial)を行い、その結果を集約ノードで最終集計(Final)する。ネットワーク転送量を劇的に減らすための高度な最適化。
実行プランの可視化:`EXPLAIN` と `PROFILE` を使いこなせ
コードレビューで「このクエリ遅い気がする」という感覚的な議論は禁止だ。必ず `EXPLAIN`(コスト見積もり)または `PROFILE`(実測値入り)を使用し、グラフィカルコンソールやAPIで実行プランを確認しろ。
— 実測値(CPU使用時間や読み込み行数)を含むプロファイル実行
EXPLAIN PROFILE
SELECT
c.CustomerID,
SUM(o.TotalAmount)
FROM Customers c
JOIN Orders o ON c.CustomerID = o.CustomerID
WHERE c.Region = ‘APAC’
GROUP BY c.CustomerID;
この出力結果を見る際、以下のポイントに目を光らせるんだ:
- Rows returned: 期待した行数に対して、途中のオペレータで何行やり取りしているか(途中で爆発的に増えていないか?)。
- Distributed Union: スプリットをまたぐ無駄な全件スキャンが発生していないか。
- Full Scan vs Index Scan: 意図したインターリーブ構造やインデックスが使われているか。
—
4. 堅牢な設計パターンとパフォーマンス上の注意点
オプティマイザーを味方につけ、数千QPSを安定して捌くための設計原則を授けよう。
① インターリーブ(Interleaved Tables)の活用
親子関係にあるテーブル(例:`Customers` と `Orders`)で、`Orders` の主キーに `CustomerID` を含め、インターリーブ構造で定義せよ。
これにより、親と子のデータがストレージ上で物理的に同じスプリット(または近傍)にコロケーションされる。オプティマイザーはこれを利用して、高コストな分散JOINではなく、ローカルなマージJOINやインデックスルックアップを選択できるようになる。
② プレディケードのプッシュダウンを阻害しない書き方
オプティマイザーの書き換えフェーズを無効化するようなSQLを書くな。
- NG例: 列に対して関数をかける(例:`WHERE UPPER(Email) = ‘TEST@EXAMPLE.COM’`)
- 理由: インデックスが張ってあっても、関数を通してしまうとCBOはインデックスを使えなくなり、フルスキャン(Distributed Full Scan)に落ちる。
- OK例: データの正規化を事前に行うか、プレフィックス検索等で対応する(例:`WHERE Email = ‘test@example.com’` ※大文字小文字を区別する設計にする、あるいは検索用の正規化カラムを持たせる)。
③ `@{FORCE_INDEX}` の慎重な使用
どうしてもオプティマイザーが最適解を選ばず、古い統計情報や特殊なデータ偏在によって誤ったプランを選ぶ場合の最終手段として、ヒント句が用意されている。
SELECT
FROM Orders@{FORCE_INDEX=OrdersByDate}
WHERE OrderDate >= ‘2023-01-01’;
【テクニカルリードからの警告】
`FORCE_INDEX` やオプティマイザーのバージョン固定は、いわば「延命治療」だ。スキーマ変更やデータの成長によって、将来的にそのヒント句が逆に足かせとなり、致命的なパフォーマンス劣化を引き起こす。ヒント句を使う前に、スキーマ設計、主キー設計、そして統計情報の健全性を疑うべきだ。
—
5. まとめ
Cloud Spannerのクエリオプティマイザーは、単なる「SQLの翻訳機」ではない。数千の分散ストレージノードのトポロジ、データの統計的分布、そしてネットワークコストを瞬時に計算し、最適な分散実行プランを編み出す極めて洗練されたAIエンジンのようなものだ。
我々エンジニアがやるべきことは、オプティマイザーと敵対することではなく、「オプティマイザーが正確なコスト計算をしやすい美しいスキーマと、適切な統計情報が維持されるデータモデル」を提供することに他ならない。
設計レビューで「なぜこのプランになるのか」「統計情報は適切か」をロジカルに語れるエンジニアであれ。
健闘を祈る。
コメント