【PostgreSQL】ハッシュパーティションの設定方法と性能確認

記事内に広告が含まれています。

postgreSQL 16 にて確認。

パーティションとは

1つのテーブルを論理的に複数テーブルに分割して管理する機能のこと。検索対象レコードを減らすことができるため、検索の高速化に有効。

PostgreSQL では継承機能をもとに実装されているため、パーティションテーブルには親子関係が成立して管理される。

-- パーティショニングの概念図
┌────────────────────────────┐
│ 👥 親テーブル (論理テーブル)                           │
│                                                        │
│ ※実データは持たない「窓口」。                         │
│   スキーマ定義やインデックス、IDENTITY列のルールを持つ │
└────────────────────────────┘
                     │            
     ┌──────────────┐
     │  PostgreSQLがハッシュ値を  │
     │  計算して自動でルーティング│
     ▼                           ▼
┌──────────────┐    ┌────────────┐
│ 📦 パーティション1         │    │ 📦 パーティション2     │
│ (物理的な実体テーブル)     │    │ (物理的な実体テーブル) │
└──────────────┘    └────────────┘ 
┌──────────────┐    ┌────────────┐
│ 📦 パーティション3         │    │ 📦 パーティション4     │
│ (物理的な実体テーブル)     │    │ (物理的な実体テーブル) │
└──────────────┘    └────────────┘

パーティションの概要

  • 後から変更不可
    • テーブル作成時にパーティションに分ける必要がある。(後から ALTER TABLE などで パーティション分割することはできない)

It is not possible to turn a regular table into a partitioned table or vice versa.

5.12. Table Partitioning
5.12.Table Partitioning # 5.12.1. Overview 5.12.2. Declarative Partitioning 5.12.3. Partitioning Using Inheritance 5.12....
 

  • 3つの分割方法
    • 範囲パーティション
      • 日付などの範囲によってパーティションテーブルを分ける
    • リストパーティション
      • 各パーティションに現れるキーの値を明示的に列挙することでテーブルをパーティションに分割
    • ハッシュパーティション
      • キーにハッシュ関数をかけて、指定した数(MODULUS)で割り算し、残りの値(REMAINDER)でパーティションを分ける
  • 検索時のコードは共通
    • テーブル検索のコマンドはパーティションあり/なしによらず変わらない。
-- テーブルがパーティションあってもなくても同じコマンドで検索可能
select * from テーブル
where ...

パーティションのデモ、性能確認

テスト用のテーブルに1,000万レコードを投入し、検索時の性能を確認する。

  • パーティションなしのテーブル logs_no_hash
  • パーティションありのテーブル logs_hashed
    • ハッシュパーティションを利用。4つのパーティションに分割する。 親テーブルのuser_id カラムの値に対し、4で割った余りを基準とする。( logs_hash_0 ~ logs_hash_3 )
-- 1. 比較用の「パーティションなし」テーブル(前回と同じ)
CREATE TABLE logs_no_hash (
    id SERIAL,
    log_date DATE NOT NULL,
    user_id INT NOT NULL,
    message TEXT
);

-- 2. 「Hashパーティションあり」の親テーブル(user_id でハッシュ分割)
CREATE TABLE logs_hashed (
    id INT GENERATED ALWAYS AS IDENTITY,
    log_date DATE NOT NULL,
    user_id INT NOT NULL,
    message TEXT
) PARTITION BY HASH (user_id);

-- 4つの子テーブル(パーティション)を作成
-- MODULUS(割る数)と REMAINDER(余り 0〜3)で割り振りを指定します
CREATE TABLE logs_hash_0 PARTITION OF logs_hashed FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE logs_hash_1 PARTITION OF logs_hashed FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE logs_hash_2 PARTITION OF logs_hashed FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE logs_hash_3 PARTITION OF logs_hashed FOR VALUES WITH (MODULUS 4, REMAINDER 3);


-- パーティションなしテーブルへ1000万件挿入
INSERT INTO logs_no_hash (log_date, user_id, message)
SELECT 
    '2026-01-01'::DATE + (random() * 90)::INT,
    (random() * 100000)::INT + 1, -- user_id を 1〜100,000 で分散
    md5(random()::TEXT)
FROM generate_series(1, 10000000);

-- Hashパーティションありテーブルへ1000万件挿入
INSERT INTO logs_hashed (log_date, user_id, message)
SELECT 
    '2026-01-01'::DATE + (random() * 90)::INT,
    (random() * 100000)::INT + 1,
    md5(random()::TEXT)
FROM generate_series(1, 10000000);

結果確認

テーブルを4分割しており、検索対象も 1/4 になっているため、時間も 1/4 程度に短くなっていることを確認できた。

テーブル構成実行時間 (Execution Time)パフォーマンス
パーティションなし
logs_no_hash
848.598 ms (約850ms)基準
パーティションあり (4分割)
logs_hashed
204.607 ms (約200ms)約4倍高速化

パーティションなしの場合: 実行時間は ~850ms

 explain analyze select count(*) from logs_hashed where user_id=1000;   

                                                                QUERY PLAN                                                                 
-------------------------------------------------------------------------------------------------------------------------------------------
 Finalize Aggregate  (cost=146541.68..146541.69 rows=1 width=8) (actual time=831.226..844.273 rows=1 loops=1)
   ->  Gather  (cost=146541.47..146541.68 rows=2 width=8) (actual time=831.050..844.258 rows=3 loops=1)
         Workers Planned: 2
         Workers Launched: 2
         ->  Partial Aggregate  (cost=145541.47..145541.48 rows=1 width=8) (actual time=798.103..798.104 rows=1 loops=3)
               ->  Parallel Seq Scan on logs_no_hash  (cost=0.00..145541.36 rows=42 width=0) (actual time=51.954..797.948 rows=33 loops=3)
                     Filter: (user_id = 1000)
                     Rows Removed by Filter: 3333300
 Planning Time: 0.447 ms
 JIT:
   Functions: 14
   Options: Inlining false, Optimization false, Expressions true, Deforming true
   Timing: Generation 6.551 ms, Inlining 0.000 ms, Optimization 4.771 ms, Emission 34.450 ms, Total 45.772 ms
 Execution Time: 848.598 ms

パーティションありの場合: 実行時間は ~200ms

 explain analyze select count(*) from logs_hashed where user_id=1000;     


                                                                   QUERY PLAN                                                                     
---------------------------------------------------------------------------------------------------------------------------------------------------
 Finalize Aggregate  (cost=37590.10..37590.11 rows=1 width=8) (actual time=190.976..204.210 rows=1 loops=1)
   ->  Gather  (cost=37589.89..37590.10 rows=2 width=8) (actual time=190.865..204.201 rows=3 loops=1)
         Workers Planned: 2
         Workers Launched: 2
         ->  Partial Aggregate  (cost=36589.89..36589.90 rows=1 width=8) (actual time=76.024..76.025 rows=1 loops=3)
               ->  Parallel Seq Scan on logs_hash_0 logs_hashed  (cost=0.00..36589.79 rows=41 width=0) (actual time=6.734..76.001 rows=33 loops=3)
                     Filter: (user_id = 1000)
                     Rows Removed by Filter: 837970
 Planning Time: 2.183 ms
 Execution Time: 204.607 ms

参考

公式ドキュメント

5.12. Table Partitioning
5.12.Table Partitioning # 5.12.1. Overview 5.12.2. Declarative Partitioning 5.12.3. Partitioning Using Inheritance 5.12....

SQl,DB
スポンサーリンク