EatSmartシステム部ブログ

ウェブサイトの開発や運営に関する情報です。

PostgreSQLのパーティショニングを利用する

もぐナビはフォローした情報を閲覧する"タイムライン"の提供を開始しました。 今回は"タイムライン"で利用するイベント記録テーブルに関する工夫を書きたいと思います。

パーティショニングの利用

記録するイベントは、商品へのクチコミやイイネなどのユーザーのアクションや商品の発売、ニュース記事の公開などが該当します。 これらのイベントは毎日大量に発生することが予想されるため、数が増えた場合に性能を落とさない工夫が必要です。 また、タイムラインの提供範囲から外れたイベントは不要となるため、削除することが可能です。

以上のことから、イベント記録テーブルはパーティショニングを利用することにしました。 パーティショニングを利用することで、テーブルのサイズを抑えることが可能です。 また、期間を指定して削除もDELETEではなく該当期間のテーブルをDROPすることが出来ます。

テーブルの作成

イベントを記録するマスタテーブルを作成します。 時系列で分割するためのevent_dateというフィールドを作成しています。

CREATE TABLE v_timeline_event (
    event_id            VARCHAR(256) NOT NULL,
    event               VARCHAR(64) NOT NULL,
    action_user_id      NUMERIC(15,0),
    event_date          TIMESTAMP WITHOUT TIME ZONE NOT NULL
);

次に子テーブルを作成します。このときCHECKという制約を指定します。 2019年以前のデータを格納するため"event_date < DATE '2019-01-01'"といいう制約を指定しています。

CREATE TABLE v_timeline_event_2018 (
    CHECK ( event_date < DATE '2019-01-01' )
) INHERITS (v_timeline_event);
CREATE INDEX idx_v_timeline_event_2018_1 ON v_timeline_event_2018 (event_date DESC, event, event_id);

データの挿入

データの挿入は、マスタテーブルに対して行います。 マスターテーブルにはあらかじめトリガを設定しておき、適切な子テーブルへの挿入するようにします。 このとき、例えば2019年のデータをv_timeline_event_2018へ挿入しようとするとエラーが発生します。

月単位で子テーブルを管理するにあたり、その作成を関数内で行うことにしました。 予めテーブルを作成しておけば良いのですが、不要なテーブルが大量に存在することが嫌なこと、当初の想定を超えて子テーブルが必要になった際にエラーが発生することからこのようにしました。 実装にあたり、既にこの仕組を実現している方の情報が参考にしました。

-- INSERT時に適切なパーティションテーブルへ保存する関数
-- 対象となるパーティションテーブルが無い場合は作成する
CREATE OR REPLACE FUNCTION func_timeline_event_insert() RETURNS TRIGGER AS
'
DECLARE
    part text;
BEGIN
    -- 2019年以前のデータは共通
    IF ( NEW.event_date <  DATE ''2019-01-01'') THEN
        INSERT INTO v_timeline_event_2018 VALUES (NEW.*);
        RETURN NULL;
    END IF;

    -- 日付からパーティションテーブルの名前を決める:v_timeline_event_YYYYMM
    part := ''v_timeline_event_'' || TO_CHAR(new.event_date, ''YYYYMM'');

    BEGIN
        EXECUTE ''INSERT INTO '' || part || '' VALUES(($1).*)'' USING new;
        RETURN NULL;
    EXCEPTION WHEN undefined_table THEN
        -- 対象となるテーブルが無い場合は作成
        RAISE NOTICE ''CREATE TABLE->%'', part;
        EXECUTE ''CREATE TABLE '' || part || '' (''
            || ''    CHECK ( event_date >= DATE '''''' || date_trunc(''MONTH'', NEW.event_date) || '''''' AND event_date < DATE '''''' || (date_trunc(''MONTH'', NEW.event_date) + ''1 MONTH''::INTERVAL) || '''''' )''
            || '') INHERITS (v_timeline_event)'';
        EXECUTE '' CREATE INDEX idx_'' || part || ''_1 ON '' || part || '' (event_date DESC, event, event_id)'';
    END;

    EXECUTE ''INSERT INTO '' || part || '' VALUES(($1).*)'' USING new;
    RETURN NULL;
END;
'
LANGUAGE plpgsql;
-- INSERT時に適切なパーティションテーブルへ保存する関数を実行するためのトリガ
CREATE TRIGGER trg_insert_v_timeline_event
    BEFORE INSERT ON v_timeline_event
    FOR EACH ROW EXECUTE PROCEDURE func_timeline_event_insert();

挿入時のエラーを捕捉して、テーブルを作成するようにしました。 試しに予めテーブルを作成した場合と速度の比較をしてパフォーマンスが落ちることを確認しましたが、挿入はバッチ処理で行うため今回は問題なしとしました。