15. パーティショニングとシャーディング
一つのノードへデータ、書き込み処理量、読み取り負荷が収まらなくなると、データを複数のパーティションへ分けます。分割により容量を増やせますが、リクエスト振り分け、パーティション横断クエリ、再均衡化、トランザクションという新しいコストが生まれます。
シャーディングの難しさは「どう分けるか」より、「分けた後にクエリとデータが変化し続けること」です。
この章で答える問い
Section titled “この章で答える問い”- 水平/垂直分割は何を分けるのか
- 範囲/ハッシュ分割はクエリ局所性と負荷分散をどう交換するのか
- 良いシャードキーはどのような性質を持つのか
- グローバルセカンダリインデックスは書き込みと整合性をどう複雑にするのか
- 高負荷パーティションとデータ偏りをどう検出・緩和するのか
- オンライン再シャーディング中の読み取り/書き込みをどう正しく振り分けするのか
分割とシャーディング
Section titled “分割とシャーディング”用語は製品によって異なります。本書では:
- 分割:一つの論理表/データ集合を複数部分へ分割する一般概念
- シャーディング:パーティションを複数ノード/障害領域へ配置する水平分割
一つのDBインスタンス内の表パーティションも、複数ノードのシャードも、キーから対象パーティションを決める点は共通します。ただしシャーディングではネットワーク、部分障害、分散トランザクションが加わります。
行をパーティションキーで分けます。
flowchart TB
Orders["注文"] --> P1["パーティションA<br/>顧客0..999"]
Orders --> P2["パーティションB<br/>顧客1000..1999"]
Orders --> P3["パーティションC<br/>顧客2000.."]
各パーティションは同じ列スキーマを持ち、行集合が異なります。
利点:
- データ容量と書き込みを分散
- パーティション刈り込み
- 古いパーティションのアーカイブ/削除
- 保守単位を小さくする
課題:
- パーティション横断クエリ
- 偏り
- パーティションメタデータ
- 再均衡化
列グループまたは機能単位で分けます。
users_core(id, メール, name)users_profile(id, bio, avatar_blob, preferences)頻繁に読む小規模列と大規模/低負荷列を分け、行幅とI/Oを減らせます。別サービス/DBへ分ける場合は結合とトランザクションを失い、アプリケーションで統合するコストが増えます。
正規化による表分割と似ていますが、垂直分割はアクセスパターンや物理配置を主目的にする場合があります。
キーの連続範囲をパーティションへ割り当てます。
P1: 2026-01-01 <= ordered_at < 2026-02-01P2: 2026-02-01 <= ordered_at < 2026-03-01P3: 2026-03-01 <= ordered_at < 2026-04-01利点:
- 範囲クエリが少数パーティションへ局所化
- 時系列データの削除/アーカイブ
- 整列による局所性
- 分割境界を理解しやすい
欠点:
- 単調キーで最新パーティションへ書き込み集中
- 範囲大きさと負荷が不均等
- 高負荷キー 範囲
- 境界管理
時系列では書き込み集中箇所を許容しつつ、バケット内をハッシュ再分割する方法があります。
ハッシュ分割
Section titled “ハッシュ分割”ハッシュ(キー) mod Nなどでパーティションを選びます。
flowchart LR
K["customer_id"] --> H["ハッシュ"]
H --> P1["シャード0"]
H --> P2["シャード1"]
H --> P3["シャード2"]
利点:
- 一様キーなら負荷を分散しやすい
- 単一キー検索の振り分けが明確
- 単調キーの書き込みを分散
欠点:
- 範囲局所性を失う
- パーティション数変更で多くのキーが移動
- 高負荷キー一つは分散できない
- ハッシュ前のキーの分布とリクエスト分布は別
一様行数でも、有名顧客へ通信量が集中すれば高負荷シャードになります。
コンシステントハッシュ法
Section titled “コンシステントハッシュ法”単純な「ハッシュ値 mod N」では、Nの変更時に多くのキーの割り当てが変わります。コンシステントハッシュ法はノードとキーをリング上へ配置し、ノード追加・削除時の移動範囲を限定します。
flowchart LR
A["ノードA"] --> B["ノードB"]
B --> C["ノードC"]
C --> A
仮想ノード/トークンを各物理ノードへ複数割り当てると:
- 負荷を細かく分散
- 異なるノード容量に応じた比例配分
- 移動単位を小さくする
ただし仮想ノード数が多いほどメタデータとレプリカ配置が複雑になります。
近年の分散DBでは範囲パーティションを自動分割し、配置管理機構が範囲をノードへ割り当てる方式もあります。コンシステントハッシュ法だけが再均衡化の解ではありません。
明示値集合で分けます。
JP, KR → 東アジアパーティションUS, CA → 北米パーティションDE, FR → 欧州パーティションデータ所在地要件やリージョン振り分けを表現しやすい一方、地域間負荷差、未分類値、リージョン変更を扱います。
シャードキー
Section titled “シャードキー”シャードキーはデータ配置とリクエスト振り分けを決める最重要設計です。
良い性質:
- 行数が十分高い
- 書き込み/読み取り負荷が均等
- 主要クエリがキーを指定する
- 同じ場所に配置したいデータで共有できる
- 値が安定して変更されにくい
- テナント分離性/リージョン要件に合う
customer_idで注文を分ける
Section titled “customer_idで注文を分ける”シャード = ハッシュ(customer_id)顧客別注文一覧と顧客単位トランザクションを一シャードへ閉じ込められます。
弱点:
- 全顧客の時間範囲レポートは分散問い合わせと集約
- 大規模顧客が集中箇所
- 顧客IDを持たないアクセス経路に全体インデックスが必要
ordered_atで分ける
Section titled “ordered_atで分ける”時系列走査とアーカイブに向きますが、最新範囲へ書き込みが集中し、顧客別履歴が複数パーティションへ散ります。
「最も選択率の高い列」ではなく、トランザクション境界、クエリ局所性、増加傾向、集中箇所を合わせて選びます。
範囲 + ハッシュなどを組み合わせます。
最初: month(ordered_at)次に: ハッシュ(customer_id)を16バケットへ振り分け時間範囲刈り込みと書き込み分散を両立できますが、パーティション数、メタデータ、小規模パーティションを増やします。
クライアント側振り分け
Section titled “クライアント側振り分け”クライアントライブラリがパーティションマップを持ち、直接対象ノードへ送ります。
- 中継回数が少ない
- クライアント更新とメタデータ更新が必要
- 多言語クライアント側へ処理が分散
プロキシ/調整役振り分け
Section titled “プロキシ/調整役振り分け”ゲートウェイがキーから対象を選びます。
- クライアントが単純
- プロキシの遅延時間/ボトルネック
- メタデータを中央管理しやすい
リダイレクト
Section titled “リダイレクト”任意ノードがリクエストを受け、正しい所有者へリダイレクト/転送します。古いパーティションマップでも動けますが追加中継回数があります。
パーティションマップにはバージョン/世代を付け、古いルーターによる誤書き込みを防ぎます。
パーティション刈り込み
Section titled “パーティション刈り込み”クエリ述語から不要パーティションを除外します。
SELECT *FROM ordersWHERE ordered_at >= DATE '2026-08-01' AND ordered_at < DATE '2026-09-01';ordered_at範囲パーティションなら8月パーティションだけ読めます。
刈り込みを妨げる例:
- パーティションキーへ関数を適用
- 型/照合順序の不一致
- 実行時まで値が不明
- OR条件
- パーティションキーを含まない述語
静的刈り込みと実行時/動的刈り込みを持つ最適化器があります。
分散問い合わせと集約
Section titled “分散問い合わせと集約”対象シャードを一つに絞れないクエリは全シャードへ送ります。
flowchart TB
Q["全体クエリ"] --> S1["シャード1"]
Q --> S2["シャード2"]
Q --> S3["シャード3"]
S1 --> M["マージ"]
S2 --> M
S3 --> M
遅延時間は最も遅いシャードに引きずられ、リクエスト分岐数がシステム負荷を増幅します。
100回のAPI要求× 100シャード= 10,000回のシャード要求ORDER BY + 上限では各シャードからローカル上位K件を取り、調整役でマージできます。GROUP BYでは部分集約をマージします。
コロケーション
Section titled “コロケーション”関連データを同じパーティションキーで配置すると局所結合/トランザクションにできます。
customersをcustomer_idで分割ordersをcustomer_idで分割paymentsをcustomer_idで分割ただし商品在庫はproduct_idで分けたいかもしれません。一つの配置で全アクセスパターンを局所にできないため、重複/派生ビューまたは分散操作が必要です。
局所セカンダリインデックス
Section titled “局所セカンダリインデックス”各シャード内だけのセカンダリインデックスです。インデックス項目と基底行が同じシャードにあり、書き込みを局所トランザクションで更新できます。
インデックスキーだけでクエリすると全シャードを検索する必要があります。
SELECT * FROM users WHERE email = 'a@example.com';usersがuser_idによるハッシュシャードに分かれ、メールから対象シャードが分からなければ、全シャードへの散布検索が必要です。
グローバルセカンダリインデックス
Section titled “グローバルセカンダリインデックス”全シャードを横断するインデックスを別の分割構造として持ちます。
flowchart LR
E["メール"] --> G["全体インデックスシャード"]
G --> U["基底利用者シャード"]
利点:
- 副系キーから対象シャードを特定
- 全体一意性を実現しやすい
課題:
- 基底書き込みとインデックス書き込みが別シャード
- 分散トランザクションまたは非同期伝播
- 古いインデックス
- インデックス集中箇所
- 再分割時の整合
同期全体インデックスは書き込み遅延時間と可用性を犠牲にし、非同期インデックスは書き込み後の読み取り保証と孤立項目を扱います。
メールなどを全シャードでUNIQUEにするには:
- メールでパーティションした全体インデックス
- 中央割り当てサービス
- 決定論的所有者シャード
- 予約プロトコル
各利用者シャードで局所UNIQUEを置くだけでは、別シャードに同じメールを作れます。
データ偏りと集中箇所
Section titled “データ偏りと集中箇所”行/バイト数が特定パーティションへ偏ります。
データ量は均等でもリクエストが偏ります。
一時的な集中箇所
Section titled “一時的な集中箇所”キャンペーンや著名人に起因するイベントで一時的に特定キーが高負荷になります。
観測するもの:
- パーティションごとの行/バイト
- 読み取り/書き込みQPS
- CPU/I/O/ネットワーク
- キュー/遅延時間
- コンパクション/レプリケーション遅延
- 高負荷キー/テナント
平均ノード使用率は高負荷パーティションを隠します。
集中箇所対策
Section titled “集中箇所対策”- 高負荷キーをソルトして複数バケットへ分ける
- 読み取りキャッシュ/レプリカ
- 書き込み集約/一括処理
- 専用シャード
- テナント流量制限
- 適応的な再分割
- キー設計変更
ソルトは書き込みを分散しますが、読み取りで複数バケットをマージし、一意性や順序付けを複雑にします。
カウンターを16バケットへ分ける例:
counter_id = post42#0 ... post42#15合計 = 全バケットのSUM正確な現在の値を読むコストと結果整合的な集約を受け入れます。
分割とマージ
Section titled “分割とマージ”範囲パーティションが大きくなったら分割します。
[A, Z)→ [A, M) + [M, Z)小規模パーティションが増えすぎたらマージします。分割/マージはメタデータ更新、レプリカ作成、振り分け変更、処理中のリクエストを調整します。
オンライン再均衡化
Section titled “オンライン再均衡化”パーティションをノードAからBへ移動する流れの一例:
- Bへスナップショットを複製
- 複製中の増分ログを転送
- BがAへ追いつく
- 所有権世代を変更
- 新しいリクエストをBへ振り分け
- 古いAへのリクエストをリダイレクト/拒否
- 安全確認後Aの複製を削除
sequenceDiagram
participant Router as ルーター
participant A as 古い所有者A
participant B as 新しい所有者B
A->>B: スナップショット
A->>B: 増分変更
B-->>A: 追従完了
Router->>Router: 世代7 → 8, 所有者=B
Router->>B: 新しい書き込み
A-->>Router: リダイレクト / 古い世代
二重書き込みの危険
Section titled “二重書き込みの危険”移行中にアプリケーションがA/Bへ個別に二重書き込みすると、一方だけ成功する部分障害が起きます。
所有者がログを一つに順序づけ、新しいレプリカへ複製する方式や、トランザクション/CDCで移行します。切り替えには世代/フェンシングトークンを使い、古い所有者の遅れて到着した書き込みを拒否します。
再シャーディングコスト
Section titled “再シャーディングコスト”データ移動はバックグラウンド負荷として本番環境通信量と競合します。
- ディスク読み取り/書き込み
- ネットワーク帯域幅
- コンパクション
- レプリカ追従
- キャッシュ未温状態
- チェックサム検証
流量制限と優先度を設定し、障害時に再開できる複製プロトコルが必要です。
分散クエリ実行
Section titled “分散クエリ実行”分散計画には交換演算子が加わります。
各ワーカー 結果を調整役へ集めます。
ブロードキャスト
Section titled “ブロードキャスト”小規模表を全ワーカーへ送ります。
結合/グループキーで両入力をネットワーク再分配します。
flowchart LR
A1["シャードA1"] --> X["ハッシュ交換"]
A2["シャードA2"] --> X
X --> W1["ワーカー 1"]
X --> W2["ワーカー 2"]
ネットワークバイト、直列化、逆圧、ワーカー 偏りがコストモデルへ加わります。
シャード数を決める
Section titled “シャード数を決める”多すぎるシャード:
- メタデータと接続増加
- 小規模ファイル/コンパクションオーバーヘッド
- 分岐数増加
- 再均衡化操作増加
少なすぎるシャード:
- 1シャードがノード容量を超える
- 並列度不足
- 移動単位が大きい
- 集中箇所を分割しにくい
将来の増加傾向を見込みつつ、仮想パーティション/範囲分割で物理ノード数と独立させます。
よくある誤解
Section titled “よくある誤解”「ハッシュシャードなら均等になる」
Section titled “「ハッシュシャードなら均等になる」”キー数は均等でもリクエスト頻度、行大きさ、テナント大きさが偏ります。
「シャードキーは後から変えればよい」
Section titled “「シャードキーは後から変えればよい」”全データ移動、インデックス再構築、振り分け、外部キー、アプリケーションクエリを巻き込む大規模移行です。
「全体インデックスがあればシャーディング前と同じ」
Section titled “「全体インデックスがあればシャーディング前と同じ」”基底/インデックスの分散書き込みと古さが加わり、トランザクション意味論が変わります。
- 水平パーティションは行、垂直パーティションは列/機能を分ける
- 範囲は局所性、ハッシュは負荷分布を得やすい
- シャードキーはクエリ振り分け、トランザクション境界、コロケーション、偏りを同時に決める
- パーティション刈り込みできないクエリは分散問い合わせと集約で分岐数する
- 局所インデックスは書き込みが単純、全体インデックスは参照を改善するが分散整合性を必要とする
- データ偏りと負荷偏りをパーティション単位で観測する
- オンライン再均衡化はスナップショット、増分追従、世代切り替え、古い所有者フェンシングを行う
- 分散クエリはブロードキャスト/再分割/集約のネットワークコストを持つ
- 範囲とハッシュ分割を時系列クエリと書き込み集中箇所から比較してください。
- customer_id シャードが顧客別クエリに向き、全体レポートに不利な理由を説明してください。
- 局所セカンダリインデックスとグローバルセカンダリインデックスの書き込み経路を比較してください。
- オンライン移行でアプリケーション二重書き込みが危険な理由は何ですか。
- ハッシュで行数が均等でも高負荷シャードが生まれる例を作ってください。
- PostgreSQL Documentation: Table Partitioning
- Google Cloud Spanner: Life of Reads and Writes
- James C. Corbett et al., “Spanner: Google’s Globally-Distributed Database”
- Dynamo Paper
次章では、複数シャード/サービスにまたがる変更を全か無かまたは補償可能なワークフローとして扱う分散トランザクションを学びます。