PostgreSQLのRANGE・LIST・HASHパーティショニングの違いと使い分け|選び方を具体例で解説

PostgreSQL

PostgreSQLで大きなテーブルをパーティショニングするとき、代表的な方式としてRANGELISTHASHがあります。どれも1つの論理的なテーブルを複数のパーティションへ分割する仕組みですが、適しているデータや検索条件は大きく異なります。

基本的な選び方は、日付や連続した数値の範囲で分けるならRANGE、地域や種別など明確な値のグループで分けるならLIST、値そのものに業務上の区切りがなく均等に分散させたいならHASHです。

ただし、単にデータを均等に分割すればよいわけではありません。PostgreSQLのパーティショニングでは、実際のWHERE条件、古いデータの削除方法、パーティションの追加頻度、データの偏りなどを考えてパーティションキーと方式を決めることが重要です。この記事ではRANGE・LIST・HASHの違いと使い分けを具体例で整理します。

PostgreSQLのパーティショニングとは

PostgreSQLの宣言的パーティショニングでは、大きなテーブルを複数の物理的な子テーブルであるパーティションに分割できます。アプリケーションからは親のパーティションテーブルに対してINSERTやSELECTを行い、PostgreSQLがパーティションキーに応じて適切なパーティションを扱います。

代表的な方式はRANGELISTHASHの3種類です。これらは「何を基準に行を振り分けるか」が異なります。

方式 分割基準 代表例
RANGE 連続した値の範囲 日付、日時、連番、価格帯
LIST 明示した値の集合 地域、国、種別、ステータス
HASH ハッシュ値の余り 顧客ID、ユーザーIDなどの均等分散

PostgreSQL公式ドキュメントでも、宣言的パーティショニングとしてRange、List、Hashの各方式が説明されています。[参照] PostgreSQL公式ドキュメント「Table Partitioning」

RANGEパーティショニングは日付・日時で分けるときに強い

RANGEは、パーティションキーの値を連続した範囲によって分割する方式です。実務で最も分かりやすい例は、注文履歴やログを月単位・年単位で分割するケースです。

たとえば注文テーブルを月ごとに分けるなら、親テーブルを次のように定義できます。

CREATE TABLE orders (
    order_id bigint,
    ordered_at timestamp NOT NULL,
    customer_id bigint,
    amount numeric
) PARTITION BY RANGE (ordered_at);

2026年8月分のパーティションは次のように作れます。

CREATE TABLE orders_202608
PARTITION OF orders
FOR VALUES FROM ('2026-08-01') TO ('2026-09-01');

FROM側は含み、TO側は含まない境界として扱われるため、この例では2026年8月1日以上、9月1日未満のデータが対象になります。

RANGEが特に有効なのは時系列データ

ログ、売上履歴、アクセス履歴、センサーデータ、監査履歴など、時間の経過とともにデータが増えるテーブルではRANGEが非常に自然です。

たとえば次のような検索が多いシステムを考えます。

SELECT *
FROM access_logs
WHERE logged_at >= '2026-08-01'
  AND logged_at < '2026-09-01';

月単位でRANGEパーティションを作成しておけば、条件から不要な月のパーティションを除外できる可能性があります。この仕組みをパーティションプルーニングと呼びます。

さらに、保存期間を過ぎた古いデータを月単位で廃棄するシステムでもRANGEは便利です。大量の古い行を1件ずつDELETEする代わりに、不要になったパーティションを切り離したり削除したりする運用を設計できます。

RANGEは連続する数値にも使える

RANGEは日付専用ではありません。連続する数値を区間に分けたい場合にも利用できます。

たとえば顧客番号を100万件単位で区切るなら、次のような設計も可能です。

CREATE TABLE customers (
    customer_id bigint NOT NULL,
    name text
) PARTITION BY RANGE (customer_id);
CREATE TABLE customers_1
PARTITION OF customers
FOR VALUES FROM (1) TO (1000001);

ただし、顧客IDが増加する順番とアクセスパターンに意味がなく、「単にデータを複数へ均等に分散したい」という目的なら、RANGEよりHASHのほうが適する場合があります。

LISTパーティショニングは明確なカテゴリで分けるときに強い

LISTは、パーティションへ入れる値を明示的に指定する方式です。値同士に連続した大小関係は必要ありません。

たとえば顧客を地域別に管理するなら、次のように定義できます。

CREATE TABLE customers (
    customer_id bigint,
    region text NOT NULL,
    name text
) PARTITION BY LIST (region);

関東と関西を別々のパーティションへ入れる例は次の通りです。

CREATE TABLE customers_kanto
PARTITION OF customers
FOR VALUES IN ('東京', '神奈川', '千葉', '埼玉');
CREATE TABLE customers_kansai
PARTITION OF customers
FOR VALUES IN ('大阪', '京都', '兵庫', '奈良');

このように、値そのものに業務上の意味があり、その分類単位で管理したい場合にLISTが適しています。

LISTが有効なのは地域・国・業務区分など

LISTに向いているのは、都道府県、国コード、店舗区分、事業部、データ種別など、あらかじめグループが明確なデータです。

たとえば世界各国のデータを地域単位に分けるなら、JPKRをアジア系パーティション、USCAを北米パーティションへ配置するといった設計が可能です。

また、「特定地域だけ別のストレージへ配置したい」「地域単位でバックアップや保守を行いたい」といった物理運用上の理由がある場合にも、業務上の分類がそのままパーティション境界になるLISTは理解しやすい方式です。

LISTではDEFAULTパーティションも検討できる

LISTパーティショニングでは、想定していない値がINSERTされた場合に、対応するパーティションがなければエラーになります。

たとえば都道府県コードを個別に定義していて、新しいコードや想定外の値が入ってくる可能性がある場合には、DEFAULTパーティションを用意できます。

CREATE TABLE customers_other
PARTITION OF customers DEFAULT;

これにより、ほかの既存パーティションに一致しない行を受け止められます。ただしDEFAULTへデータが蓄積したままにならないよう、運用上は定期的に内容を確認することも重要です。

HASHパーティショニングは均等に分散したいときに使う

HASHは、パーティションキーのハッシュ値を利用し、指定した数のパーティションへデータを振り分ける方式です。

たとえばユーザーIDを基準として4個のパーティションへ分散する場合、親テーブルは次のように定義できます。

CREATE TABLE user_events (
    user_id bigint NOT NULL,
    event_time timestamp,
    event_type text
) PARTITION BY HASH (user_id);

4分割するなら、MODULUS 4と異なるREMAINDERを指定します。

CREATE TABLE user_events_p0
PARTITION OF user_events
FOR VALUES WITH (MODULUS 4, REMAINDER 0);

CREATE TABLE user_events_p1
PARTITION OF user_events
FOR VALUES WITH (MODULUS 4, REMAINDER 1);

CREATE TABLE user_events_p2
PARTITION OF user_events
FOR VALUES WITH (MODULUS 4, REMAINDER 2);

CREATE TABLE user_events_p3
PARTITION OF user_events
FOR VALUES WITH (MODULUS 4, REMAINDER 3);

ユーザーIDそのものに「1~100万はA、100万~200万はB」と分ける業務上の意味がなく、単純に行を分散したい場合にはHASHが候補になります。

HASHの強みはデータの偏りを抑えやすいこと

RANGEでは、現在時刻に近い最新パーティションへINSERTが集中することがあります。LISTでも、特定カテゴリのデータ量が圧倒的に多ければパーティションサイズに偏りが生じます。

HASHでは十分に分散するキーを選べば、複数パーティションへ比較的均等に行を振り分けやすくなります。

たとえば数千万ユーザーのイベント履歴をユーザーIDによって8個または16個のパーティションへ分散する、といった用途が考えられます。

ただしHASHは「データを均等に分ける」ことには向いていても、「2025年以前のデータだけをまとめて削除する」といった時間軸に基づく管理には向いていません。

RANGE・LIST・HASHの選び方を一言で整理

3方式の選択は、パーティションキーの値にどのような意味があるかを考えると分かりやすくなります。

判断したいこと 向いている方式
期間や数値範囲で分けたい RANGE
月・年単位で古いデータを削除したい RANGE
地域・国・種類など明確なグループで分けたい LIST
特定カテゴリ単位で管理したい LIST
業務上の区切りはないが均等に分散したい HASH
IDなどを複数パーティションへ分散したい HASH

実務では、データの意味に自然な境界があるならRANGEまたはLIST、自然な境界がなく均等分散を目的とするならHASHと考えると選択しやすくなります。

最も重要なのは実際のWHERE条件に合うパーティションキーを選ぶこと

パーティショニングでは、方式だけでなく「どの列をパーティションキーにするか」が非常に重要です。

たとえば10億件のログをuser_idでHASH分割しても、実際の検索がほぼすべてWHERE logged_at BETWEEN ...という期間指定なら、時間条件だけでは多数のHASHパーティションを調べる必要が生じる可能性があります。

逆に、ログをlogged_atで月単位RANGE分割していれば、期間検索では不要な月のパーティションをプルーニングしやすくなります。

したがって、パーティションキーはデータの格納方法だけでなく、実際によく使う検索条件とセットで決めることが重要です。

パーティションプルーニングを意識する

PostgreSQLでは、クエリの条件からアクセス不要と判断できるパーティションを実行計画から除外できます。これがパーティションプルーニングです。

たとえば月単位RANGEパーティションで次の条件を指定した場合、対象月以外のパーティションを調べずに済む可能性があります。

SELECT sum(amount)
FROM orders
WHERE ordered_at >= DATE '2026-08-01'
  AND ordered_at < DATE '2026-09-01';

一方、パーティションキーと無関係な列だけで検索すれば、複数のパーティションを確認する必要が生じます。

パーティショニングを単なる「テーブルの分割」と考えるのではなく、よく使う検索条件によって不要なパーティションを除外できる設計にすることが性能面では重要です。

時系列データならRANGEが第一候補になりやすい理由

実務の大規模データでは、RANGE、とくに日時によるパーティショニングがよく候補になります。理由は検索だけではありません。

たとえばログを月単位で分けると、2024年分を保存期限到来時に古いパーティション単位で管理できます。また最新月だけ頻繁に更新され、過去データはほぼ読み取り専用になるというデータのライフサイクルにも合わせやすくなります。

「新しいデータが時間順に追加され、検索も期間指定が多く、古いデータを期間単位で削除する」という3条件がそろう場合、RANGEは非常に自然な選択です。

カテゴリで分けるならLISTが本当に安定しているか確認する

LISTではカテゴリを明示できる反面、その分類が頻繁に変化するシステムでは管理が面倒になることがあります。

たとえば店舗ごとに1パーティションを作っている状態で、毎月何百店舗も追加・閉鎖されるなら、多数のパーティションを管理する必要が生じます。

一方、「日本・北米・欧州」のように分類数が少なく比較的安定しているならLISTは扱いやすくなります。

つまりLISTは、値の種類が有限かつ比較的安定しており、その分類自体に運用上の意味がある場合に特に適しています。

HASHは期間削除や値単位の管理には向かない

HASHでは、どの行がどのパーティションへ入るかがハッシュによって決まります。そのため、人間から見て「このパーティションは2025年分」「このパーティションは関東」といった意味を持ちません。

たとえば2025年以前の全データを削除したい場合、それらの行はすべてのHASHパーティションに分散している可能性があります。このようなライフサイクル管理ではRANGEのほうが向いています。

HASHは、パーティション単位に業務上の意味を持たせる必要がなく、とにかくキーを基準に分散したい場合に利用する方式と考えると理解しやすくなります。

複数方式を組み合わせるサブパーティショニングも可能

PostgreSQLでは、パーティション自体をさらにパーティションテーブルにすることで、複数段階の分割も可能です。

たとえば最初に年月でRANGE分割し、その月のデータをさらに地域別LISTで分割する設計が考えられます。

orders
├─ 2026年8月(RANGE)
│  ├─ 東日本(LIST)
│  └─ 西日本(LIST)
└─ 2026年9月(RANGE)
   ├─ 東日本(LIST)
   └─ 西日本(LIST)

あるいは期間でRANGE分割した各パーティションを、顧客IDでHASH分散させる設計も技術的には可能です。

ただし階層を増やすほどDDLやインデックス、保守、監視などの運用は複雑になります。明確な必要性がなければ、最初は1段階のシンプルな構成から検討するほうが安全です。

パーティションを細かくしすぎない

「細かく分けるほど検索が速くなる」と考えて、数千・数万のパーティションを無計画に作るのは避けるべきです。

パーティション数が増えると、プランニングやDDL、VACUUM・ANALYZE、監視などの管理対象も増えます。また、1回の検索が多数のパーティションへまたがる場合は、細分化が必ずしもメリットになりません。

たとえば10年分のデータを1日単位で分割するのか、1か月単位で分割するのかは、保存期間、1日あたりの件数、クエリで指定される期間、削除単位などから決めます。

適切な粒度はデータ量とアクセスパターンによって異なるため、「月次が常に正解」「100パーティション以下なら安全」といった固定ルールで決めないことが重要です。

パーティショニングはインデックスの代わりではない

パーティショニングを導入すればインデックスが不要になるわけではありません。

たとえば注文テーブルを月ごとにRANGE分割していても、1か月分のパーティションに数千万行があり、その中からcustomer_idで検索するなら、各パーティション側に適切なインデックスが必要になる可能性があります。

パーティショニングは「どのパーティションを見るか」を絞り込み、インデックスは「そのパーティション内のどの行を見るか」を効率化する、と役割を分けて考えると分かりやすくなります。

UNIQUE制約・主キーを設計するときも注意する

パーティションテーブルへUNIQUE制約やPRIMARY KEYを定義するときには、パーティションキーとの関係にも注意が必要です。

PostgreSQLの宣言的パーティショニングでは、パーティションテーブル全体に対する一意性を各パーティション側の制約によって保証する都合上、親テーブルへ設定するUNIQUEやPRIMARY KEYの列には、パーティションキーのすべての列が含まれる必要があります。

たとえばordered_atでRANGE分割しているordersテーブルで、order_idだけを全パーティション横断のPRIMARY KEYとして宣言したい設計では、この制約が問題になる可能性があります。

そのため方式を決める際には、検索性能だけでなく主キーや一意性の要件も確認しておくことが重要です。

RANGE・LIST・HASH選択の具体例

実務を想定した代表的なケースを整理すると、方式の違いがより明確になります。

システム 特徴 第一候補
アクセスログ 期間検索・期間削除が多い RANGE
注文履歴 月別集計・古い履歴のアーカイブ RANGE
センサーデータ 日時順に大量追加 RANGE
地域別顧客データ 地域単位の検索・管理 LIST
国別データ 国・地域という明確な分類がある LIST
大量ユーザーイベント ユーザーIDで均等分散したい HASH
大量セッション情報 IDに業務上の区切りがない HASH

この表はあくまで出発点です。同じ注文履歴でも、検索が顧客ID中心で期間削除をしないシステムなら別の方式が適する可能性があります。

方式を決める前に確認したい5つのポイント

パーティショニング方式を選ぶときは、次の観点から実際のワークロードを確認すると判断しやすくなります。

  1. 最も多いWHERE条件は何か
  2. データには日付・地域など自然な境界があるか
  3. 古いデータをどの単位で削除・アーカイブするか
  4. パーティション間のデータ量に大きな偏りが生じないか
  5. パーティション数が将来どの程度まで増えるか

たとえば「日時で検索し、1年経過したデータを月単位で削除する」と分かっているならRANGEの合理性は高くなります。

一方、「どのユーザーも同じようにアクセスされ、期間単位の削除も不要」という要件なら、ユーザーIDによるHASH分割を検討できます。

EXPLAINでパーティションプルーニングを確認する

設計したパーティショニングが実際のSQLで有効に働いているかは、EXPLAINEXPLAIN ANALYZEで確認できます。

たとえばRANGEパーティションに対して期間条件を指定し、対象期間以外のパーティションが実行計画から除外されているかを確認します。

EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE ordered_at >= TIMESTAMP '2026-08-01 00:00:00'
  AND ordered_at < TIMESTAMP '2026-09-01 00:00:00';

期待したプルーニングが起きない場合は、パーティションキー、WHERE条件、データ型、式の書き方などを確認します。

方式を選んだだけで性能向上が保証されるわけではなく、実際のクエリと実行計画による検証まで含めてパーティション設計と考えることが大切です。

まとめ:自然な境界ならRANGE・LIST、均等分散ならHASH

PostgreSQLのRANGE・LIST・HASHパーティショニングは、それぞれ目的が異なります。日付や数値の連続した範囲で分けるならRANGE、地域や種類など明確な値の集合で分けるならLIST、値に自然な境界がなく均等に分散させたいならHASHが基本的な使い分けです。

特に時系列データでは、期間検索、パーティションプルーニング、古いデータの削除・アーカイブとの相性からRANGEが有力です。LISTは業務上意味のある有限のカテゴリ、HASHはIDなどを複数領域へ分散したい場合に向いています。

ただし、方式名だけで決めるのではなく、普段のWHERE条件、データの偏り、保存期間、削除単位、主キー・一意性、将来のパーティション数まで考慮する必要があります。

最終的には「どの列でデータを分ければ、普段の検索で不要なパーティションを効率よく除外でき、運用上も管理しやすいか」を基準に選び、EXPLAIN ANALYZEなどで実際の実行計画を確認することが、PostgreSQLのパーティショニングを効果的に使うポイントです。

コメント

タイトルとURLをコピーしました