【Cloud Spanner極限バイブル】Interleaved Tables:物理レイアウトの魔術と、JOINを駆逐するデータ配置戦略
こんにちは。チーフアーキテクトの私だ。
今日のコードレビュー、あるいは基本設計レビューで、またこんなテーブル定義を見かけた。
「親テーブルのIDを外部キーに持つ、独立した子テーブル」
……待て。君たちは本当にCloud Spannerを使っているのか? それとも、ただの分散していないリレーショナルデータベースのノウハウをそのまま持ち込んでいるだけか?
Cloud Spannerは、単なる「無限にスケールするMySQLやPostgreSQL」ではない。Googleの誇る真の分散RDBMSであり、物理レイアウトを完全に支配してこそ、その真価――無限のスケールと極限のレイテンシ削減――が発揮される。
今回は、Cloud Spannerのコア・アーキテクチャの真骨頂である 「Interleaved Tables(インターリーブテーブル)」 について、物理レイアウトの深層から実務での設計パターン、そして踏んではいけない地雷まで、一切の妥協なく解説する。
—
1. なぜ通常のJOINはCloud Spannerで「コスト」になるのか?
分散データベースの宿命、それは「ネットワーク・ホップ」だ。
一般的なRDBMSでは、親テーブル(例: `Users`)と子テーブル(例: `Orders`)のレコードは、それぞれ別々のB-Treeインデックスとしてディスク上にバラバラに配置される。そのため、`Users` と `Orders` を `JOIN` するクエリが走ると、データベースエンジンはストレージノード間を行き来し、データをかき集めて結合処理(Hash JoinやMerge Join)を行う。
データ量が数千万、数億件を超え、さらにノードが物理的に分散した瞬間、このネットワーク越しのデータフェッチ(跨ぎ)がレイテンシのボトルネックとなる。
ここで登場するのが、Spannerの Interleaved Tables だ。
—
2. Interleaved Tablesの物理レイアウト:ハードウェアレベルの「共存」
Interleaved Tablesとは一言で言えば、「親テーブルの1行と、それに紐づく子テーブルの複数行を、物理的に同じストレージブロック(スプリット)の同じ空間に並べて格納する」 というデータ配置戦略だ。
通常、Spannerはデータを主キーの範囲(Range)に基づいて「スプリット」という単位に分割し、複数のノードに分散配置する。
しかし、インターリーブ関係を定義すると、子テーブルのレコードは、必ず親テーブルの対応する親行の直後(物理的に至近距離)に配置される。
概念的な物理レイアウトのイメージ
[ スプリット A (Node 1) ]
├── User ID: 100 (親テーブル行)
├── User ID: 100 の Order ID: 1 (子テーブル行 – 物理的に親の直近!)
├── User ID: 100 の Order ID: 2 (子テーブル行 – 物理的に親の直近!)
├── User ID: 101 (親テーブル行)
└── User ID: 101 の Order ID: 1 (子テーブル行)
[ スプリット B (Node 2) ]
├── User ID: 200 (親テーブル行)
└── …
この物理レイアウトが意味するもの。それは、「親と子のデータ取得におけるランダムI/Oおよびネットワーク転送の完全な消滅」だ。
—
3. 実践:DDL設計とクエリの爆速化
百聞は一見に如かず。実際にインターリーブされたテーブル定義を見てみよう。
正しいDDL設計パターン
— 親テーブル:ユーザー
CREATE TABLE Users (
UserId INT64 NOT NULL,
UserName STRING(MAX),
CreatedAt TIMESTAMP,
) PRIMARY KEY(UserId);
— 子テーブル:注文(Usersにインターリーブする)
CREATE TABLE Orders (
UserId INT64 NOT NULL,
OrderId INT64 NOT NULL,
OrderAmount INT64,
OrderDate TIMESTAMP,
— 親の主キーを子テーブルの主キーの先頭に含め、ON DELETE CASCADEを指定する
) PRIMARY KEY(UserId, OrderId),
INTERLEAVE IN PARENT Users ON DELETE CASCADE;
ここで重要なポイントが2つある。
1. 主キーの継承: 子テーブルの主キーの先頭には、必ず親テーブルの主キーが含まれていなければならない。
2. ON DELETE CASCADE: 親行が削除された場合、紐づく子行も物理的に一体となって高速に削除される。
なぜこの設計でパフォーマンスが跳ね上がるのか?
この構造を持つテーブルに対し、以下のクエリを実行したとする。
— 特定のユーザーとその注文を一括取得する
SELECT
u.UserName,
o.OrderId,
o.OrderAmount
FROM Users u
JOIN Orders o ON u.UserId = o.UserId
WHERE u.UserId = 12345;
通常のRDBMSや、非インターリーブのSpannerテーブルであれば、ここでハッシュ結合などのオーバーヘッドが発生する。しかし、Interleaved Tablesの場合、ストレージエンジンレベルですでに `UserId = 12345` の親行の直後にその子行たちが並んでいる。
Spannerの実行計画(Execution Plan)を見るとわかるが、これは結合というよりは、「単一の物理シーケンシャルスキャン(あるいはダイレクト・ポイントルックアップ)」に近い挙動を示す。ネットワークを跨いだシャッフルは一切発生しない。
—
4. 厳格な設計ルール:チーフアーキテクトからの警告
この強力なInterleaved Tablesだが、使い所を誤るとシステム全体を崩壊させる「諸刃の剣」でもある。設計レビューで私が必ずチェックする3つの鉄則を授けよう。
鉄則1:孫、ひ孫…と深くしすぎない(階層の罠)
Interleavedは `Users -> Orders -> OrderItems -> ItemLogs` のように多段に組むことができる。しかし、階層が深すぎると、親のデータが書き換わった際やスプリットが分裂・移動する際のオーバーヘッドが跳ね上がる。実務上の目安として、最大でも2階層(親・子)または3階層(親・子・孫)にとどめるべきだ。
鉄則2:ホットスポットの回避(キー設計の呪縛)
物理的に近接して配置されるということは、「書き込みが特定の範囲に集中した場合、単一のスプリットに負荷が集中する(ホットスポット)」という分散DB最大の禁忌を踏み抜くリスクがある。
例えば、親テーブルの主キーに「インクリメンタルなID」や「現在時刻(`TIMESTAMP`)」を指定し、そこに子を大量にインターリーブさせると、最新のデータを持つスプリット1つにすべての書き込みが集中し、CPUが飽和する。
親テーブルの主キーには、必ずハッシュ化されたIDやUUIDv4などの「分散するキー」を選定すること。
鉄則3:子単体でのライフサイクルを考慮する
`ON DELETE CASCADE` が示す通り、親と子は運命共同体だ。「親はいらないけれど、子データ(履歴など)だけ長期間保持したい」という要件がある場合、インターリーブしてはならない。子単体の削除や独立したライフサイクルが必要なエンティティは、素直に別テーブル(リファレンス関係)として切り出すべきだ。
—
5. まとめ:アーキテクトとしての判断基準
Cloud SpannerにおけるInterleaved Tablesは、物理レイアウトを人間が明示的に最適化するための強力な武器だ。
- 「親と子が常にセットで取得され、結合頻度極めて高く、レイテンシを極限まで削りたい」 ならば、迷わずインターリーブを採用しろ。
- 「それぞれが独立してスケールし、ライフサイクルが異なり、単体での更新・削除が多い」 ならば、標準的なテーブル設計に戻せ。
ツールに仕事を合わせるな。ツールの物理特性を理解し、データ構造を従わせろ。
この知見を胸に、次の設計書を見直してほしい。君たちのシステムのレイテンシが劇的に改善されることを期待している。
コメント