PostgreSQL: PostgreSQLのパーティションテーブルとテーブル継承
最終更新:2026-08-26
1. 学習内容
- 宣言的パーティショニングの原理とユースケースを理解する
- RANGE / LIST / HASH / マルチレベルパーティションを作成する
- パーティションプルーニングを使用してクエリを高速化する
- パーティションメンテナンス: DETACH / ATTACH / DROP を実行する
- テーブル継承(INHERITS)と宣言的パーティショニングを比較する
- パーティションテーブルのインデックス戦略を設計する
2. ストーリー
AliceのEコマースプラットフォームのordersテーブルは毎日10万行の新規データが追加されます。3年後には単一テーブルが1億行を超え、インデックスがあっても月次クエリがテーブル全体をスキャンします。DBAは月次RANGEパーティショニングを提案しました。直近30日間のクエリは3年分のテーブル全体ではなく1つのパーティションのみをスキャンします。パーティショニング導入後、月次レポートクエリは12秒から0.8秒に短縮され、ディスクI/Oは95%減少しました。
3. 概念: 宣言的パーティショニングの概要
(1) パーティショニングが必要な理由
単一テーブルが数千万行を超えると、インデックスのB-Treeが深くなり、VACUUMに大幅に時間がかかり、クエリプランナーが最適でない計画を選択する可能性があります。パーティショニングは論理的な大テーブルを複数の物理的な小テーブルに分割し、それぞれが独立したインデックスとメンテナンスを持ちます。
| 指標 | 非パーティション(1億行) | 月次パーティション(36パーティション) |
|---|---|---|
| パーティションあたりの行数 | 1億 | 約280万 |
| インデックス深さ | 5〜6レベル | 3〜4レベル |
| VACUUM時間 | 30分以上 | 2分未満/パーティション |
| 月次クエリのI/O | フルテーブルスキャン | 1パーティションのみスキャン |
(2) PostgreSQLパーティショニングの進化
| バージョン | 機能 |
|---|---|
| PG 9.x | テーブル継承 + トリガーベースの手動パーティショニング |
| PG 10 | 宣言的RANGE / LISTパーティショニング |
| PG 11 | HASHパーティショニング、パーティションプルーニング強化、パーティション間UPDATE移行 |
| PG 12 | ATTACH/DETACHパーティション |
| PG 13 | マルチレベルパーティションプルーニング最適化 |
| PG 14+ | パーティションプルーニングパフォーマンスのさらなる改善 |
(3) 4つのパーティショニング戦略
| 戦略 | ユースケース | パーティションキーの要件 |
|---|---|---|
| RANGE | 時系列、数値範囲 | ソート可能な型 |
| LIST | 列挙カテゴリ(地域、ステータス) | 離散値 |
| HASH | 均等分散、明確な範囲なし | ハッシュ可能な任意の型 |
| マルチレベル | RANGE + LIST/HASHの組み合わせ | 複数カラムの組み合わせ |
4. 操作: RANGEパーティショニング
▶ サンプル: 月次RANGEパーティション注文テーブルを作成
-- 親テーブル: パーティションキーのみ
CREATE TABLE orders (
id BIGINT GENERATED ALWAYS AS IDENTITY,
user_id BIGINT NOT NULL,
order_date DATE NOT NULL,
total_amount NUMERIC(12,2),
status TEXT DEFAULT 'pending'
) PARTITION BY RANGE (order_date);
-- 2024年の月次パーティション
CREATE TABLE orders_2024_01 PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE orders_2024_02 PARTITION OF orders
FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
CREATE TABLE orders_2024_03 PARTITION OF orders
FOR VALUES FROM ('2024-03-01') TO ('2024-04-01');
-- デフォルトパーティションが範囲外の行を受け取る
CREATE TABLE orders_default PARTITION OF orders DEFAULT;
Output:
CREATE TABLE
▶ サンプル: パーティションを自動作成する関数
-- 指定した年の月次パーティションを生成
CREATE OR REPLACE FUNCTION create_monthly_partitions(
p_parent REGCLASS,
p_year INT
) RETURNS VOID AS $$
DECLARE
m INT;
p_name TEXT;
s_date TEXT;
e_date TEXT;
BEGIN
FOR m IN 1..12 LOOP
p_name := format('%s_%s_%02s', p_parent::text, p_year, m);
s_date := format('%s-%02s-01', p_year, m);
e_date := format('%s-%02s-01', p_year, m + 1);
EXECUTE format(
'CREATE TABLE IF NOT EXISTS %I PARTITION OF %s
FOR VALUES FROM (%L) TO (%L)',
p_name, p_parent::text, s_date, e_date
);
END LOOP;
END;
$$ LANGUAGE plpgsql;
SELECT create_monthly_partitions('orders', 2025);
Output:
CREATE TABLE
▶ サンプル: RANGEパーティションへの挿入と検索
INSERT INTO orders (user_id, order_date, total_amount, status)
VALUES
(1001, '2024-01-15', 299.99, 'completed'),
(1002, '2024-02-20', 159.50, 'shipped'),
(1003, '2024-03-10', 89.00, 'pending');
-- パーティションプルーニング付きクエリ
SELECT * FROM orders
WHERE order_date BETWEEN '2024-02-01' AND '2024-02-29';
id | user_id | order_date | total_amount | status
------+---------+------------+--------------+--------
1002 | 1002 | 2024-02-20 | 159.50 | shipped
(1 row)
5. 操作: LISTおよびHASHパーティショニング
▶ サンプル: LISTパーティション — ユーザーテーブルを地域で分割
CREATE TABLE users_by_region (
id BIGINT GENERATED ALWAYS AS IDENTITY,
name TEXT NOT NULL,
region TEXT NOT NULL,
email TEXT
) PARTITION BY LIST (region);
CREATE TABLE users_north_america PARTITION OF users_by_region
FOR VALUES IN ('US', 'CA', 'MX');
CREATE TABLE users_europe PARTITION OF users_by_region
FOR VALUES IN ('UK', 'DE', 'FR', 'ES');
CREATE TABLE users_asia PARTITION OF users_by_region
FOR VALUES IN ('CN', 'JP', 'KR', 'SG');
CREATE TABLE users_other PARTITION OF users_by_region DEFAULT;
Output:
CREATE TABLE
▶ サンプル: HASHパーティショニング — ログテーブルを均等分散
CREATE TABLE access_logs (
id BIGINT GENERATED ALWAYS AS IDENTITY,
user_id BIGINT,
action TEXT,
log_time TIMESTAMPTZ DEFAULT now()
) PARTITION BY HASH (user_id);
-- 8つのHASHパーティションを作成
CREATE TABLE access_logs_p0 PARTITION OF access_logs
FOR VALUES WITH (MODULUS 8, REMAINDER 0);
CREATE TABLE access_logs_p1 PARTITION OF access_logs
FOR VALUES WITH (MODULUS 8, REMAINDER 1);
CREATE TABLE access_logs_p2 PARTITION OF access_logs
FOR VALUES WITH (MODULUS 8, REMAINDER 2);
CREATE TABLE access_logs_p3 PARTITION OF access_logs
FOR VALUES WITH (MODULUS 8, REMAINDER 3);
CREATE TABLE access_logs_p4 PARTITION OF access_logs
FOR VALUES WITH (MODULUS 8, REMAINDER 4);
CREATE TABLE access_logs_p5 PARTITION OF access_logs
FOR VALUES WITH (MODULUS 8, REMAINDER 5);
CREATE TABLE access_logs_p6 PARTITION OF access_logs
FOR VALUES WITH (MODULUS 8, REMAINDER 6);
CREATE TABLE access_logs_p7 PARTITION OF access_logs
FOR VALUES WITH (MODULUS 8, REMAINDER 7);
Output:
CREATE TABLE
▶ サンプル: マルチレベルパーティショニング(RANGE + LIST)
CREATE TABLE order_details (
id BIGINT,
order_id BIGINT,
order_date DATE NOT NULL,
region TEXT NOT NULL,
product_id INT,
quantity INT
) PARTITION BY RANGE (order_date);
-- 第1レベル: 月単位
CREATE TABLE order_details_2024q1 PARTITION OF order_details
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01')
PARTITION BY LIST (region);
-- 第2レベル: 各月範囲内で地域単位
CREATE TABLE order_details_2024q1_na PARTITION OF order_details_2024q1
FOR VALUES IN ('US', 'CA', 'MX');
CREATE TABLE order_details_2024q1_eu PARTITION OF order_details_2024q1
FOR VALUES IN ('UK', 'DE', 'FR');
Output:
CREATE TABLE
6. 概念: パーティションプルーニング
(1) プルーニングの原理
パーティションプルーニングにより、クエリプランナーは計画時に無関係なパーティションを除外し、実行時のスキャンを回避します。
flowchart TD
A["SELECT * FROM orders<br/>WHERE order_date = '2024-02-15'"] --> B["クエリプランナー"]
B --> C{"パーティションプルーニング"}
C -->|"order_date が [2024-02-01, 2024-03-01) の範囲内"| D["orders_2024_02 ✓"]
C -->|"範囲外"| E["orders_2024_01 ✗"]
C -->|"範囲外"| F["orders_2024_03 ✗"]
C -->|"範囲外"| G["orders_default ✗"]
D --> H["1つのパーティションのみスキャン"]
(2) プルーニング効果の確認
-- パーティションプルーニングを有効化(デフォルトON)
SET enable_partition_pruning = on;
-- どのパーティションがスキャンされるか確認
EXPLAIN (COSTS OFF) SELECT * FROM orders
WHERE order_date = '2024-02-15';
Append
-> Seq Scan on orders_2024_02
Filter: (order_date = '2024-02-15'::date)
(3) プルーニング失敗��一般的な原因
| ケース | プルーニングさ��るか | 理由 |
|---|---|---|
WHERE order_date = '2024-02-15' |
✅ | 定数はプルーニング可能 |
WHERE order_date = $1(プリペアド文) |
✅ PG 11+ | ジェネリックパラメータプルーニング |
WHERE order_date = now() |
✅ | 安定関数はプルーニング可能 |
WHERE order_date = random_func() |
❌ | 揮発性関数はプルーニング不可 |
WHERE to_char(order_date, 'YYYY-MM') = '2024-02' |
❌ | 関数がパーティションキーをラップ |
7. 操作: パーティションメンテナンス
▶ サンプル: 古いパーティションをDETACHしてアーカイブ
-- 2023年1月のパーティションをデタッチ(データ損失なし)
ALTER TABLE orders DETACH PARTITION orders_2023_01;
-- これでスタンドアロンテーブルになり、安価なストレージに移動可能
ALTER TABLE orders_2023_01 SET TABLESPACE archive_tbs;
-- またはエクス��ートして削除
COPY orders_2023_01 TO '/archive/orders_2023_01.csv';
DROP TABLE orders_2023_01;
Output:
-- SQL statement executed successfully
▶ サンプル: 新しいパーティションをATTACH
-- 最初に新しいテーブルを作成
CREATE TABLE orders_2025_01 (LIKE orders INCLUDING DEFAULTS);
-- 検証用にCHECK制約を追加(ATTACHを高速化)
ALTER TABLE orders_2025_01
ADD CONSTRAINT orders_2025_01_check
CHECK (order_date >= '2025-01-01' AND order_date < '2025-02-01');
-- 親にアタッチ(短時間の排他ロックを取得)
ALTER TABLE orders ATTACH PARTITION orders_2025_01
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
Output:
CREATE TABLE
▶ サンプル: 同時実行DETACH(PG 14+)
-- 同時読み取りをブロックせずにDETACH
ALTER TABLE orders DETACH PARTITION orders_2023_01 CONCURRENTLY;
Output:
-- SQL statement executed successfully
▶ サンプル: パーティションインデックス戦略
-- 親に作成したインデックスは全パーティションに伝播
CREATE INDEX idx_orders_user_id ON orders (user_id);
-- 各パーティションが独自のインデックスを取得
\d orders_2024_01
Indexes:
"orders_2024_01_user_id_idx" btree (user_id)
| インデックス戦略 | 説明 |
|---|---|
| 親にインデックス作成 | 既存および将来の全パーティションに自動伝播 |
| 単一パーティションにインデックス作成 | そのパーティションのみ影響。ATTACH時に手動同期が必要 |
| 一意インデックス | パーティションキーを含める必要あり(パーティション間の一意性はパーティションキーが保証) |
▶ サンプル: 一意インデックスにはパーティションキーを含める必要あり
-- これは失敗: パーティションキーなしの一意制約
CREATE UNIQUE INDEX idx_orders_id ON orders (id);
-- エラー: 一意制約にパーティションキーを含める必要があります
-- これは成功: パーティションキー付き一意制約
CREATE UNIQUE INDEX idx_orders_id_date ON orders (id, order_date);
Output:
CREATE TABLE
8. 概念: テーブル継承(INHERITS)
(1) 継承の構文と特性
テーブル継承はPostgreSQL専用の機能で、宣言的パーティショニングより柔軟です。子テーブルは追加のカラムを持て、親の全値ドメインをカバーする必要もありません。
-- 親テーブル
CREATE TABLE people (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT
);
-- 子テーブルは全カラムを継承 + 追加カラム
CREATE TABLE employees (
salary NUMERIC(10,2),
dept TEXT,
hire_date DATE
) INHERITS (people);
-- 子に主キーを追加
ALTER TABLE employees ADD PRIMARY KEY (id);
(2) 継承と宣言的パーティショニングの比較
| 機能 | テーブル継承(INHERITS) | 宣言的パーティショニング |
|---|---|---|
| 子の追加カラム | ✅ 許可 | ❌ 同じ構造である必要あり |
| 親のデータ保存 | ✅ はい | ❌ 親は空のシェル |
| INSERTの自動ルーティング | ❌ トリガーが必要 | ✅ 自動 |
| パーティションプルーニング | ❌ CHECK制約が必要 | ✅ 自動 |
| テーブル間一意制約 | ❌ 単一テーブルのみ | ✅ パーティションキー付きでテーブル間可能 |
| 親への外部キー | ❌ 非サポート | ✅ サポート |
| 柔軟性 | 高 | 中 |
| 推奨ケース | 異種サブタイプ | 大テーブル分割最適化 |
(3) 継承クエリ: ONLYキーワード
-- 親 + 全子を検索
SELECT * FROM people;
-- 親のみを検索(子を含まない)
SELECT * FROM ONLY people;
-- 各行がどのテーブルから来たか確認
SELECT tableoid::regclass, * FROM people;
9. 操作: パーティション間クエリの最適化
▶ サンプル: パーティション統計の収集
-- 特定のパーティションを分析
ANALYZE orders_2024_02;
-- 親経由で全パーティションを分析
ANALYZE orders;
-- パーティション統計の確認
SELECT relname, n_live_tup, last_analyze
FROM pg_stat_user_tables
WHERE relname LIKE 'orders_%'
ORDER BY relname;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ サンプル: パーティショ▶間JOIN最適化
-- パーティション単位のJOIN(PG 12+)
SET enable_partitionwise_join = on;
SELECT o.id, o.total_amount, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.order_date BETWEEN '2024-01-01' AND '2024-03-31';
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
| パラメータ | デフォルト | 説明 |
|---|---|---|
enable_partition_pruning |
on | パーティションプルーニング |
enable_partitionwise_join |
off | パーティション単位JOIN |
enable_partitionwise_aggregate |
off | パーティション単位集計 |
constraint_exclusion |
partition | 制約除外(継承用) |
▶ サンプル: パーティション + 継承ハイブリッドケース
-- 基本監査テーブル
CREATE TABLE audit_log (
id BIGINT GENERATED ALWAYS AS IDENTITY,
table_name TEXT NOT NULL,
action TEXT NOT NULL,
changed_at TIMESTAMPTZ DEFAULT now()
) PARTITION BY RANGE (changed_at);
-- 月次パーティション、それぞれが型固有テーブルに継承される
CREATE TABLE audit_log_2024_01 PARTITION OF audit_log
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
-- 継承による特定監査タイプ用の追加カラム
CREATE TABLE audit_log_user_changes (
old_email TEXT,
new_email TEXT
) INHERITS (audit_log_2024_01);
Output:
CREATE TABLE
10. 総合サンプル
-- Eコマース注文の完全な月次パーティショニング設定
CREATE TABLE orders (
id BIGINT GENERATED ALWAYS AS IDENTITY,
user_id BIGINT NOT NULL,
order_date DATE NOT NULL,
total_amount NUMERIC(12,2) DEFAULT 0,
status TEXT DEFAULT 'pending',
created_at TIMESTAMPTZ DEFAULT now()
) PARTITION BY RANGE (order_date);
-- 2024年第1四半期のパーティションを作成
CREATE TABLE orders_2024_01 PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE orders_2024_02 PARTITION OF orders
FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
CREATE TABLE orders_2024_03 PARTITION OF orders
FOR VALUES FROM ('2024-03-01') TO ('2024-04-01');
CREATE TABLE orders_default PARTITION OF orders DEFAULT;
-- 一意インデックスにパーティションキーを含める
CREATE UNIQUE INDEX idx_orders_id_date ON orders (id, order_date);
CREATE INDEX idx_orders_user_date ON orders (user_id, order_date);
-- テストデータを挿入
INSERT INTO orders (user_id, order_date, total_amount, status) VALUES
(1001, '2024-01-05', 299.99, 'completed'),
(1001, '2024-02-14', 159.50, 'shipped'),
(1002, '2024-01-20', 450.00, 'completed'),
(1002, '2024-03-01', 89.00, 'pending'),
(1003, '2024-02-28', 1200.00,'completed');
-- パーティションプルーニングを確認
EXPLAIN (COSTS OFF) SELECT * FROM orders
WHERE order_date BETWEEN '2024-02-01' AND '2024-02-28';
-- 古いパーティションをデタッチしてアーカイブ
ALTER TABLE orders DETACH PARTITION orders_2024_01;
COPY orders_2024_01 TO '/archive/orders_2024_01.csv';
-- 次の四半期の新しいパーティションをアタッチ
CREATE TABLE orders_2024_04 (LIKE orders INCLUDING DEFAULTS);
ALTER TABLE orders_2024_04
ADD CONSTRAINT chk_2024_04
CHECK (order_date >= '2024-04-01' AND order_date < '2024-05-01');
ALTER TABLE orders ATTACH PARTITION orders_2024_04
FOR VALUES FROM ('2024-04-01') TO ('2024-05-01');
-- パーティションサイズを監視
SELECT relname,
pg_size_pretty(pg_total_relation_size(oid)) AS size,
reltuples::bigint AS row_estimate
FROM pg_class
WHERE relname LIKE 'orders_2024%' ORDER BY relname;
❓ よくある質問
📖 まとめ
- 宣言的パーティショニング(RANGE/LIST/HASH)はPG 10+で大規模テーブルを管理する標準的な方法
- RANGEパーティショニングは時系列、LISTは列挙カテゴリ、HASHは均等分散に適する
- パーティションプルーニングにより、クエリは関連パーティションのみをスキャン。条件はパーティションキーに直接指定が必要
- DETACH/ATTACHによりオンラインパーティションメンテナン��が可能。CONCURRENTLYでロックブロッキングを軽減
- パーティションテーブルの一意インデックスにパーティションキーを含める必要��り
- テーブル継承はより柔軟だが、自動ルーティングとプルーニングがなく、異種サブタイプのケースに適する
enable_partitionwise_join/aggregateでパーティション間クエリパフォーマンスをさらに向上可能
📝 練習問題
-
⭐ RANGEで四半期ごとにパーティション化された
paymentsテーブルを作成し、2024年の4四半期のパーティションを作成して、WHERE payment_date BETWEEN '2024-Q2'のプルーニング効果を確認してください。 -
⭐⭐ SaaSマルチテナントシステム用のLISTパーティショニング方式を設計:
tenant_idを4つのパーティションにハッシュ化し、作成文を記述して、異なるテナント間のデータ分離をテストしてください。 -
⭐⭐⭐ 年を入力として受け取り、
ordersテーブルに12の月次パーティションを自動作成し、CHECK制約を追加し、インデックスを作成し、前年1月のパーティションをアーカイブテーブルスペースにDETACHするストアドプロシージャを記述してください。INSERT、パーティション間クエリ、プルーニング確認の全フローをテストしてください。