- Authors
- Name
- 1. はじめに
- 2. パーティショニング戦略:Range・List・Hash
- 3. パーティションプルーニングと制約排除
- 4. pg_partman による自動管理
- 5. 非パーティションテーブルからのゼロダウンタイム移行
- 6. パフォーマンスベンチマーク
- 7. 障害ケースと復旧手順
- 8. パーティションテーブルの VACUUM とメンテナンス
- 9. 本番チェックリスト
- 10. まとめ
- 参考文献

1. はじめに
PostgreSQL テーブルが数億行を超えると、慣れ親しんだ操作が静かに破綻し始めます。数秒で終わっていたシーケンシャルスキャンが数分かかるようになります。メンテナンスウィンドウ内に収まっていた VACUUM がピーク時間帯にまで延長します。インデックスの肥大化が実データ以上のストレージを消費します。これらは理論上の問題ではなく、本番環境で10億行テーブルを運用する日常的な現実です。
PostgreSQL の宣言的パーティショニングは、バージョン10で導入され、バージョン12から17にかけて大幅に改善されました。単一の論理テーブルをより小さな物理パーティションに分割することで、スキャン範囲を縮小し、メンテナンス操作を高速化し、数時間かかる DELETE 操作の代わりにミリ秒単位でデータ範囲全体を削除できるようになります。
本ガイドでは、3つのコアパーティショニング戦略(Range、List、Hash)を実践的な SQL コード例、本番ベンチマーク、pg_partman による自動管理、ゼロダウンタイム移行手順、障害復旧パターン、メンテナンスのベストプラクティスとともに解説します。すべての推奨事項は、本番 PostgreSQL デプロイメントにおける10億行以上のパーティションテーブル運用経験に基づいています。
2. パーティショニング戦略:Range・List・Hash
2.1 Range パーティショニング
Range パーティショニングは、連続した値の範囲に基づいてデータを分割します。時系列データ、イベントログ、トランザクションレコードなど、クエリが通常日付やタイムスタンプでフィルタリングするワークロードで最も広く使用される戦略です。
-- 10億行イベントテーブルの月別 Range パーティショニング
CREATE TABLE events (
id BIGSERIAL,
event_time TIMESTAMPTZ NOT NULL,
user_id BIGINT NOT NULL,
event_type TEXT NOT NULL,
payload JSONB,
PRIMARY KEY (id, event_time)
) PARTITION BY RANGE (event_time);
-- 月別パーティションの作成
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');
-- 各パーティションにインデックスを作成(PG 11以降は自動継承)
CREATE INDEX idx_events_user_id ON events (user_id);
CREATE INDEX idx_events_event_type ON events (event_type);
パーティションキー(event_time)はプライマリキーに含める必要があります。これは PostgreSQL の要件であり、パーティションテーブルのユニーク制約はテーブル全体ではなく各パーティションでローカルに適用されるためです。
2.2 List パーティショニング
List パーティショニングは、離散的な列挙値に基づいて行をパーティションに割り当てます。マルチテナントシステム、地理データ、カテゴリカルなワークロードに適しています。
-- マルチリージョン注文テーブルのリージョン別 List パーティショニング
CREATE TABLE orders (
id BIGSERIAL,
region TEXT NOT NULL,
order_date TIMESTAMPTZ NOT NULL,
customer_id BIGINT NOT NULL,
total_amount NUMERIC(12,2),
status TEXT NOT NULL,
PRIMARY KEY (id, region)
) PARTITION BY LIST (region);
CREATE TABLE orders_us PARTITION OF orders
FOR VALUES IN ('us-east-1', 'us-west-2', 'us-central-1');
CREATE TABLE orders_eu PARTITION OF orders
FOR VALUES IN ('eu-west-1', 'eu-central-1', 'eu-north-1');
CREATE TABLE orders_ap PARTITION OF orders
FOR VALUES IN ('ap-northeast-1', 'ap-southeast-1', 'ap-south-1');
-- マッピングされていないリージョン用のデフォルトパーティション
CREATE TABLE orders_default PARTITION OF orders DEFAULT;
必ず DEFAULT パーティションを作成してください。これがないと、既存のパーティションでカバーされていないリージョン値を持つ INSERT がエラーで失敗します。デフォルトパーティションはセーフティネットとして機能し、新しいリージョンが正式に追加される際に後から分割できます。
2.3 Hash パーティショニング
Hash パーティショニングは、ハッシュ関数を使用して固定数のパーティションにデータを均等に分散します。自然な範囲やカテゴリがなく、均一なデータ分散が必要なワークロードに適しています。
-- 高スループットセッションストアの Hash パーティショニング
CREATE TABLE sessions (
session_id UUID NOT NULL,
user_id BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_active TIMESTAMPTZ NOT NULL DEFAULT now(),
session_data JSONB,
PRIMARY KEY (session_id)
) PARTITION BY HASH (session_id);
-- 8つのハッシュパーティションを作成(均等分散のため2のべき乗を推奨)
CREATE TABLE sessions_p0 PARTITION OF sessions FOR VALUES WITH (MODULUS 8, REMAINDER 0);
CREATE TABLE sessions_p1 PARTITION OF sessions FOR VALUES WITH (MODULUS 8, REMAINDER 1);
CREATE TABLE sessions_p2 PARTITION OF sessions FOR VALUES WITH (MODULUS 8, REMAINDER 2);
CREATE TABLE sessions_p3 PARTITION OF sessions FOR VALUES WITH (MODULUS 8, REMAINDER 3);
CREATE TABLE sessions_p4 PARTITION OF sessions FOR VALUES WITH (MODULUS 8, REMAINDER 4);
CREATE TABLE sessions_p5 PARTITION OF sessions FOR VALUES WITH (MODULUS 8, REMAINDER 5);
CREATE TABLE sessions_p6 PARTITION OF sessions FOR VALUES WITH (MODULUS 8, REMAINDER 6);
CREATE TABLE sessions_p7 PARTITION OF sessions FOR VALUES WITH (MODULUS 8, REMAINDER 7);
重大な制約があります:Hash パーティションはインクリメンタルに追加・削除できません。モジュラスを8から16に変更するには、すべてのパーティションを再作成し、すべての行を再分散する必要があります。設計時にモジュラスを慎重に計画してください。
2.4 戦略比較
| 基準 | Range | List | Hash |
|---|---|---|---|
| 最適な用途 | 時系列、ログ、イベント | マルチテナント、地理、カテゴリ | 均一分散、自然キーなし |
| パーティションプルーニング | 範囲クエリと等値クエリ | 等値クエリのみ | 等値クエリのみ |
| ホットパーティションリスク | 高(最新パーティションに全書込み) | 中(データ分布に依存) | 低(設計上均等分散) |
| パーティション追加 | 容易(将来分を追加) | 容易(新しい値リストを追加) | 困難(完全再構築が必要) |
| パーティション削除 | 即座(古い期間を DROP) | 容易(カテゴリ別に DROP) | 実用的でない |
| データアーカイブ | 優秀(古いパーティションをデタッチ) | 良好(カテゴリ別にデタッチ) | 不向き(データが分散) |
| 典型的なパーティション数 | 12-120(月次/週次) | 5-50(カテゴリ別) | 8-64(2のべき乗) |
| サブパーティショニング | RANGE-HASH, RANGE-LIST | LIST-RANGE, LIST-HASH | HASH-RANGE(稀) |
3. パーティションプルーニングと制約排除
パーティションプルーニングは、パーティショニングをクエリ性能向上に有効にするメカニズムです。クエリプランナーは各パーティションの境界制約を検査し、一致する行を含む可能性のないパーティションをデータ読み取り前に排除します。
3.1 パーティションプルーニングの検証
-- パーティションプルーニングが有効か確認
SHOW enable_partition_pruning; -- 'on' であること
-- EXPLAIN でプルーニングを検証
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM events
WHERE event_time >= '2026-03-01' AND event_time < '2026-03-15';
-- 期待される出力:3月のパーティションのみスキャン
-- Append (actual rows=1523400 loops=1)
-- -> Seq Scan on events_2026_03 (actual rows=1523400 loops=1)
-- Filter: (event_time >= '2026-03-01' AND event_time < '2026-03-15')
-- Planning Time: 0.452 ms
-- Execution Time: 892.31 ms
3.2 ランタイムパーティションプルーニング
PostgreSQL 11 ではランタイムパーティションプルーニングが導入されました。これにより、パーティションキーの値がプランニング時に不明な場合(パラメータ化されたクエリやサブクエリなど)でも、クエリ実行時にパーティションを排除できます。
-- プリペアドステートメントでのランタイムプルーニング
PREPARE get_events(timestamptz, timestamptz) AS
SELECT * FROM events WHERE event_time >= $1 AND event_time < $2;
-- 実行時にプルーニングが行われる(プランニング時ではなく)
EXECUTE get_events('2026-03-01', '2026-03-08');
3.3 プルーニングが失敗する一般的なケース
パーティションプルーニングは以下のシナリオで静かに失敗します:
- 関数のラッピング:
WHERE DATE(event_time) = '2026-03-01'はプルーニングを妨げます。代わりにWHERE event_time >= '2026-03-01' AND event_time < '2026-03-02'を使用してください。 - 型の不一致: パーティションキーが
TIMESTAMPTZで、クエリがTEXT比較を使用している場合、プランナーがプルーニングしない可能性があります。 - パーティション横断の OR 条件: 複雑な OR 句がプランナーにすべてのパーティションをスキャンさせる場合があります。
- 直接フィルタのない JOIN: パーティションキーフィルタが直接的な WHERE 句ではなく JOIN から来る場合、ランタイムプルーニングが必要ですが、常にトリガーされるとは限りません。
4. pg_partman による自動管理
大規模環境でパーティションを手動で作成・管理するのはエラーが発生しやすい作業です。pg_partman 拡張機能は、時間ベースとシリアルベースの両方のパーティションスキームに対してパーティションの作成、保持、メンテナンスを自動化します。
4.1 インストールとセットアップ
# pg_partman のインストール(Debian/Ubuntu の場合)
sudo apt-get install postgresql-17-partman
# またはソースからコンパイル
git clone https://github.com/pgpartman/pg_partman.git
cd pg_partman
make && sudo make install
-- 拡張機能を有効化
CREATE SCHEMA IF NOT EXISTS partman;
CREATE EXTENSION pg_partman SCHEMA partman;
-- まず親テーブルを作成(パーティション化済みであること)
CREATE TABLE metrics (
id BIGSERIAL,
recorded_at TIMESTAMPTZ NOT NULL,
metric_name TEXT NOT NULL,
metric_value DOUBLE PRECISION NOT NULL,
labels JSONB,
PRIMARY KEY (id, recorded_at)
) PARTITION BY RANGE (recorded_at);
-- pg_partman に登録して自動管理を開始
SELECT partman.create_parent(
p_parent_table => 'public.metrics',
p_control => 'recorded_at',
p_interval => '1 day',
p_premake => 7, -- 将来のパーティションを7つ事前作成
p_start_partition => '2026-01-01'
);
-- 保持ポリシーの設定
UPDATE partman.part_config
SET retention = '90 days',
retention_keep_table = false, -- 古いパーティションを DROP(デタッチだけでなく)
infinite_time_partitions = true -- 将来のパーティションを常に作成
WHERE parent_table = 'public.metrics';
4.2 メンテナンスのスケジューリング
pg_partman は定期的なメンテナンス呼び出しにより、新しいパーティションの作成と保持ポリシーの適用を行います:
-- すべての管理対象テーブルのメンテナンスを実行(pg_cron または crontab で呼び出し)
SELECT partman.run_maintenance();
-- または特定のテーブルのみ
SELECT partman.run_maintenance('public.metrics');
# pg_cron でスケジューリング(推奨)
SELECT cron.schedule('partman-maintenance', '*/15 * * * *',
$$SELECT partman.run_maintenance()$$);
# またはシステムの crontab で
echo "*/15 * * * * psql -d mydb -c \"SELECT partman.run_maintenance()\"" | crontab -
日次パーティションの場合は少なくとも15分ごと、月次パーティションの場合は1時間ごとにメンテナンスを実行してください。重要な原則:データが到着する前に新しいパーティションが常に事前作成されるよう、十分な頻度でメンテナンスを実行することです。
5. 非パーティションテーブルからのゼロダウンタイム移行
10億行のモノリシックテーブルをダウンタイムなしでパーティション構造に移行することは、最も困難なデータベース操作の一つです。ここでは、トリガーベースのデュアルライトパターンを使用した実績のあるアプローチを紹介します。
5.1 移行戦略
ステップ 1:元のテーブルと並行してパーティションテーブルを作成。
-- 新しいパーティションテーブルを作成(別名で)
CREATE TABLE events_partitioned (
LIKE events INCLUDING ALL
) PARTITION BY RANGE (event_time);
-- データ範囲全体をカバーするパーティションを作成
-- (pg_partman または手動の CREATE TABLE ... PARTITION OF を使用)
SELECT partman.create_parent(
p_parent_table => 'public.events_partitioned',
p_control => 'event_time',
p_interval => '1 month',
p_premake => 3,
p_start_partition => '2024-01-01'
);
ステップ 2:新しいデータをデュアルライトするトリガーをインストール。
-- パーティションテーブルへの書き込みを同期するトリガー関数
CREATE OR REPLACE FUNCTION sync_to_partitioned() RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
INSERT INTO events_partitioned VALUES (NEW.*) ON CONFLICT DO NOTHING;
ELSIF TG_OP = 'UPDATE' THEN
INSERT INTO events_partitioned VALUES (NEW.*)
ON CONFLICT (id, event_time)
DO UPDATE SET
user_id = EXCLUDED.user_id,
event_type = EXCLUDED.event_type,
payload = EXCLUDED.payload;
ELSIF TG_OP = 'DELETE' THEN
DELETE FROM events_partitioned
WHERE id = OLD.id AND event_time = OLD.event_time;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_sync_partitioned
AFTER INSERT OR UPDATE OR DELETE ON events
FOR EACH ROW EXECUTE FUNCTION sync_to_partitioned();
ステップ 3:履歴データをバッチでバックフィル。
-- 長時間ロックを避けるため、バッチ単位で移行
DO $$
DECLARE
batch_start TIMESTAMPTZ := '2024-01-01';
batch_end TIMESTAMPTZ;
total_migrated BIGINT := 0;
BEGIN
WHILE batch_start < now() LOOP
batch_end := batch_start + INTERVAL '1 day';
INSERT INTO events_partitioned
SELECT * FROM events
WHERE event_time >= batch_start AND event_time < batch_end
ON CONFLICT (id, event_time) DO NOTHING;
GET DIAGNOSTICS total_migrated = ROW_COUNT;
RAISE NOTICE 'Migrated % rows for %', total_migrated, batch_start;
batch_start := batch_end;
PERFORM pg_sleep(0.1); -- 過負荷を避けるためスロットリング
END LOOP;
END $$;
ステップ 4:低トラフィック時間帯にテーブルをスワップ。
-- 単一トランザクション内でテーブル名を変更
BEGIN;
ALTER TABLE events RENAME TO events_old;
ALTER TABLE events_partitioned RENAME TO events;
-- 古いテーブルからトリガーを削除
DROP TRIGGER IF EXISTS trg_sync_partitioned ON events_old;
COMMIT;
-- 確認後、安全な期間を経て古いテーブルを削除
-- DROP TABLE events_old;
このアプローチはほぼゼロダウンタイムを実現します。唯一の短時間ロックはステップ 4 の RENAME 操作中で、ミリ秒単位で完了します。
6. パフォーマンスベンチマーク
以下のベンチマークは、64 GB RAM、16 vCPU、NVMe SSD ストレージ、events テーブルに12億行を持つ PostgreSQL 17 インスタンスで測定されました。
| 操作 | 非パーティション | Range パーティション(月次) | 改善率 |
|---|---|---|---|
| ポイントクエリ(time + id) | 12.4 ms | 1.8 ms | 6.9倍高速 |
| 範囲スキャン(1ヶ月) | 34.2 秒 | 4.1 秒 | 8.3倍高速 |
| 範囲スキャン(1日) | 8.7 秒 | 0.32 秒 | 27.2倍高速 |
| COUNT(*) テーブル全体 | 187.3 秒 | 192.1 秒 | ほぼ同等 |
| INSERT スループット | 45,000 行/秒 | 42,800 行/秒 | 5% 低下 |
| 1ヶ月分のデータ DELETE | 47 分 | 0.003 秒(DROP) | 約100万倍高速 |
| VACUUM(テーブル全体) | 2.1 時間 | 8.2 分(パーティション単位) | 15倍高速 |
| インデックス再構築 | 45 分 | 3.2 分(パーティション単位) | 14倍高速 |
| プランニング時間(単純クエリ) | 0.08 ms | 0.45 ms | 5.6倍遅い |
主な観察結果:
- 狭い範囲クエリ は最大の改善(最大27倍)を示します。パーティションプルーニングがデータの大部分を排除するためです。
- テーブル全体スキャン は改善がなく、Append プランノードのオーバーヘッドにより若干遅くなる場合があります。
- INSERT スループット はパーティションルーティングのオーバーヘッドにより若干低下します。PostgreSQL 17 の改善されたパーティションルーティングではこの差は無視できるレベルです。
- プランニング時間はパーティション数に応じて増加 します。120の月次パーティションでプランニング時間は約1.2 msに上昇します。500パーティションを超えると、プランニングのオーバーヘッドが顕著になります。
- データ削除 が最も劇的な改善です:パーティションの DROP は行単位の DELETE と比較してほぼ即座に完了します。
7. 障害ケースと復旧手順
7.1 パーティション不足(INSERT 失敗)
INSERT が DEFAULT パーティションのない時間範囲を対象とした場合、失敗します:
ERROR: no partition of relation "events" found for row
DETAIL: Partition key of the failing row contains (event_time) = (2026-04-01 00:00:00+00).
復旧: 不足しているパーティションを直ちに作成し、失敗したトランザクションをリトライします:
CREATE TABLE events_2026_04 PARTITION OF events
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
予防: 十分な p_premake 値で pg_partman を常に使用し、partman.check_missing 関数を監視してください。
7.2 アクティブクエリ実行中のパーティションデタッチ
クエリが実行中にパーティションをデタッチすると、それらのクエリは失敗します。
安全なデタッチ手順:
-- CONCURRENTLY を使用してブロッキングを回避(PostgreSQL 14以降)
ALTER TABLE events DETACH PARTITION events_2024_01 CONCURRENTLY;
-- これは既存のすべてのクエリが完了するまで待機してから完了する
-- コマンド開始後、新しいクエリはそのパーティションを参照しない
7.3 休眠パーティションでのトランザクション ID ラップアラウンド
書き込みを受けない古いパーティションは autovacuum をトリガーせず、トランザクション ID が増加し続けます。いずれかのパーティションがラップアラウンドしきい値に達すると、データベース全体が緊急リードオンリーモードに入ります。
予防:
-- パーティションの年齢を確認
SELECT relname, age(relfrozenxid) AS xid_age
FROM pg_class
WHERE relname LIKE 'events_%'
ORDER BY xid_age DESC;
-- 古いパーティションを手動でフリーズ
VACUUM (FREEZE, VERBOSE) events_2024_01;
7.4 デフォルトパーティションの肥大化
パーティション作成が追いつかず DEFAULT パーティションにデータが蓄積されると、後からデフォルトパーティションを分割する際に排他ロックとフルデータスキャンが必要になります。
復旧: 不足しているパーティションを作成し、データをデフォルトからバッチで移動します:
-- 不足しているパーティションを作成
CREATE TABLE events_2026_04 PARTITION OF events
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
-- 上記により、一致する行が DEFAULT から新しいパーティションに
-- 自動的に移動される(PostgreSQL 13以降)
8. パーティションテーブルの VACUUM とメンテナンス
8.1 Autovacuum の動作
Autovacuum は各パーティションを独立して処理します。これは大きな利点です。複数の autovacuum ワーカーが異なるパーティションを同時に処理でき、単一のモノリシックテーブルと比較してメンテナンス時間を大幅に短縮します。
ただし、autovacuum は親パーティションテーブル自体を処理しません。クエリプランナーが使用するパーティションレベルの統計情報を更新するために、親テーブルに対して手動で ANALYZE を実行する必要があります:
-- 親テーブルの手動 ANALYZE(パーティション横断統計に必要)
ANALYZE events;
-- pg_cron でスケジューリング
SELECT cron.schedule('analyze-events', '0 4 * * *',
$$ANALYZE events$$);
8.2 パーティション別の Autovacuum チューニング
ホットパーティション(アクティブな書き込みを受けるパーティション)はコールドパーティションよりも積極的な autovacuum 設定が必要です:
-- 当月パーティションの積極的な autovacuum 設定
ALTER TABLE events_2026_03 SET (
autovacuum_vacuum_scale_factor = 0.01, -- デッドタプル1%でトリガー
autovacuum_vacuum_cost_delay = 2, -- スロットリングを軽減
autovacuum_analyze_scale_factor = 0.005 -- 頻繁に再解析
);
-- 古い読み取り専用パーティションの緩和設定
ALTER TABLE events_2025_12 SET (
autovacuum_vacuum_scale_factor = 0.2, -- デフォルトに近い動作
autovacuum_enabled = true -- XID エイジングのため有効に維持
);
8.3 フリーズと XID 管理
パーティションテーブルでは、古いパーティションの事前フリーズが不可欠です:
#!/bin/bash
# freeze_old_partitions.sh - 6ヶ月以上前のパーティションをフリーズ
DB_NAME="production"
PARENT_TABLE="events"
CUTOFF=$(date -d "6 months ago" +%Y_%m)
psql -d "$DB_NAME" -t -A -c "
SELECT relname FROM pg_class c
JOIN pg_namespace n ON c.relnamespace = n.oid
WHERE relname LIKE '${PARENT_TABLE}_%'
AND relkind = 'r'
AND relname < '${PARENT_TABLE}_${CUTOFF}'
ORDER BY relname
" | while read partition; do
echo "Freezing $partition..."
psql -d "$DB_NAME" -c "VACUUM (FREEZE, VERBOSE) $partition"
done
8.4 インデックスメンテナンス
パーティションテーブルのインデックスはパーティション単位で保存されるため、REINDEX 操作は個別のパーティションに限定されます:
-- 読み取りをブロックせずに単一パーティションを再インデックス(PostgreSQL 12以降)
REINDEX TABLE CONCURRENTLY events_2026_03;
-- すべてのパーティションのインデックス肥大化を確認
SELECT
schemaname || '.' || tablename AS partition_name,
indexname,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size,
idx_scan AS scans,
idx_tup_read AS tuples_read
FROM pg_stat_user_indexes
WHERE tablename LIKE 'events_%'
ORDER BY pg_relation_size(indexrelid) DESC
LIMIT 20;
9. 本番チェックリスト
パーティショニングを本番環境にデプロイする前に、以下を確認してください:
- プライマリキーにパーティションキーを含む: パーティションキーは任意のユニーク制約またはプライマリキー制約の一部でなければなりません。
- DEFAULT パーティションが存在する: マッピングされていない値による INSERT 失敗を防ぎます。
- pg_partman が設定済み: 自動作成、保持、将来のパーティションの事前作成。
- 監視が配置済み: パーティション不足、デフォルトパーティションサイズ、XID エイジ、autovacuum 遅延のアラートを設定。
- パーティションプルーニングが有効:
enable_partition_pruning = onを確認し、EXPLAIN で検証。 - アプリケーションクエリがパーティションキーを使用: パーティションキーでフィルタリングしないクエリはすべてのパーティションをスキャンします。
- パーティション別の autovacuum チューニング: ホットパーティションには積極的な設定、コールドパーティションにはフリーズスケジューリングが必要。
- 親テーブルの ANALYZE をスケジューリング: Autovacuum は親テーブルを自動的に解析しません。
- バックアップ戦略の更新: pg_dump や pgBackRest はパーティションを処理しますが、バックアップにすべてのパーティションが含まれることを確認してください。
- 障害対応ランブックの作成: パーティション不足、デタッチ操作、XID ラップアラウンドの手順を文書化。
10. まとめ
PostgreSQL パーティショニングは万能ではありません。プランニングのオーバーヘッドが増加し、慎重なパーティションキーの選択が必要であり、運用上の複雑さが増します。しかし、数億行を超えて成長したテーブルに対して、クエリ性能の維持、メンテナンスウィンドウの短縮、アーカイブやパージなどのデータライフサイクル操作の即時実行を可能にする、PostgreSQL 内で最も効果的なツールです。
成功の鍵は、適切な戦略の選択(時系列には Range、カテゴリカルには List、均一分散には Hash)、pg_partman による管理の自動化、そしてパーティションテーブル固有の特性を考慮した監視とメンテナンス手順への投資です。
まず、最も大きな時系列テーブルに Range パーティショニングを適用することから始めてください。実際のクエリでの改善を測定してください。そして、データがその運用オーバーヘッドを正当化する場合にのみ、他のテーブルに展開してください。
参考文献
- PostgreSQL 公式ドキュメント: テーブルパーティショニング
- pg_partman - GitHub リポジトリとドキュメント
- Crunchy Data: Native Postgres と pg_partman によるパーティショニング
- pganalyze: パーティションプルーニング、プリペアドステートメントとクエリプラン
- EDB: パーティションテーブルパフォーマンスのベンチマーク方法
- CYBERTEC: PostgreSQL パーティショニングでパフォーマンスを低下させる方法
- Tiger Data: PostgreSQL で10億行を扱う
- Percona: pg_partman で PostgreSQL パーティショニングを簡単に