Chuyển đến nội dung chính
友田 陽大
PostgreSQL internals & performance
PostgreSQL
アーキテクチャ設計
パフォーマンス

PostgreSQL 宣言的パーティショニング実践(RANGE/LIST/HASH・パーティションプルーニング・ローリングウィンドウ運用・v18対応)

PostgreSQL の宣言的パーティショニングを本番で正しく使うための実践ガイド。いつ分割すべきか(と、すべきでないか)、RANGE/LIST/HASH の構文、上限が排他という境界の罠、主キー制約のルール、パーティションプルーニングの確認方法、ATTACH/DETACH と時系列のローリングウィンドウ運用、partitionwise join、PostgreSQL 18 の改善までを公式ドキュメントに忠実な実コードで解説します。

Published
Reading time
12 min read
Author
友田 陽大
Chia sẻ

「テーブルが数億行になって遅い」「古いログの削除に何時間もかかる」——それはパーティショニングの出番かもしれません。1つの巨大テーブルを、ルールに従って物理的に複数の子テーブルに分割し、プランナーが必要な分割だけを読み古い分割をまるごと一瞬で捨てられるようにします。

ただし、パーティショニングは「とりあえず入れる」最適化ではありません。設計制約(特に主キー)と運用の手間が増えます。この記事は、「いつ・どう分割し、いつ分割すべきでないか」を、PostgreSQL 公式ドキュメントに忠実に、本番運用の手順とともに解説します。パフォーマンス総論 の §9 で挙げた「物理設計」レイヤーの深掘りです。

この記事のルール:パーティショニングの仕様・構文・設定の既定値・限界・PostgreSQL 18 の変更点は、すべて PostgreSQL 18 公式ドキュメント(2026年6月時点) に基づきます。マネージド環境では並列設定等が異なることがあるため、EXPLAIN で実際のプルーニングを確認してください。


1. いつ分割すべきか(と、すべきでないか)

公式が挙げるパーティショニングの利点

  • クエリ性能の劇的改善——よく使う行が少数のパーティションに集中するとき。索引上位ツリーの代替になり、よく使う部分がメモリに乗りやすくなる。
  • 大量更新/集計の高速化——1パーティションの大部分を読むなら、テーブル全体のランダムアクセスよりシーケンシャルスキャンが効く。
  • バルク投入・削除がメタ操作になる——DROP TABLEDETACH PARTITION は、一括 DELETE より桁違いに速く、しかも**DELETE が生む VACUUM 負荷を完全に回避**する。
  • コールドデータを安いストレージへ移せる

そしていつ効くかの目安(公式):

これらの利点が割に合うのは、通常テーブルが非常に大きい場合のみ。テーブルがパーティショニングの恩恵を受け始める正確な点はアプリ次第だが、目安はテーブルサイズがデータベースサーバーの物理メモリを超えること

重要な判断:数百万行程度なら、まず索引設計VACUUMを尽くす。パーティショニングは複雑さ(特に主キー制約)と運用コストを増やすので、「物理メモリを超える規模」「明確な時系列の破棄パターンがある」場合に絞って導入します。YAGNI——必要になってから。


2. 3つの方式:RANGE / LIST / HASH

公式の定義と構文。

方式分割の仕方典型用途
RANGEキーの値の範囲(重複なし)で分割時系列(日付・連番)
LISTキーの値を明示列挙して分割地域・テナント・カテゴリ
HASHモジュラスと剰余で分割自然な区分がないが均等に分散したい

RANGE:時系列の定番

-- 親テーブル(データは入らない。ルーティングするだけ)
CREATE TABLE measurement (
  city_id  int  NOT NULL,
  logdate  date NOT NULL,
  peaktemp int
) PARTITION BY RANGE (logdate);

-- 月次パーティション。下限は含む・上限は含まない('2026-07-01' は7月側)
CREATE TABLE measurement_y2026m06 PARTITION OF measurement
  FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');

-- どの範囲にも入らない行の受け皿(任意・hash では不可)
CREATE TABLE measurement_default PARTITION OF measurement DEFAULT;

境界の罠(公式が明記):「下限 FROM含む、上限 TO含まない」。例:範囲が 1101020 のとき、値 102つ目に入ります。隣接パーティションで上限=次の下限にすれば、隙間も重複もありません。上下限なしは MINVALUE/MAXVALUE

LIST:明示列挙

CREATE TABLE customers (id bigint, region text, name text)
  PARTITION BY LIST (region);

CREATE TABLE customers_apac PARTITION OF customers FOR VALUES IN ('JP','KR','SG');
CREATE TABLE customers_emea PARTITION OF customers FOR VALUES IN ('DE','FR','UK');
CREATE TABLE customers_other PARTITION OF customers DEFAULT;

HASH:均等分散

CREATE TABLE events (id bigint, payload jsonb) PARTITION BY HASH (id);
-- 4分割(モジュラス4・剰余0〜3)
CREATE TABLE events_p0 PARTITION OF events FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE events_p1 PARTITION OF events FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE events_p2 PARTITION OF events FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE events_p3 PARTITION OF events FOR VALUES WITH (MODULUS 4, REMAINDER 3);

3. 最大の設計制約:主キーはパーティションキーを含む

これがパーティショニング導入で最も人を驚かせる点です。公式の規則:

パーティション化テーブルにユニークまたは主キー制約を作るには、パーティションキーに式や関数呼び出しを含めず、制約の列にすべてのパーティションキー列を含めなければならない

理由は明快——各パーティションの索引は自分の中の一意性しか保証できないので、「異なるパーティション間で重複しない」ことはパーティション構造自体で担保するしかないからです。

-- ❌ これは失敗する:主キー (id) がパーティションキー logdate を含まない
-- CREATE TABLE m (id bigint PRIMARY KEY, logdate date) PARTITION BY RANGE (logdate);

-- ✅ パーティションキーを主キーに含める(複合主キー)
CREATE TABLE m (
  id      bigint GENERATED ALWAYS AS IDENTITY,
  logdate date NOT NULL,
  PRIMARY KEY (id, logdate)        -- パーティションキー logdate を含む
) PARTITION BY RANGE (logdate);

設計への影響id 単独でグローバル一意にしたい場合、パーティショニングと相性が悪い(外部キーの参照先としても (id, logdate) 複合になる)。これは「分割するか否か」を決める最重要のトレードオフです。uuidv7()(PG18)のような時刻順 UUID を使い、論理的な一意性は UUID で担保しつつ主キーに区分列を含める、といった設計で折り合いをつけます。

その他の限界(公式 §5.12.2.3):BEFORE ROW トリガーで行き先パーティションは変えられない/一時表と永続表をパーティションツリーに混在不可/通常テーブルとパーティション化テーブルを後から相互変換できない(最初から設計する)/パーティションキーを更新すると行が別パーティションへ移動する。


4. パーティションプルーニング:効いているか必ず確認

パーティショニングの性能の源泉がプルーニング——「クエリに関係ないパーティションを読み飛ばす」最適化です(公式定義どおり)。enable_partition_pruning既定 ON

プルーニングには2種類あります。

  • プラン時プルーニングWHERE logdate >= '2026-06-01' のように計画段階で値が分かる場合。
  • 実行時プルーニングPREPARE のパラメータやサブクエリ結果、ネステッドループ内側のパラメータなど、計画時には値が不明でも実行時に絞る。
-- プルーニングが効いていれば、関係するパーティションしか計画に出ない
EXPLAIN (ANALYZE)
SELECT count(*) FROM measurement WHERE logdate >= DATE '2026-06-01';
-- → measurement_y2026m06 など該当パーティションだけが現れ、他は読まれない

プラン時にプルーニングされたパーティションは EXPLAIN/EXPLAIN ANALYZE現れません。逆に「全パーティションが計画に出る」なら、WHERE がパーティションキーに触れていない=プルーニングが効いていないサインです。必ずパーティションキーで絞るクエリ設計にします。

(補足:旧来の継承ベース分割は constraint_exclusion を使いプラン時のみ。宣言的パーティショニングのプルーニングはプラン時+実行時の両方で効く、より強力な仕組みです。)


5. ローリングウィンドウ運用:時系列の本命

RANGE パーティショニングの真価は運用にあります。「直近Nヶ月だけ保持し、古い月は捨てる」というローリングウィンドウは、DELETE ではなくパーティションの付け外しで行います。

-- 古い月をまるごと削除(数百万行でも一瞬。DELETE のような VACUUM 負荷なし)
DROP TABLE measurement_y2025m06;

-- 捨てずにアーカイブしたいなら、独立テーブルとして切り離す
ALTER TABLE measurement DETACH PARTITION measurement_y2025m06;
-- DETACH CONCURRENTLY は親への重いロックを避ける(トランザクション外・DEFAULTパーティションがあると不可)

新しい月のパーティションを安全に追加する

毎月、来月分のパーティションを事前に用意します。空のテーブルを CREATE TABLE ... PARTITION OF で作るのが基本(空なのでスキャン不要)。既存テーブルを ATTACH する場合は、事前に一致する CHECK 制約を付けておくと、公式の言うとおり取り込み時の全行スキャン(と長いロック)を回避できます。

-- 既存テーブルを取り込む場合:先に CHECK を付けると ATTACH 時の全行スキャンを省ける
ALTER TABLE measurement_new
  ADD CONSTRAINT ck CHECK (logdate >= '2026-07-01' AND logdate < '2026-08-01');
ALTER TABLE measurement ATTACH PARTITION measurement_new
  FOR VALUES FROM ('2026-07-01') TO ('2026-08-01');
ALTER TABLE measurement_new DROP CONSTRAINT ck;  -- 取り込み後は冗長なので外してよい

運用は冪等な関数にして、cron(pg_cron 等)で月次実行するのが堅牢です。

-- 指定月のパーティションを「無ければ作る」冪等関数。二重実行しても安全
CREATE OR REPLACE FUNCTION ensure_measurement_partition(target date)
RETURNS void LANGUAGE plpgsql AS $$
DECLARE
  start_d date := date_trunc('month', target)::date;
  end_d   date := (date_trunc('month', target) + interval '1 month')::date;
  part    text := format('measurement_y%sm%s', to_char(start_d,'YYYY'), to_char(start_d,'MM'));
BEGIN
  IF to_regclass(part) IS NULL THEN          -- 既に在れば何もしない=冪等
    EXECUTE format(
      'CREATE TABLE %I PARTITION OF measurement FOR VALUES FROM (%L) TO (%L)',
      part, start_d, end_d);
  END IF;
END;
$$;

-- 毎月、今月と来月分を確保(実行漏れに強い)
SELECT ensure_measurement_partition(current_date);
SELECT ensure_measurement_partition((current_date + interval '1 month')::date);

可観測性のために:来月分の作成が漏れると、DEFAULT パーティションに落ちるか(用意していれば)、無ければ INSERT がエラーになります。「次の2ヶ月分が存在するか」を監視し、欠けていればアラートする運用にしておくと事故を防げます。インデックスは親に作れば全パーティションへ自動波及し、後から作るパーティションにも引き継がれます。


6. partitionwise join / aggregate:既定OFFの理由

パーティション同士の結合・集約を「対応するパーティションごと」に行う最適化が partitionwise join / aggregate両方とも既定 OFFです。

理由は公式が明記するメモリコスト:これらを有効にすると「work_mem で制限されるノード数がパーティション数に比例して線形に増え得る。実行時の総メモリ消費が大きく増える可能性があり、計画自体も CPU・メモリ的に大幅に高コストになる」。

-- パーティション数が少なく、パーティション同士の結合/集約が重いワークロードでのみ検討
SET enable_partitionwise_join = on;
SET enable_partitionwise_aggregate = on;

「速くなるなら常時ON」ではありません。パーティション数が多いと逆効果になり得るので、対象ワークロードで EXPLAIN ANALYZE してから、必要ならセッション/特定クエリ単位で有効化します。


7. PostgreSQL 18 の改善

PG18 はパーティショニングの運用と計画を実務的に改善しました(公式リリースノート)。

  • 多数パーティションのクエリ計画を効率化——パーティションが多い設計のプランニングが速くなった。
  • partitionwise join を適用できるケースを拡大し、メモリ使用も削減——§6 のトレードオフが緩和。
  • パーティションクエリのコスト見積もりを改善
  • VACUUM/ANALYZE が子を処理せず親のパーティション化テーブルだけを処理可能に
  • パーティション化テーブルに NOT VALID 外部キー制約を許可(段階的な制約追加が可能に)。
  • 非btree のユニークインデックスをパーティションキーに利用可能に。
  • unlogged なパーティション化テーブルは不可に(一貫性のため)。

アップグレード観点では、パーティション数を多く取る設計(多テナント等)ほど PG18 の計画効率化の恩恵を受けます。


8. 導入判断チェックリスト

  1. 規模は十分か? テーブルが物理メモリを超える/明確な破棄パターン(時系列)があるか。なければ索引・VACUUM が先。
  2. パーティションキーは決まるか? ほとんどのクエリの WHERE がそのキーで絞れるか(プルーニングの前提)。
  3. 主キー制約を許容できるか? 主キー/ユニークにパーティションキー列を含める設計で問題ないか。
  4. 方式は適切か? 時系列=RANGE、明示区分=LIST、均等分散=HASH。
  5. 運用を自動化したか? 来月分の事前作成・古い月の DETACH/DROP を冪等関数+スケジューラで。
  6. プルーニングを確認したか? 代表クエリを EXPLAIN し、必要なパーティションだけが読まれるか。

9. まとめ

  • パーティショニングは**「物理メモリを超える規模」+「明確な破棄パターン」**で初めて割に合う。小さい表ではまず索引・VACUUM。
  • RANGE/LIST/HASH。RANGE の境界は下限含む・上限含まない。古いデータは DROP/DETACH PARTITION で一瞬(VACUUM 負荷もなし)。
  • 主キーはパーティションキーを含める——これが最大の設計制約。導入前に必ず確認。
  • **プルーニング(既定ON)**が性能の源泉。WHERE をパーティションキーで絞り、EXPLAIN で効果を確認。
  • ローリングウィンドウは冪等関数+スケジューラで自動化。ATTACH は事前 CHECK で全行スキャン回避。
  • partitionwise join/agg は既定OFF(メモリコスト)。PG18 で多数パーティションの計画・partitionwise join・コスト見積もりが改善。

これで PostgreSQL 本体の性能設計シリーズは一巡です。パフォーマンス総論 を起点に、インデックスEXPLAINMVCC/VACUUMJSONB と合わせて、専用DBを増やさずとも本番の要求に応える PostgreSQL を設計してください。


参考(PostgreSQL 18 公式ドキュメント)

友田

友田 陽大

Người phát triển sản phẩm đoạt Giải thưởng Bộ trưởng (METI). Với TypeScript + Python + AWS, tôi một mình triển khai trọn gói SaaS, chuyển đổi số công nghiệp và AI tạo sinh (RAG) sẵn sàng cho production — từ phân tích yêu cầu đến hạ tầng và vận hành.

Got a challenge?

From design to implementation and operations — solo × generative AI

Implementation like this article's, end to end from requirements to production. Start with a free 30-minute technical consult and tell me about your situation.

Nhận cả dự án trọn gói lẫn vai trò cố vấn kỹ thuật. Hãy bắt đầu bằng buổi tư vấn miễn phí 30 phút.

最短ルート:カレンダーから直接予約

相談内容が固まっている方は、フォーム送信よりその場で日程を確定する方がスムーズです。下記から空き時間をお選びください。

  • 30分のオンライン無料相談
  • Google Meet / Zoom / Microsoft Teams
  • NDA 商談前締結可・無理な営業はいたしません
無料相談の空き枠を予約する

Cũng đáng đọc