pg_ivm(IMMV)を使ってみた(1)

# はじめに

にゃーん。
今日は最近リリースされた[pg_ivm]( https://github.com/sraoss/pg_ivm)を使ってみた話を書こうと思います。
この拡張機能、2022-05-13に流れてきた[Planet PostgreSQL](https://twitter.com/planetpostgres)のツイートで初めてその存在を知りました・・・。

> [Yugo Nagata: pg_ivm 1.0 released! https://postgr.es/p/5l2](https://twitter.com/planetpostgres/status/1524943903916441614)

ということで今日は急遽、このpg_ivmについて調べてみようかと。

# pg_ivmの概要
pg_ivmはIncremental View Maintenance(IVM)をPostgreSQLで実現するための拡張機能です。
Incremental View Maintenanceというのは、雑に一言でいうと「更新に追随するMaterilized View」を実現するものです。

Incremental View Mainrtenanceの機能は数年前から、SRA OSS.の方が主体となって、PostgreSQL本体のコードをForkして開発を続けているものです。
https://github.com/sraoss/pgsql-ivm

自分も3年くらい前にこの機能に関するQiita記事を書いたりもしてましたが・・・。
[Incremental View Maintenance](https://qiita.com/nuko_yokohama/items/ecd7b9e15aafead14f0a)

で、pg_ivmはこのIncremental View Maintenanceの機能を本体機能としてではなく、PostgreSQL拡張機能(Extension)の形で提供するもののようです。
なお、pg_ivmではIncrementally Maintainable Materialized View(IMMV)という呼称になっているっぽい（[@tzkbさん情報](https://twitter.com/tzkb/status/1525663160660684801)）。用語がちょいややこしいですね。

では、さっそく使ってみましょうか。

# インストール
pg_ivmはPostgreSQL 14以降のバージョンに対応しているとのこと。
たまたま手元にビルドしたばかりのPostgreSQL 14.3（2020-05-12リリース）があったので、その環境にpg_ivmをインストールしてみます。
現時点ではRPMパッケージ等はなさげなので、ソースを入手してビルドする必要があります。

## ソースの入手
pg_ivmのソースは
https://github.com/sraoss/pg_ivm
から入手します。`git clone`するなり、Download ZIPするなりして、手元の環境にソースを展開しておきます。今回、自分は`git clone`で入手しました。

```sh
$ git clone https://github.com/sraoss/pg_ivm
Cloning into 'pg_ivm'...
remote: Enumerating objects: 110, done.
remote: Counting objects: 100% (25/25), done.
remote: Compressing objects: 100% (15/15), done.
remote: Total 110 (delta 18), reused 10 (delta 10), pack-reused 85
Receiving objects: 100% (110/110), 66.54 KiB | 16.64 MiB/s, done.
Resolving deltas: 100% (51/51), done.
$ cd pg_ivm
$ ls
createas.c  expected  LICENSE  Makefile  matview.c  pg_ivm--1.0.sql  pg_ivm.c  pg_ivm.control  pg_ivm.h  README.md  rpm  sql
```

## ビルド
PostgreSQL 14をビルドした環境、あるいはPostgreSQL 14（``postgresql-devel``パッケージ含む）をインストール済みの環境であれば、``make USE_PGXS=1``, ``make USE_PGXS=1 install``するだけです。

```
$ make USE_PGXS=1
gcc -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Werror=vla -Wendif-labels -Wmissing-format-attribute -Wimplicit-fallthrough=3 -Wformat-security -fno-strict-aliasing -fwrapv -fexcess-precision=standard -Wno-format-truncation -O2 -fPIC -I. -I./ -I/home/ec2-user/pgsql/pgsql-14.3/include/server -I/home/ec2-user/pgsql/pgsql-14.3/include/internal  -D_GNU_SOURCE   -c -o createas.o createas.c
(略)
$ make USE_PGXS=1 install
/usr/bin/mkdir -p '/home/ec2-user/pgsql/pgsql-14.3/lib'
(略)
/usr/bin/install -c -m 644 .//pg_ivm--1.0.sql  '/home/ec2-user/pgsql/pgsql-14.3/share/extension/'
$
```

## データベースへの登録
pg_ivmを利用する場合、PostgreSQL変数`preload_shared_libiraries`の設定は不要です。
利用する場合には、データベースへの登録(`CREATE EXTENSION`)を行います。

今回はテスト用のデータベース`ivm`を作成して、CREATE EXTENSIONでpg_ivmを登録します。
無事に登録できたら、psqlのメタコマンド`\dx`で登録されたことを確認しておきます、。

```sh
$ createdb ivm
$ psql ivm -c "CREATE EXTENSION pg_ivm"
CREATE EXTENSION
$ psql ivm -c "\dx pg_ivm"
                        List of installed extensions
  Name  | Version |   Schema   |                Description
--------+---------+------------+--------------------------------------------
 pg_ivm | 1.0     | pg_catalog | incremental view maintenance on PostgreSQL
(1 row)

$
```

## pg_ivmが提供するもの
CREATE EXTENSIONでpgsql_ivmを提供すると、登録したデータベース内には以下のデータベースオブジェクトが生成されます。利用者としては`create_immv`関数だけ意識しておけば良さそう。

|名前|種別|説明|
|:--|:--|:--|
|\_\_pg_ivm__|スキーマ|`__pg_ivm_immv__`テーブル作成時に一時的に使うスキーマっぽい。|
|\_\_pg_ivm_immv__|テーブル|immvビュー名とビュー定義のクエリを管理するテーブル。<br>いったん、`__pg_ivm__`スキーマ上に作成されるけど、その後に`ALTER TABLE`で`pg_catalog`スキーマ上のテーブルに変更されている。<br>これってpg_catalog上にCREATEするのはできないけど、後でALTERで変更できるという、ビミョーなPostgreSQLの制約によるものなんだろうか。|
|create_immv|SQL関数|この関数でIncrementally Maintainable Materialized View相当を作成する。（詳細後述）<br>利用者が意識するのはこのSQL関数だけ。|
|IVM_immediate_before|トリガ関数|更新契機で動作すると思われるトリガ関数。<br>利用者が直接使うことはない。|
|IVM_immediate_maintenance|トリガ関数|更新契機で動作すると思われるトリガ関数。<br>利用者が直接使うことはない。|
|IVM_prevent_immv_change|トリガ関数|更新契機で動作すると思われるトリガ関数。<br>利用者が直接使うことはない。|
|pg_ivm_sql_drop_trigger_func|トリガ関数|イベントトリガ用のSQL関数。<br>利用者が直接使うことはない。|
|pg_ivm_sql_drop_trigger|イベントトリガ|DROP契機で動作すると思われるイベントトリガ。たぶん、IMMVとして生成されたテーブルをDROPすると、裏でごにょごにょ動く何かがあるのだろう。|

# ビューの作成とビューの検索
pg_ivmのインストールが無事に終わったので、さっそく使ってみます。
まずはビューの作成からですね。ビューの作成には、`create_immv`というSQL関数を使います。


create_immv関数は2つの引数を渡します。
|名前|型 	|説明|
|:--|:--|:--|
|immvrelid 	|regclass 	|作成したいIMMV(Incremental Maintenance Materialized View)の名前。<br>`regclass`型となっているけど、実際にはビュー名をテキストで書く、と思っておけば良さげ。|
|viewdef 	|text 	|ビュー定義のクエリツリー(nodeToString() 表現の形式)<br>実際にはビュー定義のSELECT文のテキストと思っておけば良さげ。|

## サンプル
まず、IMMVの元になるテーブルを用意する必要があります。
今回は以下の2つのテーブル(foo.foo, bar.bar)を使ったサンプルにします。

```sql
ivm=# CREATE SCHEMA foo;
CREATE SCHEMA
ivm=# CREATE TABLE foo.foo (id int primary key, data int);
CREATE TABLE
ivm=# CREATE SCHEMA bar;
CREATE SCHEMA
ivm=# CREATE TABLE bar.bar (id int primary key, data numeric);
CREATE TABLE
ivm=#
```
で、foo.fooとbar.barにそれぞれ10万件のデータをテキトーに放り込みます。

```sql
ivm=# INSERT INTO foo.foo VALUES (generate_series(1, 100000), (random()*1000)::int);
INSERT 0 100000
ivm=# INSERT INTO bar.bar VALUES (generate_series(1, 100000), (random()*1000));
INSERT 0 100000
ivm=#
```

`create_immv`関数に渡すクエリには色々と制約があります。README.mdの"Supported View Definitions and Restriction"を読むと現状は、以下のクエリには対応していないと。

* Aggregates
* sub-quereis
* CTEs
* window functions
* LIMIT/OFFSET
* UNION/INTERSECT/EXCEPT
* DISTINCT ON
* TABLEAMPLE
* VALUES
* FOR UPDATE/SHARE

うーん、Aggregatesも現状は未サポートかあ。Aggregates(集約関数)こそ、一番IMMVの効果が見えやすいパターンなんだけどなあ・・・(pgsql_ivmではAggregatesもサポートしていたはず)。

とりあえず、上記の制約に引っかからない範囲でクエリを考えてみます。
今回はfoo.fooとbar.barを結合し、foo.dataとbar.dataにそれぞれ条件を与えて絞り込む例にします。

```sql
ivm=# SELECT f.id, f.data AS f_data, b.data AS b_data
FROM foo.foo f JOIN bar.bar b ON (f.id = b.id) WHERE f.data = 1000 AND b.data >= 990;
  id   | f_data |     b_data
-------+--------+-----------------
 15033 |   1000 | 993.26757298472
(1 row)
```
これをビュー生成用のクエリにして生成してみます。

## ビューの生成
IMMVも含めるとPostgreSQLのビューには以下の3種類があります。
* 通常のビュー(baz.baz_v)
* マテリアライズド・ビュー(baz.baz_mv)
* インクリメンタル・メンテナンス・マテリアライズド・ビュー(IMMV)(baz.baz_immv)

まずはビューとマテリアライズド・ビューを作成します。ついでに作成時間も`\timing`で雑にとっておきます。

```sql
ivm=# CREATE VIEW baz.baz_v AS
ivm-# SELECT f.id, f.data AS f_data, b.data AS b_data
ivm-# FROM foo.foo f JOIN bar.bar b ON (f.id = b.id) WHERE f.data = 1000 AND b.data >= 990
ivm-# ;
CREATE VIEW
Time: 2.920 ms
ivm=# CREATE MATERIALIZED VIEW baz.baz_mv AS
ivm-# SELECT f.id, f.data AS f_data, b.data AS b_data
ivm-# FROM foo.foo f JOIN bar.bar b ON (f.id = b.id) WHERE f.data = 1000 AND b.data >= 990
ivm-# ;
SELECT 1
Time: 10.926 ms
ivm=#
```
そして、pgsql_ivmが提供するcreate_immv関数を使って`baz.baz_immv`を作成します。

```sql
ivm=# SELECT create_immv('baz.baz_immv', $$
ivm$# SELECT f.id, f.data AS f_data, b.data AS b_data
ivm$# FROM foo.foo f JOIN bar.bar b ON (f.id = b.id) WHERE f.data = 1000 AND b.data >= 990
ivm$# $$
ivm(# );
NOTICE:  could not create an index on immv "baz_immv" automatically
DETAIL:  This target list does not have all the primary key columns, or this view does not contain DISTINCT clause.
HINT:  Create an index on the immv for efficient incremental maintenance.
 create_immv
-------------
           1
(1 row)

Time: 12.217 ms
ivm=#
```
SQL関数として実行するので、SELECT文にSQL関数を書きます。
最初の引数(`'baz.baz_immv'`)として生成したいIMMVの名前を書きます。スキーマ修飾ありの名前でも問題ないようですね。
2番目の引数には、ビューの定義となるSQL文を書きます。`$$`で括っているのは、SQL文中に単一引用符があったときのエスケープが面倒だからです。これは``CREATE FUNCTION``でPL/pgSQL関数を作成する時に良く使っている手法ですが、IMMV作成時に複雑かつ文字列定数を含むようなクエリを書く場合には同様の手法を使うのがおすすめかも。

生成されたビュー、マテリアライズド・ビュー、IMMVを確認するために、psqlのメタコマンドで確認してみます。
(なお、`\dtmv`は、テーブル、マテリアライズド・ビュー、ビューをリストアップするメタコマンドです)

```sql
ivm=# \dtmv (foo|bar|baz).*
                List of relations
 Schema |   Name   |       Type        |  Owner
--------+----------+-------------------+----------
 bar    | bar      | table             | postgres
 baz    | baz_immv | table             | postgres
 baz    | baz_mv   | materialized view | postgres
 baz    | baz_v    | view              | postgres
 foo    | foo      | table             | postgres
(5 rows)

ivm=#
```
Type列を見ると、baz.baz_vはビューとして、baz_mvはマテリアライズド・ビューとして、そして**baz_immvはテーブルとして生成されている**ことがわかります。

baz_immvの定義の詳細を見てみましょう。
```sql
ivm=# \d baz.baz_immv
               Table "baz.baz_immv"
 Column |  Type   | Collation | Nullable | Default
--------+---------+-----------+----------+---------
 id     | integer |           |          |
 f_data | integer |           |          |
 b_data | numeric |           |          |

ivm=#
```
これだけみると、ほんとただのテーブルですね。

### おまけ：create_immv関数は何をしているのか
`create_immv`関数を実行すると、IMMVの管理用テーブル`pg_catalog.pg_immv`に以下のように行が追加されます。

```sql
ivm=# \x
Expanded display is on.
ivm=# SELECT * FROM pg_catalog.pg_ivm_immv
;
-[ RECORD 1 ]-------------------------------------（中略）---------------------------------------------------
immvrelid | baz.baz_immv
viewdef   | {QUERY :commandType 1 :querySource 0 :（中略） :withCheckOptions <> :stmt_location 0 :stmt_len 0}

Time: 0.366 ms
ivm=#
```
immvrelid には第1引数で渡したIMMV名、viewdef には第2引数で渡したクエリの内部表現が格納されているようです。

しかし、もちろんそれだけではなく、裏でいろんなことがごにょごにょされていたりします。
IMMVでは元になるテーブルの更新を検知し、その更新差分情報を反映する必要があります。そのテーブルの更新の検知のためにPostgreSQLのトリガ機能を使っています。
cretae_immv関数を実行すると、クエリ内で使っているテーブルに対して自動的に6種類のトリガを設定します。
今回のceretae_immv関数を実行した後、以下のようにトリガが追加されています。

```sql
ivm=# SELECT t.tgrelid, c.relname, t.tgname FROM pg_trigger t JOIN pg_class c ON (t.tgrelid = c.oid);
 tgrelid | relname  |            tgname
---------+----------+-------------------------------
   17371 | foo      | IVM_trigger_upd_after_17446
   17371 | foo      | IVM_trigger_del_after_17445
   17371 | foo      | IVM_trigger_ins_after_17444
   17371 | foo      | IVM_trigger_upd_before_17443
   17371 | foo      | IVM_trigger_del_before_17442
   17371 | foo      | IVM_trigger_ins_before_17441
   17364 | bar      | IVM_trigger_upd_after_17452
   17364 | bar      | IVM_trigger_del_after_17451
   17364 | bar      | IVM_trigger_ins_after_17450
   17364 | bar      | IVM_trigger_upd_before_17449
   17364 | bar      | IVM_trigger_del_before_17448
   17364 | bar      | IVM_trigger_ins_before_17447
   17436 | baz_immv | IVM_prevent_immv_change_17456
   17436 | baz_immv | IVM_prevent_immv_change_17455
   17436 | baz_immv | IVM_prevent_immv_change_17454
   17436 | baz_immv | IVM_prevent_immv_change_17453
(16 rows)

ivm=#
```
たぶん名前から想像すると、INSERT/UPDATE/DELETEそれぞれの前後トリガ6個がリレーションごとに登録されるっぽい。（そして、元テーブルのTRUNCATEには対応できないのかもしれない）

## ビューの検索
せっかく作ったのでそれぞれのビュー、マテリアライズド・ビュー、IMMVを検索してみましょう。
作成したビューを単純に全列全件検索してみます。

```sql
ivm=# SELECT * FROM baz.baz_v;
  id   | f_data |     b_data
-------+--------+-----------------
 15033 |   1000 | 993.26757298472
(1 row)

Time: 6.754 ms
ivm=# SELECT * FROM baz.baz_mv;
  id   | f_data |     b_data
-------+--------+-----------------
 15033 |   1000 | 993.26757298472
(1 row)

Time: 0.493 ms
ivm=# SELECT * FROM baz.baz_immv;
  id   | f_data |     b_data
-------+--------+-----------------
 15033 |   1000 | 993.26757298472
(1 row)

Time: 0.470 ms
ivm=#
```
当たり前ですが、みんな同じ結果になってますね。

で、ここでビュー生成時とビューへの検索時の時間を見てみます。

|方式|ビュー生成時間(ms)|ビューへの検索時間(ms)|
|:--|--:|--:|
|ビュー|2.920|6.754|
|マテリアライズド・ビュー|10.926|0.493|
|IMMV|12.217|0.470|

* ビューは（定義を登録するだけなので）ビュー生成時間は非常に短い。かわりにビュー検索時には、ビュー定義に書いたクエリを毎回実行するので、クエリによってはそこそこ時間がかかる。
* マテリアライズド・ビューとIMMVは、ビュー生成時にクエリを実行してその結果をビューに保存するため、ビュー生成時間はそこそこ時間がかかる。しかしビューへの検索は非常に高速になる（ビューに保存された件数に依存はするが）。

# ビュー構成元のテーブルへの更新
ここからが、マテリアライズド・ビューとIMMVの大きな違いの話になります。
PostgreSQLのマテリアライズド・ビューは元テーブルへの更新があったとしても、REFRESH MATERIALIZED VIEWコマンドを実行しないと内容が更新されません。
しかし、IMMVでは元テーブルへの更新が即時にIMMV側にも反映されます。

さっきの環境を使って、IMMVの即時反映の例を見てみます。

## 更新前の状態（再掲）
```sql
ivm=# SELECT * FROM baz.baz_v;
  id   | f_data |     b_data
-------+--------+-----------------
 15033 |   1000 | 993.26757298472
(1 row)

ivm=# SELECT * FROM baz.baz_mv;
  id   | f_data |     b_data
-------+--------+-----------------
 15033 |   1000 | 993.26757298472
(1 row)

ivm=# SELECT * FROM baz.baz_immv;
  id   | f_data |     b_data
-------+--------+-----------------
 15033 |   1000 | 993.26757298472
(1 row)
```

## 更新1(INSERT)
* foo.fooテーブルにid=100001, data=1000の行を追加します。
* bar.barテーブルにid=100001, data=995.0の行を追加します。

```sql
ivm=# BEGIN;
BEGIN
ivm=*# INSERT INTO foo.foo VALUES (100001, 1000);
INSERT 0 1
ivm=*# INSERT INTO bar.bar VALUES (100001, 995.0);
INSERT 0 1
ivm=*# COMMIT;
COMMIT
ivm=#
```

上記トランザクションコミット後に、各ビューの内容を確認します。
```sql
ivm=# SELECT * FROM baz.baz_v;
   id   | f_data |     b_data
--------+--------+-----------------
  15033 |   1000 | 993.26757298472
 100001 |   1000 |           995.0
(2 rows)

ivm=# SELECT * FROM baz.baz_mv;
  id   | f_data |     b_data
-------+--------+-----------------
 15033 |   1000 | 993.26757298472
(1 row)

ivm=# SELECT * FROM baz.baz_immv;
   id   | f_data |     b_data
--------+--------+-----------------
  15033 |   1000 | 993.26757298472
 100001 |   1000 |           995.0
(2 rows)
```
想定どおり、baz.baz_vとbaz.baz_immvには1行値が追加されています（挿入した行の結合結果が追加される）。

## 更新2(UPDATE)
* foo.fooのid=1のdataを1000にUPDATEします。
* bar.barのid=1のdataを996.0にUPDATEします。

```sql
ivm=# BEGIN;
BEGIN
ivm=*# UPDATE foo.foo SET data = 1000 WHERE id = 1;
UPDATE 1
ivm=*# UPDATE bar.bar SET data = 996.0 WHERE id = 1;
UPDATE 1
ivm=*# COMMIT;
COMMIT
ivm=#
```

上記トランザクションコミット後に、各ビューの内容を確認します。
```sql
ivm=# SELECT * FROM baz.baz_v;
   id   | f_data |     b_data
--------+--------+-----------------
  15033 |   1000 | 993.26757298472
 100001 |   1000 |           995.0
      1 |   1000 |           996.0
(3 rows)

ivm=# SELECT * FROM baz.baz_mv;
  id   | f_data |     b_data
-------+--------+-----------------
 15033 |   1000 | 993.26757298472
(1 row)

ivm=# SELECT * FROM baz.baz_immv;
   id   | f_data |     b_data
--------+--------+-----------------
  15033 |   1000 | 993.26757298472
 100001 |   1000 |           995.0
      1 |   1000 |           996.0
(3 rows)
```
これも想定どおり。

### 更新3(DELETE)
* foo.fooのid=100001の行をDELETE
* bar.barのid=100001の行をDELETE

```sql
ivm=# SELECT * FROM baz.baz_v;
  id   | f_data |     b_data
-------+--------+-----------------
 15033 |   1000 | 993.26757298472
     1 |   1000 |           996.0
(2 rows)

ivm=# SELECT * FROM baz.baz_mv;
  id   | f_data |     b_data
-------+--------+-----------------
 15033 |   1000 | 993.26757298472
(1 row)

ivm=# SELECT * FROM baz.baz_immv;
  id   | f_data |     b_data
-------+--------+-----------------
 15033 |   1000 | 993.26757298472
     1 |   1000 |           996.0
(2 rows)
```
これも想定どおり。

### TRUNCATE(未遂)
では、テーブルへのTRUNCATEはどうなっちゃうのか。
* bar.barテーブルをTRUNCATE・・・と見せかけてROLLBACK

別ターミナルを開いて以下のコマンドを実行
```sql
ivm=# BEGIN;
BEGIN
ivm=*# TRUNCATE bar.bar;
TRUNCATE TABLE
ivm=*#
```
この状態で、ビュー、マテリアライズド・ビュー、IMMVを参照すると面白いことになる。
```sql
ivm=# SELECT * FROM baz.baz_v;
^C2022-05-15 14:49:25.892 JST [15796] ERROR:  canceling statement due to user request
2022-05-15 14:49:25.892 JST [15796] STATEMENT:  SELECT * FROM baz.baz_v;
Cancel request sent
ERROR:  canceling statement due to user request
ivm=# SELECT * FROM baz.baz_mv;
  id   | f_data |     b_data
-------+--------+-----------------
 15033 |   1000 | 993.26757298472
(1 row)

ivm=# SELECT * FROM baz.baz_immv;
  id   | f_data |     b_data
-------+--------+-----------------
 15033 |   1000 | 993.26757298472
     1 |   1000 |           996.0
(2 rows)

ivm=#
```

* ビューへの参照はロック待ちになる。（なので手動でクエリをキャンセルした）
* マテリアライズド・ビューは関係なし（古い情報をそのまま表示する）
* immvは関係なし（現在の情報をそのまま表示する）

そしてTRUNCATE中のトランザクションをロールバックして、
```sql
ivm=*# ROLLBACK;
ROLLBACK
ivm=#
```
また各ビューを参照すると、当然ながらトランザクション開始前の状態に戻る。

### TRUNCATE(完遂)
こんどはTRUNCATEをコミットした場合の挙動を確認する。

```sql
ivm=# SELECT * FROM baz.baz_v;
  id   | f_data |     b_data
-------+--------+-----------------
 15033 |   1000 | 993.26757298472
     1 |   1000 |           996.0
(2 rows)

ivm=# SELECT * FROM baz.baz_mv;
  id   | f_data |     b_data
-------+--------+-----------------
 15033 |   1000 | 993.26757298472
(1 row)

ivm=# SELECT * FROM baz.baz_immv;
  id   | f_data |     b_data
-------+--------+-----------------
 15033 |   1000 | 993.26757298472
     1 |   1000 |           996.0
(2 rows)

ivm=# TRUNCATE bar.bar ;
TRUNCATE TABLE
ivm=# SELECT * FROM baz.baz_v;
 id | f_data | b_data
----+--------+--------
(0 rows)

ivm=# SELECT * FROM baz.baz_mv;
  id   | f_data |     b_data
-------+--------+-----------------
 15033 |   1000 | 993.26757298472
(1 row)

ivm=# SELECT * FROM baz.baz_immv;
  id   | f_data |     b_data
-------+--------+-----------------
 15033 |   1000 | 993.26757298472
     1 |   1000 |           996.0
(2 rows)

ivm=#
```
ビューの場合は、JOIN元の片側が0件になるので、ビューの結果も0件になりますが、TRUNCATEの場合、DMLトリガの対象にはならないのでIMMVには更新は反映されません。
これはバグではなく、現状のpg_ivmの制約事項（README.mdの記述どおり）です。

> When the TRUNCATE command is executed on a base table, nothing is changed on the IMMV.

TRUNCATEによる運用を前提にしているようなケース（子パーティションテーブルをIMMVの参照元にするようなケースや、データ削除→一括ロードをするようなケース）とIMMVは相性が良くないということか。

### おまけ：DROP(未遂)
今度はトランザクション途中でビューが依存しているテーブルをDROPしてみます。

別ターミナルからBEGINしてbar.barをDROPしてみる。DROP時にはCASCADEオプションをつけます。
```sql
ivm=# BEGIN;
BEGIN
ivm=*# DROP TABLE bar.bar CASCADE;
NOTICE:  drop cascades to 3 other objects
DETAIL:  drop cascades to view baz.baz_v
drop cascades to materialized view baz.baz_mv
drop cascades to table baz.baz_immv
DROP TABLE
ivm=*#
```
この状態で、各ビューを参照しようとすると、いずれのビューともにロック待ちになる。

```sql
ivm=# SELECT * FROM baz.baz_v;
^C2022-05-15 15:59:14.904 JST [15796] ERROR:  canceling statement due to user request
2022-05-15 15:59:14.904 JST [15796] STATEMENT:  SELECT * FROM baz.baz_v;
Cancel request sent
ERROR:  canceling statement due to user request
ivm=# SELECT * FROM baz.baz_mv;
^C2022-05-15 15:59:27.826 JST [15796] ERROR:  canceling statement due to user request
2022-05-15 15:59:27.826 JST [15796] STATEMENT:  SELECT * FROM baz.baz_mv;
Cancel request sent
ERROR:  canceling statement due to user request
ivm=# SELECT * FROM baz.baz_immv;
^C2022-05-15 15:59:36.135 JST [15796] ERROR:  canceling statement due to user request
2022-05-15 15:59:36.135 JST [15796] STATEMENT:  SELECT * FROM baz.baz_immv;
Cancel request sent
ERROR:  canceling statement due to user request
ivm=#
```
ロールバックすると、もちろんトランザクション前の状態で各ビューが参照可能になります。

# IMMVの削除
IMMVの登録時には、`create_immv`SQL関数を使いましたが、そういえば削除するSQL関数はないですね。
なので、IMMV（実態はテーブルと、元テーブルに追加されたトリガ）を削除するためには、`DROP TABLE`コマンドで、IMMVとして作成されたテーブルをDROPします。

まずDROP TABLE前の状態を確認しておきます。
```sql
ivm=# \dtmv baz.baz_*
                List of relations
 Schema |   Name   |       Type        |  Owner
--------+----------+-------------------+----------
 baz    | baz_immv | table             | postgres
 baz    | baz_mv   | materialized view | postgres
 baz    | baz_v    | view              | postgres
(3 rows)

ivm=# SELECT t.tgrelid, c.relname, t.tgname FROM pg_trigger t JOIN pg_class c ON (t.tgrelid = c.oid);
 tgrelid | relname  |            tgname
---------+----------+-------------------------------
   17371 | foo      | IVM_trigger_upd_after_17446
   17371 | foo      | IVM_trigger_del_after_17445
   17371 | foo      | IVM_trigger_ins_after_17444
   17371 | foo      | IVM_trigger_upd_before_17443
   17371 | foo      | IVM_trigger_del_before_17442
   17371 | foo      | IVM_trigger_ins_before_17441
   17436 | baz_immv | IVM_prevent_immv_change_17456
   17436 | baz_immv | IVM_prevent_immv_change_17455
   17436 | baz_immv | IVM_prevent_immv_change_17454
   17436 | baz_immv | IVM_prevent_immv_change_17453
   17364 | bar      | IVM_trigger_upd_after_17452
   17364 | bar      | IVM_trigger_del_after_17451
   17364 | bar      | IVM_trigger_ins_after_17450
   17364 | bar      | IVM_trigger_upd_before_17449
   17364 | bar      | IVM_trigger_del_before_17448
   17364 | bar      | IVM_trigger_ins_before_17447
(16 rows)
```

``baz_immv ``テーブルをDROPします。
```sql
ivm=# DROP TABLE baz.baz_immv;
DROP TABLE
ivm=#
```

DROP後にリレーションの状態とトリガの状態を確認します。

```sql
ivm=# \dtmv baz.baz_*
               List of relations
 Schema |  Name  |       Type        |  Owner
--------+--------+-------------------+----------
 baz    | baz_mv | materialized view | postgres
 baz    | baz_v  | view              | postgres
(2 rows)

ivm=# SELECT t.tgrelid, c.relname, t.tgname FROM pg_trigger t JOIN pg_class c ON (t.tgrelid = c.oid);
 tgrelid | relname | tgname
---------+---------+--------
(0 rows)

ivm=#
```
無事にIMMV用のテーブルと元になったテーブルに追加されたトリガが削除されました。

# おわりに
まだ、リリースされたばかりの拡張機能ですが、使い所がハマればマテビューとビューのいいとこ取りができそうな機能になりそうです。
とはいえ、更新性能への影響などは当然でてくるはずなので、更新性能低下の確認や、クエリの制約、作成したIMMVに対する運用上の注意点などを調べておきたいですね。
（あと、PostgreSQL 15 beta1リリースしたら、それとの組み合わせも確認しなくては）

----
# 参考情報

* [5mins of Postgres E16: Incremental Materialized Views with pg_ivm and Looking Ahead to Postgres 15 Beta 1](https://pganalyze.com/blog/5mins-postgres-15-beta1-incremental-materialized-views-pg-ivm)

    * pg_ivmをPostgreSQL 15 beta1（あれ？beta1リリースって2020-05-19じゃなかったっけ？）で動かしたという話っぽい。