PostgreSQL 実践講座 #7 パーティショニング — 巨大テーブルを時間の単位で分ける

読了 5分

イベント、ログ、注文のように時間とともに際限なく積もるテーブルは、いつか数億行になります。インデックス(実践第 4 回の BRIN まで含めて)で検索は持ちこたえても、2 つのことがだんだん重くなります。古いデータの削除と VACUUM です。この 2 つの問題の標準処方がパーティショニング、つまり 1 つの論理テーブルを物理的に複数の断片に分けることです。

パーティショニングが解決するもの #

  • 古いデータの削除が DROP 一発になります。 これが実務価値の半分以上です。「1 年経ったイベントの削除」を DELETE でやると数千万個の dead tuple が生まれて VACUUM がその後始末をすることになりますが、月別パーティションなら DROP TABLE events_2025_08; の 1 文で即座に、dead tuple なしで終わります。
  • パーティションプルーニング: クエリの条件にパーティションキーがあれば、該当するパーティションだけをスキャンします。12 か月分のテーブルで今月の検索が 1/12 だけ読むようになります。
  • 管理の単位が小さくなります: VACUUM・ANALYZE・REINDEX がパーティション単位で回るので、1 回の作業が軽く、並列化されます。

解決しないものもはっきりさせておきます。パーティショニングは性能の万能薬ではありません。パーティションキーの条件がないクエリはすべてのパーティションをスキャンするのでむしろオーバーヘッドが付き、1 行の検索はインデックスがすでに速いので、分けたからといって速くなりません。目的は「巨大テーブルのライフサイクル管理」であって「遅いクエリのチューニング」ではありません。

宣言的パーティショニング: 月別 RANGE #

月別 RANGE パーティショニング
CREATE TABLE events (
    id         bigint GENERATED ALWAYS AS IDENTITY,
    user_id    bigint NOT NULL,
    payload    jsonb NOT NULL DEFAULT '{}',
    created_at timestamptz NOT NULL DEFAULT now(),
    PRIMARY KEY (id, created_at)          -- パーティションキーが PK に含まれる必要があります
) PARTITION BY RANGE (created_at);

CREATE TABLE events_2026_08 PARTITION OF events
    FOR VALUES FROM ('2026-08-01') TO ('2026-09-01');
CREATE TABLE events_2026_09 PARTITION OF events
    FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');

アプリケーションは普通に events に入れて検索します。振り分けは PostgreSQL がやります。文法で引っかかるのが PK です。PK・UNIQUE 制約にはパーティションキーが含まれていなければなりません(各パーティションが独立したテーブルなので、グローバルな一意性を検査できないためです)。それで (id, created_at) の複合 PK になるのですが、id 自体のグローバルな一意性は IDENTITY のシーケンスが事実上保証するものの、DB の制約のレベルでは緩くなるという点、これがパーティショニングの代表的なトレードオフです。RANGE のほかに LIST(地域・テナント別)と HASH(均等分散)もありますが、実務の 8 割は時間の RANGE です。

プルーニングが効く条件 #

プルーニングはタダではなく、クエリの条件がパーティションキーを直接絞るときだけ働きます。

プルーニング条件の比較
-- プルーニングされます: created_at の条件が直接
SELECT * FROM events WHERE created_at >= '2026-08-01' AND user_id = 42;

-- プルーニングされません: キーの条件がない → 全パーティションをスキャン
SELECT * FROM events WHERE user_id = 42;

基礎第 4 回の「カラムを加工するとインデックスが効かない」と同じ系統で、date_trunc('month', created_at) = ... のような加工した条件もプルーニングを妨げます。確認の方法はいつもどおり EXPLAIN です。計画にパーティションがいくつ現れるかを見れば分かります。そしてこの特性のせいで、パーティショニングの導入はクエリパターンの点検とセットです。主要なクエリに時間の条件がないテーブルは、時間パーティショニングの候補ではありません。

運用: パーティションは作り続けなければなりません #

月別パーティショニングの宿題は「来月のパーティションを誰が作るのか」です。作っておかないと、来月最初の INSERT が失敗します(受け取るパーティションがないので。DEFAULT パーティションを置けばエラーは避けられますが、そこに積もったデータを後で移すコストのほうが大きいです)。手作業の運用は必ず忘れられるので、標準は pg_partman 拡張です。「未来の分をいくつか先に作り、何か月過ぎたものは切り離す」を宣言しておけば、定期的に勝手に回してくれます。マネージドサービスでもほとんど対応しています。

導入基準を整理するとこうです。時間とともに際限なく積もり、保存期限があり、主要なクエリに時間の条件があるテーブルがパーティショニングの居場所です。数千万行の段階で先回りしてやるより、この 3 条件が見えるテーブルに、数億行になる前に導入するのが実務的なタイミングです。

まとめ #

  • パーティショニングの最大の価値は削除です。DELETE + VACUUM の地獄が DROP TABLE 一発になります。
  • プルーニングはパーティションキーを直接絞る条件でだけ働きます。キー条件のないクエリはむしろ全パーティションをスキャンします。
  • PK にはパーティションキーが含まれる必要があります。グローバルな一意性の制約が緩むのが代表的なトレードオフです。
  • パーティションの作成は自動化が必須です。pg_partman で「先に作って、期限が過ぎたら切り離す」を宣言します。
  • 導入基準は際限ない増加 + 保存期限 + 時間条件のクエリの 3 点セットです。次回はレプリケーションと高可用性です。
X