概要
MySQL Generated Columns (生成列) は、MySQL 5.7.6で導入されたテーブル定義機能である。
テーブル内の他のカラムから値を計算して生成される特殊なカラムである。
Generated Columnsには、VIRTUAL (仮想列) と STORED (格納列) の2つのタイプがある。
- VIRTUAL (仮想列)
- データを物理的に格納せず、行読み取り時にオンザフライで計算する。
- デフォルトのタイプはVIRTUALである。
- STORED (格納列)
- INSERT / UPDATE時に計算結果を物理的に格納する。
Generated Columnsは、繰り返し計算される値の自動化、JSON列からの値抽出とインデックス作成、複雑な式のクエリ簡略化に利用される。
Generated Columnsに対してインデックスを作成することにより、計算結果に基づく高速な検索が可能になる。
ストレージエンジンは、InnoDBが完全対応している。
MyISAMは基本機能のみサポートしており、VIRTUAL列へのインデックス作成は非対応である。
Generated Columnsは、関数インデックス (MySQL 8.0.13以降) と密接に関連している。
関数インデックスは内部的にVIRTUAL Generated Columnとして実装されている。
基本構文
CREATE TABLEでの定義
Generated Columnsは、CREATE TABLE 文で定義する。
基本構文を以下に示す。
CREATE TABLE table_name (
col_name data_type [GENERATED ALWAYS] AS (expression)
[VIRTUAL | STORED]
[NOT NULL | NULL]
[UNIQUE [KEY]]
[[PRIMARY] KEY]
[COMMENT 'string']
);
下表に、構文要素を示す。
| 構文要素 | 説明 |
|---|---|
GENERATED ALWAYS
|
|
AS (expression)
|
|
VIRTUAL | STORED
|
|
NOT NULL | NULL
|
NULL許可属性 |
UNIQUE [KEY]
|
|
[PRIMARY] KEY
|
|
- フルネーム生成の例
CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, first_name VARCHAR(50), last_name VARCHAR(50), full_name VARCHAR(101) GENERATED ALWAYS AS (CONCAT(first_name, ' ', last_name)) VIRTUAL );
- 税込価格計算の例
CREATE TABLE products ( id INT PRIMARY KEY, name VARCHAR(100), price DECIMAL(10, 2), tax_rate DECIMAL(5, 2), price_with_tax DECIMAL(10, 2) AS (price * (1 + tax_rate / 100)) STORED );
GENERATED ALWAYSキーワードの省略例CREATE TABLE orders ( id INT PRIMARY KEY, order_date DATE, order_year INT AS (YEAR(order_date)) VIRTUAL, order_month INT AS (MONTH(order_date)) VIRTUAL );
ALTER TABLEでの追加
既存のテーブルにGenerated Columnを追加する場合、ALTER TABLE 文を使用する。
基本構文を以下に示す。
ALTER TABLE table_name
ADD COLUMN col_name data_type [GENERATED ALWAYS] AS (expression) [VIRTUAL | STORED];
- VIRTUAL列の追加例を以下に示す。
ALTER TABLE users ADD COLUMN email_domain VARCHAR(100) AS (SUBSTRING_INDEX(email, '@', -1)) VIRTUAL;
- STORED列の追加例
ALTER TABLE products ADD COLUMN discounted_price DECIMAL(10, 2) AS (price * 0.9) STORED;
- Generated Columnの削除は、通常のカラムと同様に
DROP COLUMN句を使用する。ALTER TABLE users DROP COLUMN full_name;
- VIRTUALからSTOREDへの変換は、ALTERで可能である。
ALTER TABLE users MODIFY COLUMN full_name VARCHAR(101) AS (CONCAT(first_name, ' ', last_name)) STORED;
ALTER TABLE 操作のアルゴリズムは、生成タイプによって異なる。
VIRTUALカラムの追加は、ALGORITHM=INSTANT で実行される。
STOREDカラムの追加は、ALGORITHM=COPY でテーブル全体を再構築する。
| 操作 | VIRTUAL | STORED |
|---|---|---|
| カラム追加 | INSTANT (メタデータ変更のみ) | COPY (テーブル全体再構築) |
| カラム削除 (8.0.29+) | INSTANT | INSTANT |
| 式の変更 | COPY | COPY |
| VIRTUAL -> STORED変換 | COPY | - |
| 並行DML許可 | 可 (追加時) | 不可 (追加時) |
式の記述ルール
Generated Columnsの式には、使用可能な要素と使用不可な要素が定義されている。
下表に、使用可能な要素を示す。
| 要素 | 説明 |
|---|---|
| リテラル値 | 数値、文字列、日付リテラル |
| 同一テーブル内の他のカラム参照 | 同じ行の他のカラムを参照可能 |
| 決定的組み込み関数 | 同じ入力に対して常に同じ結果を返す関数 |
| 演算子 | 算術演算子、比較演算子、論理演算子 |
下表に、使用不可な要素を示す。
| 要素 | 説明 |
|---|---|
| サブクエリ | SELECT文を式内で使用不可 |
| ストアドプロシージャ / 関数 | ユーザ定義関数の呼び出し不可 |
| ユーザ変数 | @variable 形式の変数不可
|
| システム変数 | @@variable 形式の変数不可
|
| 非決定的関数 | NOW(), RAND(), CURRENT_USER(), CONNECTION_ID() 等
|
AUTO_INCREMENTカラムの参照 |
AUTO_INCREMENT カラムを式で参照不可
|
| 動的パラメータ | プリペアドステートメントの ? パラメータ不可 |
使用可能な関数と使用不可な関数の一覧を以下に示す。
| カテゴリ | 使用可能 | 使用不可 |
|---|---|---|
| 文字列関数 | CONCAT, SUBSTRING, UPPER, LOWER, LENGTH |
LOAD_FILE, UUID
|
| 数学関数 | ABS, ROUND, FLOOR, CEIL, MOD |
RAND
|
| 日付関数 | YEAR, MONTH, DAY, DATE_FORMAT |
NOW, CURDATE, CURTIME, CURRENT_TIMESTAMP
|
| JSON関数 | JSON_EXTRACT, JSON_UNQUOTE |
- |
| システム関数 | - | CURRENT_USER, CONNECTION_ID, LAST_INSERT_ID
|
他のGenerated Columnを参照することも可能である。
ただし、定義順序に従って参照する必要があり、循環参照は禁止されている。
Generated Column間の参照例を以下に示す。
以下の例では、is_far カラムが distance_from_origin カラムを参照している。
distance_from_origin が先に定義されているため、参照可能である。
CREATE TABLE geometry (
x DOUBLE,
y DOUBLE,
distance_from_origin DOUBLE AS (SQRT(x * x + y * y)) VIRTUAL,
is_far BOOLEAN AS (distance_from_origin > 10) VIRTUAL
);
仮想列 (VIRTUAL)
動作原理
VIRTUAL列は、データを物理的に格納しない。
行読み取り時にオンザフライで式を評価して、値を計算する。
VIRTUAL列の特性を以下に示す。
- ストレージ消費
- 基本的にストレージを消費しない
- ただし、インデックス作成時はインデックス領域を消費する
- 計算タイミング
- SELECT文実行時、WHERE句評価時
- 行読み取りのたびに再計算される
- パフォーマンス
- 読み取り時に計算コストが発生
- 書き込み時は影響なし
- インデックス
- セカンダリインデックスを作成可能 (InnoDB 8.0以降)
- プライマリキーは作成不可
VIRTUAL列が適している用途を以下に示す。
- 計算コストが低い式
- 単純な文字列結合、算術演算
- アクセス頻度が低い列
- 稀にしか参照されない計算値
- ストレージ節約が重要な場合
- ディスク容量を節約したい
- 頻繁に更新されるテーブル
- 書き込みパフォーマンスを重視
VIRTUAL列の評価タイミングを以下に示す。
- SELECTリストに含まれる場合
- 行読み取り時に評価
- WHERE句で使用される場合
- 行フィルタリング時に評価
- ORDER BY句で使用される場合
- ソート時に評価
- GROUP BY句で使用される場合
- グループ化時に評価
使用例
- フルネーム生成の例
CREATE TABLE employees ( id INT PRIMARY KEY AUTO_INCREMENT, first_name VARCHAR(50), last_name VARCHAR(50), full_name VARCHAR(101) AS (CONCAT(first_name, ' ', last_name)) VIRTUAL );
- データ挿入の例
INSERT INTO employees (first_name, last_name) VALUES ('John', 'Doe'), ('Jane', 'Smith'), ('Alice', 'Johnson');
- VIRTUAL列を含む検索の例
SELECT id, full_name FROM employees;
- 結果を以下に示す。
+----+--------------+ | id | full_name | +----+--------------+ | 1 | John Doe | | 2 | Jane Smith | | 3 | Alice Johnson| +----+--------------+
- 日付分解の例
CREATE TABLE events ( id INT PRIMARY KEY, event_name VARCHAR(100), event_date DATE, event_year INT AS (YEAR(event_date)) VIRTUAL, event_month INT AS (MONTH(event_date)) VIRTUAL, event_day INT AS (DAY(event_date)) VIRTUAL );
- JSON値抽出の例
CREATE TABLE user_profiles ( id INT PRIMARY KEY, data JSON, email VARCHAR(100) AS (JSON_UNQUOTE(JSON_EXTRACT(data, '$.email'))) VIRTUAL, age INT AS (JSON_EXTRACT(data, '$.age')) VIRTUAL );
- データ挿入の例
INSERT INTO user_profiles (id, data) VALUES (1, '{"email": "user1@example.com", "age": 25}'), (2, '{"email": "user2@example.com", "age": 30}');
- VIRTUAL列を使った検索の例
- 以下の例では、VIRTUAL列
ageがWHERE句で評価される。 SELECT id, email, age FROM user_profiles WHERE age > 28;
- 以下の例では、VIRTUAL列
格納列 (STORED)
動作原理
STORED列は、計算結果を物理的にディスクに格納する。
INSERT/UPDATE時に式を評価して、計算結果をテーブルに保存する。
下表に、STORED列の特性を示す。
| 特性 | 説明 |
|---|---|
| ストレージ消費 |
|
| 計算タイミング |
|
| パフォーマンス |
|
| インデックス | セカンダリインデックス、プライマリキー、UNIQUE制約全て可能 |
下表に、STORED列が適している用途を示す。
| 用途 | 説明 |
|---|---|
| 計算コストが高い式 | 複雑な数学計算、文字列処理 |
| アクセス頻度が高い列 | 頻繁に検索される計算値 |
| インデックスを作成したい列 | プライマリキーやUNIQUE制約が必要な場合 |
| 参照頻度が更新頻度より高い場合 | 読み取りパフォーマンスを重視 |
下表に、STORED列の更新トリガー条件を示す。
| 条件 | 説明 |
|---|---|
| INSERT時 | 必ず計算される |
| UPDATE時 |
|
使用例
- 税込価格計算の例
CREATE TABLE products ( id INT PRIMARY KEY, name VARCHAR(100), price DECIMAL(10, 2), tax_rate DECIMAL(5, 2) DEFAULT 10.00, price_with_tax DECIMAL(10, 2) AS (price * (1 + tax_rate / 100)) STORED );
- データ挿入の例
INSERT INTO products (id, name, price) VALUES (1, 'Product A', 100.00), (2, 'Product B', 200.00);
- 格納された値の確認
SELECT id, name, price, price_with_tax FROM products;
- 結果を以下に示す。
+----+-----------+--------+----------------+ | id | name | price | price_with_tax | +----+-----------+--------+----------------+ | 1 | Product A | 100.00 | 110.00 | | 2 | Product B | 200.00 | 220.00 | +----+-----------+--------+----------------+
- 価格更新時の再計算例
UPDATE products SET price = 150.00 WHERE id = 1;
- 更新後の確認
SELECT id, name, price, price_with_tax FROM products WHERE id = 1;
- 結果を以下に示す。
+----+-----------+--------+----------------+ | id | name | price | price_with_tax | +----+-----------+--------+----------------+ | 1 | Product A | 150.00 | 165.00 | +----+-----------+--------+----------------+
- STORED列をプライマリキーとして使用する例
CREATE TABLE composite_keys ( year INT, month INT, day INT, date_key VARCHAR(10) AS (CONCAT(year, '-', LPAD(month, 2, '0'), '-', LPAD(day, 2, '0'))) STORED, PRIMARY KEY (date_key) );
- JSON列からの値抽出とSTOREDの組み合わせ例
- 以下の例では、JSON列から抽出した値をSTORED列に格納し、インデックスを作成している。
- これにより、JSON列への直接検索よりも高速な検索が可能になる。
CREATE TABLE orders ( id INT PRIMARY KEY, customer_data JSON, customer_email VARCHAR(100) AS (JSON_UNQUOTE(JSON_EXTRACT(customer_data, '$.email'))) STORED, INDEX idx_email (customer_email) );
VIRTUALとSTOREDの比較
VIRTUALとSTOREDの違いを体系的に比較する。
| 項目 | VIRTUAL | STORED |
|---|---|---|
| ストレージ使用 | なし (インデックス除く) | あり (データ型に応じた容量) |
| 計算タイミング | 読み取り時 (SELECT, WHERE, ORDER BY等) | 書き込み時 (INSERT, UPDATE) |
| 読み取りパフォーマンス | 計算コスト発生 (遅い) | 高速 (計算不要) |
| 書き込みパフォーマンス | 影響なし (高速) | 計算コスト発生 (遅い) |
| セカンダリインデックス | 対応 (InnoDB 8.0+) | 対応 |
| プライマリキー | 非対応 | 対応 |
| UNIQUE制約 | 対応 | 対応 |
| 外部キー参照 | 非対応 | 一部制約あり (CASCADE等不可) |
| デフォルト | はい | いいえ |
| ALTER TABLE追加アルゴリズム | INSTANT | COPY |
下表に、選択基準を示す。
| タイプ | 選択すべき場合 | 説明 |
|---|---|---|
| VIRTUAL | ストレージ容量を節約したい | ディスク容量が限られている |
| 計算式が単純 | 文字列結合、単純な算術演算 | |
| アクセス頻度が低い | 稀にしか参照されない | |
| 更新頻度が高い | 頻繁にINSERT / UPDATEされる | |
| セカンダリインデックスのみ必要 | プライマリキーとして使用しない | |
| STORED | 読み取りパフォーマンスが重要 | 頻繁に検索される |
| 計算式が複雑 | 複雑な文字列処理、JSON解析、数学計算 | |
| プライマリキーとして使用 | テーブルの主キーとして必要 | |
| インデックスを作成したい | 高速検索が必要 | |
| 更新頻度が低い | 書き込みパフォーマンスへの影響が小さい |
パフォーマンス比較例を以下に示す。
-- VIRTUAL版
CREATE TABLE test_virtual (
id INT PRIMARY KEY,
value INT,
calculated INT AS (value * 100) VIRTUAL,
INDEX idx_calc (calculated)
);
-- STORED版
CREATE TABLE test_stored (
id INT PRIMARY KEY,
value INT,
calculated INT AS (value * 100) STORED,
INDEX idx_calc (calculated)
);
100万行のINSERTパフォーマンス比較を以下に示す。
- VIRTUAL版
- 約10秒 (計算不要)
- STORED版
- 約12秒 (計算と格納が必要)
インデックス付き検索パフォーマンス比較を以下に示す。
SELECT * FROM test_virtual WHERE calculated = 5000;
SELECT * FROM test_stored WHERE calculated = 5000;
- VIRTUAL版
- 約0.05秒 (インデックス使用)
- STORED版
- 約0.05秒 (インデックス使用)
インデックスを使用する場合、検索パフォーマンスはほぼ同等である。
インデックスとの組み合わせ
VIRTUAL列へのインデックス
MySQL 8.0以降のInnoDBでは、VIRTUAL列にセカンダリインデックスを作成できる。
VIRTUAL列インデックスの特性を以下に示す。
- インデックス作成
- セカンダリインデックスのみ作成可能
- プライマリキーは作成不可
- 実体化タイミング
- INSERT/UPDATE時にインデックスレコード内で値が実体化される
- インデックスレコードには計算結果が格納される
- パフォーマンス影響
- INSERT/UPDATE時にインデックス計算コストが発生
- SELECT時はインデックスから直接読み取り可能
- VIRTUAL列へのインデックス作成例
CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, first_name VARCHAR(50), last_name VARCHAR(50), full_name VARCHAR(101) AS (CONCAT(first_name, ' ', last_name)) VIRTUAL, INDEX idx_full_name (full_name) );
- インデックスを使用した検索例
SELECT id, full_name FROM users WHERE full_name = 'John Doe';
- 実行計画を確認する。
EXPLAIN SELECT id, full_name FROM users WHERE full_name = 'John Doe';
- 結果を以下に示す。
- idx_full_name インデックスが使用されていることが確認できる。
+----+-------------+-------+------+---------------+---------------+---------+-------+ | id | select_type | table | type | possible_keys | key | key_len | rows | +----+-------------+-------+------+---------------+---------------+---------+-------+ | 1 | SIMPLE | users | ref | idx_full_name | idx_full_name | 404 | 1 | +----+-------------+-------+------+---------------+---------------+---------+-------+
- カバリングインデックスの例
CREATE TABLE employees ( id INT PRIMARY KEY, first_name VARCHAR(50), last_name VARCHAR(50), salary DECIMAL(10, 2), full_name VARCHAR(101) AS (CONCAT(first_name, ' ', last_name)) VIRTUAL, INDEX idx_full_name_salary (full_name, salary) );
- カバリングインデックスを使用したクエリ例
- 以下の例では、インデックスのみから結果を取得でき、テーブルアクセスが不要になる。
SELECT full_name, salary FROM employees WHERE full_name LIKE 'John%';
JSON列への仮想インデックス
JSON列に対して直接インデックスを作成することはできない。
そのため、Generated Columnを使用してJSON列から値を抽出し、その列にインデックスを作成する。
JSON列仮想インデックスのパターンを以下に示す。
- JSON列から値を抽出するGenerated Columnを定義
- Generated Columnにインデックスを作成
- WHERE句でGenerated Columnを使用して検索
- JSON列仮想インデックスの例
CREATE TABLE products ( id INT PRIMARY KEY, name VARCHAR(100), attributes JSON, category VARCHAR(50) AS (JSON_UNQUOTE(JSON_EXTRACT(attributes, '$.category'))) STORED, price DECIMAL(10, 2) AS (JSON_EXTRACT(attributes, '$.price')) STORED, INDEX idx_category (category), INDEX idx_price (price) );
- データ挿入例
INSERT INTO products (id, name, attributes) VALUES (1, 'Product A', '{"category": "electronics", "price": 299.99, "brand": "BrandX"}'), (2, 'Product B', '{"category": "books", "price": 19.99, "brand": "PublisherY"}'), (3, 'Product C', '{"category": "electronics", "price": 499.99, "brand": "BrandZ"}');
- カテゴリ検索例
SELECT id, name, category, price FROM products WHERE category = 'electronics';
- 価格範囲検索例
SELECT id, name, category, price FROM products WHERE price BETWEEN 100 AND 500;
- 複合インデックスの例
CREATE TABLE user_logs ( id INT PRIMARY KEY, user_id INT, log_data JSON, log_type VARCHAR(50) AS (JSON_UNQUOTE(JSON_EXTRACT(log_data, '$.type'))) VIRTUAL, log_timestamp DATETIME AS (JSON_UNQUOTE(JSON_EXTRACT(log_data, '$.timestamp'))) VIRTUAL, INDEX idx_type_timestamp (log_type, log_timestamp) );
- 複合インデックスを使用した検索例
SELECT id, log_type, log_timestamp FROM user_logs WHERE log_type = 'login' AND log_timestamp > '2025-01-01';
関数インデックスとの関係
MySQL 8.0.13以降では、関数インデックス (Functional Index) がサポートされている。
関数インデックスは、内部的にVIRTUAL Generated Columnとして実装されている。
関数インデックスの構文を以下に示す。
CREATE INDEX index_name ON table_name ((expression));
関数インデックスの例を以下に示す。
CREATE TABLE users (
id INT PRIMARY KEY,
email VARCHAR(100)
);
CREATE INDEX idx_email_domain ON users ((SUBSTRING_INDEX(email, '@', -1)));
上記のインデックスは、内部的に以下と等価である。
ALTER TABLE users
ADD COLUMN __hidden_generated_column VARCHAR(100)
AS (SUBSTRING_INDEX(email, '@', -1)) VIRTUAL;
CREATE INDEX idx_email_domain ON users (__hidden_generated_column);
下表に、関数インデックスとGenerated Columnの比較を示す。
| 項目 | 関数インデックス | Generated Column + インデックス |
|---|---|---|
| 構文簡潔性 | シンプル (1ステップ) | 複雑 (2ステップ) |
| カラム可視性 | 隠し列 (SELECTで見えない) | 通常列 (SELECTで見える) |
| 明示的参照 | 不可 | 可能 |
| オプティマイザ認識 | 式パターンマッチングが必要 | 明示的カラム参照で最適化 |
| 内部実装 | VIRTUAL Generated Column | VIRTUAL Generated Column |
| 対応バージョン | 8.0.13+ | 5.7.6+ |
関数インデックスを使用した検索例を以下に示す。
SELECT id, email FROM users WHERE SUBSTRING_INDEX(email, '@', -1) = 'example.com';
オプティマイザは、式を認識して関数インデックスを使用する。
| メリット | 説明 |
|---|---|
| 明示的にカラムを参照できる | SELECT句で直接使用可能 |
| 複数のインデックスで共有可能 | 同じ計算結果を複数のインデックスで利用 |
| バージョン互換性 | MySQL 5.7.6以降で使用可能 |
下表に、
| メリット | 説明 |
|---|---|
| 構文がシンプル | 1ステップでインデックス作成 |
| スキーマ汚染を回避 | 不要なカラムがテーブル定義に表示されない |
サンプルクエリ
実用的なGenerated Columnsの使用例を示す。
- フルネーム生成と検索の例
CREATE TABLE employees ( id INT PRIMARY KEY AUTO_INCREMENT, first_name VARCHAR(50), last_name VARCHAR(50), full_name VARCHAR(101) AS (CONCAT(first_name, ' ', last_name)) VIRTUAL, INDEX idx_full_name (full_name) ); INSERT INTO employees (first_name, last_name) VALUES ('John', 'Doe'), ('Jane', 'Smith'); SELECT * FROM employees WHERE full_name = 'John Doe';
- 税込価格計算の例
CREATE TABLE products ( id INT PRIMARY KEY, name VARCHAR(100), price DECIMAL(10, 2), tax_rate DECIMAL(5, 2) DEFAULT 10.00, price_with_tax DECIMAL(10, 2) AS (price * (1 + tax_rate / 100)) STORED ); INSERT INTO products (id, name, price) VALUES (1, 'Product A', 100.00); SELECT name, price, price_with_tax FROM products;
- 日付分解とパーティショニングの例
CREATE TABLE orders ( id INT PRIMARY KEY, order_date DATE, customer_id INT, amount DECIMAL(10, 2), order_year INT AS (YEAR(order_date)) STORED, order_month INT AS (MONTH(order_date)) STORED ) PARTITION BY RANGE (order_year) ( PARTITION p2023 VALUES LESS THAN (2024), PARTITION p2024 VALUES LESS THAN (2025), PARTITION p2025 VALUES LESS THAN (2026), PARTITION pmax VALUES LESS THAN MAXVALUE );
- JSON列からの値抽出とインデックスの例
CREATE TABLE user_profiles ( id INT PRIMARY KEY, username VARCHAR(50), profile_data JSON, email VARCHAR(100) AS (JSON_UNQUOTE(JSON_EXTRACT(profile_data, '$.email'))) STORED, age INT AS (JSON_EXTRACT(profile_data, '$.age')) STORED, country VARCHAR(50) AS (JSON_UNQUOTE(JSON_EXTRACT(profile_data, '$.country'))) STORED, INDEX idx_email (email), INDEX idx_age (age), INDEX idx_country (country) ); INSERT INTO user_profiles (id, username, profile_data) VALUES (1, 'user1', '{"email": "user1@example.com", "age": 25, "country": "USA"}'), (2, 'user2', '{"email": "user2@example.com", "age": 30, "country": "Japan"}'); SELECT username, email, age FROM user_profiles WHERE country = 'Japan';
- POINT型座標計算の例
CREATE TABLE locations ( id INT PRIMARY KEY, name VARCHAR(100), x DOUBLE, y DOUBLE, coordinates POINT AS (POINT(x, y)) STORED, SPATIAL INDEX idx_coordinates (coordinates) ); INSERT INTO locations (id, name, x, y) VALUES (1, 'Location A', 35.6895, 139.6917), (2, 'Location B', 34.6937, 135.5023); SELECT name FROM locations WHERE ST_Distance_Sphere(coordinates, POINT(35.0, 139.0)) < 100000;
- メールアドレスドメイン抽出の例
CREATE TABLE users ( id INT PRIMARY KEY, username VARCHAR(50), email VARCHAR(100), email_domain VARCHAR(100) AS (SUBSTRING_INDEX(email, '@', -1)) VIRTUAL, INDEX idx_email_domain (email_domain) ); INSERT INTO users (id, username, email) VALUES (1, 'user1', 'user1@gmail.com'), (2, 'user2', 'user2@yahoo.com'), (3, 'user3', 'user3@gmail.com'); SELECT username, email FROM users WHERE email_domain = 'gmail.com';
- ハッシュ値計算とユニーク制約の例
CREATE TABLE documents ( id INT PRIMARY KEY AUTO_INCREMENT, content TEXT, content_hash VARCHAR(64) AS (SHA2(content, 256)) STORED, UNIQUE KEY uk_content_hash (content_hash) ); INSERT INTO documents (content) VALUES ('Document content 1'); -- 重複挿入を試みる INSERT INTO documents (content) VALUES ('Document content 1'); -- Error: Duplicate entry for key 'uk_content_hash'
- 大文字小文字を区別しない検索の例
CREATE TABLE articles ( id INT PRIMARY KEY, title VARCHAR(200), title_lower VARCHAR(200) AS (LOWER(title)) VIRTUAL, INDEX idx_title_lower (title_lower) ); INSERT INTO articles (id, title) VALUES (1, 'MySQL Tutorial'), (2, 'Advanced MySQL'), (3, 'mysql best practices'); SELECT title FROM articles WHERE title_lower LIKE '%mysql%';
制限事項
Generated Columnsには、いくつかの制限事項がある。
DML操作の制限を以下に示す。
- INSERT時の値指定
- Generated Columnには値を直接指定できない
- DEFAULTのみ指定可能
- UPDATE時の値変更
- Generated Columnを直接更新できない
- 依存カラムを更新すると自動的に再計算される
INSERT時の値指定例を以下に示す。
-- 正常: Generated Columnを省略
INSERT INTO users (first_name, last_name) VALUES ('John', 'Doe');
-- 正常: DEFAULTを指定
INSERT INTO users (first_name, last_name, full_name) VALUES ('John', 'Doe', DEFAULT);
-- エラー: 値を直接指定
INSERT INTO users (first_name, last_name, full_name) VALUES ('John', 'Doe', 'John Doe');
-- Error: The value specified for generated column 'full_name' is not allowed
外部キー制約の制限を以下に示す。
- VIRTUAL列
- 外部キーとして使用不可
- STORED列
- 外部キーとして使用可能
- ただし、CASCADE、SET NULL、SET DEFAULTは使用不可
外部キー制約の例を以下に示す。
CREATE TABLE categories (
id INT PRIMARY KEY,
name VARCHAR(50),
code VARCHAR(10) AS (UPPER(LEFT(name, 3))) STORED
);
-- エラー: STORED列へのCASCADE制約
CREATE TABLE products (
id INT PRIMARY KEY,
category_code VARCHAR(10),
FOREIGN KEY (category_code) REFERENCES categories(code) ON DELETE CASCADE
);
-- Error: Cannot define foreign key with ON DELETE CASCADE on generated column
-- 正常: NO ACTIONまたはRESTRICTのみ
CREATE TABLE products (
id INT PRIMARY KEY,
category_code VARCHAR(10),
FOREIGN KEY (category_code) REFERENCES categories(code) ON DELETE RESTRICT
);
パーティショニングの制限を以下に示す。
- 式は決定的である必要がある
- 同じ入力に対して常に同じ結果を返す
- 非決定的関数は使用不可
- NOW(), RAND()等
パーティショニングの例を以下に示す。
-- 正常: 決定的関数
CREATE TABLE events (
id INT PRIMARY KEY,
event_date DATE,
event_year INT AS (YEAR(event_date)) STORED
)
PARTITION BY RANGE (event_year) (
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION p2025 VALUES LESS THAN (2026)
);
-- エラー: 非決定的関数
CREATE TABLE logs (
id INT PRIMARY KEY,
log_time DATETIME,
created_date DATE AS (CURDATE()) STORED
)
PARTITION BY RANGE (YEAR(created_date)) (
PARTITION p2024 VALUES LESS THAN (2025)
);
-- Error: Constant, random or timezone-dependent expressions are not allowed
レプリケーションの制限を以下に示す。
- マスタとスレーブで生成式が同一である必要がある
- 異なる式の場合、レプリケーションエラーが発生
- 非決定的関数の使用は避ける
- マスタとスレーブで結果が異なる可能性
ストレージエンジンの対応状況を以下に示す。
| ストレージエンジン | VIRTUAL | STORED | VIRTUALセカンダリインデックス |
|---|---|---|---|
| InnoDB | 完全対応 | 完全対応 | 対応 (8.0+) |
| MyISAM | 基本機能のみ | 基本機能のみ | 非対応 |
| Memory | 非対応 | 非対応 | 非対応 |
| CSV | 非対応 | 非対応 | 非対応 |
トリガーとの関係を以下に示す。
- BEFORE INSERT/UPDATE
- トリガー実行直後にGenerated Column計算
- AFTER INSERT/UPDATE
- Generated Column計算後にトリガー実行
トリガー内でのGenerated Column参照例を以下に示す。
CREATE TABLE audit_log (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
action VARCHAR(50),
full_info TEXT
);
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
first_name VARCHAR(50),
last_name VARCHAR(50),
full_name VARCHAR(101) AS (CONCAT(first_name, ' ', last_name)) STORED
);
DELIMITER $$
CREATE TRIGGER after_user_insert
AFTER INSERT ON users
FOR EACH ROW
BEGIN
INSERT INTO audit_log (user_id, action, full_info)
VALUES (NEW.id, 'INSERT', NEW.full_name);
END$$
DELIMITER ;
上記のトリガーでは、AFTER INSERTでGenerated Column NEW.full_name を参照している。
パフォーマンス
Generated Columnsのパフォーマンス特性と最適化方法を示す。
下表に、VIRTUALのパフォーマンス特性を示す。
| 特性 | 説明 |
|---|---|
| 書き込み性能 |
|
| 読み取り性能 |
|
| ストレージ |
|
下表に、STOREDのパフォーマンス特性を示す。
| 特性 | 説明 |
|---|---|
| 書き込み性能 |
|
| 読み取り性能 |
|
| ストレージ |
|
- パフォーマンステストの例
-- テストテーブル作成 CREATE TABLE perf_test_virtual ( id INT PRIMARY KEY AUTO_INCREMENT, value INT, calculated INT AS (value * 100 + value * 10 + value) VIRTUAL ); CREATE TABLE perf_test_stored ( id INT PRIMARY KEY AUTO_INCREMENT, value INT, calculated INT AS (value * 100 + value * 10 + value) STORED ); -- 100万行挿入のパフォーマンス測定 -- VIRTUAL版 INSERT INTO perf_test_virtual (value) SELECT n FROM (SELECT @row := @row + 1 as n FROM (SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3) t1, (SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3) t2, (SELECT @row:=0) t3 LIMIT 1000000) numbers; -- 実行時間: 約10秒 -- STORED版 INSERT INTO perf_test_stored (value) SELECT n FROM (SELECT @row := @row + 1 as n FROM (SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3) t1, (SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3) t2, (SELECT @row:=0) t3 LIMIT 1000000) numbers; -- 実行時間: 約13秒
- インデックス付きVIRTUAL列のパフォーマンス
CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, first_name VARCHAR(50), last_name VARCHAR(50), full_name VARCHAR(101) AS (CONCAT(first_name, ' ', last_name)) VIRTUAL, INDEX idx_full_name (full_name) ); -- 100万行挿入 -- 実行時間: 約15秒 (インデックス更新コスト含む) -- インデックスを使用した検索 SELECT * FROM users WHERE full_name = 'John Doe'; -- 実行時間: 約0.001秒 (インデックス使用)
- カバリングインデックスによる最適化
CREATE TABLE employees ( id INT PRIMARY KEY, first_name VARCHAR(50), last_name VARCHAR(50), salary DECIMAL(10, 2), full_name VARCHAR(101) AS (CONCAT(first_name, ' ', last_name)) VIRTUAL, INDEX idx_full_name_covering (full_name, salary) ); -- カバリングインデックスを使用するクエリ SELECT full_name, salary FROM employees WHERE full_name LIKE 'John%'; -- インデックスのみからデータ取得、テーブルアクセス不要 -- VIRTUAL列の再計算も不要
パフォーマンス最適化のガイドラインを以下に示す。
VIRTUALを最適化するには、以下を実施する。
- 計算式を単純に保つ
- 複雑な式は避ける
- インデックスを活用
- 検索対象列にはインデックスを作成
- カバリングインデックスを使用
- 必要なカラムを全てインデックスに含める
STOREDを最適化するには、以下を実施する。
- 更新頻度を考慮
- 頻繁に更新される列では使用を避ける
- ストレージサイズを考慮
- 大きなデータ型は避ける
- 依存カラムの更新を最小化
- 不要な更新を避ける
共通の最適化として、以下を実施する。
- 適切なタイプ選択
- VIRTUALとSTOREDの特性を理解して選択
- インデックス戦略
- 必要最小限のインデックスを作成
- 式の決定性確保
- 決定的関数のみ使用
下表に、パフォーマンス比較の目安を示す。
| 操作 | VIRTUAL | STORED | 通常カラム |
|---|---|---|---|
| INSERT (100万行) | 10秒 | 13秒 | 9秒 |
| UPDATE (10万行) | 2秒 | 3秒 | 2秒 |
| SELECT (インデックスなし) | 5秒 (計算あり) | 1秒 | 1秒 |
| SELECT (インデックスあり) | 0.05秒 | 0.05秒 | 0.05秒 |
| ストレージ消費 (100万行) | 0MB (本体) | 4MB (INT型) | 4MB (INT型) |
上記は目安であり、実際の値は式の複雑さ、データ型、ハードウェア構成により変動する。
関連ページ
- MySQL - インデックス
- インデックスの種類と作成方法
- セカンダリインデックス、関数インデックス
- MySQL - JSON関数
- JSON_EXTRACT, JSON_UNQUOTE等の関数
- JSON列からの値抽出
- MySQL - ALTER TABLE
- ALTER TABLE文の構文と操作
- ALGORITHM, LOCK句の詳細