PG15/PG16:Differentiate MERGE queries with different structures

## はじめに

にゃーん。趣味でポスグレをやっている者だ。

この記事は[PostgreSQL 16 全部ぬこ Advent Calendar 2022](https://qiita.com/advent-calendar/2022/pg16-all-nuko) 8日目の記事です。

今回はPostgreSQL 15.0のリリース直前にcommitされた
``pg_stat_statements``の改造内容を書いてみます。

## 概要

|項目|内容|
|:--|:--|
|タイトル|[Differentiate MERGE queries with different structures](https://commitfest.postgresql.org/40/3839/)|
|Topic|Monitoring & Control|
|ステータス|commited|
|Last modified|2022-09-28|
|概要|pg_stat_statementsでMERGE文のWHENのあとの複数種類の文を別々にカウントする|

## 変更内容
これはPostgreSQL 15から追加されたMERGEコマンドをpg_stat_statementsで収集するときの問題を解決したものです。

### MERGEコマンド
PostgreSQL 15から[MERGEコマンド](https://www.postgresql.org/docs/current/sql-merge.html)が使えるようになりました。
MERGEコマンドは条件によって、INSERT/UPDATE/DELETEの処理に分岐するコマンドです。

### pg_stat_statementsモジュール
[pg_stat_statements](https://www.postgresql.org/docs/15/pgstatstatements.html)は、サーバで実行されたすべてのSQL文のプラン生成時と実行時の統計情報を記録するモジュールです。
このモジュールは``pg_stat_statements``ビューと幾つかの関数を提供しているものです。

実運用時に性能問題が発生したときの、取っ掛かりとしてこの拡張機能は役立つので、多少の性能オーバヘッドがあるものの取り敢えず入れといて損はないものです。

## 動作検証
### 検証内容
postgresql.confに以下の設定を行ってPostgreSQLサーバを起動します。

```
shared_preload_libraries='pg_stat_statements'
```
検証要のデータベースにpg_stat_statementsを登録しておきます。

```sql
CREATE EXTENSION pg_stat_statements;
```

以下のような簡単なテーブルを作成します。

```sql
CREATE TABLE test (
  id  int primary key,
  data text
);
```
わかりやすくするために、pg_stat_statementsで収集した統計情報はリセットしておきます（これによりcallsが0になる）。
```sql
SELECT pg_stat_statements_reset();
```
以下の3つのSQLを実行します。

```sql
INSERT INTO test VALUES (1, 'ABC');

-- MERGE (insert)
MERGE INTO test AS t
USING (values (2, 'DEF')) AS i(id, data)
ON t.id = i.id
WHEN MATCHED THEN
    UPDATE SET data = i.data
;

-- MERGE (update)
MERGE INTO test AS t
USING (values (1, 'abc')) AS i(id, data)
ON t.id = i.id
WHEN MATCHED THEN
    DELETE;
;
```

* 最初にidが1のレコードを挿入します。
* 次のMERGE文ではマッチしたらUPDATEを行うのですが、実際にはマッチしないので、何もしません。
* 次のMERGEではマッチした行をDELETEします。idが1の行がマッチするので、id=1の行はDELETEされます。

このクエリを実行すると、結果はこうなります。

```sql
TABLE test;
 id | data
----+------
(0 rows)
```

### PostgreSQL 15以降の挙動
この状態で、pg_stat_statementsビューを参照します（今回はquery_id, query, callsのみ表示します）。

```sql!
SELECT queryid, query, calls FROM pg_stat_statements ORDER BY query;
       queryid        |                 query                  | calls
----------------------+----------------------------------------+-------
  2265416722066910701 | INSERT INTO test VALUES ($1, $2)       |     1
 -1381799517176356930 | MERGE INTO test AS t                  +|     1
                      | USING (values ($1, $2)) AS i(id, data)+|
                      | ON t.id = i.id                        +|
                      | WHEN MATCHED THEN                     +|
                      |     DELETE                             |
 -6692072224192878392 | MERGE INTO test AS t                  +|     1
                      | USING (values ($1, $2)) AS i(id, data)+|
                      | ON t.id = i.id                        +|
                      | WHEN MATCHED THEN                     +|
                      |     UPDATE SET data = i.data           |
 -7024813306266399419 | SELECT pg_stat_statements_reset()      |     1
  7533749921200599032 | TABLE test                             |     1
(5 rows)
```

testテーブルに対するMERGE文は2回発行されてますが、WHEN句の内容が異なるものはそれぞれ別に1回ずつカウントされています。


### PostgreSQL 15 beta版までの挙動
この修正なんですが、実はこの改修(2022-09-28にcommit)が入らなかったら、WHEN句が異なるMERGE文も同じものとしてカウントされていたようです。
以下は同じ検証をPostgreSQL 15 beta1で実施したものです（本当はbeta4やrc1でやりたかったけど、もうソースが入手できなかった・・・）。

```sql!
SELECT queryid, query, calls FROM pg_stat_statements ORDER BY query;
       queryid        |                 query                  | calls
----------------------+----------------------------------------+-------
 -5907294925155083993 | INSERT INTO test VALUES ($1, $2)       |     1
  7315424997101376355 | MERGE INTO test AS t                  +|     2
                      | USING (values ($1, $2)) AS i(id, data)+|
                      | ON t.id = i.id                        +|
                      | WHEN MATCHED THEN                     +|
                      |     UPDATE SET data = i.data           |
 -2766515315050639636 | SELECT pg_stat_statements_reset()      |     1
 -2578912257006329778 | TABLE test                             |     1
(4 rows)

```

このようにMERGE文の種類は1つしか登録されず（MATCH DELETEのケースは登録されない）、かつ同じMERGEは文として2回実行されたとpg_stat_statementsで収集されてしまっていたようです。

なお、PostgreSQL 15.0のリリースノート改修項目としてはこの改造内容は明記されていないように思われます。既に消えてしまった、PostgreSQL 15 rc1版のリリースノートには記載されていたのかもしれませんが・・・。

## おわりに
今回はPostgreSQL 15.0リリース時点では解決していた、pg_stat_statementsの問題が、PostgreSQL 15.0リリース直前のcommitで解決された、ということを書きました。

このcommitfest項目を確認したときに、PostgreSQL 15でもPostgreSQL 16でも挙動が変わらなくて何故だ・・・と悩みました。
今回のCommitfestにはこのようにPostgreSQL 15にも反映済みの項目もあることがわかったので、きちんとcommitされた日付も確認しないといけませんね。