Cloud Spannerの「ユニークインデックス」は、甘美な罠である。
Cloud Spannerの設計レビューで最も頭を悩ませるのが、この「ユニークインデックス」の扱いだ。RDBMSに慣れ親しんだエンジニアほど、反射的に `UNIQUE INDEX` を定義して「よし、これで整合性は完璧だ」と安堵する。
だが、Spannerのアーキテクチャを理解している我々にとって、その「安堵」はシステムの寿命を縮めるサインに他ならない。今日は、ユニークインデックスがSpannerの分散環境でどのような「コスト」として跳ね返ってくるのか、そして我々はどう設計すべきかについて、腹を割って話そう。
—
1. ユニークインデックスの「見えない代償」
Spannerにおいて、インデックスは単なる検索の高速化ツールではない。「インデックスもまた、独立したテーブルである」という事実を忘れてはならない。
ユニークインデックスを貼るということは、その列に対する書き込みのたびに、以下のプロセスが強制的に走ることを意味する。
1. Look-up: 新たな値が既存のインデックスに存在しないか、読み取り(Read)が発生する。
2. Validation: 分散トランザクション内での一意性チェックが走る。
3. Commit: 書き込みの成否が確定する。
ここでのボトルネックは、分散環境における「同期的なチェック」だ。特に頻繁に更新される列や、トラフィックが集中するキーに対してユニーク制約をかけると、「ホットスポット」を誘発する。結果として、書き込みレイテンシは増大し、トランザクションの競合によるリトライが急増する。
2. なぜ「アプリケーション側の制約」を優先すべきか
「でも、ビジネスロジック的に一意性を保証しなければならない」と反論されるかもしれない。その通りだ。しかし、Spannerのスケールメリットを最大限に活かすのであれば、以下の優先順位で検討すべきだ。
パターンA:アプリケーション層での制御(推奨)
可能であれば、ユニークインデックスを避け、アプリケーション側で「一意性」を担保する設計に倒すべきだ。例えば、Redisなどの高速なKVSで排他制御を行ったり、そもそも「一意な値」を主キー(PK)そのものに組み込む設計にする。
パターンB:ユニークインデックスを「最終手段」にする
どうしてもデータベースレベルでの制約が必要な場合のみ、`UNIQUE INDEX` を使う。その際、以下の鉄則を忘れてはならない。
— 推奨されない設計: 頻繁に更新される列へのユニークインデックス
— これをやってしまうと、書き込みのたびにインデックスの巨大なツリーが動的に更新される
CREATE UNIQUE INDEX Users_Email_Idx ON Users(Email);
— 堅牢な設計へのアプローチ:
— 制約が必要な列を物理的に分離し、書き込み負荷を限定的にする
3. 知っておくべき「実装上の極限の知見」
実務において、パフォーマンスを殺さないためのテクニックをいくつか伝授しよう。
① インデックスの「スパース」を意識する
もし全レコードではなく、特定条件のレコードのみ一意性を担保すれば良いのであれば、`NULL` を活用せよ。Spannerのインデックスは `NULL` 値を含めない(`NULL_FILTERED`)ことができる。
— 削除済みのレコード(DeletedAt IS NULL)だけ一意にしたい場合
CREATE UNIQUE NULL_FILTERED INDEX Users_Email_Unique_Idx ON Users(Email)
WHERE DeletedAt IS NULL;
これにより、インデックスのサイズを抑制し、不要なチェックコストを削ぎ落とせる。
② ホットスポットの回避
`UUID` をインデックスの先頭に持ってくると、書き込みが分散されてホットスポットを回避できる。これは基本中の基本だが、ユニークインデックスを貼る際も、「書き込みの分散」を意識したキー設計が不可欠だ。単調増加する値(タイムスタンプなど)でユニーク制約をかけると、Spannerのノードが悲鳴を上げる。
4. リードアーキテクトからの助言
私が設計レビューでユニークインデックスの追加提案を見たとき、必ずこう尋ねる。
「そのインデックスは、読み取りのために必要なのか? それとも単なる制約のためか?」
もし後者であれば、アプリケーションのロジックで解決できないか再考を促す。Spannerは「巨大なデータセットをグローバルにスケールさせる」ための怪物だ。その怪物に、レガシーなRDBMSと同じような「何でも制約をかける」運用を強いてはならない。
まとめ:
- ユニークインデックスは「重い」: 分散トランザクションにおける同期的なチェックコストを理解せよ。
- 設計を見直せ: 可能な限りアプリケーションロジックで解決し、データベースを「堅牢なストレージ」に徹させよ。
- NULL_FILTEREDを活用せよ: インデックスの肥大化を防ぎ、真に必要な行だけに制約をかけろ。
Spannerを使いこなすということは、制約と向き合うことだ。安易な `UNIQUE` キーに逃げず、システムの特性に合わせたロジカルな設計を追求してほしい。健闘を祈る。
コメント