なぜGenerated Columnsに注目すべきなのか?
数十ものクエリの中で (price * quantity) AS total_price という記述を何度も繰り返すのは、退屈なだけでなくミスも誘発しやすくなります。さらに深刻なのは、JSONデータを保存していて、その中の特定のフィールドで頻繁にフィルタリングを行う場合です。テーブルが数百万行に達すると、JSON_EXTRACT 関数はCPUに多大な負荷をかけることになります。
以前は、補助的なカラムを更新するために Trigger を使用したり、Backend 側で直接計算を行ったりするのが一般的でした。しかし、Trigger はメンテナンスが難しい「ブラックボックス」になりがちです。一方で、アプリケーション層で計算を行うと、検索を高速化するための Index を貼ることが不可能になります。
私が参加したあるプロジェクトの実例: 約1,000万レコード(容量50GB)の orders テーブルにおいて、計算処理をクエリから Generated Columns に移行したことで、CPU負荷を70%から25%にまで削減できました。Generated Columns(MySQL 5.7から導入)を使用すると、同じ行内の既存データから自動的に計算されるカラムを作成できます。
リソースの無駄を避けるために、これら2つのカラムタイプの違いを明確に理解しておく必要があります:
- Virtual Generated Columns(デフォルト): データが読み取られるときにのみ値が計算されるため、ディスク容量を消費しません。重要なのは、この仮想カラムに対しても Index を貼ることができる という点です。
- Stored Generated Columns:
INSERTまたはUPDATE時に値が物理的にディスクへ保存されます。ストレージ容量を消費しますが、MySQLが再計算する必要がないため、読み取り速度は非常に高速になります。
実践的な実装:構文と使い方
これはコア機能であるため、追加のエクステンションをインストールする必要はありません。以下は、テーブル作成時に自動計算カラムを定義する方法です:
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
price DECIMAL(10, 2),
quantity INT,
-- 仮想カラム:ストレージ容量の節約
total_price DECIMAL(10, 2) AS (price * quantity) VIRTUAL,
-- 保存済みカラム:CPU負荷の最適化
total_price_stored DECIMAL(10, 2) AS (price * quantity) STORED
);
稼働中のシステムであれば、ALTER TABLE を使用できます。私は通常、アプリケーションのロジック構造を壊すことなく、遅いクエリを最適化するためにこの方法を使用します:
ALTER TABLE products
ADD COLUMN discounted_price DECIMAL(10, 2)
AS (original_price * 0.9) VIRTUAL;
重要な注意点:これらのカラムに手動でデータを INSERT することはできません。MySQLが完全に制御権を持ちます。アプリケーション側から値を上書きしようとする試みは、システムによって即座に拒否されます。
大規模システムを救う2つのケーススタディ
1. JSONデータの検索を25倍高速化する
これが最も実用的な例です。ユーザー設定を settings カラム(JSON型)に保存していると仮定します。theme = 'dark' に設定している全ユーザーを検索したい場合:
-- このクエリはフルテーブルスキャン(Full Table Scan)を発生させ、非常に低速です!
SELECT * FROM user_profiles WHERE settings->"$.theme" = 'dark';
解決策は、theme を抽出する Virtual Column を作成し、そこに Index を貼ることです:
ALTER TABLE user_profiles
ADD COLUMN user_theme VARCHAR(20) AS (settings->>"$.theme") VIRTUAL,
ADD INDEX idx_user_theme (user_theme);
これを適用すると、MySQLは各JSONファイルをパースする代わりに B-Tree Index を使用して検索を行います。レスポンスタイムは 500ms から 20ms 以下に短縮される可能性があります。
2. フルネーム(Full Name)による検索
WHERE 句で CONCAT(first_name, ' ', last_name) を使用して Index を無効化してしまう代わりに、Stored Column を使用しましょう:
ALTER TABLE employees
ADD COLUMN full_name VARCHAR(255) AS (CONCAT(first_name, ' ', last_name)) STORED,
ADD INDEX idx_full_name (full_name);
Virtual と Stored、どちらを選ぶべきか?
サーバーのリソースに基づいて検討してください:
- Virtual を選ぶ場合: ディスク容量を節約したいとき。データの変更は少ないが、フィルタリング(Filter)によく使われるフィールドに Index を貼る場合に非常に効果的です。
- Stored を選ぶ場合: 計算が非常に複雑で、文字列処理に多大なCPUリソースを消費する場合。または、仮想カラムへの Index サポートが不十分な古いバージョンの MySQL を使用している場合。
最適化効果の確認
変更後は、必ず EXPLAIN を使って検証してください。key カラムに作成した Index 名が表示されていれば成功です。
EXPLAIN SELECT * FROM user_profiles WHERE user_theme = 'dark';
システム内の自動計算カラムの一覧を管理するには、information_schema を介して素早く照会できます:
SELECT table_name, column_name, generation_expression
FROM information_schema.columns
WHERE is_generated = 'ALWAYS' AND table_schema = 'your_db_name'; -- your_db_name は実際のデータベース名に置き換えてください
実体験に基づいたアドバイス:巨大なテーブルで Stored Columns を使用する場合は、ディスク容量(Data_length)を注意深く監視してください。逆に、複雑な正規表現(Regex)関数を含む Virtual Columns を使用する場合は、サーバーのCPUグラフに注意を払ってください。
結論として、Generated Columns は利便性とパフォーマンスの完璧なバランスを実現するソリューションです。Backend のロジックを大きく変えることなく、SQLコードをスッキリさせ、Index の力を最大限に引き出すことができます。

