PostgreSQL 13がやってくる！(3) - 進捗どうでしょう

# はじめに
巨大なPostgreSQLデータベースクラスタをpg_basebackupを使ってバックアップを取得するとき、大量データロード後のANALYZE・・・。
こういう結構時間がかかる処理って、どこまで処理が進んでいるのか、やっぱり気になりますよね。
今回は、PostgreSQL 13の新機能、`pg_stat_progress_basebackup`と`pg_stat_progress_analyze`を使って、pg_basebackup処理やANALYZE処理の進捗確認がどんな感じでできるのか試してみました。

## 注意
まだ、β版にもなっていない時期なので、ビューの列などは今後、追加/削除/変更があるかもしれません。brata1がリリースされたら、列の変更等がないか再度確認する予定。

# pg_stat_progress_basebackup
pg_basebackupは、データベースクラスタ全体をバックアップファイル(あるいはレプリケーションのベースとして)取得するユーティリティですが、データベースクラスタのサイズが大きいときには当然ながら時間がかかります。
この`pg_stat_progress_basebackup`は、pg_basebackupなどのアプリケーション（ストリーミングレプリケーションプロコロルを使うなら、pg_basebackupに限らないかもしれないけど）などのアプリケーションの実行中の進捗を占めるビューです。

なので、以前のpg_start_basebackup() →　OSコマンドによるバックアップ処理 → pg_stop_backup()という手順の場合には、このビューによる進捗状況の監視はできません。

## pg_stat_progress_basebackupの内容
バックアップ処理のの進捗状況を見るには`pg_stat_progress_basebackup`というビューを検索します。このビューの内容は[Base Backup Progress Reporting](https://www.postgresql.org/docs/devel/progress-reporting.html#BASEBACKUP-PROGRESS-REPORTING)に説明があります。

|列名|型|内容|
|:--|:--|:--|
|pid|integer|walsenderのプロセスID|
|phase|text|現在の処理フェーズ。フェーズの詳細については後述。|
|backup_total|bigint|ストリーミングされるデータの合計量。 これは、データベースファイルのストリーミングフェーズの開始時に推定および報告されます。 ストリーミングデータベースファイルの段階でデータベースが変更され、WALログが後でバックアップに含まれる可能性があるため、これは概算にすぎないことに注意してください。 ストリーミングされるデータの量が推定合計サイズを超えると、これは常にbackup_streamedと同じ値になります。 pg_basebackupで見積もりが無効になっている場合（つまり、`-no-estimate-size`オプションが指定されている場合）、ここはNULLになります。|
|backup_streamed|bigint|ストリーミングされたデータの量。 このカウンタは、フェーズがデータベースファイルをストリーミングしているとき、またはwalファイルを転送しているときにのみ進みます。|
|tablespaces_total|bigint|ストリーミングされるテーブルスペースの総数。|
|tablespaces_streamed|bigint|ストリーミングされたテーブルスペースの数。 このカウンタは、データベースファイルをストリーミングしているフェーズがのときにのみ進みます。|

(pg_basebackupユーティリティの)-no-estimate-sizeオプション！
そういうのもあるのか。

## バックアップフェーズ
phase列に設定される値は以下の値域を持つ。

|フェーズ|意味|
|:--|:--|
|initializing|walsenderプロセスがバックアップを開始する準備をしています。 このフェーズは非常に短いと予想されます。|
|waiting for checkpoint to finish|walsenderプロセスはpg_start_backupを実行してベースバックアップを取得するためのセットアップを行い、バックアップ開始チェックポイントが完了するのを待っています。<br>`--checkpoint`がデフォルトの場合、チェックポイント契機が来るまではこの状態になるっぽい。|
|estimating backup size|ベースバックアップとしてストリーミングされるデータベースファイルの総量を推定するフェーズ|
|streaming database files|データベースファイルをベースバックアップとしてストリーミングしているフェーズ。|
|waiting for wal archiving to finish|pg_stop_backup)を実行してバックアップを完了し、ベースバックアップに必要なすべてのWALファイルが正常にアーカイブされるのを待機しています。 <br>--wal-method = noneまたは--wal-method = streamのいずれかがpg_basebackupで指定されている場合、このフェーズが完了するとバックアップが終了します。|
|transferring wal files|WAL送信者プロセスは現在、バックアップ中に生成されたすべてのWALログを転送しています。 このフェーズは、-wal-method = fetchがpg_basebackupで指定されている場合、walアーカイブが完了するフェーズを待機した後に発生します。 このフェーズが完了すると、バックアップが終了します。|

## 試してみた

以下のようなテーブルを定義しておく。

```sql
postgres=# \d backup_progress
                          Table "public.backup_progress"
        Column        |           Type           | Collation | Nullable | Default
----------------------+--------------------------+-----------+----------+---------
 ts                   | timestamp with time zone |           |          |
 pid                  | integer                  |           |          |
 phase                | text                     |           |          |
 backup_total         | bigint                   |           |          |
 backup_streamed      | bigint                   |           |          |
 tablespaces_total    | bigint                   |           |          |
 tablespaces_streamed | bigint                   |           |          |

postgres=#
```

このテーブルに対して以下のようなINSERT文を定期的に発行し、別端末からpg_basebackupユーティリティを起動する。

```sql
INSERT INTO backup_progress 
SELECT clock_timestamp() ts, * FROM pg_stat_progress_basebackup ;
```
で、pg_basebackupユーティリティが終了した後で、backup_progressテーブルに対して、以下のようなSELECT文を発行する。

```sql
postgres=# SELECT * FROM backup_progress ORDER by ts;
              ts               | pid  |                phase                | backup_total | backup_streamed | tablespaces_total | tablespaces_streamed
-------------------------------+------+-------------------------------------+--------------+-----------------+-------------------+----------------------
 2020-03-28 12:08:16.138108+00 | 2311 | waiting for checkpoint to finish    |            0 |               0 |                 0 |                    0
（略）
 2020-03-28 12:08:16.978108+00 | 2311 | waiting for checkpoint to finish    |            0 |               0 |                 0 |                    0
 2020-03-28 12:08:17.07976+00  | 2311 | streaming database files            |   3292090368 |         5392896 |                 1 |                    0
 2020-03-28 12:08:17.181573+00 | 2311 | streaming database files            |   3292090368 |        10215424 |                 1 |                    0
（略）
 2020-03-28 12:09:08.735793+00 | 2311 | streaming database files            |   3292090368 |      3292052992 |                 1 |                    0
 2020-03-28 12:09:09.127488+00 | 2311 | waiting for wal archiving to finish |   3292100096 |      3292100096 |                 1 |                    1
(317 rows)
```

## 検索結果から進捗状況を見てみる。
今回の検証モデルでは、デフォルトテーブルスペースしかないデータベースクラスタを対象にしているので、結構シンプルな結果になる。

* 最初に、` waiting for checkpoint to finish`のフェーズが始まる。これはCHECKPOINT処理が終わるまで継続する。
* 次に。`streaming database files`フェーズになる。このときに、注目すべきは、`backup_total`の数値と`backup_streamed`の数値。この比率がデータベースファイルのバックアップの進捗状況となる。
  * バックアップ中にリレーションのサイズが増えることがあれば、`backup_total`の数値よりも`backup_streamed`の数値が上回ることになるのだろう（未検証）
* 最後に、`waiting for wal archiving to finish`フェーズになる。それが終わるとバックアップが完了する。

# pg_stat_progress_analyze
ANALYZEの処理も、巨大なテーブルが大量にある場合には、そこそこ時間がかかるので、そういう時に、進捗情報が見れるとありがたいときもあると思います。

## pg_stat_progress_analyzeの内容
ANALZYE実行の進捗状況を見るには`pg_stat_progress_analyze`というビューを検索します。このビューの内容は[ANALYZE Progress Reporting](https://www.postgresql.org/docs/devel/progress-reporting.html#ANALYZE-PROGRESS-REPORTING)に説明があります。

|列名|型|内容|
|:--|:--|:--|
|pid|integer|ANALYZE処理が実行されているバックエンドプロセス。|
|datid|oid|ANALYZE対象となるデータベースのoid。|
|datname|name|ANALYZE対象となるデータベース名。|
|relid|oid|ANALYZE処理中のテーブルのoid。データベース全体へのANALYZEの実行時には、ここがくるくる変わるのだろう。|
|phase|text|処理フェーズを示す文字列。→[詳細](https://www.postgresql.org/docs/devel/progress-reporting.html#ANALYZE-PHASES)<br>個人的には略称と正式名称の2つが欲しいなあ・・・。(そういう要望も既に出ているかも。あとでcommitfest内容を確認しよ)|
|sample_blks_total|bigint|サンプリングされるヒープブロックの総数。|
|sample_blks_scanned|bigint|スキャンされたヒープブロックの数。|
|ext_stats_total|bigint|拡張統計の数。|
|ext_stats_computed|bigint|計算された拡張統計の数。 このカウンターは、拡張統計を計算しているフェーズのときにのみ進みます。|
|child_tables_total|bigint|子テーブルの数|
|child_tables_done|bigint|スキャンされた子テーブルの数。 このカウンターは、継承されたサンプル行を取得しているフェーズがのときにのみ進みます。|
|current_child_table_relid|oid|現在スキャンされている子テーブルのOID。 このフィールドは、継承されたサンプル行を取得しているフェーズのときにのみ有効です。|

注目すべきは拡張統計情報に関する列(`ext_stats_total`, `ext_stats_computed`)や、パーティションテーブルに関する列(`child_tables_total`, `child_tables_done`, `current_child_table_relid`)があることか。

## phase
すごーく雑にいうと、ANALYZE処理は対象となるリレーションに対して、順々に以下のフェーズの処理を行っているのだろう。→[PostgreSQL Document ANALYZE phases](https://www.postgresql.org/docs/devel/progress-reporting.html#ANALYZE-PHASES)

|フェーズ|意味|
|:--|:--|
|initializing|スキャン開始準備のフェーズ。これを表示させるのは結構難しそう。|
|acquiring sample rows|サンプル行を取得するためのテーブルスキャン中のフェーズ。ここのフェーズがテーブルが大きいと時間がかかる部分。|
|acquiring inherited sample rows|子テーブルをスキャン中。<br>child_tables_total、child_tables_done、およびcurrent_child_table_relidには、このフェーズの進捗情報が含まれている。|
|computing statistics|テーブルスキャン中に取得されたサンプル行から統計を計算するフェーズ。|
|computing extended statistics|テーブルスキャン中に取得されたサンプル行から統計を計算するフェーズ。<br>拡張統計はPostgreSQL 10から入った機能。以前調べたPostgreSQL 10の拡張統計情報の記事→[multi-column optimizer statistics](https://qiita.com/nuko_yokohama/items/b5f7146416dc34e78b9a)|
|finalizing analyze|計算した統計情報内容をpg_classに反映するフェーズ。 このフェーズが完了すると、（そのテーブル対する）ANALYZEは終了する。|

## 試してみた
以下のようなテーブル構成のデータベースに対して、ANALZYE処理を実行したときの進捗状況を監視するときの例を示す。

```sql
testdb=# \d
                    List of relations
 Schema |       Name       |       Type        |  Owner
--------+------------------+-------------------+----------
 public | test             | partitioned table | postgres
 public | test2            | table             | postgres
 public | test_p0          | table             | postgres
 public | test_p1          | table             | postgres
 public | test_p2          | table             | postgres
(6 rows)
```
test_p0, test_p1, test_p2 はtestテーブルのパーティションテーブルとなる。

### 検証手順
このデータベースに対してANALYZEをかけつつ、別の端末から、`pg_stat_progress_analyze`テーブルを定期的に検索する。で、検索結果をそのまま垂れ流すと後で見るのが大変になるから、その検索結果を以下のようなテーブルに格納する。

```sql
testdb=# \d analyze_progress
                          Table "public.analyze_progress"
       Column        |            Type             | Collation | Nullable | Default
---------------------+-----------------------------+-----------+----------+---------
 relname             | name                        |           |          |
 datname             | name                        |           |          |
 phase               | text                        |           |          |
 sample_blks_total   | bigint                      |           |          |
 sample_blks_scanned | bigint                      |           |          |
 child_tables_total  | bigint                      |           |          |
 child_tables_done   | bigint                      |           |          |
 ts                  | timestamp without time zone |           |          |

testdb=#
```

（上記のテーブルは拡張統計情報に関する検索結果は含んでいない）

で、以下のようなINSERT文を定期的に実行しつつ(こういうときには、psqlの`\watch`メタコマンドが便利）、別端末からデータベース全体に対するANALYZEを実行する。

```sql
INSERT INTO analyze_progress 
SELECT c.relname, pa.datname, phase, sample_blks_total, sample_blks_scanned, child_tables_total, child_tables_done, clock_timestamp() 
FROM pg_stat_progress_analyze pa JOIN pg_class c ON (pa.relid = c.oid);
```

で、ANALYZE処理を終わった後に、この`analyze_progress`テーブルの内容を以下のSELECT文で確認する。

```sql
testdb=# SELECT relname, phase, sample_blks_total, sample_blks_scanned, child_tables_total, child_tables_done FROM analyze_progress ORDER BY ts;
     relname      |              phase              | sample_blks_total | sample_blks_scanned | child_tables_total | child_tables_done
------------------+---------------------------------+-------------------+---------------------+--------------------+-------------------
 test             | acquiring inherited sample rows |             34102 |                2878 |                  3 |                 0
 test             | acquiring inherited sample rows |             34102 |                7478 |                  3 |                 0
 test             | acquiring inherited sample rows |             34102 |               10893 |                  3 |                 0
（略）
 test             | acquiring inherited sample rows |             34084 |               34084 |                  3 |                 1
 test             | acquiring inherited sample rows |             34078 |                2038 |                  3 |                 2
 test             | acquiring inherited sample rows |             34078 |                6771 |                  3 |                 2
（略）
 test             | acquiring inherited sample rows |             34078 |               34078 |                  3 |                 2
 test             | computing statistics            |             34078 |               34078 |                  3 |                 3
（略）
 test             | computing statistics            |             34078 |               34078 |                  3 |                 3
 test             | finalizing analyze              |             34078 |               34078 |                  3 |                 3
 test             | finalizing analyze              |             34078 |               34078 |                  3 |                 3
 test_p0          | acquiring sample rows           |             34102 |                 493 |                  0 |                 0
 test_p0          | acquiring sample rows           |             34102 |                5095 |                  0 |                 0
（略）
 test_p2          | computing statistics            |             34078 |               34078 |                  0 |                 0
 test_p2          | finalizing analyze              |             34078 |               34078 |                  0 |                 0
 test_p2          | finalizing analyze              |             34078 |               34078 |                  0 |                 0
 test2            | acquiring sample rows           |            102263 |                1736 |                  0 |                 0
 test2            | acquiring sample rows           |            102263 |                6359 |                  0 |                 0
（略）
 test2            | computing statistics            |            102263 |              102263 |                  0 |                 0
 test2            | finalizing analyze              |            102263 |              102263 |                  0 |                 0
 test2            | finalizing analyze              |            102263 |              102263 |                  0 |                 0
(1337 rows)
```

### 検索結果から進捗状況を見てみる。

* まず、`test`テーブルに対する処理が行われる。`acquiring inherited sample rows`フェーズがしばらく続く。
  * `sample_blks_scanned`の数が増加していく。
  * が、一旦、この数が減ってまた増加している。(子テーブル毎にリセットするのかな？）
* その後、`test`テーブルに対する`computing statistics`フェーズが始まる。
* その後、`test`テーブルに対する`finalizing analyze`フェーズになる。これが終わると、`test`テーブルに対する一連の処理が終わる。
* 次に、`test`テーブルの子テーブルにあたる`test_p0`テーブルに対する`acquiring sample rows`フェーズが始まる。
* その後に`computing statistics`フェーズになる。
* そして`finalizing analyze`フェーズを経て、`test_p0`テーブルに対する処理が終わる。
* 以下、`test_p1`, `test_p2`も同様に処理される。
* パーティションテーブルの場合、全パーティションテーブル数が最初に`child_tables_total`にセットされ、パーティションテーブルが処理される度に、`child_tables_done`の数がカウントアップされていく。
* その後に、別テーブル`test2`に対しても同様に、`acquiring sample rows`→`computing statistics`→`finalizing analyze`のフェーズを経る。
* 各テーブルごとの処理に繊維するごとに、`sample_blks_total`の値はリセットされ、`sample_blks_scanned`の値がカウントアップしていく。

### わかったこと
さて、このANALYZE検証で自分なりに理解したこと。

* データベース内にいくつのテーブルがあるかどうかの総数/総ブロック数を算出するわけではない。
  * なので、データベース内に何百もテーブルがある場合、残り何テーブルの処理が残っているのか、という進捗状況の確認はできない。
* パーティションテーブルについては、子テーブル総数を最初に算出するので、`child_tables_total`と`child_tables_done`の数から、おおよその処理状況は推測できるようになった。

# おわりに
今回は、PostgreSQL 13から利用可能なバックアップ/ANALYZE処理の進捗状況を監視するビューを紹介しました。
PostgreSQLの適用領域が広がり、巨大なデータを扱うようになるとバックアップやANALYZE処理にも時間がかかるようになってきています。そういうときに、こういうビューを使って進捗状況を監視できるようになると、待ち時間のストレスが軽減されるかもしれません。
ただ、ANALYZEに関しては、パーティションテーブルでない、多数のテーブルが存在するケースでの進捗状況確認に課題があるな、という印象を持ちました。