【実務・中級編】 クエリ最適化コストモデル – Cloud Spanner

Spannerクエリオプティマイザの深淵:分散実行コストモデルをハックする

テックリードの私だ。コードレビューや設計レビューで、「なぜこのクエリはこんなにスキャンコストが高いんだ」「なぜインデックスが効かないんだ」と頭を抱えているエンジニアを何度も見てきた。

Cloud Spannerは「マジックボックス」ではない。リレーショナルデータベースの堅牢性と、グローバル分散の水平スケーリングを両立させた怪物だが、その内部で動くCost-based Optimizer(CBO:コストベースオプティマイザ)の挙動を理解していなければ、宝の持ち腐れだ。

今回は、Spannerのオプティマイザが内部で何を考え、どうやって実行プランを選択しているのか、その「コストモデルの核心」をロジカルかつシャープに伝授しよう。

—

1. Spanner CBOの前提:共有何もない(Shared-Nothing)環境でのコスト計算

単体ノードのRDB(PostgreSQLやMySQLなど)であれば、オプティマイザの関心事は主に「ディスクI/O(ランダム vs シーケンシャル)」と「メモリバッファヒット率」だ。

しかし、Cloud Spannerの本質はグローバル分散ストレージ(Colossus)と、メモリ上に展開される分布式ストレージエンジン(MilliByte)の分離、そしてPaxosグループ間をまたぐ分散処理にある。

SpannerのCBOが評価するコストは、単なるCPUサイクルやディスクアクセス数ではない。最大のボトルネックとなるのは以下の3点だ。

1. ストレージ層(Colossus/MilliByte)からのデータフェッチコスト
2. 分散トランザクション・ノード間通信(RPC)のオーバーヘッド
3. CPUバウンドな処理(演算、動的フィルタリング、シリアライズ)

これらを数値化し、最小のコストとなる実行プランを導き出すのがSpannerのCBOの仕事である。

—

2. 統計情報(Statistics)のメカニズム

オプティマイザが優秀なプランを組めるかどうかは、統計情報の鮮度と精度に100%依存している。統計情報が狂っていれば、オプティマイザは目隠しで地雷原を歩くようなものだ。

自動統計収集とパッケージ

Spannerは、バックグラウンドで定期的にテーブルとインデックスの統計情報を収集している。
ここで重要なのが 統計パッケージ(Statistics Packages) だ。Spannerでは、オプティマイザの挙動を変更するために、異なる統計情報セットを切り替えることができる。

— 現在使用されている統計パッケージの確認
SELECT FROM INFORMATION_SCHEMA.DATABASE_OPTIONS
WHERE option_name = ‘optimizer_statistics_package’;

もし、大規模なデータ移行やバッチ処理の直後にクエリ性能が急激に劣化したなら、それは統計情報が追いついていない証拠だ。手動で統計情報を更新するか、適切なタイミングで統計収集を促す必要がある。

オプティマイザバージョンと予測の歪み

Spannerは `optimizer_version` を指定できる。最新バージョンでは、より高度なカーディナリティ(行数)推定アルゴリズムが導入されている。

— クエリ単位でのオプティマイザバージョンの固定(ヒント句)
SELECT / @{ optimizer_version = 5 } /
UserID,
SUM(Amount)
FROM Transactions
GROUP BY UserID;

実務での鉄則として、クリティカルなバッチやレイテンシが命取りになるAPIクエリでは、オプティマイザバージョンを固定し、Google側の自動アップデートによる性能変動リスクを排除するのがシニアの選択だ。

—

3. 分散実行プランのコスト評価手法

Spannerのクエリは、単一のノードで完結することは稀だ。多くの場合、Distributed Union(分散ユニオン) と呼ばれる、複数スプリット(Split)への並行プッシュダウン実行が行われる。

オプティマイザは、以下の2つのアプローチのコストを天秤にかけている。

A. プッシュダウン(Distributed Seek / Scan)

データを保持しているスプリット(ストレージノード)の近くまで計算を押し下げる方式。

  • コストが低いケース: フィルタリング条件が厳しく、転送するデータ量が少ない場合。
  • コストが高いケース: スプリットをまたぐ結合(Join)が多く、ネットワーク帯域を大量消費する場合。

B. ローカルアグリゲーションとマージ

各スプリットで部分的な集計(Partial Aggregation)を行い、その結果だけをコーディネーターノードに集めて最終集約(Final Aggregation)する方式。

  • 典型的な `GROUP BY` や `COUNT()` の挙動だ。

ここでオプティマイザが計算するコスト方程式の概念をシンプルに示そう:

$$\text{Total Cost} = \text{CPU Cost} + (\text{Row Count} \times \text{Network Cost}) + (\text{Read Cost} \times \text{Storage Latency})$$

この数式において、「Row Count(行数の見積もり)」が大きく外れると、プランが完全に崩壊する。

—

4. 実務で遭遇する「最悪のプラン」と設計パターンの克服

コードレビューでよく見かける「アンチパターン」と、それをCBOに愛されるクエリに変えるためのアプローチを解説する。

アンチパターン1:不適切なWHERE句による全スプリットスキャン

スプリットのキー構造を無視したワイルドカード検索や、関数をラップしたカラム条件は、CBOのインデックス選択を完全に殺す。

— 【悪手】ファンクションラップによりインデックスが効かず、全スプリットへのScatter読込が発生
SELECT FROM Users WHERE LOWER(Email) = ‘test@example.com’;

【堅牢な設計】
検索用の正規化カラム(例:`LowerEmail`)を別途持ち、そこに生成的インデックス(Generated Column)または通常のセカンダリインデックスを貼る。

— 【正解】生成列にインデックスを張り、コストを最小化する
ALTER TABLE Users ADD COLUMN LowerEmail STRING(MAX) AS (LOWER(Email)) STORED;
CREATE INDEX UsersByLowerEmail ON Users(LowerEmail);

SELECT FROM Users@{FORCE_INDEX=UsersByLowerEmail} WHERE LowerEmail = ‘test@example.com’;

アンチパターン2:巨大なテーブル間の分散JOIN

インターリーブ(Interleave)されていない独立した大テーブル同士をJOINする場合、Spannerは高価なDistributed Hash JoinやMerge Joinを選択せざるを得ない。これがネットワークホップを爆発させ、レイテンシ悪化の主原因となる。

【堅牢な設計】
親子関係が明確なエンティティ(例:`Customers` と `Orders`)であれば、インターリーブテーブル(Interleaved Tables)として設計し、物理的に同じスプリット(同一物理ノード群)にデータを共局所化(Colocation)させる。

これにより、CBOはネットワークを跨がない超高速なInterleaved Join(ローカルJOIN)を選択でき、コストは劇的に低下する。

— 親テーブル
CREATE TABLE Customers (
CustomerID INT64,
Name STRING(MAX),
) PRIMARY KEY(CustomerID);

— 子テーブル(物理的に親と同一スプリットに配置される)
CREATE TABLE Orders (
CustomerID INT64,
OrderID INT64,
OrderDate DATE,
) PRIMARY KEY(CustomerID, OrderID),
INTERLEAVE IN PARENT Customers ON DELETE CASCADE;

—

5. 実行プラン(EXPLAIN)の読み解き方

パフォーマンスチューニングの基本は `EXPLAIN`(または `EXPLAIN ANALYZE`)だ。SpannerのコンソールやCLIでプランを見たとき、以下のポイントをチェインして確認せよ。

1. `Distributed Union` のスコープ:無駄な全スプリットスキャン(Full Scan)になっていないか?
2. `Index Scan` vs `Table Scan`:意図したインデックスが使われているか?(予期せぬテーブルスキャンはカーディナリティの誤認を疑え)
3. `Estimated Rows` と `Actual Rows` の乖離:`EXPLAIN ANALYZE` を実行し、オプティマイザの「見積もり」と「現実」のギャップが10倍以上ある場合は、統計情報の刷新やクエリ構造の改修が必要だ。

— 実行計画と実際のコストを暴く
EXPLAIN ANALYZE
SELECT UserID, COUNT()
FROM Orders
WHERE OrderDate >= ‘2023-01-01’
GROUP BY UserID;

—

結びにかえて:真のSpannerエンジニアへ

Cloud Spannerのクエリオプティマイザは非常に高度だが、魔法の杖ではない。データモデリング(特にプライマリキーの設計とインターリーブ)、インデックス戦略、そして統計情報のコンディション。これらが揃ってはじめて、CBOはその真価を発揮し、ミリ秒単位の超高速分散クエリを奏でる。

「なぜこのプランが選ばれたのか?」をCBOの視点(コストモデル)に立って逆算できるようになれば、あなたもSpannerのアーキテクチャを完全によみ切ったと言えるだろう。

次の設計レビューでは、ぜひ「そのクエリのコストモデル、どう見積もっている?」とチームメンバーに問いかけてみてほしい。プロダクトの品質が一段階跳ね上がるはずだ。

コメント

タイトルとURLをコピーしました