目次を表示する

RDB内部構造完全ガイド

パーティショニングとシャーディング ── テーブルを分割してスケールする

パーティショニングとシャーディング ── テーブルを分割してスケールする

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


パーティショニングとシャーディング — Range・Hash・Citus

この章で何ができるようになるか:パーティショニングの種類(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ハッシュ値均等分散・大規模