【実務・中級編】 NULLフィルタリングインデックス – Cloud Spanner

Cloud Spannerのインデックス戦略:`NULL_FILTERED`で「無駄」を削ぎ落とし、パフォーマンスを極限まで研ぎ澄ます

Cloud Spannerを触るエンジニアの多くが、インデックスを「検索を高速化するための魔法の杖」だと勘違いしている。だが、真のアーキテクトは知っている。インデックスとは、書き込みコストとストレージ容量を代償に得られる「諸刃の剣」であることを。

特に、データ量が増大する大規模システムにおいて、NULL値が混在するカラムへのインデックスは、往々にして「負債」となる。今日は、Spannerが提供する強力な最適化オプションである`NULL_FILTERED`について、実務における「使いどころ」と「設計思想」を語ろう。

—

1. なぜ「NULL」をインデックスに含めてはいけないのか?

Spannerのインデックスは、本質的に別のテーブル(Sorted Table)として保持される。インデックスにNULL値が含まれると、以下のコストが発生する。

  • ストレージの無駄遣い: 検索クエリでまず使われないNULL値の行に対して、インデックスのレコードが生成され、ストレージを消費し続ける。
  • 書き込みスループットの低下: `INSERT`や`UPDATE`のたびに、不要なNULL値のエントリをインデックスへ書き込むオーバーヘッドが生じる。
  • クエリの非効率性: インデックスのサイズが肥大化すれば、検索時のページ読み込み数が増え、メモリ効率(キャッシュヒット率)が悪化する。

「とりあえずこのカラムにインデックスを貼ろう」という思考停止は、Spannerのパフォーマンスを確実に蝕む。

2. `NULL_FILTERED`の真価

`NULL_FILTERED`を指定すると、SpannerはそのカラムがNULLである行をインデックスから完全に除外する。

— 悪い例: 全てのステータスをインデックスに含める
CREATE INDEX UsersByDeletedAt ON Users(DeletedAt);

— 良い例: 削除されていないユーザー(NULL)はインデックス不要
CREATE INDEX UsersByDeletedAt ON Users(DeletedAt)
NULL_FILTERED;

このオプションを適用すべき典型的なケースは、「特定のフラグやタイムスタンプがセットされた時だけ検索する」というパターンだ。

実務での適用例:論理削除

多くのシステムで採用される論理削除(`deleted_at`カラム)を考えてみよう。アクティブなユーザーを検索する際、`deleted_at IS NULL`でフィルタリングすることがほとんどだろう。この時、NULLの行までインデックスに含める必要はあるか? 答えはNOだ。

3. 実践設計:注意すべき「罠」

この機能を導入する際、シニアエンジニアとして必ず意識してほしいポイントが2つある。

① インデックスの「カバーリング」を考慮する

インデックスに含めていないカラムをクエリで取得しようとすると、Spannerはメインテーブルへのルックアップ(Row Lookup)を行う。もし`SELECT `のようなクエリを多用する場合、`NULL_FILTERED`によってインデックスサイズを絞っても、結局ルックアップコストで相殺される可能性がある。

戦略: `STORING`句を併用し、頻出する列をインデックスに含めることで、インデックスだけでクエリを完結させる設計(カバードインデックス)を優先せよ。

CREATE INDEX UsersByDeletedAt ON Users(DeletedAt)
STORING (UserName, Email)
NULL_FILTERED;

② クエリの書き方とオプティマイザ

`NULL_FILTERED`を指定したインデックスは、`IS NULL`を条件とするクエリの最適化には寄与しない(そもそもNULLを捨てているのだから当然だ)。このインデックスが効くのはあくまで「値が入っている行」を検索する時だけである。アプリケーション側のクエリが、インデックスの意図と合致しているか、`EXPLAIN ANALYZE`で常に確認する癖をつけてほしい。

4. 最後に:アーキテクトからの提言

Cloud Spannerにおけるパフォーマンスチューニングとは、「何を加えるか」ではなく「何を削ぎ落とすか」の引き算だ。

`NULL_FILTERED`は、単なるストレージ節約ツールではない。インデックスを「疎」にし、書き込みパスを最適化し、スキャン範囲を最小化するための強力な武器だ。

レビュー中に「このカラム、NULL多くないか?」と感じたら、迷わず`NULL_FILTERED`を検討してほしい。それがシステム全体のレイテンシを数ミリ秒削り、将来的なストレージコストを劇的に改善する第一歩となる。

我々エンジニアの仕事は、コードを書くことではない。「効率的な仕組みを設計し、無駄を排除し続けること」だ。さあ、今すぐインデックス定義を見直してほしい。そこに最適化の余地は残っていないか?

コメント

タイトルとURLをコピーしました