Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -83,19 +83,20 @@ postgres=# SELECT * FROM people;
| 検証項目 | 結果 |
| :--- | :--- |
| 1. 生成列で自分自身の列を参照できるか | 不可(生成列は生成列を参照できないた) |
| 2. 他のテーブルの列を参照できるか | 不可(サブクエリの利用は不可) |
| 3. ネストした生成列定義は可能か | 不可(生成列は生成列を参照できないため) |
| 4. 計算途中でnull値が混入したらどうなるか | 普通のクエリと同じように `null` になる |
| 5. NOT NULL制約を付けることができるか | 可能。仮想生成列も登録時チェックになる |
| 6. 生成列は一意制約を付けることができるか | 格納生成列は可能。仮想生成列は不可。式インデックスで代用 |
| 7. 生成列はインデックスに使えるか | 格納生成列は可能。仮想生成列は式インデックスで代用 |
| 8. 生成列はPKにできるか | 格納生成列は可能。仮想生成列はインデックスを持てないので不可 |
| 9. 生成列はパーティションキーに使えるか | 不可(`STORED` / `VIRTUAL` 共に不可) |
| 10. テーブル作成後に後から生成列を追加できるか | 可能。AccessExclusiveLock を取る |
| 11. NOT NULL制約を付けた生成列を後から追加すると処理時間はどうなるか | `NOT NULL`検証のためのテーブルフルスキャンが発生し、変更時間が長くなる |
| 12. 生成列の定義変更はできるか | 不可(`DROP COLUMN` & `ADD COLUMN` で対応する必要がある) |
| 13. 利用しているカラムをRENAMEしたら? | PostgreSQLが自動で定義式を追随・更新してくれる |
| 14. 利用しているカラムをDROPしたら? | エラーになる。 `CASCADE` を付けると依存した列ごと削除可能) |
| 2. IDENTITY列を参照できるか | 可能(格納・仮想の両方で可能) |
| 3. 他のテーブルの列を参照できるか | 不可(サブクエリの利用は不可) |
| 4. ネストした生成列定義は可能か | 不可(生成列は生成列を参照できないため) |
| 5. 計算途中でnull値が混入したらどうなるか | 普通のクエリと同じように `null` になる |
| 6. NOT NULL制約を付けることができるか | 可能。仮想生成列も登録時チェックになる |
| 7. 生成列は一意制約を付けることができるか | 格納生成列は可能。仮想生成列は不可。式インデックスで代用 |
| 8. 生成列はインデックスに使えるか | 格納生成列は可能。仮想生成列は式インデックスで代用 |
| 9. 生成列はPKにできるか | 格納生成列は可能。仮想生成列はインデックスを持てないので不可 |
| 10. 生成列はパーティションキーに使えるか | 不可(`STORED` / `VIRTUAL` 共に不可) |
| 11. テーブル作成後に後から生成列を追加できるか | 可能。AccessExclusiveLock を取る |
| 12. NOT NULL制約を付けた生成列を後から追加すると処理時間はどうなるか | `NOT NULL`検証のためのテーブルフルスキャンが発生し、変更時間が長くなる |
| 13. 生成列の定義変更はできるか | 不可(`DROP COLUMN` & `ADD COLUMN` で対応する必要がある) |
| 14. 利用しているカラムをRENAMEしたら? | PostgreSQLが自動で定義式を追随・更新してくれる |
| 15. 利用しているカラムをDROPしたら? | エラーになる。 `CASCADE` を付けると依存した列ごと削除可能) |

## 1. 生成列で自分自身の列を参照できるか

Expand All @@ -116,7 +117,36 @@ DETAIL: A generated column cannot reference another generated column.

結果はNGです。「生成列は他の生成列を参照できない」とありますね。実現したいことも意味不明なので、失敗して当然なので想定通りかなと。この結果は、`VIRTUAL` を `STORED` に変えても同じです。

## 2. 他のテーブルの列を参照できるか
## 2. IDENTITY列を参照できるか

IDENTITY列(シリアル)を参照できるか確認します。

```sql
postgres=# CREATE TABLE m_product (
item_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
product_name TEXT,

-- 格納生成列
item_code_stored TEXT GENERATED ALWAYS AS (item_id*10) STORED,

-- 仮想生成列
item_code_virtual TEXT GENERATED ALWAYS AS (item_id*100) VIRTUAL
);
CREATE TABLE

postgres=# INSERT INTO m_product (product_name) VALUES ('Apple');
INSERT 0 1

postgres=# SELECT * FROM m_product;
item_id | product_name | item_code_stored | item_code_virtual
---------+--------------+------------------+-------------------
1 | Apple | 10 | 100
(1 row)
```

格納生成列・仮想生成列のどちらも問題なく、IDENTITY列を参照することができました。

## 3. 他のテーブルの列を参照できるか

まず、税率を保持するテーブルを作成します。

Expand Down Expand Up @@ -148,7 +178,7 @@ LINE 8: base_price * (1.0 + (SELECT tax_rate FROM m_tax WHER...

[ドキュメント](https://www.postgresql.org/docs/18/sql-createtable.html#SQL-CREATETABLE-PARMS-GENERATED-STORED)にも `References to other tables are not allowed.` (他のテーブルは参照できない)と書いていますので、その通りの結果です。格納生成列、仮想生成列ともに結果は変わりません。

## 3. ネストした生成列定義は可能か
## 4. ネストした生成列定義は可能か

「割引額」という生成列を参照する、「料金」という生成列の定義を試みます。

Expand All @@ -174,7 +204,7 @@ DETAIL: A generated column cannot reference another generated column.

ドキュメントにも、 `The generation expression can refer to other columns in the table, but not other generated columns. `(生成式はテーブル内の他の列を参照できますが、他の生成列を参照することはできません。)とあるので、記載通りの挙動です。

## 4. 計算途中でnull値が混入したらどうなるか
## 5. 計算途中でnull値が混入したらどうなるか

例えば、総額=単価x数量 という生成列を定義します。この時、単価がNULLの場合にはどのように挙動するか確かめます。

Expand All @@ -201,7 +231,7 @@ SELECT * FROM t_order;

unit_priceがNULLの場合は、total_priceもNULLという結果です。SQL的に自然な挙動ですね。回避するには、unit_priceやquantityにNOT NULL制約を付けたり、COALESCEでNULLを実値に置き換える必要があります。

## 5. NOT NULL制約を付けることができるか
## 6. NOT NULL制約を付けることができるか

ちょっとテクニカルなテーブル定義に書き換えます。生成列のtotal_priceのみNOT NULL制約を付けて、ソースのunit_price, quantity はNULL許容にします。

Expand Down Expand Up @@ -233,7 +263,7 @@ DETAIL: Failing row contains (1, null, 5, virtual).

この検証は「11」節ではさらに詳しく調べています。

## 6. 生成列は一意制約を付けることができるか
## 7. 生成列は一意制約を付けることができるか

格納生成列の場合は成功します。

Expand Down Expand Up @@ -262,7 +292,7 @@ ERROR: unique constraints on virtual generated columns are not supported
これは後述するインデックスのサポート有無の挙動の差でしょう。なお、これまた後述する式インデックスに一意制約をつけることで、実質的に、仮想生成列に一意制約をつけることはできます。


## 7. 生成列はインデックスに使えるか
## 8. 生成列はインデックスに使えるか

格納生成列、仮想生成列それぞれにインデックスを追加してみます。

Expand Down Expand Up @@ -318,7 +348,7 @@ WHERE full_name = 'Yamada Taro';
実行計画レベルで、式インデックスが使われていることを確認できました。多少の回避方法が必要ですが、仮想列も事実上、インデックスを貼れると思ってよいでしょう。


## 8. 生成列はPKにできるか
## 9. 生成列はPKにできるか

例えば、受注明細トランで、受注番号+商品IDを組み合わせてPKにするケースを考えます(普通は、サロゲートにして欲しい案件ですが、あくまで動作確認上の"例"です)。

Expand Down Expand Up @@ -391,7 +421,7 @@ DETAIL: Key (((order_id::text || '-'::text) || item_id::text))=(1003-202) alrea

無事動作しました。ただし、あくまで仮想列自体に一意制約+NOT NULL制約をつけたわけではなく、仮想列と同等の定義を持った式インデックスに、一意制約+NOT NULL制約をつけたことになります。そのため、外部キー制約の参照の対象にはできないでしょう。

## 9. 生成列はパーティションキーに使えるか
## 10. 生成列はパーティションキーに使えるか

受注日時から受注日付(yyyy-MM-dd)を生成列で作成し、それをパーティションキーとするようなケースで試します。

Expand All @@ -413,7 +443,7 @@ DETAIL: Column "order_date" is a generated column.

もちろん[ドキュメント](https://www.postgresql.org/docs/18/ddl-generated-columns.html#:~:text=A%20generated%20column%20cannot%20be%20part%20of%20a%20partition%20key.)にも、`A generated column cannot be part of a partition key.`(生成列はパーティションキーには利用できません。)と書かれています。

## 10. テーブル作成後に後から生成列を追加できるか
## 11. テーブル作成後に後から生成列を追加できるか

`m_user` に格納生成列、仮想生成列の順番で足してみます。

Expand All @@ -439,7 +469,7 @@ ALTER TABLE

結果は成功でした。ちなみに、ALTER文実行前にはBEGINEを実行し、別プロセスで`pg_locks` を確認したところ、どちらも `AccessExclusiveLock` を取っていました。格納生成列は既存行が多ければ長時間、参照もできないので注意が必要です。仮想生成列はメタデータの書き換えのみで済むため、`AccessExclusiveLock` を取りますが一瞬で終わります。

## 11. NOT NULL制約を付けた生成列を後から追加すると処理時間はどうなるか
## 12. NOT NULL制約を付けた生成列を後から追加すると処理時間はどうなるか

以下の `t_order` に1万件のダミーデータを登録し、[格納|生成] x[NOT NULL有無]の4パターンで処理時間を計測しました。

Expand Down Expand Up @@ -502,13 +532,37 @@ ALTER TABLE t_order DROP COLUMN total_price;

格納生成列もNOT NULL化すると少し処理時間が増します。理由を深く調査はしていませんが、NOT NULL計算分が上乗せになるからでしょう。そして、仮想生成列ですが、NOT NULL制約を追加すると大幅に時間がかかります。これはおそらくテーブルフルスキャンでNOT NULLにならないかチェックするからでしょう。

## 12. 生成列の定義変更はできるか
**(2025.11.7追記)**

ちなみに、元テーブルに生成列の計算元列にNOT NULL制約を付けると、フルスキャンが論理的にはスキップできるのでは?という声をもらいましたので、検証しました。

テーブル定義だけ以下で、残りは同じです。

```sql テーブル定義
CREATE TABLE t_order (
item_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
unit_price NUMERIC NOT NULL, -- NOT NULL制約を追加
quantity INT NOT NULL -- NOT NULL制約を追加
);
```

| 検証パターン | 処理結果 |
| :--- | :--- |
| 1.格納生成列(NULL許容) | 9.0秒 |
| 2.格納生成列(NOT NULL) | 25.5秒 |
| 3.仮想生成列 (NULL許容) | 0.014秒 |
| 4.仮想生成列(NOT NULL) | 4.9秒 |

元列のNOT NULL制約無し版に比べ、少し早くなっていますが、実行の度に処理時間は変動するため気にしないでください。

重要なのは、NOT NULL制約をつけても、4の結果は4.9秒かかっている(≒フルスキャンが発生していると推測できる)ことです。現状のPostgreSQLでは、元列にNOT NULL制約がついていていたとしても、生成列の「式」を確認して、この結果だとNOT NULLになりえないから、チェックは不要であると言った判定は行っていないと言えます。

## 13. 生成列の定義変更はできるか

[ドキュメント](https://www.postgresql.org/docs/18/sql-altertable.html)を読む限り、生成列の定義を直接変更することはできないように思えます(文法の読み取りが間違っていたらご指摘ください)。

そのため、一度そのカラムを削除してから作り直すことになると思われます。例えば、先程の `email_lower` をいう検索専用の生成列を、さらに前後の空白をトリムする処理を追加します。


```sql
-- (1) 既存の格納列を削除
ALTER TABLE m_user DROP COLUMN email_lower;
Expand All @@ -520,7 +574,7 @@ ADD COLUMN email_lower TEXT GENERATED ALWAYS AS (LOWER(TRIM(email))) STORED;

流れ自体は仮想生成列でも同様です。格納生成列の場合は、(2)の処理でテーブルサイズによってはかなり時間がかかると思うので、注意が必要そうです(格納生成列のまま、瞬時に切り替える手順は今のところ、テーブル単位で新旧Verを作ってリネームする方法しか思いつきませんでした。また、格納生成列をDROP & ADDするということは、統計情報も消えるということなので、インデックス項目の場合はANALYZEもしたほうが良いでしょう)。

## 13. 利用しているカラムをRENAME COLUMNしたらどうなるか
## 14. 利用しているカラムをRENAME COLUMNしたらどうなるか

生成列で利用しているカラムをリネームはできるのでしょうか?試してみます。

Expand Down Expand Up @@ -569,7 +623,7 @@ Indexes:

リネームにも追随してくれるの、気が効いていますね。賢い。

## 14. 利用しているカラムをDROP COLUMNしたときどうなるか
## 15. 利用しているカラムをDROP COLUMNしたときどうなるか

格納生成列、仮想生成列それぞれで利用しているカラムを、DROPできるか試しました。

Expand Down
Loading