Cloud Spanner「生成列(Generated Columns)」:単なる便利機能と侮るな、これはクエリ最適化の最終兵器だ
設計レビューの場で、よくこんな議論になる。「この計算ロジック、アプリケーション側でやりますか? それともデータベース側でやりますか?」
若手エンジニアは往々にして「アプリケーション側で吸収します」と言う。しかし、大規模な分散データベースであるCloud Spannerを相手にする場合、その回答はしばしば技術的負債への片道切符となる。
今日は、Cloud Spannerにおける「生成列(Generated Columns)」という武器について話そう。これは単に「列を自動計算する」ための機能ではない。分散クエリの効率を劇的に高め、読み取り負荷を劇的に下げるためのアーキテクチャ上の戦略的選択なのだ。
—
1. 生成列とは何か:透過的かつ強制的な計算層
生成列とは、他の列の値を用いて特定の式(Expression)で計算された値を格納する列だ。Cloud Spannerでは `STORED`(保存型)のみをサポートしている。
重要なのは、これが「書き込み時に計算され、データとして物理的に格納される」という点だ。
— 例:ユーザーの姓名から検索用の正規化文字列を生成するテーブル
CREATE TABLE Users (
UserId INT64 NOT NULL,
FirstName STRING(MAX),
LastName STRING(MAX),
— 検索用に全角半角を揃え、小文字化した文字列を生成
SearchKey STRING(MAX) AS (LOWER(CONCAT(FirstName, LastName))) STORED,
) PRIMARY KEY (UserId);
この定義により、`SearchKey` は常に `FirstName` と `LastName` の整合性を保った状態でディスク上に存在する。アプリケーション側でわざわざ計算して、複数の列を `UPDATE` 文で投げ分ける必要は一切ない。
2. なぜ「生成列」がクエリ最適化の鍵なのか
多くのエンジニアが犯すミスは、クエリ実行時に `WHERE LOWER(FirstName) = …` のような関数を多用することだ。
Cloud Spannerにおいて、`WHERE` 句でカラムに関数を噛ませると、インデックスが効かなくなる(Full Table Scanを誘発する)という致命的なパフォーマンス低下を招く。
生成列を使えば、この問題は魔法のように消える。
- インデックスとの強力な親和性: 生成列に対してインデックスを作成すれば、クエリプランは自動的にそのインデックスを利用する。複雑な計算結果を即座に引けるようになるのだ。
- 計算負荷のオフロード: 読み取りのたびに計算するのではなく、書き込み時に一度だけ計算する。これは読み取りアクセスが集中する高負荷システムにおいて、CPUリソースの節約に直結する。
3. 実務で「刺さる」設計パターン
私がプロジェクトで推奨する「生成列」の活用シーンは以下の2点だ。
A. 複雑なフィルタリングの高速化(非正規化)
例えば、JSONB型のデータから特定のキーを取り出してインデックスを貼るパターン。
ALTER TABLE Orders ADD COLUMN OrderDate DATE AS (JSON_VALUE(Attributes, ‘$.date’)) STORED;
CREATE INDEX OrdersByDate ON Orders(OrderDate);
JSONのパースコストをクエリ実行時に支払う必要はなくなる。`OrderDate` は普通の列としてインデックスに乗り、検索速度は爆速になる。
B. データの整合性担保(ビジネスロジックの埋め込み)
分散システムにおいて、アプリケーションコードのバージョン差異による「計算ロジックの不一致」は悪夢だ。生成列にロジックを閉じ込めることで、どの言語(Go, Java, Python)からアクセスしても、計算結果は常にデータベースが保証する唯一の真実(Single Source of Truth)として維持される。
4. 注意せよ:パフォーマンスの「落とし穴」
ここまで持ち上げたが、伝説のアーキテクトとして忠告もしておく。以下の点に注意しなければ、生成列は毒にもなる。
1. 書き込みコストの増大: 生成列は「書き込み時に計算する」ため、複雑すぎる式を定義すると、`INSERT/UPDATE` のレイテンシが確実に増える。計算式は極力シンプルに保て。
2. スキーマ変更のコスト: 生成列を追加・変更する場合、既存データに対するバックフィル(データ埋め込み)が発生する。巨大なテーブルに対して無計画に実行すれば、スループットに影響が出る。計画的な `ALTER TABLE` を心がけること。
3. 非決定的な関数は使えない: `CURRENT_TIMESTAMP()` などの非決定的な関数は生成列には使えない。これはSpannerが分散環境で一貫性を維持するために避けて通れない制約だ。
結論:プロの設計は「計算」を外に出さない
Cloud Spannerにおける生成列は、単なる「便利な記法」ではない。それは「データが物理的にどう保持されるか」をコントロールし、クエリの検索パスを最適化するための強力な設計ツールだ。
アプリケーション層に複雑な計算ロジックを放置するな。データベースの特性を理解し、計算をデータの中に閉じ込めよ。それが、スケールアウトを前提としたSpannerシステムを構築する上での、最も堅牢なアプローチだ。
レビューでこのコードを見かけたら、私はこう言うだろう。「よく分かっている。そのまま採用しよう」と。
コメント