Cloud Spannerのインデックス戦略:その「隠れたコスト」を制御せよ
Cloud Spannerを単なる「SQLが動く分散RDB」だと思っているなら、今すぐその認識を改めるべきだ。
Spannerは、TrueTimeによる分散トランザクションの魔法の下で、巨大な分散キー・バリューストア(KV)として動いている。セカンダリインデックスは、Spannerという巨大な分散エンジンの中では、「別のテーブル」として物理的に実体化される独立したエンティティに他ならない。
アーキテクトとして、この事実が何を意味するのか。なぜ「とりあえずインデックスを貼る」という行為が、パフォーマンスを崩壊させ、コストを爆発させるのか。その極限の知見を紐解いていく。
—
1. インデックスは「独立したテーブル」である
Spannerにおいて、セカンダリインデックスは内部的に`INDEX_TABLE`として管理される。具体的には、`(インデックスのキー列 + ベーステーブルのプライマリキー列)`を結合したキーを持つKVペアの集合だ。
ここで肝に銘じるべきは、インデックスの書き込みもトランザクションの対象であるという点だ。
- 書き込み増幅(Write Amplification): ベーステーブルに1行挿入すれば、当然インデックスにも1行挿入が必要だ。複数のインデックスがあれば、書き込みのたびに分散トランザクションの「参加者」が増える。
- レイテンシの非対称性: インデックスが多いほど、Commit待ち時間は伸びる。Paxosグループを跨ぐ更新が発生すれば、ネットワークの往復回数(RPC)が指数関数的に増大する。
アーキテクトの戒め: インデックスは「読み取りのための無料のギフト」ではない。書き込みスループットを削り取って得られる「読み取り最適化の債務」なのだ。
—
2. NULL値の取り扱いと「スパース性」の物理的真実
多くの開発者が誤解しているが、Spannerのインデックスにおいて`NULL`は「値」として格納される。
— NULL値を含む列へのインデックス作成
CREATE INDEX UsersByEmail ON Users(Email) STORING (DisplayName);
もし`Email`が稀にしか設定されない列であれば、インデックスの中には「値がNULLの行」が大量に生成される。これは検索効率を劇的に低下させるだけでなく、インデックスの物理サイズを不必要に膨らませる。
限界突破の知見:NULLのフィルタリング
Spannerでは `NULL` をインデックスのキーに含めない `NULL_FILTERED` インデックスが利用できる。
— NULL値をインデックスから除外する(スパースインデックス)
CREATE NULL_FILTERED INDEX UsersByEmail ON Users(Email) STORING (DisplayName);
なぜこれが重要か?
内部ストレージ層において、インデックスが小さければ小さいほど、メモリキャッシュ(Block Cache)のヒット率が向上する。インデックスのサイズがメモリに収まれば、検索はディスクI/Oを発生させずに完結する。`NULL_FILTERED`は、メモリ帯域とレイテンシを最適化するための必須の武器だ。
—
3. ストレージ戦略:`STORING`句の最適化
`STORING`句(カバリングインデックス)は、インデックスにベーステーブルの列を「コピー」して持たせる仕組みだ。
— STORING句によるカバリングインデックス
CREATE INDEX UsersByAge ON Users(Age) STORING (DisplayName, Email);
アーキテクトの視点:なぜ「全列」を含めてはいけないのか
`STORING`は、ベーステーブルへの「Base Table Lookup(キー探索)」を回避し、インデックスのスキャンだけでクエリを完結させるためのものだ。これはリード性能の飛躍的な向上をもたらす。
しかし、ストレージ容量の増大と引き換えに、以下のリスクを負う。
1. メモリ枯渇: インデックスが巨大化し、ノードのメモリ上に乗り切らなくなると、スキャン時にディスクI/Oが発生し、レスポンスがミリ秒から百ミリ秒単位へ跳ね上がる。
2. 更新コスト: `STORING`した列を更新するたびに、インデックスの書き込みが発生する。
極限の最適化:
- 頻繁に検索され、かつ更新頻度が低い列だけを `STORING` する。
- 「あとで必要になるかも」という不安で `STORING` に列を詰め込むな。それは技術的負債となる。
—
4. 物理レイアウトとキー順序の哲学
インデックスのキー順序は、ストレージ上の「ソート順」そのものである。
— 範囲スキャンに特化したインデックス
CREATE INDEX UsersByAgeAndCreated ON Users(Age, CreatedAt DESC);
このインデックスにおいて、`Age`を固定して`CreatedAt`の範囲でスキャンを行う場合、物理ストレージ上では連続したメモリ配置になる。これはCPUのプリフェッチャーにとって極めて好都合であり、キャッシュミスを最小限に抑えることができる。
結論: インデックスの設計は、クエリパターンだけでなく、「データがどのように物理的に並んでいるか」を可視化することから始まる。
—
チーフアーキテクトからの提言
Spannerのインデックスは、単なるDBの機能ではない。それは「データアクセスのためのインメモリ・キャッシュの物理設計」そのものである。
1. 書き込みコストを計算せよ: 書き込み頻度が高いテーブルに無闇にインデックスを貼るな。
2. `NULL_FILTERED`でメモリを救え: 不要なNULLを排除し、インデックスの密度を高めろ。
3. カバリングインデックスは「切り札」: 本当に読み取り頻度が高いクエリにのみ、最小限の列を `STORING` せよ。
技術とは、魔法ではない。物理的な制約を理解し、その限界のギリギリを攻めることこそが、エンジニアリングの真髄である。次回の設計で、インデックスをただの「機能」としてではなく、Spannerエンジンの「物理構造」として捉え直してほしい。
コメント