運用で役立つツールpg_repackのご紹介


この記事は [PostgreSQL Advent Calendar 2015 - Qiita](http://qiita.com/advent-calendar/2015/postgresql) の9日目です。
8日目は [osapon](http://qiita.com/osapon) さんに書いていただきました。

この記事では、PostgreSQLを運用する上で役立つかもしれないツールの一つ [pg_repack](https://github.com/reorg/pg_repack) を紹介したいと思います。

# pg_repackとは

pg_repack はPostgreSQLの拡張ツール（エクステンション）の一つで、肥大化したテーブルやインデックスを再編成し、さらに指定したインデックスにしたがってレコードを並び替えることができます。 
PostgreSQLの CLUSTER や VACUUM FULL コマンドと違って、pg_repackは処理の間対象テーブルへの排他ロックを保持し続けないため、オンライン中に動作させることができます。 

どういうことなのか、説明していきますね。

## テーブルやインデックスの肥大化

PostgreSQLを運用していると、テーブルやインデックスが肥大化するという事象が起きることがあります。
ここで、「肥大化」という言葉で表しているのは、

**テーブル(インデックス)が実際に見えるレコード総数のサイズよりも、大きなサイズをディスク上で占めるようになっている 
= 余分なものがくっついて太っている**

という状態です。

何故こんなことが起きるのでしょうか。
それは、PostgreSQLがテーブルやインデックスの保存方法として「追記型アーキテクチャ」を採用しているからです。

PostgreSQLでは、テーブルのレコードをDELETEする際は実際にレコードがテーブルデータを保存したファイルから削除されるのではありません。単に、該当行に削除済みマークを付けるだけです。
同様に、UPDATEの際も更新前のレコードは残したまま、新しいレコードを追記します。 
これが追記型アーキテクチャと呼ばれるものです。

このため、更新・削除が繰り返されるテーブルにおいては、たとえ実際にテーブルから取り出せるレコード数が変わらなくても、古くなったレコードが残り続けるため、物理的なファイルサイズは増加していきます。

ファイルサイズが大きくなると、データアクセスの際にディスクIOが発生しやすくなり、性能が急激に悪化する恐れがあります。

## VACUUM

こうしたテーブルやインデックスの肥大化をなるべく勧めないように、PostgreSQLでは（有名な）VACUUM処理を適切に行う必要があります。

VACUUMは、テーブルやインデックスのファイルからすでに不要になったレコードを回収し、再利用できるようにします。[^1]
PostgreSQLの8.3から自動的にVACUUMを行うautovacuumという機能がデフォルトで有効になっているため、基本的には適切なタイミングでVACUUMが行われることで肥大化が起きにくくなっています。[^2]

[^1]: VACUUMはこの他にも、FREEZE処理やVisibility Mapの更新などのこれまた大事な処理も行います。
[^2]: その他、HOT更新という機能により、更新処理時に不要レコードを回収する機能も入っています。

ただし、VACUUMは回収したレコードが占めていた領域を再利用可能にするだけであって、肥大化の進行を食い止めることはできますが、肥大化を解消することはできません。

そのため、バッチ処理等で一度に大量のレコードを削除・更新したり、autovacuumが何らかの原因で動作していなかったりすると、テーブルやインデックスの肥大化が進んでしまいます。
また、ロングトランザクションによりVACUUMが動作しているにも関わらず、不要レコードを回収できていない状態になることもあります。[^3]

[^3]: ロングトランザクションはこの他にも各種の異常を引き起こします。PostgreSQLトラブルの主役級の悪役です。

不要になったレコードが占めていた領域を取り除き、実際のレコードのみでテーブルのファイルを作りなおすにはVACUUM FULLという処理を行う必要があります。
同じく、肥大化したインデックスをキレイな状態にするにはREINDEXを行う必要があります。

が、VACUUM FULL, REINDEXには実行にあたって大きな問題があります。

## VACUUM FULL, REINDEXは気軽に実行できない

それは、VACUUM FULLやREINDEX実行中は対象のテーブルに対してACCESS EXCLUSIVEという最も強い排他ロックが掛かるため、テーブルの参照処理すらもアクセスできなくなる点です。
そのため、サービス中のPostgreSQLに対して実行してしまうと、オンライン処理に影響を与えてしまいます。

**こうした欠点をカバーするのが、pg_repackです。**

pg_repackは、排他ロックを瞬間的にだけ掛けて、あとは必要最小限のロックだけかけるため、処理中もテーブルへの参照・更新処理が実施できます。
さらに、テーブルを作りなおす際に、レコードを特定のインデックスキーの順番で並びなおすこともできます。これはCLUSTERに該当する処理です。こちらもCLUSTERがVACUUM FULLやREINDEXと同じ最も強い排他ロックをかけ続けるが、pg_repackはそうではないという点は同様です。

# pg_repackの使い方

## インストール

インストールはいつもの方法(make & make install)です。

```
$ git clone https://github.com/reorg/pg_repack.git
$ cd pg_repack
$ make
$ su
# make install
```

インストールできたら、今度はPostgreSQLデータベースにpg_repackエクステンションを登録します。

```
$ psql -c "CREATE EXTENSION pg_repack" -d postgres
```

## 肥大化したテーブルの準備

では、実際にテーブルを肥大化させてみましょう。

用意するのは、こちらの簡単テーブルです。

```
[local] 36186 postgres=# \d tbl
      Table "public.tbl"
 Column |  Type   | Modifiers
--------+---------+-----------
 a      | integer | not null
 b      | text    |
Indexes:
    "tbl_pkey" PRIMARY KEY, btree (a)
```

レコードは1件も登録していないです。

```
[local] 36186 postgres=# SELECT count(*) FROM tbl;
 count
-------
     0
(1 row)

[local] 36186 postgres=# SELECT pg_size_pretty(pg_total_relation_size('tbl'));
 pg_size_pretty
----------------
 16 kB
(1 row)
```

テキトーに10,000件くらいレコードを初期登録します。この時点でサイズは712kBでした。

```
[local] 36186 postgres=# INSERT INTO tbl VALUES(generate_series(1,10000), 'initial');
INSERT 0 10000

[local] 36186 postgres=# SELECT pg_size_pretty(pg_total_relation_size('tbl'));
 pg_size_pretty
----------------
 712 kB
(1 row)
```

では、UPDATEをしてみましょう。容赦のない全行UPDATEです。

```
[local] 36186 postgres=# UPDATE tbl SET b = 'updated-001';
UPDATE 10000
[local] 36186 postgres=# SELECT pg_size_pretty(pg_total_relation_size('tbl'));
 pg_size_pretty
----------------
 1576 kB
(1 row)
```

テーブルとインデックス合わせたサイズが2倍くらいになりました。
これを繰り返すと、、、

```
[local] 36186 postgres=# SELECT count(*) FROM tbl;
 count
-------
 10000
(1 row)

[local] 36186 postgres=# SELECT pg_size_pretty(pg_total_relation_size('tbl'));
 pg_size_pretty
----------------
 9728 kB
(1 row)

```

レコードのサイズおよびレコード数は変わらず、当初の10倍以上にサテーブルとインデックスのサイズが膨らみました。

ちなみに、この状態でVACUUMをしても

```
[local] 36186 postgres=# VACUUM tbl;
VACUUM
[local] 36186 postgres=# SELECT pg_size_pretty(pg_total_relation_size('tbl'));
 pg_size_pretty
----------------
 9728 kB
(1 row)
```

サイズは小さくなりません。


## pg_repackによるテーブル再編成

再編成するテーブル名および、そのテーブルが存在するデータベースを指定して実行します。

```
$ pg_repack -d postgres -t tbl
INFO: repacking table "tbl"
```

再編成後のテーブル・インデックスサイズを確認してみると、、、

```
[local] 37006 postgres=# SELECT pg_size_pretty(pg_total_relation_size('tbl'));
 pg_size_pretty
----------------
 688 kB
(1 row)

```

サイズが小さくなっていますね！


## pg_repackの詳細な使い方

[pg_repackのドキュメント](http://reorg.github.io/pg_repack/)を参照ください。
現時点では英語のドキュメントしかありませんが、日本語化したドキュメントを追加するパッチを投稿中です。

-- *2015/12/17 追記* --
日本語ドキュメントがマージされました！

[こちら](https://github.com/reorg/pg_repack/blob/master/doc/pg_repack_jp.rst)から参照ください。

## [参考] pg_reorg と pg_repack

pg_repackは [pg_reorg](https://github.com/ossc-db/pg_reorg) というツールからforkしたプロジェクトです。

実は私はpg_reorgの中の人です。

pg_reorgの方もPostgreSQLのバージョンアップ追随など細々と開発を続けていたのですが、

* 後発のpg_repackの方がコミュニティがより活発であること
* pg_repackのみの機能があること
* 似たプロジェクトが2つ存在する意味が少ないこと

などを受けて内部で議論した結果、**今後はpg_reorgの開発は中止して、pg_repackの方に貢献していくことになりました。**

もうすくリリースされるはずのPostgreSQL 9.5からはpg_repackを推していくことになります。
pg_reorgを使っている方がいらっしゃいましたら、今後はpg_repackに移行していただければ幸いです。
pg_reorgと使い勝手は変わらない上、インデックスのみの再編成や、再編成後のテーブル空間移動、インデックス再編成処理の並列化などpg_reorgになかった機能も追加されています。

# 終わりに

ということで、今日のエントリはサービス中に実行が可能なメンテナンスツールpg_repackの紹介でした。

明日の PostgreSQL Advent Calender の担当は [kwatch](http://qiita.com/kwatch) さんです。よろしくお願いします。