PostgreSQL 実践講座 #7 パーティショニング — 巨大テーブルを時間の単位で分ける
イベント、ログ、注文のように時間とともに際限なく積もるテーブルは、いつか数億行になります。インデックス(実践第 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 #
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 点セットです。次回はレプリケーションと高可用性です。