Cloud SpannerのインデックスとNULLの深淵:パフォーマンスを殺さないための設計論
Spannerのインデックス設計において、多くのエンジニアが「なんとなく」で済ませているのがNULLの扱いだ。
RDBMSの経験が長いエンジニアほど、SQL ServerやPostgreSQLの感覚で「NULLなんてインデックスには関係ないだろう」と高を括る。だが、SpannerはGoogleの分散システムだ。この「NULLの解釈」一つで、クエリの実行計画は劇的に変わり、最悪の場合、広大なスキャン範囲を発生させてデータベースを瀕死に追い込む。
今日は、SpannerにおけるNULLとインデックスの、実務で絶対に避けて通れない「最適解」を伝授する。
—
1. SpannerにおけるNULLのインデックス・エントリ仕様
結論から言おう。Spannerのインデックスは、NULL値を「値」として保持する。
多くのレガシーなRDBMSでは、NULL値をインデックスに含めない、あるいは特定のフラグで管理する実装があるが、Spannerは違う。インデックスのキーの一部がNULLであっても、それは「インデックス上の1つのエントリ」として、B-tree(厳密には分割されたSSTable上のキー)の適切な位置に格納される。
これが意味すること
- NULLを含めた検索がインデックスで高速化される: `WHERE column IS NULL` は、インデックスフルスキャンではなく、インデックスシーク(Seek)で解決できる。
- インデックスのサイズ: インデックス列がNULL許容であれば、データが存在しないレコードであってもインデックスエントリは生成される。これはストレージ消費と書き込み負荷に直結する。
—
2. 現場で直面する「NULL罠」:インデックス設計の鉄則
罠:疎な(Sparse)列へのインデックス
例えば、「削除フラグ」や「特定のイベントが発生した時のみ値が入る列」にインデックスを貼るケースだ。
— よくあるアンチパターン
CREATE INDEX idx_user_deleted_at ON Users(deleted_at);
もし `deleted_at` が99% NULLであるなら、インデックスの大部分は「NULL」という同じ値で埋め尽くされることになる。これはホットスポットのリスクを助長するだけでなく、クエリエンジンにとって「NULLばかりのインデックス」は統計情報的に最適化しづらい対象となりがちだ。
プロの設計パターン:NULLを除外する論理
SpannerでNULLをインデックスから「間引きたい」場合、物理的にインデックスからNULLを除外する設定は現時点では存在しない。しかし、以下の設計で回避せよ。
1. 関数インデックス(Computed Columns)の活用:
値が存在する場合だけ特定の値を返す列を作成し、そこにインデックスを貼る。
— 削除されていないものだけをフィルタリングするインデックス
ALTER TABLE Users ADD COLUMN active_id INT64 AS (IF(deleted_at IS NULL, user_id, NULL)) STORED;
CREATE INDEX idx_active_users ON Users(active_id) WHERE active_id IS NOT NULL; — 正確にはNULLをインデックスに含めない運用を設計で模倣する
※補足: Spannerの `NULL_FILTERED` インデックスを活用するのがベストプラクティスだ。
—
3. `NULL_FILTERED` インデックス:隠れた必殺技
Spannerには、インデックス作成時にNULL値をエントリとして保持しないよう指定できる強力なオプションがある。
CREATE NULL_FILTERED INDEX idx_user_email_active
ON Users(email)
WHERE email IS NOT NULL;
これがなぜ重要なのか?
- インデックスの肥大化抑制: 不要なNULLエントリを排除することで、インデックスサイズを物理的に小さくできる。
- キャッシュ効率の向上: B-treeのノード密度が高まり、メモリ上のキャッシュヒット率が劇的に向上する。
大規模システムでは、この「NULL_FILTERED」を使うか使わないかで、インデックスのRead/Write性能が数%〜数十%変わる。レビューで「なぜ普通のインデックスにしたのか?」と問われて即答できないなら、それは設計不足だ。
—
4. パフォーマンス上の注意点:クエリプランの読み解き方
エンジニアがインデックスを設計した後、必ず `EXPLAIN ANALYZE` を実行してほしい。
もし `WHERE col = ?` と `WHERE col IS NULL` を混ぜたクエリを発行する際、NULL対応のインデックスが適切に使われていないと、Spannerは `Index Scan` ではなく `Table Scan` を選択することがある。
特に、NULLを含むカラムを結合キーに使用する場合には細心の注意が必要だ。
- SQL標準では `NULL = NULL` は `FALSE` となる。
- Spannerの結合においても、NULLが含まれると結合が期待通りにヒットしない。
この挙動を回避するために、アプリケーション層でNULLを何らかの「デフォルト値(例: -1 や “”)」に置き換えて格納する設計をとるチームがある。これについては賛否両論あるが、「データ整合性と検索パフォーマンスのどちらを優先するか」を明確に定義してから実装するように。
—
終わりに:アーキテクトからの助言
Cloud Spannerのインデックスは、単なる検索の高速化ツールではない。分散環境における「データの配置戦略」そのものだ。
- NULLはデータである。
- NULL_FILTERED インデックスを検討せよ。
- インデックスサイズを常に監視せよ。
「なんとなくインデックスを貼る」という行為は、将来のシステムの足を引っ張る技術的負債を今すぐ作る行為に等しい。次回の設計レビューでは、NULLがどの程度の割合で存在し、それがインデックスにどのような影響を与えるかを論理的に説明できるように準備しておくこと。
君たちが書くコードが、Spannerの真価を最大限に引き出すことを期待している。
コメント