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