Linuxサーバー上でPostgreSQLを運用していると、レコードが数千万行を超えたあたりからこうした問題に直面することがあります。そのときの解決策の一つが、PostgreSQL 10以降で正式実装された宣言的パーティショニングです。
この記事では、RHEL 9系/Rocky Linux 9を想定環境として、PostgreSQLのRANGE・LIST・HASHの3方式でパーティションを設定する実践的な手順を解説します。パーティション作成から主キー・インデックスの設定、古いパーティションのDETACHアーカイブ、よくあるトラブルの対処まで順を追って説明します。
この記事のポイント
・RANGEは日付ログ、LISTはカテゴリ分類、HASHは均等負荷分散と使い分ける
・親テーブルをPARTITION BYで作成し、子テーブルをPARTITION OFで追加する
・PostgreSQL 11以降は親テーブルへのINDEX作成で全パーティションへ自動継承される
・DETACH PARTITIONで古いパーティションをクエリ対象から切り離してアーカイブできる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
PostgreSQLの宣言的パーティショニングとは
PostgreSQLのパーティショニングとは、論理的に1つのテーブルを複数の物理テーブル(パーティション)に分割する機能です。PostgreSQL 10でネイティブの宣言的パーティショニングが実装され、従来のトリガー・ルール方式よりはるかに簡潔に設定できるようになりました。パーティショニングを使う主な目的は3つです。
・クエリの高速化: WHERE句でパーティションキーを指定すると、対象パーティションのみスキャンされる(パーティションプルーニング)
・バルク削除の高速化: 古い月次パーティションをDROP TABLEするだけで大量行を即座に削除できる
・ディスク管理の分散: パーティションごとにテーブルスペースを指定し、異なるディスクに配置できる
宣言的パーティショニングが使えるのはPostgreSQL 10以降です。バージョンが上がるにつれて機能が拡充されており、11でのインデックス自動継承、12でのパーティションプルーニング改善と段階的に強化されています。
パーティション方式の選び方
PostgreSQLのパーティション方式は3種類あります。| 方式 | 分割基準 | 典型的なユースケース |
|---|---|---|
| RANGE | 連続する値の範囲 | 日付・タイムスタンプ(月別・年別ログ) |
| LIST | 値のリスト(列挙) | 地域コード・ステータス・カテゴリ |
| HASH | ハッシュ値による均等分割 | 均等負荷分散・ランダムIDの分散格納 |
RANGEパーティションを設定する手順
RANGEパーティションは、日付ログやトランザクションデータなど「時系列で増えるデータ」に最も広く使われます。ここでは注文ログテーブルを月別に分割する例を示します。1. パーティション親テーブルを作成する
# 宣言的パーティショニング(RANGE)の親テーブル CREATE TABLE orders ( id BIGSERIAL, order_date DATE NOT NULL, customer_id INTEGER NOT NULL, amount NUMERIC(12,2) ) PARTITION BY RANGE (order_date);
2. 月別パーティションを追加する
# 2026年1月分のパーティション CREATE TABLE orders_2026_01 PARTITION OF orders FOR VALUES FROM ('2026-01-01') TO ('2026-02-01'); # 2026年2月分のパーティション CREATE TABLE orders_2026_02 PARTITION OF orders FOR VALUES FROM ('2026-02-01') TO ('2026-03-01'); # 2026年3月分のパーティション CREATE TABLE orders_2026_03 PARTITION OF orders FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
3. データを投入して確認する
# データの投入(親テーブルに挿入すると自動的に振り分けられる) INSERT INTO orders (order_date, customer_id, amount) VALUES ('2026-01-15', 100, 5000.00); # どのパーティションに入ったか確認 SELECT tableoid::regclass AS partition_name, * FROM orders WHERE order_date = '2026-01-15';
partition_name | id | order_date | customer_id | amount -----------------+----+------------+-------------+--------- orders_2026_01 | 1 | 2026-01-15 | 100 | 5000.00
4. デフォルトパーティションを設定する(推奨)
定義済みパーティションの範囲外のデータが挿入されるとエラーになります。デフォルトパーティションを設けておくと、想定外の値を受け取ってもエラーを防げます。# デフォルトパーティション(範囲外データの受け皿) CREATE TABLE orders_default PARTITION OF orders DEFAULT;
LISTパーティションを設定する手順
LISTパーティションは、地域コードやステータス値など「取りうる値が決まっている」カラムに向いています。1. 親テーブルとLISTパーティションを作成する
# 地域別の顧客テーブル(LIST分割) CREATE TABLE customers ( id BIGSERIAL, region VARCHAR(20) NOT NULL, name VARCHAR(100) ) PARTITION BY LIST (region); # 東日本パーティション CREATE TABLE customers_east PARTITION OF customers FOR VALUES IN ('tokyo', 'kanagawa', 'chiba', 'saitama'); # 西日本パーティション CREATE TABLE customers_west PARTITION OF customers FOR VALUES IN ('osaka', 'kyoto', 'hyogo', 'nara'); # その他のデフォルトパーティション CREATE TABLE customers_other PARTITION OF customers DEFAULT;
HASHパーティションを設定する手順
HASHパーティションは、特定の値の偏りがないランダムなIDやUUIDで均等にデータを分散させたい場合に適しています。大量INSERTの負荷を複数テーブルに分散する用途でも利用されます。1. HASHパーティションの作成
# ユーザーイベントログ(HASH分割・4分割) CREATE TABLE user_events ( id BIGSERIAL, user_id BIGINT NOT NULL, event_time TIMESTAMPTZ DEFAULT NOW(), event_type VARCHAR(50) ) PARTITION BY HASH (user_id); # MODULUS(総分割数)と REMAINDER(余り)で各パーティションの担当範囲を指定 CREATE TABLE user_events_0 PARTITION OF user_events FOR VALUES WITH (MODULUS 4, REMAINDER 0); CREATE TABLE user_events_1 PARTITION OF user_events FOR VALUES WITH (MODULUS 4, REMAINDER 1); CREATE TABLE user_events_2 PARTITION OF user_events FOR VALUES WITH (MODULUS 4, REMAINDER 2); CREATE TABLE user_events_3 PARTITION OF user_events FOR VALUES WITH (MODULUS 4, REMAINDER 3);
パーティションにインデックスを設定する
パーティションテーブルのインデックス設計には2つのアプローチがあります。1. 親テーブルへのインデックス作成(PostgreSQL 11以降推奨)
PostgreSQL 11以降では、親テーブルにインデックスを作成するとすべての既存パーティション・新規追加パーティションへ自動的に継承されます。# 親テーブルへのインデックス作成 → 全パーティションへ自動継承 CREATE INDEX idx_orders_customer_id ON orders (customer_id); CREATE INDEX idx_orders_order_date ON orders (order_date);
2. 特定パーティションのみにインデックスを追加する場合
# 1月パーティションのみに追加インデックスを作成する場合 CREATE INDEX idx_orders_2026_01_amount ON orders_2026_01 (amount);
# NG: パーティションキー(order_date)が主キーに含まれていない ALTER TABLE orders ADD PRIMARY KEY (id); # OK: パーティションキーを主キーに含める ALTER TABLE orders ADD PRIMARY KEY (id, order_date);
パーティションの確認・追加・削除
1. パーティション一覧の確認
# pg_inherits でパーティション一覧を確認する SELECT parent.relname AS parent_table, child.relname AS partition_name 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';
parent_table | partition_name --------------+------------------ orders | orders_2026_01 orders | orders_2026_02 orders | orders_2026_03 orders | orders_default
2. 翌月のパーティションを事前追加する
RANGEパーティションでは、対応するパーティションが存在しない状態でINSERTするとエラーになります。cron等で毎月末に翌月分を追加する運用が推奨されます。# 4月分のパーティションを事前に追加する例 CREATE TABLE orders_2026_04 PARTITION OF orders FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
3. 古いパーティションをDETACHしてアーカイブする
DETACHすると、そのパーティションが親テーブルから切り離されて通常の独立テーブルになります。データを保持したままクエリ対象から外せるため、アーカイブ用途に最適です。# 2026年1月パーティションを親テーブルから切り離す ALTER TABLE orders DETACH PARTITION orders_2026_01; # 切り離し後はpg_dumpで単独バックアップ可能 # 完全に削除する場合は通常のDROPを使う DROP TABLE orders_2026_01;
よくあるトラブルと対処法
「no partition of relation found for row」が出てINSERTが失敗する
対応するパーティションが定義されていない値を挿入しようとしたときに発生します。デフォルトパーティションを作成するか、エラーが発生した値の範囲に合うパーティションを追加してください。# デフォルトパーティションを後から追加する場合 CREATE TABLE orders_default PARTITION OF orders DEFAULT;
EXPLAINでパーティションプルーニングが効いていない
全パーティションがスキャン対象になっている場合、WHERE句のカラム型とパーティションキーの型が一致しているか確認します。また `enable_partition_pruning` が off になっていないかも確認してください。# パーティションプルーニングが効いているか確認 EXPLAIN SELECT * FROM orders WHERE order_date = '2026-01-15';
QUERY PLAN ----------------------------------------------------- Append (cost=0.00..X.XX rows=X width=XX) -> Seq Scan on orders_2026_01 (cost=...) Filter: (order_date = '2026-01-15'::date)
「ERROR: insufficient columns in PRIMARY KEY constraint」が出る
パーティション化テーブルの主キーには、パーティションキーを必ず含める必要があります。# NG: パーティションキーが主キーに含まれていない ALTER TABLE orders ADD PRIMARY KEY (id); # OK: パーティションキー(order_date)を主キーに含める ALTER TABLE orders ADD PRIMARY KEY (id, order_date);
本記事のまとめ
PostgreSQLの宣言的パーティショニングをRANGE・LIST・HASHの3方式で解説しました。主なポイントをまとめます。| 操作 | SQLの書き方 | ポイント |
|---|---|---|
| RANGE親テーブル作成 | CREATE TABLE t (...) PARTITION BY RANGE (列) |
親テーブルにはデータが入らない |
| パーティション追加 | CREATE TABLE t_p PARTITION OF t FOR VALUES FROM (...) TO (...) |
TO値は含まない(未満) |
| デフォルトパーティション | CREATE TABLE t_def PARTITION OF t DEFAULT |
範囲外INSERTのエラー防止に必須 |
| インデックス自動継承 | CREATE INDEX ON 親テーブル (列) |
PostgreSQL 11以降で全パーティションへ自動継承 |
| パーティション確認 | SELECT ... FROM pg_inherits ... |
pg_classと結合して一覧取得 |
| アーカイブ切り離し | ALTER TABLE t DETACH PARTITION t_p |
PG14以降はCONCURRENTLYでロック回避 |
>> Linux Master Pro Seminarの詳細はこちら
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:MySQLのSHOW PROCESSLISTでアクティブセッションを確認する方法|長時間クエリの特定とKILL文での強制終了手順
- この記事の属するカテゴリ:データーベース管理へ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます