PostgreSQL 18がやってくる(13) ロジカルレプリケーションに生成列を許容する

# はじめに
にゃーん。
今回は、PostgreSQL 18に入る予定のロジカルレプリケーション改善の一つについて調べてみました。

# 改善項目概要
今回調査したのは、PostgreSQL 18 Beta1 リリースノートの
``Allow the values of generated columns to be logically replicated (Shubham Khanna, Vignesh C, Zhijie Hou, Shlok Kyal, Peter Smith)``
の項目になる。
この項目は要するに、ロジカルレプリケーションに生成列を許容する、というものになる。

## 生成列（おさらい）
生成列というのは、PostgreSQL 12から使えるようになった特殊な列。
この列は同じテーブル内の値、あるいは関数の変動性区分[^1]がIMMUTABLEな式を使って演算した結果を列として格納する、というものである。
PostgreSQL 12～17までは、演算結果を格納する(STORED)な生成列に対応してた。また、PostgreSQL 18からは「仮想生成列」(VIRTUAL)という種別の仮想列も追加された。
→[PostgreSQL 18がやってくる(6) 仮想生成列](https://qiita.com/nuko_yokohama/items/79c95d92255fc6e46c95)参照。

たとえばこんな感じで使う。
```sql
$ psql -p 18001 -U postgres testdb -e -f pub_script_4.sql
DROP PUBLICATION test_pub;
DROP PUBLICATION
DROP TABLE IF EXISTS test;
DROP TABLE
CREATE TABLE test (
  id int primary key,
  data text,
  gc_data text GENERATED ALWAYS AS ( 'stored ' || data ) STORED,
  vgc_data text GENERATED ALWAYS AS ( 'virtual ' || data ) VIRTUAL,
  dummy text
);
CREATE TABLE
INSERT INTO test (id, data) VALUES
(1, 'ABC'),(2, 'DEF'),(3, 'XYZ')
;
INSERT 0 3
                                       Table "public.test"
  Column  |  Type   | Collation | Nullable |                       Default
----------+---------+-----------+----------+------------------------------------------------------
 id       | integer |           | not null |
 data     | text    |           |          |
 gc_data  | text    |           |          | generated always as ('stored '::text || data) stored
 vgc_data | text    |           |          | generated always as ('virtual '::text || data)
 dummy    | text    |           |          |
Indexes:
    "test_pkey" PRIMARY KEY, btree (id)

TABLE test;
 id | data |  gc_data   |  vgc_data   | dummy
----+------+------------+-------------+-------
  1 | ABC  | stored ABC | virtual ABC |
  2 | DEF  | stored DEF | virtual DEF |
  3 | XYZ  | stored XYZ | virtual XYZ |
(3 rows)
```
``gc_data``列や``vgc_data``を検索すると``data``列と文字列連結演算子の結果が取得できる。

## PostgreSQL 18での改善内容
生成列のロジカルレプリケーションに関する改善内容は以下となる。
* ``publish_generated_columns``オプションの追加
このオプションには``none``または``stored``を指定する。
* 列指定した列が仮想列であった場合には、それをロジカルレプリケーション対象とする。

# 検証
## 検証手順
さきほど「仮想列（おさらい）」で示した``test``テーブルをロジカルレプリケーションする例で考えてみる。
* 今回は同一Linuxサーバ上に、別々のポート番号（18001, 18002）で待ち受けるデータベースクラスタを2つ作成する。
* port=18001 側のデータベースクラスタ/データベースに、仮想列を含むテーブルを作成する。
* port=18001 側のデータベースクラスタ/データベースに、パブリッシャを``CREATE PUBLICATION``文で作成する。
* port=18002 側のデータベースクラスタ/データベースに、同じデータタイプ（但し仮想列は含まない）のテーブルを作成する。
* port=18002 側のデータベースクラスタ/データベースに、サブスクライバを作成してport=18001 側のパブリッシャに接続してロジカルレプリケーションを行う。
* port=18002 側にpsqlで接続して、テーブル内容を確認する。

## デフォルト指定
この場合、生成列``gc_data``の内容はサブスクライバ側には反映されない。

パブリッシャ側
```sql
CREATE PUBLICATION test_pub
  FOR TABLE test
;
CREATE PUBLICATION
                                           List of publications
   Name   |  Owner   | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root
----------+----------+------------+---------+---------+---------+-----------+-------------------+----------
 test_pub | postgres | f          | t       | t       | t       | t         | none              | f
(1 row)
```
サブスクライバ側
```sql
CREATE SUBSCRIPTION test_pub_sub
  CONNECTION 'dbname=testdb user=postgres port=18001'
  PUBLICATION test_pub
;
psql:sub_script.sql:18: NOTICE:  created replication slot "test_pub_sub" on publisher
CREATE SUBSCRIPTION
SELECT pg_sleep(1);
 pg_sleep
----------

(1 row)

TABLE test;
 id | data | gc_data | vgc_data | dummy
----+------+---------+----------+-------
  1 | ABC  |         |          |
  2 | DEF  |         |          |
  3 | XYZ  |         |          |
(3 rows)
```
※以降の例では、``TABLE test``の結果だけ示す。

## publish_generated_columns = stored 指定
この場合、サブスクライバ側には生成列``gc_data``の内容が伝播される。
なお、サブスクライバ側には仮想生成列``vgc_data``の内容は伝播されない。

パブリッシャ側
```sql
CREATE PUBLICATION test_pub
  FOR TABLE test
  WITH (publish_generated_columns = stored)
;
CREATE PUBLICATION
                                           List of publications
   Name   |  Owner   | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root
----------+----------+------------+---------+---------+---------+-----------+-------------------+----------
 test_pub | postgres | f          | t       | t       | t       | t         | stored            | f
(1 row)

```

サブスクライバ側
```sql
TABLE test;
 id | data |  gc_data   | vgc_data | dummy
----+------+------------+----------+-------
  1 | ABC  | stored ABC |          |
  2 | DEF  | stored DEF |          |
  3 | XYZ  | stored XYZ |          |
(3 rows)
```


## 生成列を列リストに指定
生成列（``gc_data``）を明示的に指定した場合は、``publish_generated_columns = stored``を指定しなくてもサブスクライバに伝播される。

パブリッシャ側
```sql
CREATE PUBLICATION test_pub
  FOR TABLE test
  (id, data, gc_data)
;
CREATE PUBLICATION
                                           List of publications
   Name   |  Owner   | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root
----------+----------+------------+---------+---------+---------+-----------+-------------------+----------
 test_pub | postgres | f          | t       | t       | t       | t         | none              | f
(1 row)
```

サブスクライバ側
```sql
TABLE test;
 id | data |  gc_data   | vgc_data | dummy
----+------+------------+----------+-------
  1 | ABC  | stored ABC |          |
  2 | DEF  | stored DEF |          |
  3 | XYZ  | stored XYZ |          |
(3 rows)
```
## publish_generated_columns = stored 指定かつ、列リストに生成列を含めない
この場合、列リストの指定が優先されるため、サブスクライバ側には生成列の内容は伝播されない（列リストで指定した列の更新のみ伝播されるため）。
この挙動には注意する必要がある。

パブリッシャ側
```sql
CREATE PUBLICATION test_pub
  FOR TABLE test
  (id, data)
  WITH (publish_generated_columns = stored)
;
CREATE PUBLICATION
                                           List of publications
   Name   |  Owner   | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root
----------+----------+------------+---------+---------+---------+-----------+-------------------+----------
 test_pub | postgres | f          | t       | t       | t       | t         | stored            | f
(1 row)
```

サブスクライバ側
```sql
TABLE test;
 id | data | gc_data | vgc_data | dummy
----+------+---------+----------+-------
  1 | ABC  |         |          |
  2 | DEF  |         |          |
  3 | XYZ  |         |          |
(3 rows)
```
## 仮想生成列の扱い

仮想生成列を列リストに含めた場合には、``CREATE PUBLICATION``文がエラーになる。
エラーメッセージにもあるように仮想生成列を列リストに含めることはできないので注意が必要。

パブリッシャ側
```sql
CREATE PUBLICATION test_pub
  FOR TABLE test
  (id, data, vgc_data)
;
psql:pub_script_5.sql:22: ERROR:  cannot use virtual generated column "vgc_data" in publication column list
```


# まとめ
* PostgreSQL 18からは仮想列(STORED)をロジカルレプリケーションできるようになった。
* ``CREATE PUBLICATION``文で``WITH (publish_generated_columns = stored)``を指定または、明示的に仮想列(STORED)を列リストに指定する。
* 列リストへの指定が優先される。
* 仮想生成列(VIRTUAL)はロジカルレプリケーションの対象にされない。
* 仮想生成列を列リストに指定するとパブリッシャの作成に失敗する。

# おわりに
今回はロジカルレプリケーションの生成列の対応について調べてみました。
この機能によって、サブスクライバ側で生成列のための演算を行う必要がなくなりそうです。
また、今回は試していませんが、ロジカルデコーディングで（生成列機能を持たない）他のDBMSへのレプリケーション時も役立ちそうです。

[^1]: 関数変動性には以下の種別がある。<br>``VOLATILE``：同一引数で続けて呼び出したとしても異なる結果を返却可能。例えば``random()``など。CREATE FUNCTIONで作成した関数は明示的に指定しない限り、この関数変動性区分になる。<br>``STABLE``：単一の文内ですべての行に対して同一の引数を渡した場合に同一の結果を返却する。例えばロケールに依存するような関数はこの関数変動性区分になる。<br>``IMMUTABLE``：同一引数に対する呼び出しは常に同一の結果を返すことが保証する。式インデックスや、仮想列ではこの関数変動性の式を使わなければならない。