PostgreSQLのタイムスタンプ・パーティション用汎用トリガ関数

## はじめに
この記事では、
[PostgreSQLのパーティションテーブル自動生成](http://qiita.com/yancya/items/960180ed1d6faa510d65)を参考に、**timestamp型のtimeカラムを分割キーとするパーティション**を自動生成するための汎用トリガ関数を例示します。

## パーティション用汎用トリガ関数
このトリガ関数は、参考記事のサンプルと同様に、まずはパーティションの子テーブルにINSERTを試みます。子テーブルが未作成でINSERTが失敗したときには、その子テーブルを自動作成し、再度INSERTを試みます。

このトリガ関数では、パーティション分割の単位(日単位や月単位など)と子テーブル名の添え字フォーマットをCREATE TRIGGER実行時にユーザが指定します。つまり、異なる単位のパーティションを作成するにあたって、別途トリガ関数を用意する必要はありません。このトリガ関数のみで様々な分割単位のパーティションを生成できます。

```plpgsql
-- パーティションの子テーブルを作成する関数
CREATE OR REPLACE FUNCTION create_timerange_partition (
  partition_key timestamp,  -- INSERTするレコードの分割キーの値
  parent_table name,        -- パーティションの親テーブルの名前
  range_interval text,      -- パーティションの分割単位(例えば、hour, day, monthなど)
  suffix_format text)       -- 子テーブルの名前の添え字フォーマット
                            -- ※分割単位と添え字フォーマットは整合していること
RETURNS void AS $$
DECLARE
  mintime timestamp := date_trunc(range_interval, $1);
  maxtime timestamp := mintime + ('1 ' || range_interval)::interval;
BEGIN
  -- 引数の値から子テーブルの名前やパーティション範囲を計算して、子テーブルを作成する
  EXECUTE 'CREATE TABLE IF NOT EXISTS ' ||
    parent_table || to_char(partition_key, suffix_format) ||
    ' (LIKE ' || parent_table || ' INCLUDING INDEXES, CHECK(''' ||
    mintime || ''' <= time AND time < ''' || maxtime ||
    ''')) INHERITS (' || parent_table || ')';
END;
$$ LANGUAGE plpgsql;


-- パーティションへのINSERTトリガ関数
CREATE OR REPLACE FUNCTION insert_timerange_partition ()
RETURNS TRIGGER AS $$
DECLARE
  -- 親テーブルの名前はトリガ関数の変数から自動的に取得
  parent_table name := TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME;
  -- パーティションの分割単位は、トリガ作成時に指定
  range_interval text := TG_ARGV[0];
  -- 子テーブル名の添え字フォーマットは、トリガ作成時に指定
  suffix_format text := TG_ARGV[1];
  retry_count integer;
BEGIN
  -- 無限ループに陥らないように、INSERT→子テーブル作成→INSERTの試行は1回のみに制限
  FOR retry_count IN 0 .. 1 LOOP
    BEGIN
      EXECUTE 'INSERT INTO ' || parent_table ||
        to_char(NEW.time, suffix_format) ||
        ' VALUES (($1).*)' USING NEW;
      RETURN NULL;
    EXCEPTION WHEN undefined_table THEN
      -- 無限ループに陥りそうな場合は例外発生
      IF retry_count >= 1 THEN
        RAISE EXCEPTION 'could not insert data into "%" partition',
          parent_table;
      END IF;

      -- 子テーブルが未作成の場合は、作成して再度INSERTを試みる
      PERFORM create_timerange_partition(
        NEW.time, parent_table,
        range_interval, suffix_format);
    END;
  END LOOP;
END;
$$ LANGUAGE plpgsql;
```

## 汎用トリガ関数を使ったパーティション化
ここでは、上記の汎用トリガ関数を使い、[psql経由でのCPU使用率のテーブル蓄積](http://qiita.com/fujii_masao/items/7bb84817d0308da96893)で利用したcpu_usageテーブルをパーティション化してみます。cpu_usageテーブルの定義は以下のとおりです。

```plpgsql
CREATE TABLE cpu_usage (
  time timestamp DEFAULT now(),
  us smallint,
  sy smallint,
  id smallint,
  wa smallint
);
```

### 日単位パーティション
cpu_usageテーブルを日単位にパーティション化したい場合は、CREATE TRIGGER実行時に、パーティションの分割単位として**'day'**、子テーブル名の添え字フォーマットとして(例えば)**'_YYYYMMDD'**を指定します。

```plpgsql
CREATE TRIGGER insert_cpu_usage_partition
  BEFORE INSERT ON cpu_usage FOR EACH ROW
  EXECUTE PROCEDURE
  -- 第１引数に分割単位、第2引数に添え字フォーマットを指定
  insert_timerange_partition('day', '_YYYYMMDD');
```

これでcpu_usageテーブルのパーティション化は終わりです。cpu_usageテーブルにレコードをINSERTすると、日単位パーティションの子テーブルが自動的に作成されて、そこにレコードがINSERTされます。

```plpgsql
-- COPY FROM PROGRAMを使い、vmstatからCPU使用率を取得してテーブルにコピー
COPY cpu_usage(us, sy, id, wa)
  FROM PROGRAM 'vmstat | tail -1 | awk ''{print $13 "," $14 "," $15 "," $16}'''
  WITH (FORMAT csv);

-- 日単位で子テーブルが自動作成されたのを確認
\dt
               List of relations
 Schema |        Name        | Type  |  Owner   
--------+--------------------+-------+----------
 public | cpu_usage          | table | postgres
 public | cpu_usage_20161007 | table | postgres

```

### 月単位パーティション
汎用トリガ関数を使って、CREATE TRIGGER実行時の指定を変えるだけで、cpu_usageテーブルを月単位にパーティション化できます。

```plpgsql
CREATE TRIGGER insert_cpu_usage_partition
  BEFORE INSERT ON cpu_usage FOR EACH ROW
  EXECUTE PROCEDURE
  insert_timerange_partition('month', '_YYYYMM');
```