分散クエリ実行プランの深層:Cloud Spannerオプティマイザを飼いならす技術
テックリードの私だ。本日のコードレビュー、あるいは設計レビューの場において、Cloud Spannerのクエリパフォーマンスについて議論しよう。
「なぜこのクエリはレイテンシが大きいのか?」
「なぜスキャン数が想定より膨れ上がっているのか?」
お前たちがなんとなく発行しているそのSQL、そしてCloud Spannerのオプティマイザが裏側でどう動いているか、本質を理解しているか?
Cloud Spannerは、単なる「水平分散されたリレーショナルデータベース」ではない。数千、数万のノードにまたがるスプリット(Split)の海から、一瞬にしてデータをかき集めてくる巨大な分散計算エンジンだ。今回は、このSpannerの心臓部である「分散クエリ実行プラン(Distributed Query Execution Plan)」の仕組みを丸裸にし、実務で最速のクエリを引き出すための設計パターンを授けよう。
—
1. 基礎:オプティマイザはスプリットの海でどう動くか
まず、Spannerのデータ構造の基本を思い出せ。テーブルの行は主キー(Primary Key)の順にソートされ、連続するキー範囲ごとにスプリットという単位に分割されて分散配置されている。
単一ノードのRDBであれば、オプティマイザは「インデックスを使うか、フルテーブルスキャンするか」を考えればよかった。だが、Spannerのオプティマイザは次元が違う。数千のスプリットにまたがるデータを「いかに並列に、いかにネットワーク転送量を削りながらかき集めるか」をコンマ数秒の間に計算し、分散実行ツリー(Distributed Execution Tree)を構築する。
分散クエリの基本フェーズ
Spannerがクエリを受け取ると、オプティマイザは以下のステップを踏む。
1. パースと最適化(Optimizer):
SQLを抽象構文木(AST)に変換し、統計情報をもとにコストベースの最適化(CBO)を行い、分散実行プラン(Distributed Execution Plan)を生成する。
2. ルートノード(Root/Coordinator)の指示:
クエリを発行したインスタンス(または内部のコーディネーター)が、各スプリットのリーダー(Leader Replica)に対してサブタスクをばら撒く。
3. 分散並列実行(Distributed Execution):
各スプリットが自ら持つデータを並列スキャンし、必要に応じてローカルでフィルターや部分集約(Partial Aggregation)を行う。
4. マージと最終集約(Distributed Merge / DistApply):
分散された結果をネットワーク経由で回収し、最終的なソートや結合(JOIN)、全体集約を行ってクライアントに返す。
ここで重要なのは、「ネットワークを流れるデータ量(Cross-node communication)」と「分散処理の並列度(Parallelism)」のトレードオフだ。これをコントロールするのが、お前たちが書くSQLとインデックスの設計に他ならない。
—
2. 実行プランの解剖:`EXPLAIN` と分散演算子たち
クエリのチューニングをするなら、勘に頼るな。`EXPLAIN` または `PROFILE` を使え。特に `PROFILE` は、各演算子が「実際に何行処理したか」「CPU時間をどれだけ消費したか」「どのスプリットでボトルネックが起きたか」のリアルなメトリクスを叩き出してくれる。
実務で頻出する主要な分散演算子を頭に叩き込んでおけ。
- Distributed Cross Apply (DistApply):
最凶にして最強の演算子。左側の結果の各行に対して、右側のスプリット群へリモートクエリを並列実行する。いわゆる分散Nested Loop JOINの親玉だ。
- Distributed Union:
複数のスプリットやインデックスから並列にデータを取得し、そのまま合流させる。
- Distributed Group By:
各スプリットでローカルに集約(Partial Aggregation)を行わせた後、ルート側で最終集約(Final Aggregation)を行う。ネットワーク転送量を劇的に減らすための必須演算子。
悪い例:意図しない `DistApply` の爆発
次のクエリを見てほしい。
— 【アンチパターン】巨大な注文テーブルに対し、動的にユーザー情報を引く
SELECT
o.OrderId,
o.Amount,
u.UserName
FROM
Orders o
JOIN
Users u ON o.UserId = u.UserId
WHERE
o.OrderDate >= ‘2023-10-01’
もし `Orders` のスキャン結果が数百万行あり、かつ `Orders` 側の `UserId` と `Users` の主キー結合においてオプティマイザが `DistApply` を選択した場合、数百万回ものリモートRPCがネットワーク越しに発生する。これが「分散クエリの性能劣化(N+1問題のデータベース版)」だ。
—
3. 実務で使える堅牢な設計パターン
では、この分散クエリの魔獣をどう飼いならすのか。設計レビューで私が後輩に叩き込む3つのパターンを伝授する。
パターンA:インターリーブ(Interleave)による「物理的コロケーション」の強制
Spannerの真骨頂はこれだ。親テーブルと子テーブルを親子関係(Interleave)として定義すると、親の行と、それに対応する子の行が同一のスプリット(物理的に同じストレージ領域)に隣接して配置される。
— 親テーブル
CREATE TABLE Customers (
CustomerId INT64,
CustomerName STRING(100),
) PRIMARY KEY(CustomerId);
— 子テーブル(インターリーブ定義)
CREATE TABLE Orders (
CustomerId INT64,
OrderId INT64,
OrderDate DATE,
Amount INT64,
) PRIMARY KEY(CustomerId, OrderId),
INTERLEAVE IN PARENT Customers ON DELETE CASCADE;
なぜこれが効くのか?
`Customers` と `Orders` を `CustomerId` でJOINする場合、通常のテーブルであれば分散JOIN(ネットワークを跨ぐ通信)が必要になる。だが、インターリーブされていれば、ストレージ層でデータが物理的に結合されているため、単一のスプリット内で完結する超高速なローカルJOINに化ける。分散クエリプランから `DistApply` が消え去り、局所的なスキャンだけで処理が完了するのだ。
パターンB:インターリーブIndexの活用
親子関係にないテーブル同士であっても、結合キーを先頭に持たせたインターリーブIndex(Interleaved Index)を作ることで、実質的にデータを特定の親テーブルの物理領域にぶら下げることができる。
参照頻度が圧倒的に高いリレーションがあるならば、インデックスのインターリーブ化を検討せよ。
パターンC:大規模集約クエリにおけるプレフィックス設計
集約(`GROUP BY`)を行う際、スプリットをまたぐデータ転送を最小限にするためには、集約キーが主キーのプレフィックス(左側)と一致していることが望ましい。
スプリットがすでにそのキー順にソートされているため、オプティマイザは各スプリット内でのローカル集約を確信し、ネットワーク帯域の枯渇を防ぐことができる。
—
4. パフォーマンス上の注意点とアンチパターン
最後に、コードレビューで一発レッドカードを出す「やってはいけない実装」を挙げておく。
1. WHERE句での非効率な関数利用(Sargabilityの喪失)
`WHERE STARTS_WITH(Name, ‘A’)` はインデックスやスプリットの範囲を絞り込める(Sargable)が、`WHERE LOWER(Name) == ‘abc’` のようにカラムに変数をかませると、オプティマイザはスプリットの範囲を特定できず、フルテーブルスキャン(全スプリットへのバラマキ)を余儀なくされる。カラムはそのまま比較しろ。
2. 大きすぎる `IN` リストの直叩き
アプリケーション側で動的に生成した数千件のIDを `WHERE Id IN (…)` で渡すと、オプティマイザのプラン生成コストが跳ね上がるだけでなく、巨大なパラメータとしてネットワークや内部バッファを圧迫する。数が多い場合は、一時テーブル(Temporary Table / Staging Table)にバルクインサートしてからJOINしろ。
3. 統計情報の古さによる誤ったプラン選択
データが急激にバーストした直後などは、Spannerのバックグラウンド統計情報更新が追いついていない場合がある。不自然に遅いクエリに出くわしたら、オプティマイザが古い統計情報に基づいて誤ったプラン(フルスキャンなど)を選んでいないか疑え。
—
チーフアーキテクトからの総括
Cloud Spannerの分散クエリ実行プランは、魔法の箱ではない。オプティマイザは優秀だが、お前たちが定義する「スキーマ」「主キー」「インターリーブ構造」「インデックス」という物理的な設計図が間違っていれば、最高のパフォーマンスを発揮することはできない。
次にコードを書くとき、あるいはSQLチューニングをするときは、頭の中で「今、何万個ものスプリットのどこで何が並列実行され、どれだけのデータがネットワークを飛び交っているか」をビジュアライズしろ。
分散原理を制する者が、Cloud Spannerを制す。
以上だ、次の設計に移る。
コメント