- Authors
- Name
- 1. パーティショニングとは?
- 2. Range パーティショニング
- 3. List パーティショニング
- 4. Hash パーティショニング
- 5. Partition Pruning の確認
- 6. パーティション自動作成(pg_partman)
- 7. 古いパーティションの削除
- 8. 運用のコツ
- 9. クイズ

1. パーティショニングとは?
パーティショニングは、1つの大きなテーブルを複数の物理的なパーティションに分割する技法です。テーブルのデータが数億件以上の場合、クエリパフォーマンスを大幅に向上させることができます。
パーティショニングの利点
- クエリパフォーマンスの向上: Partition pruning で必要なパーティションのみスキャン
- 大量データの削除: DROP PARTITION で即時削除(DELETE より数百倍高速)
- 並列処理: パーティション別の並列スキャンが可能
- 管理の容易さ: パーティション別のインデックス、VACUUM、バックアップが可能
2. Range パーティショニング
最もよく使用される方式で、日付や数値の範囲で分割します。
-- 親テーブルの作成
CREATE TABLE orders (
id BIGSERIAL,
customer_id INTEGER NOT NULL,
order_date DATE NOT NULL,
amount DECIMAL(10, 2),
status VARCHAR(20),
created_at TIMESTAMP DEFAULT NOW()
) PARTITION BY RANGE (order_date);
-- 月別パーティションの作成
CREATE TABLE orders_2026_01 PARTITION OF orders
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE orders_2026_02 PARTITION OF orders
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
CREATE TABLE orders_2026_03 PARTITION OF orders
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
-- パーティション別インデックス(自動的に継承)
CREATE INDEX idx_orders_customer ON orders (customer_id);
CREATE INDEX idx_orders_status ON orders (status, order_date);
-- デフォルトパーティション(どの範囲にも合わないデータを収容)
CREATE TABLE orders_default PARTITION OF orders DEFAULT;
3. List パーティショニング
特定の値リストで分割します。地域やカテゴリなどに適しています。
CREATE TABLE events (
id BIGSERIAL,
event_type VARCHAR(50) NOT NULL,
payload JSONB,
created_at TIMESTAMP DEFAULT NOW()
) PARTITION BY LIST (event_type);
CREATE TABLE events_user PARTITION OF events
FOR VALUES IN ('user_signup', 'user_login', 'user_logout');
CREATE TABLE events_order PARTITION OF events
FOR VALUES IN ('order_created', 'order_paid', 'order_cancelled');
CREATE TABLE events_system PARTITION OF events
FOR VALUES IN ('health_check', 'deploy', 'config_change');
CREATE TABLE events_default PARTITION OF events DEFAULT;
4. Hash パーティショニング
ハッシュ関数で均等に分配します。特定のキーの分布が均一な場合に適しています。
CREATE TABLE user_sessions (
id BIGSERIAL,
user_id INTEGER NOT NULL,
session_id UUID NOT NULL,
data JSONB,
expires_at TIMESTAMP
) PARTITION BY HASH (user_id);
-- 4つのパーティションに均等分配
CREATE TABLE user_sessions_0 PARTITION OF user_sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE user_sessions_1 PARTITION OF user_sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE user_sessions_2 PARTITION OF user_sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE user_sessions_3 PARTITION OF user_sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 3);
5. Partition Pruning の確認
-- Partition pruning の有効化確認
SHOW enable_partition_pruning; -- on
-- EXPLAIN で pruning を確認
EXPLAIN (ANALYZE, COSTS, BUFFERS)
SELECT * FROM orders
WHERE order_date >= '2026-03-01'
AND order_date < '2026-04-01';
-- 結果例:
-- Append (actual rows=50000)
-- -> Seq Scan on orders_2026_03 (actual rows=50000)
-- Filter: (order_date >= '2026-03-01' AND order_date < '2026-04-01')
-- orders_2026_01、orders_2026_02 はスキャンされない!
6. パーティション自動作成(pg_partman)
-- pg_partman のインストール
CREATE EXTENSION pg_partman;
-- 自動パーティション管理の設定
SELECT partman.create_parent(
p_parent_table => 'public.orders',
p_control => 'order_date',
p_type => 'native',
p_interval => 'monthly',
p_premake => 3 -- 3ヶ月先まで事前作成
);
-- メンテナンス関数(cron で毎日実行)
SELECT partman.run_maintenance();
cron 設定
# pg_partman メンテナンス(毎日午前2時)
0 2 * * * psql -U postgres -d mydb \
-c "SELECT partman.run_maintenance();" \
>> /var/log/pg_partman.log 2>&1
手動自動化スクリプト
-- pg_partman なしで直接自動化
CREATE OR REPLACE FUNCTION create_monthly_partition(
p_table TEXT,
p_year INTEGER,
p_month INTEGER
) RETURNS VOID AS $$
DECLARE
partition_name TEXT;
start_date DATE;
end_date DATE;
BEGIN
partition_name := format('%s_%s_%s',
p_table,
p_year,
LPAD(p_month::TEXT, 2, '0')
);
start_date := make_date(p_year, p_month, 1);
end_date := start_date + INTERVAL '1 month';
EXECUTE format(
'CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
FOR VALUES FROM (%L) TO (%L)',
partition_name, p_table, start_date, end_date
);
RAISE NOTICE 'Created partition: %', partition_name;
END;
$$ LANGUAGE plpgsql;
-- 使用例
SELECT create_monthly_partition('orders', 2026, 4);
SELECT create_monthly_partition('orders', 2026, 5);
7. 古いパーティションの削除
-- パーティションの分離(データ保存、クエリから除外)
ALTER TABLE orders DETACH PARTITION orders_2025_01;
-- 分離されたパーティションを別テーブルとして維持または削除
DROP TABLE orders_2025_01; -- 即時削除(数億件でも一瞬)
-- 比較:DELETE は非常に遅い
-- DELETE FROM orders WHERE order_date < '2025-02-01'; ← これはやめましょう!
8. 運用のコツ
パーティション状態のモニタリング
-- パーティション別行数の確認
SELECT
schemaname || '.' || relname AS partition,
n_live_tup AS row_count,
pg_size_pretty(pg_relation_size(relid)) AS size
FROM pg_stat_user_tables
WHERE relname LIKE 'orders_%'
ORDER BY relname;
-- パーティション一覧の照会
SELECT
parent.relname AS parent,
child.relname AS partition,
pg_get_expr(child.relpartbound, child.oid) AS bounds
FROM pg_inherits
JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
JOIN pg_class child ON pg_inherits.inhrelid = child.oid
WHERE parent.relname = 'orders'
ORDER BY child.relname;
注意事項
1. PRIMARY KEY にパーティションキーを含める必要あり
CREATE TABLE orders (...) PARTITION BY RANGE (order_date);
→ PK は (id, order_date) の形式でなければならない
2. UNIQUE 制約にもパーティションキーを含める必要あり
3. パーティション数が多すぎるとプランニングオーバーヘッドが増加
→ 1000以下を推奨
4. クロスパーティション UPDATE は PostgreSQL 11+ でのみサポート
9. クイズ
Q1: Range パーティショニングにおける DEFAULT パーティションの役割は?
DEFAULT パーティションは、どのパーティションの範囲にも属さないデータを収容します。例えば、2026年のパーティションしかないのに2027年のデータが INSERT されると DEFAULT パーティションに保存されます。DEFAULT パーティションがない場合、範囲外のデータ INSERT 時にエラーが発生します。
Q2: DROP PARTITION が DELETE より速い理由は?
DELETE は各行を1つずつ削除しながら WAL ログを記録し、dead tuple が残り VACUUM が必要です。一方 DROP TABLE(パーティション削除)はテーブルのデータファイル自体を即座に削除するため、数億件のデータも瞬時に除去されます。行数に関係なくほぼ一定の時間で完了します。
Q3: PRIMARY KEY にパーティションキーを含めなければならない理由は?
PostgreSQL はパーティションテーブルのユニーク制約を各パーティションのローカルインデックスで実装します。パーティションキーが PK に含まれていないと、異なるパーティションに同じ id が存在する可能性があり、テーブル全体レベルのユニーク性を保証できません。そのため (id, order_date) のようにパーティションキーを PK に含める必要があります。