PostgreSQLを数十億行規模にスケールさせる:パーティショニング実践ガイド

Database tutorial - IT technology blog
Database tutorial - IT technology blog

12億行の壁を突破する

かつて私が携わったロギングシステムでは、単一の logs テーブルが12億行まで膨れ上がりました。以前はミリ秒単位で終わっていた単純な分析クエリが、15〜20秒もかかるようになったのです。VACUUM やインデックスの追加といった日常的なメンテナンスは、週末中ずっと続くようなリスクの大きな作業となりました。これは、データ量が単一の物理テーブルの効率性を超えたときに直面する、避けられないスケーリングの壁です。

多くのチームがこの段階で複雑な NoSQL クラスターへの移行を検討しますが、PostgreSQL の宣言的パーティショニング(declarative partitioning)は、よりスマートな解決策を提供してくれます。これにより、1つの巨大な論理テーブルを小さな物理的な塊に分割することができます。このアーキテクチャの転換により、インデックスを小さく保ち、クエリを高速化し、メンテナンス時間を管理可能な範囲に収めることが可能になります。

レンジパーティショニングの設定

PostgreSQL は PARTITION BY 句を使用してパーティショニングを処理します。時系列データはこの手法の最も一般的な候補です。例えば、毎月500万件の新しい行が追加される orders テーブルを想像してください。パーティショニングを行わないと、インデックスはやがて RAM に収まらないほど巨大化します。

まず、親テーブルを定義します。このテーブルはテンプレートとして機能し、それ自体にはデータは保存されないことに注意してください。

CREATE TABLE orders (
    order_id SERIAL,
    customer_id INT NOT NULL,
    order_date DATE NOT NULL,
    total_amount NUMERIC,
    PRIMARY KEY (order_id, order_date)
) PARTITION BY RANGE (order_date);

ここで重要な詳細があります。パーティションキー(order_date)は主キーに含まれている必要があります。親テーブルが作成されたら、特定の月を保存するための実際のバケットを作成できます。

-- 2024年1月のパーティション
CREATE TABLE orders_2024_01 PARTITION OF orders
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

-- 2024年2月のパーティション
CREATE TABLE orders_2024_02 PARTITION OF orders
    FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');

1月の日付を持つ行を挿入すると、PostgreSQL は自動的にそれを orders_2024_01 テーブルにルーティングします。クエリの実行中、プランナーは「パーティション・プルーニング(Partition Pruning)」を使用します。WHERE 句で1月をフィルタリングした場合、エンジンは他のすべてのパーティションを無視します。500GBのテーブルをスキャンする代わりに、わずか10GBのスライスにアクセスするだけで済むのです。

戦略の選択

レンジパーティショニング(Range Partitioning)は日付データの標準ですが、唯一の手段ではありません。データのアクセス方法に応じて、リストまたはハッシュパーティショニングがより効果的な場合があります。

1. レンジパーティショニング

日付、タイムスタンプ、または連続したIDに使用します。自然なライフサイクルを持つデータに最適です。DROP TABLE orders_2023_01 を実行して1ヶ月分の古いデータを削除する操作は、ほぼ一瞬で完了します。これは、標準的な DELETE コマンドによって発生する膨大な I/O オーバーヘッドやトランザクションログの肥大化を回避します。

2. リストパーティショニング

これは、地域や部門IDなどのカテゴリデータに最適です。アプリケーションが country_code で頻繁にフィルタリングを行う場合、地理的にデータを分離して、クエリを特定の地域に限定させることができます。

CREATE TABLE users (id INT, name TEXT, country_code TEXT) 
PARTITION BY LIST (country_code);

CREATE TABLE users_vn PARTITION OF users FOR VALUES IN ('VN');
CREATE TABLE users_us PARTITION OF users FOR VALUES IN ('US');

3. ハッシュパーティショニング

ハッシュパーティショニングは、論理的な範囲が存在しない場合に I/O 負荷を分散させるのに理想的です。sessions テーブルへの書き込みが非常に多く、単一のディスクでは処理しきれない場合、ハッシュパーティショニングを使用して、モジュロ演算(余り)ロジックによりデータを複数の物理ファイルに分散させます。

CREATE TABLE web_sessions (session_id UUID, data TEXT) 
PARTITION BY HASH (session_id);

-- データを4つのバケットに均等に分割
CREATE TABLE web_sessions_0 PARTITION OF web_sessions FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE web_sessions_1 PARTITION OF web_sessions FOR VALUES WITH (MODULUS 4, REMAINDER 1);
-- 余り2と3についても同様に繰り返す...

インデックス作成とメンテナンスの実態

パーティション化されたデータベースの管理には、インデックスに対する考え方の転換が必要です。PostgreSQL は、すべてのパーティションにまたがる「グローバルインデックス」をサポートしていません。親テーブルにインデックスを定義すると、PostgreSQL はすべてのパーティションに対してローカルインデックスを作成します。これにより、個々のインデックスツリーが浅く高速に保たれますが、プランナーがどのインデックスを使用すべきか判断できるように、クエリには必ずパーティションキーを含める必要があります。

pg_partman による自動化

手動でテーブルを作成するのはリスクを伴います。来月のパーティションを作成し忘れると、時計が深夜0時を打った瞬間にアプリケーションがエラーになります。そこで、pg_partman 拡張機能の使用をお勧めします。これはパーティションの作成を自動化し、定義したスケジュールに基づいてデータの保持期間を管理してくれます。

-- 1時間ごとのパーティション管理を自動化
SELECT partman.create_parent('public.server_logs', 'event_time', 'native', 'hourly');

効率的なアーカイブ

パーティショニングにより、データのアーカイブがシームレスになります。パーティションを「デタッチ(切り離し)」して、スタンドアロンのテーブルに変換できます。これにより、稼働中の親テーブルのパフォーマンスに影響を与えることなく、コールドデータを安価なストレージに移動したり、S3 にダンプしたりすることが可能になります。

ALTER TABLE orders DETACH PARTITION orders_2023_01;

現場からの教訓

いくつかの高トラフィックシステムをこのアーキテクチャに移行した結果、避けるべき一般的な落とし穴がいくつか見えてきました。

  • 過剰なパーティショニングを避ける: 1日に1万行程度しか増えないテーブルに対して、日次パーティションを作成している例を見たことがあります。これは数千もの小さなファイルを作成することになり、クエリプランナーを混乱させます。パーティションのサイズは1GBから10GBの間を目指すのが理想的です。
  • パーティションキーを慎重に決定する: テーブルが稼働し始めると、パーティションキーを簡単に変更することはできません。パフォーマンスを左右するのは WHERE 句で最も頻繁に使用されるカラムであるため、それを選択してください。
  • 一意性制約: UNIQUEPRIMARY KEY には、必ずパーティションキーを含める必要があります。これは、グローバルな UUID のみに依存するスキーマでは頭の痛い問題になる可能性があるため、ID 戦略は早期に計画してください。
  • バキューム(Vacuuming): パーティショニングは VACUUM を不要にするものではありませんが、より効率的にします。autovacuum プロセスが個別の小さなテーブルに対して動作できるため、ハードウェア上のロック競合の時間を短縮できます。

パーティショニングはすべての遅いクエリに対する万能薬ではありませんが、10億行規模に達するデータセットにとっては、不可欠なアーキテクチャパターンです。データを管理可能なサイズに分割することで、データベースのパフォーマンスと長期的な健全性を再びコントロールできるようになります。

Share: