【Oracle】仮想列(Virtual Column)完全ガイド|GENERATED ALWAYS AS・インデックス・パーティションキー・制限事項まで実例で解説

【Oracle】仮想列(Virtual Column)完全ガイド|GENERATED ALWAYS AS・インデックス・パーティションキー・制限事項まで実例で解説 Oracle

仮想列(Virtual Column)は Oracle 11g から使えるテーブルの機能で、列に式(計算式・関数呼び出し)を定義しておくと、SELECT 時に自動的に計算された値が返ってくる列です。データは物理的に格納されず、アクセスのたびに式が評価されます。アプリケーション側での計算不要・インデックス付き・パーティションキー化が可能と、実務でも活躍する場面が多い機能です。

この記事でわかること

  • 仮想列の定義構文(CREATE TABLE / ALTER TABLE ADD)
  • INSERT・UPDATE での挙動と制限
  • 仮想列へのインデックス作成と関数ベースインデックスとの比較
  • 仮想列をパーティションキーとして使う方法
  • 仮想列の変更・削除
  • USER_TAB_COLS で仮想列を確認する方法
  • 使用できない型・式(制限事項一覧)
スポンサーリンク

仮想列の基本構文

仮想列は GENERATED ALWAYS AS (式) で定義します。末尾の VIRTUAL キーワードは省略可能です。

CREATE TABLE で仮想列を定義する
-- 基本構文
CREATE TABLE 表名 (
    列名  データ型  GENERATED ALWAYS AS (式) VIRTUAL,
    -- VIRTUAL は省略可(省略しても仮想列として扱われる)
);

-- 実例: 税込金額・フルネームを仮想列で定義する
CREATE TABLE products (
    product_id     NUMBER          PRIMARY KEY,
    product_name   VARCHAR2(200)   NOT NULL,
    price          NUMBER(10, 2)   NOT NULL,
    tax_rate       NUMBER(5, 4)    DEFAULT 0.10 NOT NULL,

    -- 仮想列: 税込金額(格納しない・SELECT 時に計算される)
    price_with_tax NUMBER(12, 2)
        GENERATED ALWAYS AS (ROUND(price * (1 + tax_rate), 2)) VIRTUAL,

    -- 仮想列: 税額(小数切り捨て)
    tax_amount     NUMBER(10, 2)
        GENERATED ALWAYS AS (ROUND(price * tax_rate, 2)) VIRTUAL
);

-- データ投入: 仮想列(price_with_tax, tax_amount)は指定しない
INSERT INTO products (product_id, product_name, price, tax_rate)
VALUES (1, 'ノートPC', 100000, 0.10);

INSERT INTO products (product_id, product_name, price, tax_rate)
VALUES (2, 'マウス', 3000, 0.10);

COMMIT;

-- SELECT: 仮想列が自動計算される
SELECT product_name, price, tax_amount, price_with_tax
FROM products;
-- ノートPC | 100000 | 10000 | 110000
-- マウス   |   3000 |   300 |   3300
文字列結合・日付変換でも仮想列を定義できる
CREATE TABLE employees_v (
    employee_id    NUMBER          PRIMARY KEY,
    first_name     VARCHAR2(50)    NOT NULL,
    last_name      VARCHAR2(50)    NOT NULL,
    hire_date      DATE            NOT NULL,
    salary         NUMBER(10, 2),

    -- フルネーム(姓名の結合)
    full_name      VARCHAR2(101)
        GENERATED ALWAYS AS (last_name || ' ' || first_name) VIRTUAL,

    -- 入社年(数値型)
    hire_year      NUMBER(4)
        GENERATED ALWAYS AS (EXTRACT(YEAR FROM hire_date)) VIRTUAL,

    -- 入社月
    hire_month     NUMBER(2)
        GENERATED ALWAYS AS (EXTRACT(MONTH FROM hire_date)) VIRTUAL
);

-- 使い方例
SELECT full_name, hire_year, hire_month, salary
FROM employees_v
WHERE hire_year >= 2020
ORDER BY full_name;
-- → WHERE や ORDER BY でも仮想列を通常の列と同じように使える

ALTER TABLE で仮想列を追加する

既存テーブルへの仮想列追加(ALTER TABLE ADD)
-- 既存テーブルに仮想列を追加する
ALTER TABLE products
ADD (
    discount_price NUMBER(12, 2)
        GENERATED ALWAYS AS (ROUND(price * 0.9, 2)) VIRTUAL   -- 10%割引価格
);

-- 複数の仮想列を一度に追加する
ALTER TABLE employees_v
ADD (
    annual_salary  NUMBER(12, 2)
        GENERATED ALWAYS AS (salary * 12) VIRTUAL,
    monthly_bonus  NUMBER(10, 2)
        GENERATED ALWAYS AS (ROUND(salary * 0.1, 2)) VIRTUAL
);

-- 注意: 仮想列に DEFAULT は指定できない
-- ALTER TABLE products ADD (virt_col NUMBER DEFAULT 0 GENERATED ALWAYS AS (price * 2));
-- → ORA-54012: default value cannot be specified for a virtual column

INSERT・UPDATE での挙動と制限

仮想列はデータを持たないため、INSERT や UPDATE で値を指定すると ORA-54013 エラーになります。

仮想列への INSERT/UPDATE は ORA-54013 エラー
-- NG: 仮想列に値を INSERT しようとするとエラー
INSERT INTO products (product_id, product_name, price, tax_rate, price_with_tax)
VALUES (3, 'キーボード', 5000, 0.10, 5500);
-- ORA-54013: INSERT operation disallowed on virtual columns

-- NG: 仮想列を UPDATE しようとするとエラー
UPDATE products SET price_with_tax = 6000 WHERE product_id = 3;
-- ORA-54013: INSERT operation disallowed on virtual columns

-- OK: 仮想列を SELECT リストや WHERE で使うのは問題ない
SELECT * FROM products WHERE price_with_tax > 5000;

-- NG: NULL の代入も不可(DEFAULT NULL での定義も不可)
-- INSERT INTO products (..., price_with_tax) VALUES (..., NULL);
-- → ORA-54013

-- 列リストを省略した INSERT も仮想列を含めることになるため注意
-- INSERT INTO products VALUES (4, 'テスト', 1000, 0.10, NULL, NULL, NULL);
-- → ORA-54013(列数が合えばエラーになる)
-- → 列名を明示的に指定して仮想列を除外するのが安全
INSERT 文では列名を明示して仮想列を除外する
仮想列が含まれるテーブルでは、INSERT INTO 表名 VALUES (...) のように列名を省略すると仮想列の位置に値を渡そうとしてエラーになります。常に列名を明示して仮想列を含まない INSERT 文を書くのが安全な運用方法です。

仮想列へのインデックス作成

仮想列にはインデックスを作成できます。これにより関数ベースインデックス(FBI)と同等の効果を得ながら、式の定義をテーブル定義で一元管理できます。

仮想列インデックス vs 関数ベースインデックス
-- ===== 方法1: 関数ベースインデックス(FBI)=====
-- テーブル定義と独立しており、式を WHERE 句に必ず同じ形で書く必要がある
CREATE INDEX idx_price_with_tax_fbi ON products (ROUND(price * (1 + tax_rate), 2));

-- FBI を使うには WHERE 句の式が完全に一致しなければならない
SELECT * FROM products WHERE ROUND(price * (1 + tax_rate), 2) > 5000;  -- インデックス使用
SELECT * FROM products WHERE price * 1.10 > 5000;  -- → 式が違うためインデックス不使用

-- ===== 方法2: 仮想列にインデックス =====
-- 仮想列 price_with_tax は既に ROUND(price * (1 + tax_rate), 2) として定義済み
CREATE INDEX idx_price_with_tax ON products (price_with_tax);

-- 仮想列名で直接フィルタできる(式を書く必要がない)
SELECT * FROM products WHERE price_with_tax > 5000;  -- インデックス使用
-- → オプティマイザは price_with_tax = ROUND(price*(1+tax_rate),2) を理解してインデックスを選択

-- インデックスの確認
SELECT index_name, column_name, column_position
FROM user_ind_columns
WHERE table_name = 'PRODUCTS'
ORDER BY index_name, column_position;
仮想列インデックスの利点
関数ベースインデックスでは SQL を書く開発者が式を正確に記述する必要がありますが、仮想列に対するインデックスでは列名を使うだけでインデックスが効きます。式の変更もテーブル定義の1か所を直すだけで済みます(ただし式変更時はインデックス再作成が必要)。

仮想列をパーティションキーとして使う

仮想列は Oracle 11g R2 以降、パーティションキーとして利用できます。これにより実際の列にパーティション用の値を格納せずに、既存の列から計算した値でパーティション分割できます。

仮想列で「年月」パーティションを実現する
-- 注文日から年月(YYYYMM 形式の数値)を仮想列として定義し、パーティションキーに使う
CREATE TABLE orders_partitioned (
    order_id     NUMBER         PRIMARY KEY,
    order_date   DATE           NOT NULL,
    customer_id  NUMBER,
    amount       NUMBER(12, 2),

    -- 仮想列: YYYYMM 形式の数値(パーティションキーに使う)
    order_yyyymm NUMBER(6)
        GENERATED ALWAYS AS (
            TO_NUMBER(TO_CHAR(order_date, 'YYYYMM'))
        ) VIRTUAL
)
-- 仮想列を RANGE パーティションのキーに指定
PARTITION BY RANGE (order_yyyymm) (
    PARTITION p_202401 VALUES LESS THAN (202402),
    PARTITION p_202402 VALUES LESS THAN (202403),
    PARTITION p_202403 VALUES LESS THAN (202404),
    PARTITION p_maxval VALUES LESS THAN (MAXVALUE)
);

-- INSERT: order_date のみ指定(order_yyyymm は自動計算)
INSERT INTO orders_partitioned (order_id, order_date, customer_id, amount)
VALUES (1001, DATE '2024-01-15', 501, 15000);

INSERT INTO orders_partitioned (order_id, order_date, customer_id, amount)
VALUES (1002, DATE '2024-02-20', 502, 8000);

COMMIT;

-- パーティションプルーニングが効く(2024年1月のデータのみスキャン)
SELECT * FROM orders_partitioned
WHERE order_date BETWEEN DATE '2024-01-01' AND DATE '2024-01-31';
-- → p_202401 パーティションのみアクセスされる(EXPLAIN PLAN で確認可能)

仮想列パーティションの利点は、実データ列(order_date)だけ管理すればよい点です。order_yyyymm 列を別途追加・管理する必要がなく、INSERT 時の列指定も簡潔になります。

仮想列の確認・変更・削除

USER_TAB_COLS で仮想列を確認する
-- USER_TAB_COLS: VIRTUAL_COLUMN 列で仮想列か否かを識別できる
SELECT
    column_name,
    data_type,
    data_length,
    nullable,
    virtual_column,   -- YES = 仮想列、NO = 通常列
    data_default      -- 仮想列の場合は定義式が格納される
FROM user_tab_cols
WHERE table_name = 'PRODUCTS'
ORDER BY column_id;

-- 仮想列だけを抽出
SELECT column_name, data_default AS virtual_expression
FROM user_tab_cols
WHERE table_name = 'PRODUCTS'
  AND virtual_column = 'YES';

-- 全テーブルの仮想列一覧(スキーマ全体)
SELECT table_name, column_name, data_default
FROM user_tab_cols
WHERE virtual_column = 'YES'
ORDER BY table_name, column_id;
仮想列の変更・削除
-- 仮想列の削除(通常の列削除と同じ構文)
ALTER TABLE products DROP COLUMN discount_price;

-- 仮想列の式を変更する
-- ※ 仮想列の式は直接 ALTER COLUMN で変更できない
-- → 一度削除してから再追加する
ALTER TABLE products DROP COLUMN price_with_tax;

ALTER TABLE products
ADD price_with_tax NUMBER(12, 2)
    GENERATED ALWAYS AS (ROUND(price * (1 + tax_rate) * 0.95, 2)) VIRTUAL;
-- 5% 割引後の税込価格に変更した例

-- 仮想列に NOT NULL 制約を追加する
-- ※ 式が NULL を返さないことが保証されていれば NOT NULL を付けられる
-- ※ NULL が返り得る式に NOT NULL を付けると、INSERT 時に ORA-01400 になる場合がある

-- 仮想列に基づくインデックスがある場合は、列を削除する前にインデックスを先に削除する
DROP INDEX idx_price_with_tax;
ALTER TABLE products DROP COLUMN price_with_tax;

仮想列の制限事項

制限 詳細
使用できないデータ型 LOB(CLOB・BLOB・BFILE)・LONG・XMLType・NCLOB のデータ型には仮想列を定義できない
参照できる列 同じテーブルの列のみ参照可。他のテーブル・別の仮想列の一部制限あり(仮想列を参照する仮想列は定義できない)
シーケンス 式に NEXTVAL・CURRVAL を使用できない
サブクエリ 式にサブクエリを含めることはできない
非決定論的関数の制限 SYSDATE・SYSTIMESTAMP・ROWNUM など行ごとに結果が変わる関数は基本的に使用不可(DBMS_RANDOM も不可)
データの格納 仮想列はデータを持たない(Oracle の Virtual Column は常に都度計算)。物理的に保存されないため、計算コストが高い式は毎回実行される
外部表・IOT 外部表(External Table)や索引構成表(IOT)では仮想列は使用できない
DEFAULT 値の指定不可 仮想列に DEFAULT 値を設定することはできない(式で値が決まるため)
制限事項の確認例
-- NG: LOB 型の仮想列
-- ALTER TABLE t ADD vcol CLOB GENERATED ALWAYS AS (col1 || col2);
-- ORA-54012 / ORA-54003 など型に関するエラー

-- NG: 別仮想列を参照する仮想列
-- (同じ式を別々の列に分けた場合は問題ないが、仮想列Aを仮想列Bの式で参照するのは不可)
-- CREATE TABLE t (
--     price  NUMBER,
--     tax    NUMBER GENERATED ALWAYS AS (price * 0.1),   -- OK
--     total  NUMBER GENERATED ALWAYS AS (price + tax)    -- NG: tax は仮想列
-- );

-- NG: SYSDATE を使う仮想列
-- CREATE TABLE t (price NUMBER, vdate DATE GENERATED ALWAYS AS (SYSDATE));
-- ORA-54002: only pure functions can be specified in virtual column expression

-- OK: 決定論的な組み込み関数(UPPER, LOWER, ROUND, TO_NUMBER, SUBSTR など)
ALTER TABLE employees_v
ADD upper_last_name VARCHAR2(50) GENERATED ALWAYS AS (UPPER(last_name)) VIRTUAL;

実務での活用パターン

  • 税込価格・割引価格の計算:price・tax_rate から price_with_tax を仮想列で定義。SQL で毎回 ROUND(price * 1.10, 2) と書かずに済む
  • 氏名の結合:first_name・last_name から full_name を生成。検索・ソートに仮想列インデックスを適用
  • 年月パーティション:日付列から YYYYMM を仮想列化し、パーティションキーに利用。ログ・履歴テーブルの高速化に有効
  • 大文字化列でのインデックス検索:UPPER(column) を仮想列にしてインデックスを付けると、大文字小文字を問わない検索が高速化される
  • 正規化コードの表示:コード列(”01″, “02”…)から名称を返すケースで、ルールが単純なら仮想列で DECODE や CASE を使える

まとめ

  • GENERATED ALWAYS AS (式) VIRTUAL:式の結果を SELECT 時に自動計算する列を定義する。VIRTUAL は省略可
  • INSERT・UPDATE 不可:仮想列への値の指定は ORA-54013 になる。INSERT 文では列名を明示して仮想列を除外する
  • インデックス化できる:仮想列にインデックスを付けると列名を使うだけでインデックスが効く。関数ベースインデックスより式の管理がシンプル
  • パーティションキーに使える:日付から年月を計算する仮想列をパーティションキーにすれば、実データを複製せずにパーティションプルーニングが効く
  • USER_TAB_COLS の VIRTUAL_COLUMN 列:’YES’ で仮想列を識別できる。DATA_DEFAULT に定義式が格納される

仮想列の式を活用したインデックス戦略の詳細はインデックス完全ガイドを、仮想列をパーティションキーに使う設計についてはパーティションテーブル完全ガイドも参照してください。既存テーブルへの仮想列追加はALTER TABLE完全ガイドの列追加・変更手順と合わせて確認してください。