Skip to content

Commit 1b52d65

Browse files
authored
Merge pull request #1724 from future-architect/feature
フィードバック対応
2 parents 8cf2b65 + 5f6abe9 commit 1b52d65

File tree

1 file changed

+81
-27
lines changed

1 file changed

+81
-27
lines changed

source/_posts/2025/20251030a_PostgreSQL_18の新機能、仮想生成列の使い方や制約、格納生成列との使い分けについて.md

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

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

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

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

119-
## 2. 他のテーブルの列を参照できるか
120+
## 2. IDENTITY列を参照できるか
121+
122+
IDENTITY列(シリアル)を参照できるか確認します。
123+
124+
```sql
125+
postgres=# CREATE TABLE m_product (
126+
item_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
127+
product_name TEXT,
128+
129+
-- 格納生成列
130+
item_code_stored TEXT GENERATED ALWAYS AS (item_id*10) STORED,
131+
132+
-- 仮想生成列
133+
item_code_virtual TEXT GENERATED ALWAYS AS (item_id*100) VIRTUAL
134+
);
135+
CREATE TABLE
136+
137+
postgres=# INSERT INTO m_product (product_name) VALUES ('Apple');
138+
INSERT 0 1
139+
140+
postgres=# SELECT * FROM m_product;
141+
item_id | product_name | item_code_stored | item_code_virtual
142+
---------+--------------+------------------+-------------------
143+
1 | Apple | 10 | 100
144+
(1 row)
145+
```
146+
147+
格納生成列・仮想生成列のどちらも問題なく、IDENTITY列を参照することができました。
148+
149+
## 3. 他のテーブルの列を参照できるか
120150

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

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

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

151-
## 3. ネストした生成列定義は可能か
181+
## 4. ネストした生成列定義は可能か
152182

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

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

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

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

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

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

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

204-
## 5. NOT NULL制約を付けることができるか
234+
## 6. NOT NULL制約を付けることができるか
205235

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

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

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

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

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

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

264294

265-
## 7. 生成列はインデックスに使えるか
295+
## 8. 生成列はインデックスに使えるか
266296

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

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

320350

321-
## 8. 生成列はPKにできるか
351+
## 9. 生成列はPKにできるか
322352

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

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

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

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

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

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

414444
もちろん[ドキュメント](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.`(生成列はパーティションキーには利用できません。)と書かれています。
415445

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

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

@@ -439,7 +469,7 @@ ALTER TABLE
439469

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

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

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

@@ -502,13 +532,37 @@ ALTER TABLE t_order DROP COLUMN total_price;
502532

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

505-
## 12. 生成列の定義変更はできるか
535+
**(2025.11.7追記)**
536+
537+
ちなみに、元テーブルに生成列の計算元列にNOT NULL制約を付けると、フルスキャンが論理的にはスキップできるのでは?という声をもらいましたので、検証しました。
538+
539+
テーブル定義だけ以下で、残りは同じです。
540+
541+
```sql テーブル定義
542+
CREATE TABLE t_order (
543+
item_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
544+
unit_price NUMERIC NOT NULL, -- NOT NULL制約を追加
545+
quantity INT NOT NULL -- NOT NULL制約を追加
546+
);
547+
```
548+
549+
| 検証パターン | 処理結果 |
550+
| :--- | :--- |
551+
| 1.格納生成列(NULL許容) | 9.0秒 |
552+
| 2.格納生成列(NOT NULL) | 25.5秒 |
553+
| 3.仮想生成列 (NULL許容) | 0.014秒 |
554+
| 4.仮想生成列(NOT NULL) | 4.9秒 |
555+
556+
元列のNOT NULL制約無し版に比べ、少し早くなっていますが、実行の度に処理時間は変動するため気にしないでください。
557+
558+
重要なのは、NOT NULL制約をつけても、4の結果は4.9秒かかっている(≒フルスキャンが発生していると推測できる)ことです。現状のPostgreSQLでは、元列にNOT NULL制約がついていていたとしても、生成列の「式」を確認して、この結果だとNOT NULLになりえないから、チェックは不要であると言った判定は行っていないと言えます。
559+
560+
## 13. 生成列の定義変更はできるか
506561

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

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

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

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

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

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

@@ -569,7 +623,7 @@ Indexes:
569623

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

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

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

0 commit comments

Comments
 (0)