- パーティショニングが必要なタイミング
- この記事の範囲と基準バージョン
- パーティションキーを選ぶ順序
- Range パーティショニング:時系列データ
- List パーティショニング:カテゴリ別分割
- Hash パーティショニング:均等分配
- マルチレベルパーティショニング
- パーティションの自動管理
- パフォーマンス比較:パーティショニング前 vs 後
- 注意事項と制約
- モニタリング
- プルーニングが実際に効く条件
- 戦略ごとの失敗パターンと診断の順序
- パーティショニングしないほうがよい場合
- 参考資料
- クイズ
パーティショニングが必要なタイミング
テーブルが大きくなるとパフォーマンスの問題が発生します:
- インデックスサイズの増加による INSERT パフォーマンスの低下
- フルテーブルスキャンコストの増加
- VACUUM 作業時間の増加
- データ保管/削除コストの増加
一般的に、テーブルサイズが数十 GB 以上の場合や、時系列データで一定期間後に削除が必要な場合にパーティショニングを検討します。
この記事の範囲と基準バージョン
この記事が扱うのは どのパーティショニング戦略を選ぶか、パーティションキーを何にするか です。パーティションをアタッチ・デタッチする DDL が実際にどんなロックを取るのか、無停止でインデックスを作るにはどの順序で進めるのかといった運用のメカニクスは PostgreSQL Partitioning 実践ガイド で別途扱います。ここに出てくる構文例は「この戦略がどんな形か」を示すためのもので、紙面は判断の根拠のほうに使います。
基準エンジンは PostgreSQL 18 です。この記事で名前と既定値を挙げる設定はすべて PostgreSQL 18 のドキュメントで確認した値であり、URL は最後の参考資料の節にまとめてあります。パーティショニングはメジャーバージョンごとにプランナの挙動が変わってきた領域なので、別のバージョンを使っているなら既定値をそのバージョンのドキュメントで確認し直してください。
戦略選択にこれだけの紙面を使う理由は単純です。パーティショニングで最も戻しにくい決定がパーティションキーだからです。インデックスは間違えれば消せば済みます。パーティション 1 つはデタッチすれば済みます。ところがパーティションキーを間違えるとテーブル全体を作り直すことになります。しかも間違えたという事実は、たいていデータが十分に溜まったあと、つまり作り直しが最も高くつく時点で明らかになります。
パーティションキーを選ぶ順序
戦略(Range・List・Hash)を先に選んでキーをあとから選ぶ順序は、ほぼ必ず間違いです。順序は逆です。
ステップ 1: クエリが何でフィルタしているかを数える
パーティショニングの利点はほぼ全部プルーニングから来ます。そしてプルーニングは WHERE 句にパーティションキーがあるときだけ効きます。したがってパーティションキーの候補は「自分たちのクエリの WHERE 句に最も頻繁に、最も選択的に現れる列」です。勘で決めず、数えてください。
-- pg_stat_statements が有効なら、実際のクエリテキストから数えるのが最も正確。
-- 列名はバージョンによって異なるので、使用中のバージョンのドキュメントを確認してください。
SELECT calls, total_exec_time, left(query, 120) AS query
FROM pg_stat_statements
WHERE query ILIKE '%from events%'
ORDER BY calls DESC
LIMIT 20;
ここで見るべきは 2 つです。どの列が WHERE によく出るか、そしてその条件が実際にデータをどれだけ減らすか。よく出てくるが半分を残すような条件は、パーティションキーとしては役に立ちません。
ステップ 2: 保持ポリシーがあるかを見る
「90 日を過ぎたら消す」があるなら、時間ベースの Range が事実上の正解です。パーティショニングの 2 つ目の利点である DROP TABLE は、時間で切れているときにしか使えないからです。保持ポリシーが時間ではない別の軸(解約した顧客のデータ削除など)なら、その軸がパーティションキーの候補になります。
ステップ 3: 候補列の分布を確認する
SELECT region,
count(*) AS rows,
round(100.0 * count(*) / sum(count(*)) OVER (), 1) AS pct
FROM orders
GROUP BY region
ORDER BY rows DESC;
出力例
region | rows | pct
--------+-----------+-------
KR | 412000000 | 82.4
US | 51000000 | 10.2
JP | 28000000 | 5.6
DE | 9000000 | 1.8
この一度の照会が List パーティショニングの運命を決めます。82% が 1 つの値に偏っていれば orders_kr パーティションは元のテーブルとほぼ同じ大きさで、パーティショニングで得たものは実質ありません。このときよく出る誤答が「では Hash にしよう」です。Hash はサイズを均等にしてくれますが、その代償として範囲クエリのプルーニングを完全に手放すことになります。サイズが揃うことと、クエリが速くなることは別の問題です。
ステップ 4: ここで戦略を選ぶ
前の 3 つが決まれば、戦略はほぼ自動的に決まります。
| 状況 | 戦略 | 理由 |
|---|---|---|
| 時間でフィルタし古いデータを消す | Range(時間) | プルーニングと DROP TABLE の両方が得られる |
| 値が有限で分布が均等、その値でフィルタする | List | 値 1 つがパーティション 1 つにきれいに対応する |
| 値の分布が激しく偏っている | List に加え大きい値だけ下位分割 | 偏りは Hash では解決しない |
| 特定キーの等値検索だけで範囲検索がない | Hash | 均等分配が必要で範囲プルーニングは使わないため |
| WHERE に安定して現れる列がない | パーティショニングしない | プルーニングが効かなければ得るものがない |
実務では最後の行が最も頻繁に出る答えです。
ステップ 5: 一意性の要件と衝突しないかを確認する
この確認を最後に回すと手遅れです。ドキュメントの表現どおり、パーティションテーブルに一意制約や主キーを作るには、パーティションキーに式や関数呼び出しが含まれていてはならず、制約の列がパーティションキーの列をすべて含んでいる必要があります。理由もドキュメントが説明しています。制約を構成する個々のインデックスは自分のパーティションの中でしか一意性を強制できないため、異なるパーティションに重複がないことはパーティション構造そのものが保証しなければならないからです。
これは構文上の制約ではなくドメイン上の制約です。email が全体で一意でなければならないユーザーテーブルは、created_at でパーティショニングできません。無理にやるなら一意性の保証をアプリケーションや別テーブルに移すことになりますが、その費用はパーティショニングで得る利点を上回ることがほとんどです。ですからこの確認はステップ 4 のあとではなく、ステップ 1 の横で同時に行ってください。
Range パーティショニング:時系列データ
最もよく使用される戦略で、日付や ID の範囲でデータを分割します。
月別パーティションの作成
-- 親テーブルの作成
CREATE TABLE events (
id BIGSERIAL,
event_type VARCHAR(50) NOT NULL,
payload JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);
-- 月別パーティションの作成
CREATE TABLE events_2026_01 PARTITION OF events
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE events_2026_02 PARTITION OF events
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
CREATE TABLE events_2026_03 PARTITION OF events
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
-- デフォルトパーティション(範囲に合うパーティションがない場合)
CREATE TABLE events_default PARTITION OF events DEFAULT;
パーティション別インデックス
-- 各パーティションに自動作成されるグローバルインデックス
CREATE INDEX idx_events_type ON events (event_type);
CREATE INDEX idx_events_payload ON events USING GIN (payload);
-- パーティション別ローカルインデックス
CREATE INDEX idx_events_2026_03_type
ON events_2026_03 (event_type, created_at DESC);
パーティションプルーニングの確認
-- パーティションプルーニングが動作するか EXPLAIN で確認
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM events
WHERE created_at >= '2026-03-01'
AND created_at < '2026-03-15'
AND event_type = 'purchase';
-- 結果: events_2026_03 パーティションのみスキャン
-- Append
-- -> Index Scan using events_2026_03_type on events_2026_03
-- Index Cond: (event_type = 'purchase')
-- Filter: (created_at >= '2026-03-01' AND created_at < '2026-03-15')
List パーティショニング:カテゴリ別分割
特定の値リストでデータを分割します:
-- 地域別パーティショニング
CREATE TABLE orders (
id BIGSERIAL,
customer_id BIGINT NOT NULL,
amount DECIMAL(12,2) NOT NULL,
region VARCHAR(10) NOT NULL,
status VARCHAR(20) NOT NULL,
ordered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (id, region)
) PARTITION BY LIST (region);
CREATE TABLE orders_kr PARTITION OF orders
FOR VALUES IN ('KR');
CREATE TABLE orders_jp PARTITION OF orders
FOR VALUES IN ('JP');
CREATE TABLE orders_us PARTITION OF orders
FOR VALUES IN ('US');
CREATE TABLE orders_eu PARTITION OF orders
FOR VALUES IN ('DE', 'FR', 'GB', 'IT', 'ES');
CREATE TABLE orders_other PARTITION OF orders DEFAULT;
Hash パーティショニング:均等分配
特定カラムのハッシュ値でデータを均等に分配します:
-- ユーザー ID ベースのハッシュパーティショニング(4パーティション)
CREATE TABLE user_activities (
id BIGSERIAL,
user_id BIGINT NOT NULL,
activity VARCHAR(100) NOT NULL,
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (id, user_id)
) PARTITION BY HASH (user_id);
CREATE TABLE user_activities_0 PARTITION OF user_activities
FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE user_activities_1 PARTITION OF user_activities
FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE user_activities_2 PARTITION OF user_activities
FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE user_activities_3 PARTITION OF user_activities
FOR VALUES WITH (MODULUS 4, REMAINDER 3);
マルチレベルパーティショニング
Range と List を組み合わせたマルチレベルパーティショニング:
-- 第1レベル: 日付(Range)、第2レベル: 地域(List)
CREATE TABLE sales (
id BIGSERIAL,
product_id BIGINT NOT NULL,
region VARCHAR(10) NOT NULL,
amount DECIMAL(12,2) NOT NULL,
sold_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (id, sold_at, region)
) PARTITION BY RANGE (sold_at);
-- 月別サブパーティション
CREATE TABLE sales_2026_03 PARTITION OF sales
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01')
PARTITION BY LIST (region);
CREATE TABLE sales_2026_03_kr PARTITION OF sales_2026_03
FOR VALUES IN ('KR');
CREATE TABLE sales_2026_03_jp PARTITION OF sales_2026_03
FOR VALUES IN ('JP');
CREATE TABLE sales_2026_03_other PARTITION OF sales_2026_03 DEFAULT;
パーティションの自動管理
pg_partman 拡張の使用
-- pg_partman のインストール
CREATE EXTENSION pg_partman;
-- 自動パーティション管理の設定
SELECT partman.create_parent(
p_parent_table := 'public.events',
p_control := 'created_at',
p_type := 'native',
p_interval := '1 month',
p_premake := 3, -- 3ヶ月先まで事前作成
p_start_partition := '2026-01-01'
);
-- 自動メンテナンス(cron で実行)
-- 新しいパーティション作成 + 古いパーティション管理
SELECT partman.run_maintenance();
シェルスクリプトによる自動作成
#!/bin/bash
# create_monthly_partitions.sh
PGHOST="localhost"
PGDB="mydb"
PGUSER="admin"
# 今後3ヶ月分のパーティションを作成
for i in 0 1 2 3; do
MONTH=$(date -d "+${i} months" +%Y-%m-01)
NEXT_MONTH=$(date -d "+$((i+1)) months" +%Y-%m-01)
TABLE_NAME="events_$(date -d "+${i} months" +%Y_%m)"
psql -h $PGHOST -d $PGDB -U $PGUSER -c "
CREATE TABLE IF NOT EXISTS ${TABLE_NAME}
PARTITION OF events
FOR VALUES FROM ('${MONTH}') TO ('${NEXT_MONTH}');
" 2>/dev/null
echo "Created partition: ${TABLE_NAME}"
done
古いパーティションの削除/アーカイブ
-- パーティションの分離(データ保存、クエリから除外)
ALTER TABLE events DETACH PARTITION events_2025_01;
-- 分離したパーティションを圧縮テーブルスペースに移動
ALTER TABLE events_2025_01 SET TABLESPACE archive_tablespace;
-- または完全削除(DROP は DELETE よりはるかに高速!)
DROP TABLE events_2025_01;
-- vs.
-- DELETE FROM events WHERE created_at < '2025-02-01';
-- ↑ この方法は数百万行の削除に数十分かかる
パフォーマンス比較:パーティショニング前 vs 後
テスト環境
-- 1億行のテーブル作成(パーティショニングなし)
CREATE TABLE events_no_part (
id BIGSERIAL PRIMARY KEY,
event_type VARCHAR(50),
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 同一データでパーティショニングテーブルを作成(月別)
-- ...(上記の events テーブルを使用)
クエリパフォーマンスの比較
-- 1ヶ月分のデータ照会
-- パーティショニングなし: 15.2秒(Full Table Scan)
-- パーティショニングあり: 0.8秒(Partition Pruning → 単一パーティションスキャン)
-- インデックスサイズ
-- パーティショニングなし: 2.1 GB(単一インデックス)
-- パーティショニングあり: 175 MB/パーティション × 12 = 2.1 GB(合計は同じだが個別インデックスが効率的)
-- データ削除(1ヶ月分)
-- パーティショニングなし: DELETE → 45分 + VACUUM 30分
-- パーティショニングあり: DROP TABLE → 0.01秒
注意事項と制約
PRIMARY KEY の制約
パーティションキーは必ず PRIMARY KEY に含めなければなりません:
-- エラー!パーティションキー(created_at)が PK にない
CREATE TABLE events (
id BIGSERIAL PRIMARY KEY, -- ERROR
created_at TIMESTAMPTZ NOT NULL
) PARTITION BY RANGE (created_at);
-- 正しい方法:複合 PK
CREATE TABLE events (
id BIGSERIAL,
created_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);
UNIQUE 制約
-- UNIQUE 制約にもパーティションキーを含める必要あり
CREATE UNIQUE INDEX idx_events_unique
ON events (event_type, created_at); -- OK
-- パーティションキーのない UNIQUE は不可
-- CREATE UNIQUE INDEX ON events (event_type); -- ERROR
クロスパーティション JOIN のパフォーマンス
-- パーティションキーでフィルタリングしないとすべてのパーティションをスキャン
-- 必ず WHERE 句にパーティションキーを含めること!
SELECT * FROM events
WHERE created_at >= '2026-03-01' -- パーティションプルーニングが動作
AND event_type = 'purchase';
-- enable_partition_pruning 設定の確認
SHOW enable_partition_pruning; -- 'on' でなければならない
モニタリング
-- パーティション別サイズの確認
SELECT
schemaname,
tablename,
pg_size_pretty(pg_total_relation_size(schemaname || '.' || tablename)) as total_size,
pg_size_pretty(pg_relation_size(schemaname || '.' || tablename)) as table_size
FROM pg_tables
WHERE tablename LIKE 'events_%'
ORDER BY pg_total_relation_size(schemaname || '.' || tablename) DESC;
-- パーティション別行数の確認
SELECT
relname as partition_name,
n_live_tup as row_count
FROM pg_stat_user_tables
WHERE relname LIKE 'events_%'
ORDER BY relname;
-- パーティションプルーニング効果の確認
EXPLAIN (ANALYZE, COSTS, BUFFERS, FORMAT TEXT)
SELECT count(*) FROM events
WHERE created_at >= '2026-03-01' AND created_at < '2026-04-01';
プルーニングが実際に効く条件
戦略をどれだけうまく選んでも、プルーニングが効かなければパーティショニングは純粋な損です。ですから「効いたかどうかをどう知るか」と「効かないとき何を見るか」が、戦略選択の最後の検証になります。
プルーニングは 2 つの時点で起こります。ドキュメントは両者を区別して説明しています。
計画時プルーニング は、プランナがパーティション定義を見て「このパーティションは見る必要がない」と証明することです。刈り取られたパーティションは EXPLAIN の出力にそもそも現れません。
実行時プルーニング は、実際の実行中にしか値が分からないパラメータに対して起こります。サブクエリから出た値や、パラメータ化されたネステッドループ結合から渡ってくる実行時パラメータがその例です。ドキュメントは、この段階でパーティションが刈り取られたかを判断するには EXPLAIN ANALYZE 出力の loops の値を注意深く見る必要がある、と書いています。
2 つの時点を区別すべき理由は、診断が変わるからです。計画時に効かないならクエリを直す必要があり、実行時プルーニングに依存する計画は素の EXPLAIN だけでは判断できません。
出力例 — プルーニングが効いた場合
Aggregate (actual time=88.412..88.413 rows=1 loops=1)
-> Seq Scan on events_2026_03 events (actual time=0.019..61.203 rows=1204411 loops=1)
Filter: ((created_at >= '2026-03-01') AND (created_at < '2026-04-01'))
Planning Time: 0.412 ms
Execution Time: 88.501 ms
出力例 — プルーニングが効かなかった場合
Aggregate (actual time=2140.882..2140.883 rows=1 loops=1)
-> Append (actual time=0.021..2004.114 rows=1204411 loops=1)
-> Seq Scan on events_2026_01 events_1 (actual rows=0 loops=1)
-> Seq Scan on events_2026_02 events_2 (actual rows=0 loops=1)
-> Seq Scan on events_2026_03 events_3 (actual rows=1204411 loops=1)
-> Seq Scan on events_default events_4 (actual rows=0 loops=1)
Planning Time: 1.882 ms
Execution Time: 2140.994 ms
読み方は単純です。Append の下にパーティションがずらりと並び、その大半が actual rows=0 ならプルーニングは効いていません。スキャンしたのに何も出なかったパーティションたちがその証拠です。逆に刈り取られたパーティションはこの一覧にそもそも登場しません。ですから「見えないから刈られたのだろう」ではなく「残っているものの中に rows=0 があるか」で判断します。プリペアドステートメントでは、いくつ除去されたかが Subplans Removed として表示されます。
プルーニングが効かない原因は実質 4 つだけです。この順に確認してください。
- WHERE 句にパーティションキーがそもそもない。最も多く、最も呆気ない原因です。
- パーティションキーに関数やキャストをかぶせた。
WHERE date_trunc('month', created_at) = '2026-03-01'はプルーニングを使えません。created_at >= '2026-03-01' AND created_at < '2026-04-01'と書き直せば効きます。タイムゾーンのキャストも同じ罠です。 enable_partition_pruningがオフになっている。ドキュメント上の既定値はonなので、オフなら誰かが明示的に切ったということです。- プリペアドステートメントが汎用計画(generic plan)を使っている。計画時にパラメータ値が分からなければ計画時プルーニングは効きません。ドキュメント上
plan_cache_modeの既定値はautoで、これをforce_custom_planに変えて計画が変わるなら原因はこれです。
パーティション間の結合と集約についても、もう 1 つ知っておく必要があります。ドキュメント上、enable_partitionwise_join と enable_partitionwise_aggregate の既定値はどちらも off です。有効にするとパーティション同士を対応させて結合・集約でき、大きな利益になることもありますが、ドキュメントは代償も併記しています。最終的な計画の中で work_mem によってメモリ使用が制限されるノードの数が、スキャンするパーティション数に比例して増えうること、その結果クエリ実行中の全体のメモリ消費が大きく増加しうること、そして計画の立案自体もメモリと CPU の面でかなり高価になること、です。パーティションが数百あるテーブルで軽い気持ちで有効にすると OOM になって返ってきます。有効にする前にパーティション数を数えてください。
戦略ごとの失敗パターンと診断の順序
症状: Range で切ったのに特定の月だけ圧倒的に遅い
確認は 3 つ、この順です。
SELECT relname, n_live_tup, last_analyze, last_autoanalyze
FROM pg_stat_user_tables
WHERE relname LIKE 'events_%'
ORDER BY relname;
- パーティションごとのサイズを比べます。プロモーションや障害で特定の月だけトラフィックが数倍だったなら、パーティションのサイズもその分違います。時間ベース Range の前提である「期間が同じならサイズも近い」が崩れた状態です。
- そのパーティションの
last_analyzeとlast_autoanalyzeを見ます。作りたてのパーティションに大量投入すると、統計情報が追いつく前にクエリが先に到着します。 - 親のインデックスがそのパーティションにもきちんと付いたかを確認します。パーティションごとに個別にインデックスを作ってアタッチする運用なら、1 つだけ漏れることは実際に起きます。
症状: List で切ったら 1 つのパーティションが全体の 80% を占める
ステップ 3 の分布確認を飛ばした結果です。Hash に変えればサイズは均等になりますが範囲プルーニングを失います。実務的により良い答えは、大きい値だけをさらに分割することです。この記事のマルチレベルパーティショニングの節のやり方で、支配的な値のパーティションだけを下位分割すれば(KR をさらに月別 Range にするなど)、単に均等なサイズではなく実際のクエリパターンに合った分割になります。小さい値はわざわざ分ける必要がないのでそのまま残します。
症状: Hash で切ったら日付の範囲検索が全部フルスキャンになる
設計どおりの動作です。Hash パーティショニングはキーのハッシュ値で分けるため、範囲条件ではどのパーティションも除外できません。ハッシュは順序を保存しないからです。Hash は「このキーで等値検索しかしない」が真であるときだけ正しい戦略で、その判断はステップ 4 で終わっていなければなりません。作ってから気づいた場合、テーブルを作り直す以外の方法はありません。
もう 1 つ、Hash パーティションはあとから個数を変えるのが難しいです。MODULUS を変えるとどのキーがどのパーティションに属するかが全部変わるので、事実上の全面再配置です。最初の個数を決めるときに、今後何倍まで大きくなるかを一緒に考える必要があります。
症状: パーティションを増やしたら計画時間が実行時間を超えた
パーティションが多すぎます。ドキュメントは、プランナはおおむね数千個規模のパーティション階層まではうまく扱えると書いていますが、条件を付けています。一般的なクエリが少数を残して残りを刈り取れる場合に限る、という条件です。つまり問題はパーティション数そのものではなく、プルーニング後に残る数です。
診断は EXPLAIN 出力の Planning Time と Execution Time を比べるだけで足ります。計画時間が実行時間に迫るか超えるなら、パーティションの粒度が細かすぎる合図です。日別を月別に変えるだけでパーティション数はおよそ 30 分の 1 になります。
ドキュメントはワークロードによって答えが違うことも明示しています。データウェアハウス的なワークロードでは OLTP より多くのパーティションが妥当な場合がある、というものです。そして「パーティションは多いほうが常に良い、あるいは常に悪い、と決めつけてはいけない」と付け加えています。
症状: 新しいパーティションを付けるたびに数十分かかる
これは戦略ではなく運用 DDL の問題です。DEFAULT パーティションのスキャンや CHECK 制約の欠落が原因で、ロックレベルまで含めた診断と解決は 実践ガイド のほうにまとめてあります。
パーティショニングしないほうがよい場合
上の判断手順をすべて踏むと、かなりの場合に答えが「しない」になります。それが正常です。
クエリがパーティションキーでフィルタしないならやめてください。 プルーニングが効かないパーティショニングは、1 つの大きなテーブルスキャンを N 個の小さなテーブルスキャンに変えたうえで計画コストを上乗せするだけです。この記事の性能比較の節に出てくる劇的な数字は、すべてプルーニングが効くという前提の上にあります。
テーブルがまだ小さいならやめてください。 パーティショニングは戻しにくいスキーマ変更です。「いつか大きくなるから先に」はたいてい損です。そのときに行う移行の費用より、それまでに払う運用コストと制約のほうが大きい場合が多いからです。
全体で一意であるべき列がパーティションキーと無関係ならやめてください。 ステップ 5 の制約は回避できますが、回避の費用がパーティショニングの利点を上回ることがほとんどです。
保持ポリシーがなければ利点の半分は使えません。 データを決して消さないテーブルでは DROP TABLE を永遠に使わず、残るのはプルーニングだけです。そしてプルーニングだけが必要なら、たいていインデックスでも足ります。パーティショニングを検討する前にインデックスとクエリを先に見てください。
そして正直に言えば、この記事の性能比較の表は皆さんのデータでは再現しないかもしれません。 あの数字は特定のスキーマ、特定の分布、特定のクエリから出たものです。パーティショニングを決める前に、自分たちの実際のクエリを EXPLAIN ANALYZE で測ってください。それがこの記事全体で最も重要な一文です。
参考資料
- PostgreSQL 18 — Table Partitioning — 計画時と実行時のプルーニングの区別、Subplans Removed、一意制約がパーティションキーを含む必要がある理由、パーティション数の指針とワークロードによる差。2026-08-16 確認
- PostgreSQL 18 — Planner Method Configuration — enable_partition_pruning の既定値 on、enable_partitionwise_join と enable_partitionwise_aggregate の既定値 off とそのメモリコスト、plan_cache_mode の既定値 auto。2026-08-16 確認
- PostgreSQL 18 — CREATE TABLE — RANGE 境界の包含・非包含、MINVALUE と MAXVALUE、DEFAULT パーティションの定義と制約。2026-08-16 確認
- PostgreSQL 18 — ALTER TABLE — ATTACH・DETACH が取るロックと DEFAULT パーティションに関する注意。2026-08-16 確認
- pg_partman ドキュメント — create_parent のシグネチャと 5.0 以降の変更。2026-08-16 確認
この記事の自動管理の節の pg_partman の例は p_type := 'native' を使っていますが、pg_partman 5.0 のドキュメントを基準にすると p_type が受け取る値は range と list で、既定値は range です。5.x からはトリガーベースのパーティショニングが廃止され、すべてのパーティショニングが宣言的パーティショニングで処理されます。p_premake のドキュメント上の既定値は 4 です。インストール済み拡張のバージョンを先に確認し、そのバージョンのドキュメントに合わせて引数を書いてください。
確認クイズ(6問)
Q1. PostgreSQL でサポートされている3つのパーティショニング戦略は?
Range、List、Hash パーティショニング
Q2. パーティションプルーニング(Partition Pruning)とは?
クエリの WHERE 条件に基づいて不要なパーティションをスキャンせずにスキップする最適化技法です。
Q3. パーティションキーが PRIMARY KEY に含まれなければならない理由は?
PostgreSQL の宣言的パーティショニングでは各パーティションが独立したテーブルであるため、テーブル全体にわたるユニーク保証のためにパーティションキーが PK に含まれている必要があります。
Q4. 古いデータの削除に DELETE の代わりに DROP TABLE を使用する利点は?
DELETE は行単位で削除し VACUUM が必要ですが、DROP TABLE はパーティション全体を即座に削除するため、数十分 → 0.01秒に短縮されます。
Q5. DETACH PARTITION の用途は?
パーティションを親テーブルから分離し、クエリ対象から除外しつつデータは保存します。アーカイブやバックアップに有用です。
Q6. Hash パーティショニングはどのような場合に適していますか?
特定の範囲やカテゴリなくデータを均等に分配する必要がある場合に適しています。特にホットスポットを防止し、並列処理パフォーマンスを向上させるのに有用です。
クイズ
Q1: 「PostgreSQL パーティショニング完全ガイド: Range, List, Hash
戦略とパフォーマンス最適化」の主なトピックは何ですか?
PostgreSQL の宣言的パーティショニングを活用して大容量テーブルのパフォーマンスを劇的に改善する方法を実習します。Range、List、Hash パーティショニング戦略とパーティションプルーニング、自動管理まで解説します。
Q2: パーティショニングが必要なタイミングとは何ですか?
テーブルが大きくなるとパフォーマンスの問題が発生します: インデックスサイズの増加による INSERT
パフォーマンスの低下 フルテーブルスキャンコストの増加 VACUUM 作業時間の増加
データ保管/削除コストの増加 一般的に、テーブルサイズが数十 GB
以上の場合や、時系列データで一定期間後に削除が必要な場合にパーティショニングを検討します。
Q3: Range パーティショニング:時系列データの核心的な概念を説明してください。
最もよく使用される戦略で、日付や ID の範囲でデータを分割します。 月別パーティションの作成
パーティション別インデックス パーティションプルーニングの確認
Q4: Hash パーティショニング:均等分配の主な特徴は何ですか?
特定カラムのハッシュ値でデータを均等に分配します:
Q5: マルチレベルパーティショニングはどのように機能しますか?
Range と List を組み合わせたマルチレベルパーティショニング: