PostgreSQL10でテーブルパーティションへのINSERT性能が大幅Upする件

これは[PostgreSQL Advent Calendar 2016](http://qiita.com/advent-calendar/2016/postgresql)の13日目の記事です。

# はじめに

PostgreSQLには、継承とトリガを利用したテーブルパーティション機能が従来からありましたが、パーティションへのINSERTが**非常に遅い**という問題がありました。一方、それらの従来のテーブルパーティション機能の様々な問題を解決するため、NTT OSSセンタのAmit Langoteさん([@amitlan](https://github.com/amitlan))が中心となり、[新たな改善版テーブルパーティション機能が(記事執筆時点で)開発中のPostgreSQL10に取り込まれました](https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=f0e44751d7175fa3394da2c8f85e3ceb3cdbfe63)。PostgreSQL10では、従来版と新機能版の2つのテーブルパーティション機能を利用できることになります。
この記事では、PostgreSQL10新機能版テーブルパーティションのINSERT性能が、従来のものに比べてどれだけ改善されているのか簡単に比較検証します。

# 注意事項

この記事の内容は、2016年12月8日JST時点で開発中のPostgreSQLバージョン10のソースコードをベースにしています。今後、PostgreSQL10の正式リリースまでにテーブルパーティション機能が大幅に改変され、機能と記事の内容に大きな乖離が発生する可能性があることに注意してください。
性能検証には、開発中のソースコードからコンパイルしたPostgreSQLを使います。

# 検証準備

INSERT性能は、各テーブルパーティション機能で「2016年12月1日から12月10日まで」の10日間の日単位のパーティションを作成し、以下のクエリで生成される864,000件の日時データをINSERTしたときの時間を計測します。

``` plpgsql
-- 2016-12-01 00:00:00 から 2016-12-10 23:59:59 までの10日間を1秒1件でレコード化
SELECT time, 'TEST'
FROM generate_series('2016-12-01 00:00:00', '2016-12-10 23:59:59', '1 second'::interval) time;
```

まずは、継承とトリガを利用した従来のテーブルパーティション機能でパーティションをtraditionalスキーマに作成します。

``` plpgsql
-- 従来のテーブルパーティション機能によるパーティションを配置するスキーマを作成
CREATE SCHEMA traditional;

-- 作成したスキーマをデフォルトで参照するようにsearch_pathを設定
SET search_path TO traditional, "$user", public;

-- パーティションの親テーブルを作成
CREATE TABLE parent (time timestamp PRIMARY KEY, val text);

-- 親テーブルに対するINSERTを子テーブルに振り分けるためのトリガを定義
CREATE OR REPLACE FUNCTION insert_parent_function () RETURNS TRIGGER AS $$
BEGIN
  EXECUTE 'INSERT INTO child_' || to_char(NEW.time, 'YYYYMMDD') || ' VALUES (($1).*)' USING NEW;
	RETURN NULL;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER insert_parent_trigger BEFORE INSERT ON parent FOR EACH ROW EXECUTE PROCEDURE insert_parent_function();

-- 2016年12月1日から10日までの各日付の子テーブルを作成
CREATE TABLE child_20161201 (LIKE parent INCLUDING INDEXES, CHECK('2016-12-01 00:00:00' <= time AND time < '2016-12-02 00:00:00')) INHERITS (parent);
CREATE TABLE child_20161202 (LIKE parent INCLUDING INDEXES, CHECK('2016-12-02 00:00:00' <= time AND time < '2016-12-03 00:00:00')) INHERITS (parent);
CREATE TABLE child_20161203 (LIKE parent INCLUDING INDEXES, CHECK('2016-12-03 00:00:00' <= time AND time < '2016-12-04 00:00:00')) INHERITS (parent);
CREATE TABLE child_20161204 (LIKE parent INCLUDING INDEXES, CHECK('2016-12-04 00:00:00' <= time AND time < '2016-12-05 00:00:00')) INHERITS (parent);
CREATE TABLE child_20161205 (LIKE parent INCLUDING INDEXES, CHECK('2016-12-05 00:00:00' <= time AND time < '2016-12-06 00:00:00')) INHERITS (parent);
CREATE TABLE child_20161206 (LIKE parent INCLUDING INDEXES, CHECK('2016-12-06 00:00:00' <= time AND time < '2016-12-07 00:00:00')) INHERITS (parent);
CREATE TABLE child_20161207 (LIKE parent INCLUDING INDEXES, CHECK('2016-12-07 00:00:00' <= time AND time < '2016-12-08 00:00:00')) INHERITS (parent);
CREATE TABLE child_20161208 (LIKE parent INCLUDING INDEXES, CHECK('2016-12-08 00:00:00' <= time AND time < '2016-12-09 00:00:00')) INHERITS (parent);
CREATE TABLE child_20161209 (LIKE parent INCLUDING INDEXES, CHECK('2016-12-09 00:00:00' <= time AND time < '2016-12-10 00:00:00')) INHERITS (parent);
CREATE TABLE child_20161210 (LIKE parent INCLUDING INDEXES, CHECK('2016-12-10 00:00:00' <= time AND time < '2016-12-11 00:00:00')) INHERITS (parent);
```

次に、開発中のPostgreSQLのテーブルパーティション機能でパーティションをmodernスキーマに作成します。

``` plpgsql
-- PostgreSQL10のテーブルパーティション機能によるパーティションを配置するスキーマを作成
CREATE SCHEMA modern;

-- 作成したスキーマをデフォルトで参照するようにsearch_pathを設定
SET search_path TO modern, "$user", public;

-- パーティションの親テーブルを作成
CREATE TABLE parent (time timestamp, val text) PARTITION BY RANGE (time);

-- 2016年12月1日から10日までの各日付の子テーブルを作成
CREATE TABLE child_20161201 PARTITION OF parent (PRIMARY KEY(time)) FOR VALUES FROM ('2016-12-01 00:00:00') TO ('2016-12-02 00:00:00');
CREATE TABLE child_20161202 PARTITION OF parent (PRIMARY KEY(time)) FOR VALUES FROM ('2016-12-02 00:00:00') TO ('2016-12-03 00:00:00');
CREATE TABLE child_20161203 PARTITION OF parent (PRIMARY KEY(time)) FOR VALUES FROM ('2016-12-03 00:00:00') TO ('2016-12-04 00:00:00');
CREATE TABLE child_20161204 PARTITION OF parent (PRIMARY KEY(time)) FOR VALUES FROM ('2016-12-04 00:00:00') TO ('2016-12-05 00:00:00');
CREATE TABLE child_20161205 PARTITION OF parent (PRIMARY KEY(time)) FOR VALUES FROM ('2016-12-05 00:00:00') TO ('2016-12-06 00:00:00');
CREATE TABLE child_20161206 PARTITION OF parent (PRIMARY KEY(time)) FOR VALUES FROM ('2016-12-06 00:00:00') TO ('2016-12-07 00:00:00');
CREATE TABLE child_20161207 PARTITION OF parent (PRIMARY KEY(time)) FOR VALUES FROM ('2016-12-07 00:00:00') TO ('2016-12-08 00:00:00');
CREATE TABLE child_20161208 PARTITION OF parent (PRIMARY KEY(time)) FOR VALUES FROM ('2016-12-08 00:00:00') TO ('2016-12-09 00:00:00');
CREATE TABLE child_20161209 PARTITION OF parent (PRIMARY KEY(time)) FOR VALUES FROM ('2016-12-09 00:00:00') TO ('2016-12-10 00:00:00');
CREATE TABLE child_20161210 PARTITION OF parent (PRIMARY KEY(time)) FOR VALUES FROM ('2016-12-10 00:00:00') TO ('2016-12-11 00:00:00');
```

# 検証

では、各スキーマのパーティションに対して、以下の日時データ864,000件のINSERTを3回実行し、それぞれの実行時間を計測します。

``` plpgsql
INSERT INTO parent SELECT time, 'TEST' FROM generate_series('2016-12-01 00:00:00', '2016-12-10 23:59:59', '1 second'::interval) time;
```

# 検証結果

日時データ864,000件のINSERT時間は以下のとおりで、PostgreSQL10新機能のテーブルパーティションにより、INSERT性能が大幅に改善(今回の検証結果だと**10倍の性能改善**)されていることを確認できました！！

| 検証  | 従来   | PostgreSQL10 |
|:-----|:------|-------------:|
| 1回目 | 52.7s | 5.2s         |
| 2回目 | 52.9s | 4.9s         |
| 3回目 | 52.9s | 5.1s         |