PostgreSQL 10の Unlogged Table と Declarative Partitioning

#はじめに
この記事は [PostgreSQL Advent Calendar 2017](https://qiita.com/advent-calendar/2017/postgresql) の5日目の記事です。昨日は[vidaisuki](https://twitter.com/vidaisuki)さんの「[Amazon Performance Insightsをとりあえず触ってみる](http://vidaisuki.hatenablog.com/entry/advent2017pgsql04)」でした。モニタリングは大事ですよね。

本稿ではPostgreSQL 9.1で導入されたUnlogged TableとPostgreSQL 10で導入されたDeclarative Partitioning（宣言的パーティショニング）を組み合わせて使用する際の注意点などをご紹介します。
なお、本稿での動作確認にはRed Hat Enterprise Linux 7上のPostgreSQL 10.1を使用しています。

#Unlogged Tableとは
最初に Unlogged Tableのおさらいをします。
ご存じのとおり Unlogged Tableはテーブルに対する書き込み処理のトランザクションログ（WAL）を出力しないテーブルです。

```
testdb=# create unlogged table t1(c1 numeric, c2 varchar(10)) ;
```

のように、CREATE TABLE文に"unlogged"というオプションを指定することで作成できます。

#### Unlogged Tableのメリット 
Unlogged Tableに対する書き込みはWALを出力しないため、通常のテーブルへの書き込み処理と比較すると書き込み処理性能が向上します。特に多くのセッションが書き込み処理を行うケースで大きな性能向上が見込めます。

#### Unlogged Tableの注意点
書き込み処理性能が向上するUnlogged Tableですが、使用する上で注意すべき点もあります。特に注意すべき点は以下の2点です。

* ストリーミング・レプリケションやロジカル・レプリケーションでレプリケーションすることができない。
* **PostgreSQLが異常終了した場合、再起動の際にTruncateされてデータが消えてしまう。**

ストリーミング・レプリケションもロジカル・レプリケーションもWALを利用したレプリケーションですので、1つ目の注意点は当然といえば当然ですね。2つ目の注意点については、個人的には強制的に消さないでほしい、と思っています。せめて異常終了後のPostgreSQLの起動時にUnlogged TableをTruncateしない起動オプションがあるとうれしいのですが...

#### Unlogged属性の変更
テーブルを作成した後に、通常のテーブルをUnlogged Tableへ変更することや、その逆を行うことができます。以下に通常のテーブルとして作成したt1テーブルをUnlogged Tableに変更する例を示します。なお、テーブルがUnlogged Tableかどうか確認するには、pg_classカタログのrelpersistence列の値を確認します。列値が'p'であれば通常のテーブル、'u'であればUnlogged Tableです。

```
testdb=# create table t1(c1 numeric, c2 varchar(10)) ;
CREATE TABLE
testdb=# select relname, relfilenode, relpersistence from pg_class where relname ='t1' ;
 relname | relfilenode | relpersistence
---------+-------------+----------------
 t1      |      237569 | p
(1 row)

testdb=# alter table t1 set unlogged;
ALTER TABLE
testdb=# select relname, relfilenode, relpersistence from pg_class where relname ='t1' ;
 relname | relfilenode | relpersistence
---------+-------------+----------------
 t1      |      237575 | u
(1 row)
```

ALTER TABLE文の実行により、relpersistence列の値が'p'から'u'に変わっていることがわかります。
ちなみに、relfilenode列の値も変わっています。これは、ALTER TABLE文によってUnlogged属性を変更すると、内部的にはデータをコピーして新しいテーブルを作る操作を行っているためです。このため、データ量が多いテーブルに対して属性を変更する操作は長時間となってしまうことがありますので注意が必要です。

#### PostgreSQL 異常終了後のUnlogged Tableの状態
PostgreSQLが異常終了した後、再起動させたときのUnlogged Tableの状態を確認してみましょう。

以下の例では、t1テーブルにレコードを追加後、Unlogged Tableに変更しています。レコード数をcountすると1レコードあることがわかります。

```
testdb=# insert into t1 values ( 100, 'data');
INSERT 0 1
testdb=# alter table t1 set unlogged;
ALTER TABLE
testdb=# select relname, relpersistence from pg_class where relname ='t1' ;
 relname | relpersistence
---------+----------------
 t1      | u
(1 row)

testdb=# select count(*) from t1;
 count
-------
     1
(1 row)
```

その状態でpostgresプロセスを強制終了させ、その後PostgreSQLを再起動させます。

```
$ killall -9 postgres
$ pg_ctl -w -D ${PGDATA} start
pg_ctl: another server might be running; trying to start server anyway
waiting for server to start....2017-11-30 16:35:35.927 JST [17151] LOG:  listening on IPv4 address "0.0.0.0", port 5432
2017-11-30 16:35:35.927 JST [17151] LOG:  listening on IPv6 address "::", port 5432
2017-11-30 16:35:35.929 JST [17151] LOG:  listening on Unix socket "/tmp/.s.PGSQL.5432"
2017-11-30 16:35:35.937 JST [17151] LOG:  redirecting log output to logging collector process
2017-11-30 16:35:35.937 JST [17151] HINT:  Future log output will appear in directory "log".
 done
server started
```

ログを確認すると、前回終了時に正常終了していないためにリカバリ処理が行われていることがわかります。ちなみに、PostgreSQL 10からデフォルトのログディレクトリが pg_log から log に変更されています。

```
$ cat $PGDATA/log/`ls $PGDATA/log | tail -1`
2017-11-30 16:35:35.940 JST [17153] LOG:  database system was interrupted; last known up at 2017-11-30 16:33:46 JST
2017-11-30 16:35:35.948 JST [17153] LOG:  database system was not properly shut down; automatic recovery in progress
・・・
```

サーバー起動後にt1テーブルのレコード数を確認すると0レコードになっていることが確認できます。

```
$ psql testdb
testdb=# select count(*) from t1;
 count
-------
     0
(1 row)
```

これを知らずにUnlogged Tableを使用すると、いつの日か大悲劇が起きそうですね...


#Declarative Partitioning（宣言的パーティショニング）と Unlogged tableの組み合わせ
前置きがだいぶ長くなりましたが、今まで説明してきましたUnlogged TableはPostgreSQL 10で導入された Declarative Partitioning（宣言的パーティショニング）と組み合わせて使うことができます。
Declarative Partitioning自体についてはいろいろなところで説明されていますので、この場での説明は割愛させていただきます。例えば [篠田の虎の巻 PostgreSQL 10 Beta1 新機能 検証結果](http://h50146.www5.hpe.com/products/software/oe/linux/mainstream/support/lcc/pdf/PostgreSQL_10_New_Features_ja_20170522-1.pdf) にも詳しく解説されていますので、ご興味があればご覧ください。

Declarative Partitioningの親テーブルと子テーブルは、個別にUnlogged Tableにするかどうか指定ができます。以下の例では、親テーブルとしてtp1、tp2を作成し、それぞれに2つのリストパーティションとなる子テーブルを作成しています。それぞれのテーブルの属性は以下の表のように設定します。

| テーブル名  | Unloggedか？   | 備考                            |
|:-----------|:---------------|:-------------------------------|
| tp1        | 通常           | 親テーブル                       |
| tp1_v100   | Unlogged Table | tp1のリストパーティションテーブル |
| tp1_v200   | 通常           | tp1のリストパーティションテーブル |
| tp2        | Unlogged Table | 親テーブル                      |
| tp2_v100   | Unlogged Table | tp2のリストパーティションテーブル |
| tp2_v200   | 通常           | tp2のリストパーティションテーブル |


```
testdb=# create table tp1(c1 numeric, c2 varchar(10)) partition by list (c1) ;
CREATE TABLE
testdb=# create unlogged table tp1_v100 partition of tp1 for values in (100) ;
CREATE TABLE
testdb=# create table tp1_v200 partition of tp1 for values in (200) ;
CREATE TABLE

testdb=# create unlogged table tp2(c1 numeric, c2 varchar(10)) partition by list (c1) ;
CREATE TABLE
testdb=# create unlogged table tp2_v100 partition of tp2 for values in (100) ;
CREATE TABLE
testdb=# create table tp2_v200 partition of tp2 for values in (200) ;
CREATE TABLE

testdb=# select relname, relfilenode, relpersistence from pg_class where relname like 'tp%' order by relname;
 relname  | relfilenode | relpersistence
----------+-------------+----------------
 tp1      |      245760 | p
 tp1_v100 |      245766 | u
 tp1_v200 |      245772 | p
 tp2      |      245778 | u
 tp2_v100 |      245784 | u
 tp2_v200 |      245790 | p
(6 rows)
```

では、tp1親テーブルをUnlogged Tableに変更してみましょう。

```
testdb=# alter table tp1 set unlogged ;
ALTER TABLE
```

特にエラーもなくALTER TABLE文を実行できました。結果を確認してみましょう。

```
testdb=# select relname, relfilenode, relpersistence from pg_class where relname like 'tp1%' order by relname;
 relname  | relfilenode | relpersistence
----------+-------------+----------------
 tp1      |      245760 | p
 tp1_v100 |      245766 | u
 tp1_v200 |      245772 | p
(3 rows)
```

おや、**relpersistence が'p'のまま**ですね。relfilenodeも変わっていません。なぜでしょう!？
tp1テーブルはひとまずおいておき、今度は子テーブルのtp1_v200 を Unlogged Tableにしてみましょう。

```
testdb=# alter table tp1_v200 set unlogged ;
ALTER TABLE
testdb=# select relname, relfilenode, relpersistence from pg_class where relname like 'tp1%' order by relname;
 relname  | relfilenode | relpersistence
----------+-------------+----------------
 tp1      |      245760 | p
 tp1_v100 |      245766 | u
 tp1_v200 |      245796 | u
(3 rows)
```

子テーブルの場合、relpersistenceは'u'に変わりました。

もしかすると、全子テーブルがUnlogged Tableになれば親テーブルもUnlogged Tableできるかもと思い、この状態で再度親テーブルに対して ALTER TABLE文を実行してみました。

```
testdb=# alter table tp1 set unlogged ;
ALTER TABLE
testdb=# select relname, relfilenode, relpersistence from pg_class where relname like 'tp1%' order by relname;
 relname  | relfilenode | relpersistence
----------+-------------+----------------
 tp1      |      245760 | p
 tp1_v100 |      245766 | u
 tp1_v200 |      245796 | u
(3 rows)
```

しかし、やはり親テーブルはUnlogged Tableにはなりませんでした。

念のため、Unlogged Tableとして作成したtp2を通常テーブルに変更する処理も試してみましたが、やはり処理は正常終了するものの属性は変わらず Unlogged Tableのままでした。

```
testdb=# alter table tp2 set logged ;
ALTER TABLE
testdb=# select relname, relfilenode, relpersistence from pg_class where relname like 'tp2%' order by relname;
 relname  | relfilenode | relpersistence
----------+-------------+----------------
 tp2      |      245778 | u
 tp2_v100 |      245784 | u
 tp2_v200 |      245790 | p
(3 rows)
```

うーん、なんかモヤモヤする動作ですね...
まあ、親テーブルにはデータが入るわけではないから子テーブルの属性が変更できれば実運用上は問題ないかもしれないですね。

と、思ったのですが、
PostgreSQLが異常終了した場合はどうなるのでしょうか？
気になりますね。

親テーブルがUnlogged Tableの場合に、再起動時に親テーブルにTruncateがかけられて全子テーブルのデータが消されてしまったらまずいですね。

というわけで、以下のケースで異常終了後の状態を確認してみました。

* 親テーブルが通常テーブル(tp1)、子テーブルがUnlogged Table（tp1_v100）
* 親テーブルが通常テーブル(tp1)、子テーブルも通常テーブル（tp1_v200）
* 親テーブルがUnlogged Table(tp2)、子テーブルもUnlogged Table（tp2_v100）
* 親テーブルがUnlogged Table(tp1)、子テーブルが通常テーブル（tp2_v200）

結果としては、親テーブルの属性にかかわらず、子テーブルが通常テーブルの場合はTruncateされず、Unlogged Table の場合はTruncateされる、という結果になりました。そのため親テーブルの属性が変更できないという動作は実運用的には問題にはならなさそうです。

以下は確認結果です。
まず、tp1テーブル、tp2テーブルの各パーティションに1レコードずつデータをインサートし、Unlogged属性の設定をします。

```
testdb=# insert into tp1 values ( 100, 'data'),( 200, 'data');
INSERT 0 2
testdb=# insert into tp2 values ( 100, 'data'),( 200, 'data');
INSERT 0 2
testdb=#
testdb=# alter table tp1_v200 set logged ;
ALTER TABLE
testdb=# alter table tp2_v100 set unlogged ;
ALTER TABLE
testdb=#
```

確認するとtp1、tp2のそれぞれの子テーブルに1レコードずつ、親テーブルには合計で2レコード格納されていることがわかります。

```
testdb=# select c.relname, c.relpersistence, t.cnt from
testdb-#   (select relname, relpersistence from pg_class
testdb(#     where relname like 'tp%') c,
testdb-#   (select 'tp1' as relname, count(*) cnt from tp1
testdb(#    union all
testdb(#    select 'tp1_v100' as relname, count(*) cnt from tp1_v100
testdb(#    union all
testdb(#    select 'tp1_v200' as relname, count(*) cnt from tp1_v200
testdb(#    union all
testdb(#    select 'tp2' as relname, count(*) cnt from tp2
testdb(#    union all
testdb(#    select 'tp2_v100' as relname, count(*) cnt from tp2_v100
testdb(#    union all
testdb(#    select 'tp2_v200' as relname, count(*) cnt from tp2_v200) t
testdb-# where c.relname = t.relname
testdb-# order by relname;
 relname  | relpersistence | cnt
----------+----------------+-----
 tp1      | p              |   2
 tp1_v100 | u              |   1
 tp1_v200 | p              |   1
 tp2      | u              |   2
 tp2_v100 | u              |   1
 tp2_v200 | p              |   1
(6 rows)
```

この状態でpostgresプロセスを強制終了させ、再起動します。

```
$ killall -9 postgres
$ pg_ctl -w -D ${PGDATA} start
```

再起動後、各テーブルの状態を確認します。

```
$ psql testdb
testdb=# select c.relname, c.relpersistence, t.cnt from
testdb-#   (select relname, relpersistence from pg_class
testdb(#     where relname like 'tp%') c,
testdb-#   (select 'tp1' as relname, count(*) cnt from tp1
testdb(#    union all
testdb(#    select 'tp1_v100' as relname, count(*) cnt from tp1_v100
testdb(#    union all
testdb(#    select 'tp1_v200' as relname, count(*) cnt from tp1_v200
testdb(#    union all
testdb(#    select 'tp2' as relname, count(*) cnt from tp2
testdb(#    union all
testdb(#    select 'tp2_v100' as relname, count(*) cnt from tp2_v100
testdb(#    union all
testdb(#    select 'tp2_v200' as relname, count(*) cnt from tp2_v200) t
testdb-# where c.relname = t.relname
testdb-# order by relname;
 relname  | relpersistence | cnt
----------+----------------+-----
 tp1      | p              |   1
 tp1_v100 | u              |   0
 tp1_v200 | p              |   1
 tp2      | u              |   1
 tp2_v100 | u              |   0
 tp2_v200 | p              |   1
(6 rows)
```

親テーブルtp1、tp2がUnlogged Tableかどうかに関わらず、**子テーブルがUnlogged Tableの場合のみデータが消されている**ことが確認できました。一安心ですね。


#おわりに
以上のように、Declarative Partitioningでは、親テーブルをUnlogged Tableから通常テーブルへ、またはその逆の変更はできないという制限があることがわかりました。
また、異常終了後の再起動時には親テーブルの設定に関わらず、子テーブルがUnlogged Tableの場合のみデータが消されるということがわかりました。
親テーブルをあえてUnlogged Tableとして作成するメリットはなさそうなので、親テーブルはUnlogged Tableにしないほうが無難ですね。

[PostgreSQL Advent Calendar 2017](https://qiita.com/advent-calendar/2017/postgresql)、明日は[noborus](https://twitter.com/noborus)さんです。