PostgreSQL: PostgreSQLのパーティションテーブルとテーブル継承

最終更新:2026-08-26

1. 学習内容


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パーティション注文テーブルを作成

SQL
-- 親テーブル: パーティションキーのみ
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:

TEXT 📖 参照専用
CREATE TABLE

▶ サンプル: パーティションを自動作成する関数

SQL
-- 指定した年の月次パーティションを生成
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:

TEXT 📖 参照専用
CREATE TABLE

▶ サンプル: RANGEパーティションへの挿入と検索

SQL
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';
TEXT 📖 参照専用
  id  | user_id | order_date | total_amount | status
------+---------+------------+--------------+--------
 1002 |    1002 | 2024-02-20 |       159.50 | shipped
(1 row)

5. 操作: LISTおよびHASHパーティショニング

▶ サンプル: LISTパーティション — ユーザーテーブルを地域で分割

SQL
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:

TEXT 📖 参照専用
CREATE TABLE

▶ サンプル: HASHパーティショニング — ログテーブルを均等分散

SQL
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:

TEXT 📖 参照専用
CREATE TABLE

▶ サンプル: マルチレベルパーティショニング(RANGE + LIST)

SQL
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:

TEXT 📖 参照専用
CREATE TABLE

6. 概念: パーティションプルーニング

(1) プルーニングの原理

パーティションプルーニングにより、クエリプランナーは計画時に無関係なパーティションを除外し、実行時のスキャンを回避します。

100%
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) プルーニング効果の確認

SQL
-- パーティションプルーニングを有効化(デフォルトON)
SET enable_partition_pruning = on;

-- どのパーティションがスキャンされるか確認
EXPLAIN (COSTS OFF) SELECT * FROM orders
WHERE order_date = '2024-02-15';
TEXT 📖 参照専用
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してアーカイブ

SQL
-- 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:

TEXT 📖 参照専用
-- SQL statement executed successfully

▶ サンプル: 新しいパーティションをATTACH

SQL
-- 最初に新しいテーブルを作成
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:

TEXT 📖 参照専用
CREATE TABLE

▶ サンプル: 同時実行DETACH(PG 14+)

SQL
-- 同時読み取りをブロックせずにDETACH
ALTER TABLE orders DETACH PARTITION orders_2023_01 CONCURRENTLY;

Output:

TEXT 📖 参照専用
-- SQL statement executed successfully

▶ サンプル: パーティションインデックス戦略

SQL
-- 親に作成したインデックスは全パーティションに伝播
CREATE INDEX idx_orders_user_id ON orders (user_id);

-- 各パーティションが独自のインデックスを取得
\d orders_2024_01
TEXT 📖 参照専用
Indexes:
    "orders_2024_01_user_id_idx" btree (user_id)
インデックス戦略 説明
親にインデックス作成 既存および将来の全パーティションに自動伝播
単一パーティションにインデックス作成 そのパーティションのみ影響。ATTACH時に手動同期が必要
一意インデックス パーティションキーを含める必要あり(パーティション間の一意性はパーティションキーが保証)

▶ サンプル: 一意インデックスにはパーティションキーを含める必要あり

SQL
-- これは失敗: パーティションキーなしの一意制約
CREATE UNIQUE INDEX idx_orders_id ON orders (id);
-- エラー: 一意制約にパーティションキーを含める必要があります

-- これは成功: パーティションキー付き一意制約
CREATE UNIQUE INDEX idx_orders_id_date ON orders (id, order_date);

Output:

TEXT 📖 参照専用
CREATE TABLE

8. 概念: テーブル継承(INHERITS)

(1) 継承の構文と特性

テーブル継承はPostgreSQL専用の機能で、宣言的パーティショニングより柔軟です。子テーブルは追加のカラムを持て、親の全値ドメインをカバーする必要もありません。

SQL
-- 親テーブル
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キーワード

SQL
-- 親 + 全子を検索
SELECT * FROM people;

-- 親のみを検索(子を含まない)
SELECT * FROM ONLY people;

-- 各行がどのテーブルから来たか確認
SELECT tableoid::regclass, * FROM people;

9. 操作: パーティション間クエリの最適化

▶ サンプル: パーティション統計の収集

SQL
-- 特定のパーティションを分析
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:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ サンプル: パーティショ▶間JOIN最適化

SQL
-- パーティション単位の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:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)
パラメータ デフォルト 説明
enable_partition_pruning on パーティションプルーニング
enable_partitionwise_join off パーティション単位JOIN
enable_partitionwise_aggregate off パーティション単位集計
constraint_exclusion partition 制約除外(継承用)

▶ サンプル: パーティション + 継承ハイブリッドケース

SQL
-- 基本監査テーブル
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:

TEXT 📖 参照専用
CREATE TABLE

10. 総合サンプル

SQL
-- 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;

❓ よくある質問

Q パーティションテーブルの主キーにパーティションキーを含める必要がありますか?
A はい。PostgreSQLではパーティションテーブルの一意制約(主キーを含む)にパーティションキーカラムを含める必要があります。含めないとパーティション間の一意性が保証できません。
Q 既存の通常テーブルをパーティションテーブルに変換できますか?
A 直接はできません。新しいパーティション親テーブルを作成し、INSERT INTO...SELECTまたはATTACHでデータを移行する必要があります。
Q DEFAULTパーティションのリスクは何ですか?
A DEFAULTパーティションは他に一致しないすべてのデータを受け取ります。新しいパーティションの範囲がDEFAULTに既にあるデータと重なる場合、新しいパーティションのアタッチは失敗します。DEFAULTパーティションの中身を定期的に確認してください。
Q パーティション数に上限はありますか?
A PostgreSQLには厳格な上限はありませんが、各パーティションがプランナーのオーバーヘッドを追加します。単一テーブルのパーティション数を数百未満に抑えてください。1000パーティションを超えると計画時間が顕著に長くなる可能性があります。
Q テーブル継承で宣言的パーティショニングを置き換えられま��か?
A 一般的に推奨されませ��。継承は自動ルーティング、プルーニング、テーブル間制約がなく、メンテナンスコストが高いで��。異種の子テーブル(追加カラム)が必要な場合にのみ継承を使用してください。
Q HASHパーティショニングでパーティションプルーニングは使えますか?
A はい。WHERE条件にパーティションキーの等価比較が含まれていれ��、PGはデータがどのハッシュバケットに入るかを計算し、他のパーティションをプルーニングできます。
Q パーティション間をまたぐ行移動のUPDATEはテーブルをロックしますか?
A PG 11+はパーティション間UPDATEをサポートします。内部的にはDELETE + INSERTであり、テーブル全体をロックせず、2つのパーティションそれぞれで行ロックを取得します。

📖 まとめ


📝 練習問題

  1. ⭐ RANGEで四半期ごとにパーティション化されたpaymentsテーブルを作成し、2024年の4四半期のパーティションを作成して、WHERE payment_date BETWEEN '2024-Q2'のプルーニング効果を確認してください。

  2. ⭐⭐ SaaSマルチテナントシステム用のLISTパーティショニング方式を設計: tenant_idを4つのパーティションにハッシュ化し、作成文を記述して、異なるテナント間のデータ分離をテストしてください。

  3. ⭐⭐⭐ 年を入力として受け取り、ordersテーブルに12の月次パーティションを自動作成し、CHECK制約を追加し、インデックスを作成し、前年1月のパーティションをアーカイブテーブルスペースにDETACHするストアドプロシージャを記述してください。INSERT、パーティション間クエリ、プルーニング確認の全フローをテストしてください。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%