【Cloud Spannerの極意】インターリーブ徹底解説:物理レイアウトを支配し、JOINを「無かったこと」にせよ
こんにちは。テックリードの私だ。
今日のコードレビュー、あるいはアーキテクチャ設計レビューで、こんなスキーマを見かけたとしよう。
— よくあるアンチパターン:論理的な外部キーだけに頼ったテーブル定義
CREATE TABLE Users (
UserId INT64 NOT NULL,
UserName STRING(MAX),
) PRIMARY KEY(UserId);
CREATE TABLE UserOrders (
UserId INT64 NOT NULL,
OrderId INT64 NOT NULL,
OrderDate DATE,
TotalAmount NUMERIC,
) PRIMARY KEY(UserId, OrderId),
CONSTRAINT FK_UserOrders_Users FOREIGN KEY(UserId) REFERENCES Users(UserId);
「ほう、`Users` と `UserOrders` に外部キーが張ってあって綺麗ですね」――なんて思ったとしたら、君のSpannerの習熟度はまだ「初級」を抜け出せていない。
Cloud Spannerは、デフォルトではテーブルごとにデータを分散配置する。つまり、`Users`の行と、それに対応する`UserOrders`の行が、ストレージ層(スプリット)の全く異なる物理ノードの遠く離れた場所に置かれている可能性がある。この状態で `JOIN` を発行すれば、ノード間ネットワークを跨いだシャッフルが発生し、レイテンシは跳ね上がる。
ここで登場するのが、Spannerの真骨頂「テーブルのインターリーブ(Interleave)」だ。
これを使いこなせば、親子のデータを物理的に同じストレージブロックに極限まで密着させ、JOINの概念そのものを過去のものにできる。
本稿では、実務の現場で絶対に外せないインターリーブ階層構造の設計、制約、そしてパフォーマンスを極限まで引き出すためのデザインパターンを叩き込む。
—
1. インターリーブの本質:物理レイアウトの支配
インターリーブとは一言で言えば、「子テーブルのレコードを、親テーブルの対応するレコードの物理的な直下に配置する」というストレージ最適化のメカニズムだ。
通常のRDBであれば、テーブルはテーブルごとに独立した領域(B+樹など)に格納される。しかし、Spannerのインターリーブ構造(親-子関係)を定義すると、次のような物理的配置になる。
- 親テーブルのプライマリキーのプレフィックスと、子テーブルのプライマリキーが組み合わされ、単一のソート済みデータ構造としてストレージにストアされる。
- 結果として、親の1行(例: `User(1)`)と、それに紐づく子の複数行(例: `UserOrders(1, 100)`, `UserOrders(1, 101)`)が、物理的に同じスプリット(Spannerの分散ストレージの単位)に常時コロケーション(共存)される。
DDLの書き方:インターリーブの構文
実際にDDLを見てみよう。先ほどのスキーマをSpanner流の正しいインターリーブ構造に書き換える。
— 親テーブル
CREATE TABLE Users (
UserId INT64 NOT NULL,
UserName STRING(MAX),
CreatedAt TIMESTAMP,
) PRIMARY KEY(UserId);
— 子テーブル:INTERLEAVE IN PARENT で親を指定する
CREATE TABLE UserOrders (
UserId INT64 NOT NULL,
OrderId INT64 NOT NULL,
OrderDate DATE,
TotalAmount NUMERIC,
) PRIMARY KEY(UserId, OrderId),
INTERLEAVE IN PARENT Users ON DELETE CASCADE;
ここで注目すべきポイントは2つある。
1. プライマリキーの継承: 子テーブルのプライマリキーの先頭カラムは、必ず親テーブルのプライマリキーと完全一致させなければならない(ここでは `UserId`)。
2. ON DELETE CASCADE: 親が削除されたとき、子も自動的に消滅する。実務上、親子関係が強いドメイン(ECの注文と明細、アカウントと設定など)ではほぼ必須のオプションだ。
—
2. なぜインターリーブが「最強」なのか?(パフォーマンスの恩恵)
正しく設計されたインターリーブ階層は、システムに圧倒的な優位性をもたらす。
① 局所的なルックアップとインメモリ同等のJOIN
親のキーと子のキーが物理的に同じ場所にあるため、`Users` と `UserOrders` の JOIN クエリは、ネットワークを跨ぐデータ転送(RPC)を劇的に削減できる。Spannerのオプティマイザはこれを検知し、極めて効率的なローカル読み取りを実行する。
② 範囲クエリの高速化
例えば、「特定のユーザーの、特定の期間の注文を取得する」といったクエリにおいて、プライマリキーの順序通りにデータが並んでいるため、シークコストが最小化される。
—
3. 現場で血を流さないための「厳格な制約」とアンチパターン
チーフアーキテクトとして最も伝えたいのは、「インターリーブは万能の銀の弾丸ではない。誤用すればシステムの拡張性を破壊する劇薬である」ということだ。以下の制約を頭に叩き込んでほしい。
制約1: 深すぎる階層(Deep Hierarchy)の罠
Spannerでは、親-子-孫-ひ孫…と何段階もインターリーブを重ねることができる(最大68階層まで理論上可能だが、実務での推奨は最大でも2〜3階層だ)。
階層が深くなりすぎると、スキーマ変更やデータ移行の際のロック競合が複雑化し、メンテナンス性が最悪になる。
制約2: ホットスポット(Hotspotting)の致命傷
これこそが最大の罠だ。インターリーブされた子テーブルにデータをインサートする際、親のプライマリキーが単調増加(例: 時刻ベースのUUIDや、連番IDなど)していると、すべての書き込みが特定の1つのスプリットに集中し、レイテンシが爆発する(ホットスポットの発生)。
- 対策: 親テーブルのプライマリキーには、必ずハッシュ化されたIDや、ランダム性を帯びたUUIDv4などを用いるか、「ビット反転(Bit-reversal)」機能を利用して書き込みを分散させなければならない。
— 悪い例:連番やタイムスタンプを親のPKにすると、新規作成時に特定のノードが焼き切れる
— 良い例:書き込みが全スプリットに分散するようなPK設計にする、あるいはCloud Spannerのバケット化を検討する
—
4. 実務で使える設計パターン:「親・子・孫」の黄金比
では、実際のエンタープライズシステムではどう設計すべきか。
分かりやすい例として、「テナント > プロジェクト > タスク」というSaaSの階層構造を考えてみよう。
— 1. テナントテーブル(親)
— 組織ごとにデータが完全に分離されるため、テナントIDをPKにする。
CREATE TABLE Tenants (
TenantId STRING(36) NOT NULL,
TenantName STRING(100) NOT NULL,
Tier STRING(20),
) PRIMARY KEY(TenantId);
— 2. プロジェクトテーブル(子)
— テナントに属するプロジェクト。テナントIDをプレフィックスに持つ。
CREATE TABLE Projects (
TenantId STRING(36) NOT NULL,
ProjectId STRING(36) NOT NULL,
ProjectName STRING(200) NOT NULL,
) PRIMARY KEY(TenantId, ProjectId),
INTERLEAVE IN PARENT Tenants ON DELETE CASCADE;
— 3. タスクテーブル(孫)
— プロジェクトに属するタスク。テナントIDとプロジェクトIDをプレフィックスに持つ。
CREATE TABLE Tasks (
TenantId STRING(36) NOT NULL,
ProjectId STRING(36) NOT NULL,
TaskId STRING(36) NOT NULL,
Title STRING(500),
Status STRING(50),
) PRIMARY KEY(TenantId, ProjectId, TaskId),
INTERLEAVE IN PARENT Projects ON DELETE CASCADE;
この設計が美しい理由
- マルチテナント分離: あるテナント(`TenantId`)に関するすべてのデータ(プロジェクトもタスクも)が、ストレージ上で物理的に一箇所にまとまる。
- 効率的な一括削除: `Tenants` のレコードを1行削除するだけで、その下にある数百万件のプロジェクトもタスクも、Spannerのストレージエンジンがバックグラウンドで高速かつ安全にゴミ収集(GC)してくれる(`ON DELETE CASCADE` の真価)。
—
5. チーフアーキテクトからの最終提言
Cloud Spannerにおけるテーブルのインターリーブは、「物理ストレージのレイアウトを開発者が意図的にデザインする」という、リレーショナルデータベースの枠を超えた高度なエンジニアリングだ。
設計レビューで子テーブルを見かけたら、必ずこう問うてほしい。
- 「この子は、本当に親と物理的に運命を共にするべき(同じスプリットにいるべき)か?」
- 「親のプライマリキーは、書き込みのホットスポットを生み出さないか?」
- 「階層が深すぎて、将来のメンテンスで窒息しないか?」
これらをクリアしたインターリーブ設計は、君のシステムを数千万QPSに耐えうる「モンスターシステム」へと昇華させるだろう。
理論を理解したなら、今すぐスキーマを見直し、物理レイアウトを支配せよ。健闘を祈る。
コメント