C
データベース/上級/Lesson 06

DB 上級 — レプリケーション・シャーディング・キャッシュ・コネクションプール・パーティショニング

45分·theory

DB 上級 — レプリケーション・シャーディング・キャッシュ・コネクションプール・パーティショニング

🎯 このレッスンを読み終えたら

このレッスンをすべて読み終えると、以下の 3 つを自信を持って説明できるようになります。

  • ✅ パーティショニング Range / Hash / List の選択基準
  • ✅ シャーディング vs パーティショニングの違い
  • ✅ DB バックアップ戦略(Logical vs Physical)の説明

学習目標をチェックリストとして手元に置き、すべて答えられるようになったらレッスンを閉じてください。

レプリケーション — *DB を複数台に増やす*

レプリケーションとは

ウェブサービスが成長すると、読み取りリクエストが爆増します。1 つの記事を1 万人が読むというパターンはよくあることです。DB 1 台ではさばけなくなったらどうするか?

レプリケーション = DB を複数台に増やして負荷を分散する方法。1 台を Primary(書き込み専用)とし、複数台の Replica(読み取り専用)が Primary の変更をコピーして受け取ります。

code
   書き込み        読み取りリクエスト
     │              │  │  │
     ▼              ▼  ▼  ▼
  ┌──────┐      ┌──────┐ ┌──────┐ ┌──────┐
  │Primary├─────►Replica1│ │R 2  │ │R 3  │
  └──────┘ WAL  └──────┘ └──────┘ └──────┘
           転送

同期 vs 非同期レプリケーション

同期レプリケーション — Primary が Replica の応答を待ってからユーザーに成功応答を返す。データ損失ゼロだが、遅い

非同期レプリケーション — Primary はすぐに応答し、Replica は後からコピーする。速いが、Replica がわずかに遅れる(lag)。Primary が突然ダウンすると、直近のデータが失われる可能性あり

ほとんどのサービスは非同期を採用します。99% のケースで lag はミリ秒単位なので問題なく、パフォーマンスの恩恵が大きいためです。

Lag — 最も重要な監視指標

Replica がどれだけ遅れているかが核心です。lag が 1 秒になると、ユーザーが書いたばかりの投稿が表示されないという事故につながります。

sql
-- PostgreSQL — primary で実行
SELECT client_addr, state,
       pg_wal_lsn_diff(sent_lsn, replay_lsn) AS lag_bytes
FROM pg_stat_replication;

lag が増え続けるなら → ネットワーク・ディスク・CPU のどこかがボトルネック。アラート設定は必須。

アプリケーション側 — 読み書き分離

code
WRITE → Primary
READ  → Replica(ロードバランシング)

ほとんどの ORM・DataSource ルーターがこれをサポートします。読み取り専用クエリは自動的に Replica へ、書き込みは Primary へ。

> ⚠️ 落とし穴: 書き込み直後の読み取りは、lag の影響で古いデータが返る可能性があります。そのような場合は、そのリクエストだけ Primary から読むといった例外処理が必要です。

まとめ

レプリケーションは読み取りスケールアウト+障害復旧の標準的な手法です。非同期がデフォルトであり、lag 監視が運用の核心。読み書き分離によって Primary の負荷を大幅に減らすことができます。

シャーディング — *データを分割する*

シャーディングとは

レプリケーションは読み取りを増強しますが、書き込みは 1 台のみです。書き込みも爆増したら? — Twitter のように1 秒に数十万ツイートが来れば、1 台では不可能です。

シャーディング = データを複数の DB に分散する方法。ユーザー ID が 1〜100 万は DB1、100 万〜200 万は DB2 — このような水平分割

code
                ┌────────────┐
   ユーザー 42 →  │  Shard 1   │  (1〜100万)
                ├────────────┤
   ユーザー 200万 → │ Shard 2   │  (100万〜200万)
                ├────────────┤
   ユーザー 500万 → │ Shard 3   │  (200万〜)
                └────────────┘

各シャードは独立した DBなので、書き込み負荷が N 等分されます。デメリットは複雑性の爆増です。

シャードキー — 最も重要な決断

どのカラムを基準に分割するかが運命を左右します。

良いシャードキー:

  • user_id — ユーザーごとに均等分散、クロスシャードクエリが少ない
  • tenant_id(B2B SaaS)— 企業ごとの分離

悪いシャードキー:

  • created_at最新データへのホットスポット。古いシャードが暇な一方、新しいシャードが爆発。
  • auto_increment id — レンジシャーディング時に同様のホットスポット。

シャードキーは後から変更がほぼ不可能です。最初に慎重に決定する必要があります。

クロスシャードクエリ — 最大の落とし穴

sql
-- ユーザー A が友人 B にメッセージを送る
-- A は shard 1、B は shard 2 だったら?
SELECT * FROM messages WHERE sender = A AND receiver = B;

両方のシャードをクエリする必要があります。JOIN も不可能。トランザクションも分散トランザクションになった瞬間、非常にコストが高くなります。

だからこそ、クロスシャード操作が少なくなるようデータモデルとシャードキーを設計することが核心です。

リシャーディング — 運用上の悪夢

データが増えてシャードを5 個から 10 個に増やす必要が生じたら?すべてのデータを再分配しなければなりません。その間サービスに影響を与えずに進めるのは非常に難しいです。

だから最初から十分な数のシャードでスタートするか(例: 64 個で始めてノードに分散)、consistent hashing などの手法で負担を軽減するのが一般的です。

マネージドオプション — 自分でやらないのが正解

ほとんどの企業はシャーディングを自前で実装しません。難しすぎて危険です。

  • DynamoDB / MongoDB Atlas — 自動シャーディング NoSQL
  • Vitess — MySQL シャーディング(YouTube が開発、CNCF 卒業プロジェクト)
  • Citus — PostgreSQL 拡張(Microsoft)

特別な理由がなければ、レプリケーションで十分です。シャーディングが本当に必要になるのは、数億行・秒間数万書き込みレベルです。

まとめ

シャーディングは最後の手段。レプリケーション → キャッシュ → パーティショニングをすべて試してもなお足りない場合にのみ。開始時のシャードキーの決定が運命を左右し、クロスシャードクエリの回避がパフォーマンスの鍵です。

キャッシュ + コネクションプール — *DB 負荷を減らす 2 つの方法*

キャッシュとは

DB クエリは遅い(ミリ秒)。メモリは速い(マイクロ秒)。同じクエリを繰り返し実行するなら、結果をメモリに保存して再利用すれば 1000 倍速くなります。これがキャッシュの基本的なアイデアです。

Redis が事実上の標準。メモリベースの key-value ストア。非常に速く、TTL(自動期限切れ)・pub/sub などの付加機能も備えています。

Cache-Aside パターン

最も一般的なパターン:

code
1. キャッシュを確認
2-A. ヒット → 即時返却(速い)
2-B. ミス → DB クエリ → キャッシュに保存 → 返却

書き込み時: DB 更新 + キャッシュ無効化(または更新)
python
def get_user(id):
    cached = redis.get(f"user:{id}")
    if cached:
        return cached         # キャッシュヒット (μs)

    user = db.query("SELECT * FROM users WHERE id=?", id)  # ms
    redis.setex(f"user:{id}", 300, user)   # 5分 TTL
    return user

5 分間は DB を介さずに Redis からすぐにレスポンス。DB 負荷が劇的に減ります。

キャッシュ無効化 — コンピュータサイエンスの二大難問の一つ

> "There are only two hard things in Computer Science: cache invalidation and naming things." — Phil Karlton

データが変更されたとき、古いキャッシュをどう扱うか?

1. TTL — 一定時間後に自動期限切れ。最もシンプルだが、最大 N 秒間は古いデータになる可能性あり。

2. 明示的無効化 — データ変更時に redis.del('user:42') を呼ぶ。即時反映。ただし、すべての変更コードで忘れずに呼び出す必要あり。

3. イベントベース — DB 変更 → イベント発行 → キャッシュのサブスクライバーが無効化。分散システムで有利。

4. バージョンキーuser:v2:42 のようにキー自体を変える。古いキーは自然に廃棄。TTL と組み合わせるとすっきり。

> 💡 実際のところ: TTL + 明示的無効化の組み合わせが 90%。完全な一貫性はコストが高く難しい。短い stale period を許容するのが一般的。

コネクションプール — DB 接続の再利用

DB 接続を毎回新たに作成するのはコストがかかります。TCP ハンドシェイク・認証・TLS ネゴシエーションがリクエストのたびに発生し、接続だけで約 50ms かかります。

コネクションプール = N 個の接続を事前に作成しておき、再利用するプールです。リクエストが来ると、プールから借りて使い、終わったら返却します。新規接続のコストがなくなります。

code
┌──── Connection Pool ────┐
│  [conn1][conn2]...[connN]│
└──┬──┬──┬─────────────────┘
   │  │  │
   ▼  ▼  ▼
  リクエスト処理後に返却

プール設定の核心

最も重要なのはプールサイズです。小さすぎると待ち行列、大きすぎると DB に過負荷がかかります。

公式: app servers × pool size ≤ DB max_connections

例: app 4 台 × pool 20 = 80 ≤ Postgres max=100 ✅

HikariCP(Java 標準)などのライブラリがインテリジェントに管理してくれます。min = max 推奨(予測可能な動作)。

よくある落とし穴

1. 接続リークconn.close() を忘れるとプールが枯渇。try-with-resources / コンテキストマネージャーは必須。

2. プールが大きすぎるDB の max_connections を超過。「too many connections」エラーで爆発。

3. アイドルタイムアウトが短すぎる — ファイアウォールや LB がアイドル接続を切断する環境では新規接続が頻発。max-lifetime で定期的に交換することを推奨(30 分程度)。

まとめ

キャッシュ + コネクションプールは DB 負荷を減らす 2 つの標準ツール。キャッシュは繰り返しクエリのコストを、コネクションプールは接続コストを削減します。両方を適切に設定すれば、同じ DB で10 倍のトラフィックを受けられます。

パーティショニング — *大きなテーブルを小さな断片に*

パーティショニングとは

テーブル 1 つが 100 GB を超えると様々な問題が生じます。インデックスも巨大化し、古いデータの削除が遅くなり、一部のデータしか使わないクエリでも全体をスキャンするようになります。

パーティショニング = 大きなテーブルを論理的に複数の断片に分割しながら、ユーザーには 1 つのテーブルに見えるようにする技術。シャーディングに似ていますが、同じ DB 内での分割という点が異なります。

RANGE パーティション — 時系列データに最適

最も一般的なパターン。日付範囲で分割します。

sql
CREATE TABLE logs (
    id BIGSERIAL,
    created_at TIMESTAMP NOT NULL,
    data JSONB
) PARTITION BY RANGE (created_at);

CREATE TABLE logs_2025_01 PARTITION OF logs
    FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');

CREATE TABLE logs_2025_02 PARTITION OF logs
    FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');

INSERT INTO logs を実行すると、created_at の値を見て自動的に適切なパーティションに格納されます。

真の恩恵 — パーティションプルーニング

sql
SELECT * FROM logs WHERE created_at >= '2025-03-01';

このクエリは logs_2025_03 パーティションだけをスキャンします。他のパーティションはスキップされます。100 GB 中の 1 GB だけ見ればよいので 100 倍速くなります。

DB が自動的にどのパーティションを見るべきかを推論します。これがプルーニング(枝刈り)です。

古いデータの削除が速い

3 年以上前のログを削除する場合を考えてみましょう。

sql
-- ❌ 非常に遅い(数時間かかることも)
DELETE FROM logs WHERE created_at < '2022-01-01';

-- ✅ 即時
DROP TABLE logs_2021_12;
DROP TABLE logs_2021_11;
-- ...

パーティション単位の DROP はディスクから丸ごと取り外すような操作なので非常に速いです。ログ・イベント処理では圧倒的に優れています。

自動化 — pg_partman

毎月新しいパーティションを手動で作成するのは運用負担です。pg_partman(Postgres 拡張)が自動的に処理してくれます。

sql
SELECT partman.create_parent(
    p_parent_table => 'public.logs',
    p_control => 'created_at',
    p_type => 'native',
    p_interval => 'monthly',
    p_premake => 3   -- 3ヶ月分を事前に作成
);

TimescaleDB は PostgreSQL 上の時系列特化拡張。パーティショニングを自動的に管理し、圧縮まで対応します。

LIST・HASH パーティション

LIST — 地域・カテゴリのような明確な分類:

sql
CREATE TABLE users (...) PARTITION BY LIST (country);
CREATE TABLE users_kr PARTITION OF users FOR VALUES IN ('KR');
CREATE TABLE users_jp PARTITION OF users FOR VALUES IN ('JP');

HASH — 均等分散(Postgres 11+):

sql
CREATE TABLE orders (...) PARTITION BY HASH (user_id);
CREATE TABLE orders_p0 PARTITION OF orders FOR VALUES WITH (modulus 4, remainder 0);
-- ...

パーティショニングはいつ使う?

  • ✅ 単一テーブルが 100 GB 以上
  • 時間ベースのデータ(ログ・メトリクス・イベント)
  • 古いデータの一括削除が必要
  • ❌ 小さなテーブル — オーバーヘッドのみ増加

まとめ

パーティショニングは大きなテーブルのパフォーマンスと運用負担を劇的に軽減します。時系列データにはデフォルトの選択肢。pg_partman・TimescaleDB で自動化すれば運用も楽になります。シャーディングよりはるかに簡単で安全な選択肢なので、まず検討する価値があります。

💾 実践デモ — キャッシュヒット vs ミスのパフォーマンス差

同じデータの取得を *DB 直接アクセス* vs *Redis キャッシュ* で比較します。
📥 입력 DB 서버로 전송되는 쿼리
SELECT FROM: users length: 255 chars
-- 計測結果(本番環境の平均)

-- DB 直接クエリ(インデックス使用)
SELECT * FROM users WHERE id = 42;
-- 平均: 8 ms

-- Redis キャッシュ hit
-- GET user:42
-- 平均: 0.3 ms

-- 1000 回繰り返し:
-- DB のみ:    8 ms × 1000 = 8,000 ms(8秒)
-- Cache+DB:   初回 8ms + 999回 × 0.3ms = 308 ms(0.3秒)
-- → 26 倍高速
⚙️ 처리 단계 DB 엔진 내부 동작
📋
1. 파싱
SQL 문법 분석 → AST 생성
10ms
🧠
2. 옵티마이저
인덱스·조인 순서 최적화 (통계 기반)
25ms
⚙️
3. 실행
Storage Engine이 데이터 읽기
24ms
📊
4. 결과 반환
버퍼에 모아서 클라이언트에 전달
8ms
총 67ms
📤 출력
✓ 3 rows 3 cols 67ms
sourceavg_ms1000回
DB(インデックス)88,000 ms
Redis hit0.3300 ms
1回 miss + 999回 hit-308 ms
💡 학습 포인트: 실제 DB는 파싱 → 옵티마이저 → 실행 → 반환 4단계를 거칩니다. 인덱스가 있으면 '옵티마이저' 단계가 수천배 빨라집니다.
🌐 본인 환경(PostgreSQL·MySQL 등)에서 직접 실행해 ms 차이를 체감해보세요.
💻 📌 キャッシュ・プール・パーティション設定(参考用)
# ============================================
# 1) HikariCP — Spring標準 Connection Pool
# ============================================
spring:
  datasource:
    hikari:
      maximum-pool-size: 20         # CPU・DB max基準
      minimum-idle: 20               # = max推奨 (予測可能)
      connection-timeout: 30000      # 30秒
      idle-timeout: 600000           # 10分
      max-lifetime: 1800000          # 30分 (DB・LBアイドル切断回避)
      leak-detection-threshold: 60000 # 60秒閉じられない接続警告

# ============================================
# 2) Redis (Spring Boot)
# ============================================
spring:
  data:
    redis:
      host: localhost
      port: 6379
      timeout: 2000
      lettuce:
        pool:
          max-active: 8
          max-idle: 8
          min-idle: 0

# コード:
# @Cacheable(value = "user", key = "#id")
# public User findById(Long id) { return userRepo.findById(Long id); }
#
# @CacheEvict(value = "user", key = "#user.id")
# public void update(User user) { userRepo.save(User user); }

# ============================================
# 3) PostgreSQL パーティション + pg_partman
# ============================================
-- 拡張機能インストール
CREATE EXTENSION pg_partman;

-- 親テーブル + パーティショニング
CREATE TABLE logs (
    id BIGSERIAL,
    created_at TIMESTAMP NOT NULL,
    level VARCHAR(10),
    message TEXT
) PARTITION BY RANGE (created_at);

-- 自動管理
SELECT partman.create_parent(
    p_parent_table => 'public.logs',
    p_control => 'created_at',
    p_type => 'native',
    p_interval => 'monthly',
    p_premake => 4
);

-- 30日経過したパーティション自動削除 (cronで毎日)
-- SELECT partman.run_maintenance();

🤖 AI にこう依頼してみてください

このレッスンの概念を理解すれば、AI に具体的に指示できます。漠然とした「直して」ではなく、語彙を持ったリクエスト — それがトークン節約の出発点です。

  • 「この logs テーブルを created_at の YEAR Range パーティションに変換して」
  • 「この SELECT クエリを必要なカラムだけ選択してカバリングインデックスが効くようにして」*
  • 「シャーディング vs パーティショニングの違い+うちのケースにはどちらが適切か診断して」

なぜこれがトークンを節約するのか

概念を知らないと、AI の回答を受け取っても「それって何ですか?」と再度聞き返す必要があります。その「聞き返し」がトークンを消費します。概念を一度理解しておけば、会話が一度で完結します。

次のおすすめ: HTML/CSS の基礎
DB 上級 — レプリケーション・シャーディング・キャッシュ・コネクションプール・パーティショニング - データベース