パーティショニングとシャーディング ── テーブルを分割してスケールする
このレイヤーの役割:単一テーブルが大きくなりすぎたときに分割する。パーティショニングは1台のDB内、シャーディングは複数のDB間。

この章で何ができるようになるか:パーティショニングの種類(Range / List / Hash)を使い分け、シャーディングの設計判断(キー選定・ホットスポット回避)ができるようになる。
パーティショニング:1つのテーブルを論理的に分割
PostgreSQL 10+ のネイティブパーティショニング。
Range パーティショニング
CREATE TABLE events (
id BIGSERIAL,
created_at TIMESTAMP NOT NULL,
event_type TEXT,
payload JSONB
) PARTITION BY RANGE (created_at);
CREATE TABLE events_2026_01 PARTITION OF events
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE events_2026_02 PARTITION OF events
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
CREATE TABLE events_2026_03 PARTITION OF events
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
-- クエリは親テーブルに対して行う
SELECT * FROM events WHERE created_at >= '2026-02-15';
-- → プランナーが events_2026_02 と events_2026_03 だけをスキャン(Partition Pruning)
向いているケース:時系列データ(ログ、イベント、センサーデータ)。古いパーティションの一括削除が瞬時(DROP TABLE events_2025_01)。
List パーティショニング
CREATE TABLE orders (
id BIGSERIAL,
region TEXT NOT NULL,
total NUMERIC
) PARTITION BY LIST (region);
CREATE TABLE orders_jp PARTITION OF orders FOR VALUES IN ('JP');
CREATE TABLE orders_us PARTITION OF orders FOR VALUES IN ('US');
CREATE TABLE orders_eu PARTITION OF orders FOR VALUES IN ('DE', 'FR', 'GB');
Hash パーティショニング
CREATE TABLE sessions (
id BIGSERIAL,
user_id BIGINT NOT NULL,
data JSONB
) PARTITION BY HASH (user_id);
CREATE TABLE sessions_0 PARTITION OF sessions FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE sessions_1 PARTITION OF sessions FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE sessions_2 PARTITION OF sessions FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE sessions_3 PARTITION OF sessions FOR VALUES WITH (MODULUS 4, REMAINDER 3);
-- → user_id を均等に4分割
Partition Pruning:不要なパーティションをスキップ
EXPLAIN SELECT * FROM events WHERE created_at = '2026-02-15';
-- Append (cost=...)
-- -> Seq Scan on events_2026_02 (cost=...) ← このパーティションだけスキャン
-- Filter: (created_at = '2026-02-15')
-- events_2026_01 と events_2026_03 はプランナーが除外(Pruning)
Pruning が効かないケース:
-- ❌ 関数を噛ませるとプルーニングが効かないことがある
SELECT * FROM events WHERE EXTRACT(MONTH FROM created_at) = 2;
-- → 全パーティションをスキャン
-- ✅ 直接の比較ならプルーニングが効く
SELECT * FROM events WHERE created_at >= '2026-02-01' AND created_at < '2026-03-01';
パーティショニングのメリットとコスト
メリット:
✅ 古いデータの削除が高速(パーティションを DROP するだけ)
✅ VACUUM の対象が小さくなる(パーティション単位)
✅ Partition Pruning でクエリが高速化
✅ パーティションごとに別のテーブルスペース(SSD / HDD)に配置可能
コスト:
❌ プランナーのオーバーヘッド(パーティション数が多いと計画時間が増加)
❌ パーティションキーを含まないクエリは全パーティションをスキャン
❌ パーティション間のユニーク制約にはパーティションキーが必要
❌ 外部キーの制約が制限される
シャーディング:複数のDBサーバーに分割
パーティショニングは1台のDB内の話。テーブルが1台のDBに収まらなくなったらシャーディングが必要。
シャーディングキーの選定
✅ 良いシャーディングキー:
- カーディナリティが高い(user_id, order_id)
- クエリの WHERE 句に頻出する
- データが均等に分布する
❌ 悪いシャーディングキー:
- カーディナリティが低い(country: 数十種類 → 偏りが大きい)
- クエリで使われない(シャードを特定できず全シャードにクエリ)
- ホットスポットが生じる(auto_increment ID → 最新シャードに集中)
シャーディング戦略
Range-based:
user_id 1-1000000 → Shard 1
user_id 1000001-2000000 → Shard 2
→ 範囲検索が1シャードで完結
→ ホットスポット問題(新規ユーザーが最後のシャードに集中)
Hash-based:
hash(user_id) % 4 → Shard 0-3
→ 均等に分散
→ 範囲検索が全シャードに分散(不利)
→ シャード追加時にリバランスが必要
Directory-based:
マッピングテーブルで user_id → shard_id を管理
→ 柔軟にリバランス可能
→ マッピングテーブル自体がボトルネック・SPOF になりうる
クロスシャードクエリ
シャーディングの最大の課題: 複数シャードにまたがるクエリ
例: 「全ユーザーの注文を金額順にソート」
→ 全シャードにクエリを投げ、結果をアプリ側でマージソート
→ レイテンシ = 最も遅いシャードの応答時間
例: 「ユーザーAの注文とユーザーBの注文をJOIN」
→ AとBが異なるシャードにいると、クロスシャード JOIN が必要
→ アプリ側で2つのクエリ結果を結合
Citus:PostgreSQL のシャーディング拡張
-- Citus: PostgreSQL を分散データベースにする拡張
-- コーディネーターノード + ワーカーノード
-- テーブルを分散テーブルにする
SELECT create_distributed_table('orders', 'user_id');
-- → user_id をシャーディングキーとして自動分散
-- クエリは通常の SQL で書ける
SELECT user_id, SUM(total) FROM orders GROUP BY user_id;
-- → Citus が各シャードに部分クエリを投げ、結果をマージ
まとめ
| 手法 | スコープ | 分割単位 | 主な用途 |
|---|---|---|---|
| Range パーティション | 1台のDB | 値の範囲 | 時系列データ |
| List パーティション | 1台のDB | 値のリスト | 地域別・カテゴリ別 |
| Hash パーティション | 1台のDB | ハッシュ値 | 均等分散 |
| Range シャーディング | 複数DB | 値の範囲 | 大規模スケールアウト |
| Hash シャーディング | 複数DB | ハッシュ値 | 均等分散・大規模 |