Cloud Spanner分散結合の深層:スプリット境界を越えてスケールするJOINの哲学
こんにちは。テックリードの私だ。
今日のコードレビュー、あるいはアーキテクチャ設計レビューで、こんなクエリを見かけて冷や汗をかいたことはないか?
— レビュー対象のクエリ
SELECT
o.order_id,
c.customer_name,
i.item_name
FROM
Orders o
JOIN
Customers c ON o.customer_id = c.customer_id
JOIN
OrderItems i ON o.order_id = i.order_id
WHERE
o.created_at >= ‘2024-01-01’
「お、綺麗に正規化されたスキーマだな。よし、マージしよう」と思ったそこの君。ちょっと待ってほしい。
Cloud Spannerは、単なる「分散リレーショナルデータベース」ではない。数千、数万のノードにデータを水平分散させながら、ACIDトランザクションを担保するモンスターマシンだ。
このクエリが、巨大なテーブル群に対して発行されたとき、Spannerの内部で何が起きているか想像できるか?
ネットワークを何回ホップし、どれだけのCPUサイクルが分散結合(Distributed Join)の神々に捧げられているか。
今日は、Cloud Spannerのコアアーキテクチャの心臓部である「分散結合戦略」について、実務で明日から使えるレベルを超えて、「Spannerの物理配置をハックする設計思想」まで徹底的に叩き込んでやる。心して聞いてほしい。
—
1. 前提知識:なぜSpannerのJOINは「普通のRDB」と違うのか?
従来のRDB(PostgreSQLやMySQLなど)であれば、単一のメモリ空間、あるいは単一のストレージノード内でJOINが完結する。しかし、Cloud Spannerの世界では、データは「スプリット(Split)」という単位に分割され、世界中の(あるいはリージョン内の)異なるストレージサーバーに物理的にバラ撒かれている。
ここで基本原理を思い出そう。Cloud Spannerのデータモデルの根幹は インターリーブ(Interleave) だ。
[Customers (Parent)]
└── [Orders (Child: INTERLEAVE IN PARENT Customers)]
└── [OrderItems (Grandchild: INTERLEAVE IN PARENT Orders)]
もし、あなたが親テーブルと子テーブルをインターリーブ関係として定義し、主キーで結合している場合、それは結合コストが「ゼロ」に等しい。なぜなら、親の行と子の行は、物理的に同一のスプリット(=同一のストレージノードの近傍)にコロケーション(共配置)されているからだ。
だが現実のシステムは、すべての要件をインターリーブで綺麗に表現できるほど甘くない。異なる親を持つテーブル同士の結合、いわゆる「ノンインターリーブ結合(Non-interleaved JOIN)」が必要になる。ここからが、Spannerの分散結合エンジンの出番だ。
—
2. Spannerの分散結合エンジン:裏側で何が動いているのか
Spannerのクエリパーサーとオプティマイザは、コストベースで最適な分散実行プランを組み立てる。分散結合において、オプティマイザが駆使する主要な戦略は主に以下の2つだ。
① Distributed Hash Join(分散ハッシュ結合)
- どう動くか: 結合キーをハッシュ関数に通し、ネットワークを越えてデータをシャッフル(再分散)し、それぞれのノードでハッシュテーブルを作って結合する。
- ユースケース: 大規模なテーブル同士の結合で、事前のデータ共配置がない場合。
- コストの代償: ネットワークI/Oの爆発。数百万行のデータがノード間を行き来するため、クロスノード通信のレイテンシがボトルネックになりやすい。
② Distributed Apply / Merge Join(分散マージ結合 / ループ結合)
- どう動くか: 片方のテーブル(外側)のストリームを読み込みながら、もう片方のテーブル(内側)のインデックスをリモートから引く。
- ユースケース: 片方の絞り込み条件が非常に厳しく、数行〜数十行しかヒットしない場合。
- コストの代償: ランダムリードの嵐。外側の行数が多い場合、リモートRPCの回数が $O(N)$ で跳ね上がり、クエリレイテンシが数秒に悪化する。
—
3. 実務で直面するパフォーマンス・アンチパターン
コードレビューでよくある「やってはいけない設計」を具体的に見ていこう。
アンチパターン:全テーブルがバラバラの独立テーブル
以下のようなスキーマ設計を考えてみる。
— アンチパターン:すべて独立したグローバルテーブル
CREATE TABLE Users (
user_id INT64,
…
) PRIMARY KEY(user_id);
CREATE TABLE Orders (
order_id INT64,
user_id INT64,
…
) PRIMARY KEY(order_id);
この状態で、`Users` と `Orders` を `user_id` でJOINするクエリを流すと、Spannerはこれらを結合するために分散ハッシュ結合を選択せざるを得ない。結果として、データ量がスケールするにつれて、クエリのレイテンシは直線的に悪化していく。
—
4. 堅牢な設計パターン:Spannerを極限まで速くするアプローチ
では、テックリードとして我々はどう設計すべきか。実務で使える3つのアプローチを伝授する。
アプローチ A: インターリーブ構造の徹底的な活用(物理コロケーション)
親子関係にあるエンティティ(例:顧客と注文、注文と注文明細)は、絶対にインターリーブで定義しろ。
— 模範解答:インターリーブによる物理コロケーション
CREATE TABLE Customers (
customer_id INT64,
customer_name STRING(MAX),
) PRIMARY KEY(customer_id);
CREATE TABLE Orders (
customer_id INT64,
order_id INT64,
order_date DATE,
) PRIMARY KEY(customer_id, order_id),
INTERLEAVE IN PARENT Customers ON DELETE CASCADE;
なぜこれが最強か?
`Customers` のある行(`customer_id = 100`)と、それに紐づく `Orders` の行は、Spannerのストレージ層で完全に同じ物理スプリットに存在することが保証される。このため、`customer_id` を介したJOINや検索は、ネットワークを1バイトも跨がずに、単一ノードのメモリ/ストレージ内だけで完結する(Local Join)。
アプローチ B: アプリケーション層での非正規化(マテリアライゼーション)
リレーショナルデータベースの美徳に反するように思えるかもしれないが、超大規模トラフィックを捌くSpannerにおいて、「読み込みのための非正規化」はプロの常套手段だ。
頻繁に結合されるデータを、あらかじめJSONカラムとして持たせたり、必要な属性を子テーブル側に冗長に持たせたりすることで、JOINそのものを排除する。
— 読み込み性能を極限まで高めるための非正規化テーブルの例
CREATE TABLE OrderSummaries (
customer_id INT64,
order_id INT64,
— 結合先の名前を非正規化して保持
customer_name STRING(MAX),
order_amount NUMERIC,
) PRIMARY KEY(customer_id, order_id);
「データの二重持ちになるのでは?」という懸念はあるだろう。しかし、Cloud Spannerの無限に近い水平スケーラビリティとストレージコストを天秤にかけたとき、「レイテンシの削減」と「CPU負荷の低下」のメリットは、ストレージコストの増加を遥かに凌駕する。
アプローチ C: 強制ヒント(Optimizer Hints)によるプランの制御
オプティマイザが稀に迷い、最悪の結合プランを選ぶことがある。そのときは、明示的にヒント句を与えて制御する。
— オプティマイザに結合順序とアルゴリズムを強制する例
SELECT
o.order_id,
c.customer_name
FROM
Orders o@{FORCE_JOIN_ORDER=TRUE}
JOIN
Customers c@{JOIN_METHOD=HASH}
ON o.customer_id = c.customer_id
WHERE
o.order_id = 12345;
※ただし、ヒント句はあくまで最後の手段だ。スキーマ設計やインデックス設計の根本的な欠陥を、ヒント句でごまかそうとしてはならない。
—
5. パフォーマンス上の注意点とモニタリングの極意
スプリット境界を越える結合を避けて通れない場合もある。その際に、システムが健全か、あるいは爆発寸前かを検知するためのチェックリストを授けよう。
1. Query Inspectorの「Cross-cluster / Cross-node CPU」を見る
Cloud SpannerのコンソールにあるQuery Insightsを注視しろ。CPU使用率の大部分が「Remote Read」や「Shuffle」に費やされている場合、それは分散結合が炎上しているサインだ。
2. スプリットのホットスポットを警戒する
インターリーブ設計をしていても、主キーの設計をミスしてタイムスタンプや連番を先頭に置くと、特定のスプリットに書き込みと読み込みが集中(ホットスポット化)し、分散結合の恩恵も台無しになる。主キーの第1カラムには必ずUUIDv4、ハッシュ値、あるいは逆順ビット等の分散キーを採用しろ。
—
結びに代えて
Cloud Spannerにおける分散結合戦略の本質、それは一言で言えば「ネットワークを跨がせるな、物理的に寄せろ」だ。
- データの物理配置(スプリット)を意識してスキーマを切れ。
- インターリーブを愛し、ノンインターリーブ結合はコストが高いことを常に念頭に置け。
- どうしてもJOINコストが落ちないなら、非正規化を恐れるな。
「なんとなく動くクエリ」を書く時代は終わった。
Cloud Spannerという最高の分散エンジンを手の内に入れ、ミリ秒単位で最適化された美しいシステムを一緒に作り上げていこう。
今日のレビューからは以上だ。君たちのコードに、最高の分散最適化が宿ることを期待している。
コメント