Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyで管理するPostgreSQLパーティションテーブル ― 古いデータの効率的な削除を実現するpg_partition_manager

なぜパーティションテーブルが必要なのか

私たちの主力PostgreSQLデータベースでは、古いデータを効率的に期限切れにするために、パーティションテーブルを活用しています。巨大なテーブルからまとめて大量のデータをDELETEすると、データベースのパフォーマンスが大きく低下してしまうためです。

実はPostgreSQL 10より前のバージョンには、パーティションテーブルのネイティブサポートが存在しませんでした。そのため当時は、pg_partman拡張機能を使ってパーティショニングを実装していました。この拡張機能は、PostgreSQLのテーブル継承を利用してパーティション対象テーブルの子テーブルを作成し、トリガーによって親テーブルではなく子テーブルへデータを挿入する仕組みです。長年安定して動いてくれましたが、1つ大きな難点があります。Amazon RDSではサポートされていないため、RDS環境では利用できないのです。幸い、PostgreSQLがネイティブパーティションを公式サポートするようになった今こそ、この拡張機能を外してRDSを選択肢に入れる好機だと考えました。

時間ベースのパーティショニング方式

私たちのユースケースはとてもシンプルです。テーブルを時間軸で分割し、全パーティション合計で保存したい行数やデータの保持期間に応じて、日次・週次・月次のいずれかの単位で新しいパーティションを作成していきます。すべてのパーティションテーブルにはcreated_atカラムがあり、各行がどのパーティションに格納されるかをこの値で判定します。

たとえば、以下のようなテーブルを定義します。

create table events (
  project_id integer,
  data jsonb,
  created_at timestamp
)
partition by range (created_at);

週次パーティションにする場合は、次のように作成します。

create table events_p2019_10_28 partition of events for values from ('2019-10-28') to ('2019-11-04');
create table events_p2019_11_04 partition of events for values from ('2019-11-04') to ('2019-11-11');

古いデータの削除は「パーティションをDROPする」だけ

時間ベースのパーティショニングの最大の利点は、古いデータの削除が該当パーティションを削除するだけで完結することです。したがって、日常的に必要なメンテナンスは次の2つだけです。

  • 日付範囲が近づいてきたら、新しいパーティションを作成する
  • 不要になったデータを含む古いパーティションを削除する

このメンテナンス作業を簡単にするために、私はpg_partition_managerというgemを作成しました。もちろん、これまで大変お世話になったpg_partman拡張機能での経験にインスピレーションを得ています。

pg_partition_manager gemの使い方

上記のeventsテーブルとパーティショニング構成を例に、実際の使い方を見てみましょう。以下のようなスクリプトまたはrakeタスクを作成します。

require "pg_partition_manager"

PgPartitionManager::Time.process([{parent_table: "public.events", period: "week", premake: 1, retain: 3}])

各オプションの意味は次のとおりです。

  • parent_tableschema.table_name形式で指定します。Rails開発者が普段使うのはデフォルトスキーマのpublicだけであることが多いでしょう。
  • period:「day」「week」「month」のいずれかを指定できます。
  • premake:現在の期間以降に、いくつのテーブルを事前作成しておくかを指定します。
  • retain:現在の期間より過去にさかのぼって、いくつのテーブルを保持するかを指定します。

premakeを指定しない場合のデフォルトは4テーブルの事前作成です。またretainを省略した場合、データは日次パーティションなら7日間、週次なら4週間、月次なら6か月間保持されます。

あとはこのスクリプト/rakeタスクを毎日のcronジョブで実行するだけで準備完了です。必要に応じてテーブルの作成と削除が自動的に行われます。

ActiveRecordのコードは変更不要

ActiveRecordのクエリは、非パーティションテーブルの場合とまったく同じように動作します。アプリケーションコード側で変更すべき箇所はありません。つまり、Event.createEvent.whereなどは従来どおり動作し、INSERT時にはPostgreSQLが適切なパーティションへ自動的にデータを振り分けてくれます。

ただし、大量のデータを扱っている場合には、1つの嬉しい違いに気づくかもしれません。クエリのWHERE句にcreated_at条件を含めると、PostgreSQLはすべてのパーティションをスキャンする必要がなくなり、指定された期間をカバーするパーティションだけを参照するようになるのです。これはクエリパフォーマンス面でも大きなメリットといえます。

まとめ

期限が来たら削除したい時系列データが大量にあるなら、PostgreSQLのネイティブパーティションテーブルとpg_partition_manager gemの組み合わせが強力な味方になります。パーティションの作成・削除をcronで自動化すれば、運用手間も最小限に抑えられます。ぜひ試してみてください :)

  1. TCMallocを使ってRubyのメモリ割り当てをプロファイリングする方法

    Rubyでは、メモリの割り当てはどのように行われているのでしょうか? Rubyは「ページ(page)」と呼ばれる塊単位でメモリを確保し、新しく生成されたオブジェクトはこのページの中に格納されていきます。 しかし、 ページがいっぱいになると、さらなるメモリが必要になります。 そのときRubyは、malloc関数を使ってオペレーティングシステム(OS)に追加のメモリを要求します。 このmalloc関数はOS標準で提供されているものですが、代替となる実装に差し替えることも可能です。 その代表的な実装のひとつが、Google製のTCMallocです。 TCMallocは「gperftools(Goog

  2. Rubyでパーサーを自作する方法!StringScannerを使った実装手順を徹底解説

    パース(構文解析)とは、文字列の集まりから意味を読み取り、プログラムが扱える形のデータへと変換する技術です。正規表現でも文字列の解析は可能ですが、すべての場面に適しているわけではありません。 たとえば、正規表現でHTMLを解析するのはあまり良い方法ではないというのは、プログラミング界隈ではよく知られた話です。 Rubyにはnokogiriという強力なライブラリがあり、HTMLの解析はこれに任せられます。しかし、自分でパーサーを一から作ってみると、文字列処理や構文解析の仕組みについて多くのことを学べます。それでは早速始めていきましょう! Rubyでのパースの基本:StringScannerクラス