Spannerの深淵:NULLとインデックス、そして分散ストレージの最適化戦略
Cloud Spannerを単なる「SQLが動く分散DB」と捉えているのであれば、それは巨大な氷山の一角しか見ていない。Spannerの真価は、Googleの分散トランザクションプロトコル(TrueTime)と、独自の実装であるColossus上に構築された、高度に最適化されたLSMツリーベースのストレージエンジンにある。
今日は、多くのエンジニアがブラックボックスとして処理している「NULL値とインデックス」の挙動について、その内部メカニズムとアーキテクチャ上の合理性を解き明かす。
—
1. NULLのインデックス・エントリ:仕様の裏側
まず結論から言おう。Spannerにおいて、NULL値はインデックスに含まれる。
標準的なRDBMSの多くがNULLを無視するケースがある中で、なぜSpannerはNULLをインデックスに格納するのか。それはSpannerが分散されたKey-Valueストア(Sorted Map)の上にリレーショナルレイヤーを構築しているからだ。
内部表現のメカニズム
Spannerのインデックスは、実質的に以下のスキーマで構成されたテーブルとして振る舞う。
— 概念的なインデックスの格納構造
— Index Key + Primary Key の組み合わせがソート順のキーとなる
(INDEX_COL_1, INDEX_COL_2, …, PK_COL_1, …)
NULL値はこのソートキーの中で「最小値(または最大値、設定による)」として扱われる。Spannerのストレージエンジンは、このKeyをバイト列としてエンコードする際、NULLフラグを付与した特別な符号化を行う。これにより、`WHERE col IS NULL` というクエリが、フルスキャンではなく「特定のインデックス範囲の先頭」をピンポイントでリードする、O(log N)の効率性を担保できるのだ。
2. ストレージエンジンと「NULLのコスト」
ここでアーキテクトが考慮すべきは、「NULLの存在がスキャン性能に与える物理的な影響」である。
Spannerのストレージ層では、データは圧縮されたSSTable(Sorted String Table)として管理される。NULLは内部的に「値の欠如」を示す小さなメタデータとしてシリアライズされる。
なぜこれが重要なのか?
1. インデックスサイズの肥大化: NULLが多発する疎な列(Sparse Columns)にインデックスを貼ることは、ストレージのオーバーヘッドだけでなく、メモリ上のキャッシュ効率(Block Cache)を悪化させる。
2. スキャンの分断: NULL値がインデックスの先頭に固まることで、範囲スキャンの際、最初の数MBがNULLだけで埋まるという現象が起きる。これは、インデックスを活用したクエリの実行計画において、最初のページロードが「無意味なNULL」を読み込むコストを強いることを意味する。
—
3. 熟練者のための最適化戦略:Sparse Indexの回避
もし君が大規模なシステムを設計しているなら、以下の知見を胸に刻んでほしい。
「NULLをインデックスに入れない」という禁じ手
もし特定の列で「値が存在する場合のみ検索できればいい」のであれば、NULLが頻出する列へのインデックスは、ストレージ効率とクエリ性能を著しく低下させる。
対策として、「NULLを排除したフィルタリング・インデックス」を擬似的に実装する設計が有効だ。
— 推奨される設計パターン:
— NULLを省いた中間テーブル(または専用の集約テーブル)を生成する
CREATE TABLE EventData (
EventId INT64 NOT NULL,
Metadata STRING(MAX),
— NULLを許容する列
Category STRING(MAX)
) PRIMARY KEY (EventId);
— インデックスのサイズを抑制するため、
— NULLが含まれる列を単純にインデックス化するのではなく、
— 必要な値のみを持つ外部テーブルを切り出す設計
CREATE TABLE CategorizedEvents (
Category STRING(MAX) NOT NULL, — NULLを排除
EventId INT64 NOT NULL,
CONSTRAINT FK_Event FOREIGN KEY (EventId) REFERENCES EventData(EventId)
) PRIMARY KEY (Category, EventId);
このアプローチは、Spannerの分散環境において、データが特定のSplit(タブレット)に偏るのを防ぐ効果もある。NULLがすべて先頭に集まるインデックスは、特定のノードに読み取り負荷を集中させ、ホットスポットを誘発するリスクがあるからだ。
—
4. 伝説的アーキテクトからの最終警告
Spannerにおいてインデックスは「魔法の杖」ではない。それは、物理メモリとCPUを消費する「別のテーブル」である。
- NULLは値である: データベースエンジンにとっては、NULLも特定のバイトパターンを持つ「実データ」である。
- 物理レイアウトを意識せよ: インデックスの先頭列にNULLが多い場合、それはスキャンの最初のセグメントがスカスカであることを意味し、キャッシュミスを誘発する。
- NULL判定の最適化: `IS NULL` クエリを多用する場合、NULLをあえて `””` や `0` などの定数に置き換える「正規化」が、Spannerの内部エンコーディング上、ストレージ密度を高める結果になることもある。
Spannerの真髄は、分散システムでありながら、単一ノードDBと同様の確実なクエリプランナーを持つことにある。そのプランナーを飼い慣らすには、NULLが単なる「空」ではなく、「ソートキーという名の巨大な地図上の、特定の位置を占めるノード」であることを理解することから始まる。
この深い理解が、君の作るシステムを数億リクエストに耐えうる堅牢なものへと昇華させるはずだ。次回の設計で、インデックスを貼る前に一度だけ深呼吸して考えてみてほしい。「このNULLは、本当にインデックスの検索パスに存在すべきか?」と。
コメント