データベースのスケーリング戦略: リードレプリカ、シャーディングなど
Web アプリケーションのスケーリング問題の 78% において、データベースのパフォーマンスがボトルネックとなっています。 アプリケーションは最小限の労力で水平方向にスケーリングできますが、データベースは水平方向のスケーリングに抵抗します。データベースのスケーリングのために選択した戦略によって、アプリケーションが許容可能なパフォーマンスで 100 ユーザーにサービスを提供するか、100,000 ユーザーにサービスを提供できるかが決まります。
このガイドでは、スケーリングの必要性を遅らせる単純な最適化から、水平シャーディングなどの高度な技術に至るまで、データベースのスケーリング戦略の全範囲をカバーしています。
重要なポイント
- インフラストラクチャを追加する前にクエリを最適化し、インデックスを追加します --- これにより、データベースのパフォーマンスの問題の 60% が解決されます
- リードレプリカは最もリスクの低いスケーリング戦略であり、読み取り負荷の高いワークロードの 80% を処理します
- アプリケーションが 10 を超えるインスタンスを実行する場合、接続プーリングは必須です
- 水平シャーディングは、アプリケーションが大幅に複雑になる最後の手段です
スケーリングラダー
この順序でスケールします。各ステップは次のステップよりも安価でリスクが低くなります。
ステップ 1: クエリの最適化 (無料)
インフラストラクチャを追加する前に、既存のデータベースが最適に実行されていることを確認してください。
-- Find slow queries in PostgreSQL
SELECT
calls,
mean_exec_time::numeric(10,2) AS avg_ms,
total_exec_time::numeric(10,2) AS total_ms,
query
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;
一般的な最適化:
- 頻繁にフィルターされる列に欠落しているインデックスを追加する
SELECT *を特定の列リストに置き換えますEXPLAIN ANALYZEを使用して、大きなテーブルの順次スキャンを識別します- 複数列の WHERE 句に複合インデックスを追加する
OFFSETの代わりにキーセット ページネーションを使用してページネーションを実装します。
-- Bad: OFFSET pagination (scans all skipped rows)
SELECT * FROM orders ORDER BY created_at DESC LIMIT 20 OFFSET 10000;
-- Good: Keyset pagination (index-only scan)
SELECT * FROM orders
WHERE created_at < '2026-03-15T10:00:00Z'
ORDER BY created_at DESC
LIMIT 20;
ステップ 2: 垂直スケーリング ($)
既存のデータベース サーバーの CPU、RAM、ストレージを増設します。これにより時間を稼ぐことができ、アプリケーションの変更は必要ありません。
| インスタンスのサイズ | vCPU | RAM | 接続 | 月額費用 (RDS) |
|---|---|---|---|---|
| db.t3.medium | 2 | 4GB | 100 | $65 |
| db.r6g.large | 2 | 16GB | 200 | $175 |
| db.r6g.xlarge | 4 | 32GB | 400 | $350 |
| db.r6g.2xlarge | 8 | 64GB | 800 | 700ドル |
ほとんどのアプリケーションは、64 GB RAM と 8 vCPU で制限に達します。それを超えると、垂直方向のスケーリングには法外なコストがかかります。
ステップ 3: 接続プーリング ($)
Application (50 pods x 20 connections = 1,000 connections)
|
v
PgBouncer (25 database connections, transaction pooling)
|
v
PostgreSQL (25 active connections, manageable)
PgBouncer の構成:
[databases]
app = host=db.example.com port=5432 dbname=production
[pgbouncer]
listen_port = 6432
listen_addr = 0.0.0.0
auth_type = md5
pool_mode = transaction
default_pool_size = 25
max_client_conn = 1000
min_pool_size = 5
reserve_pool_size = 5
reserve_pool_timeout = 3
ステップ 4: リードレプリカ ($$)
リードレプリカは SELECT クエリを処理し、データベース負荷の 60 ~ 90% をプライマリからオフロードします。
アーキテクチャ:
Write queries --> Primary database
|
Replication (async)
|
+----+----+
| |
Read queries --> Replica 1 Replica 2
アプリケーションレベルのルーティング (Drizzle ORM の例):
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
const primaryPool = new Pool({ connectionString: process.env.DATABASE_URL });
const replicaPool = new Pool({ connectionString: process.env.DATABASE_REPLICA_URL });
export const primaryDb = drizzle(primaryPool);
export const replicaDb = drizzle(replicaPool);
// In service code:
// Write operations use primaryDb
async createOrder(data: OrderInput) {
return primaryDb.insert(orders).values(data).returning();
}
// Read operations use replicaDb
async getOrders(organizationId: string) {
return replicaDb.select().from(orders)
.where(eq(orders.organizationId, organizationId))
.orderBy(desc(orders.createdAt));
}
レプリケーションの遅延に関する考慮事項: 非同期レプリケーションでは遅延が発生します (通常は 10 ~ 100 ミリ秒)。書き込み直後にレプリカから読み取ると、古いデータが返される場合があります。同じユーザー フロー内の書き込みに続く読み取りにはプライマリを使用します。
ステップ 5: キャッシュ ($$)
Redis キャッシュにより、データベース クエリの繰り返しが完全に排除されます。
async getProduct(id: string): Promise<Product> {
const cacheKey = `product:${id}`;
// Check cache first
const cached = await this.redis.get(cacheKey);
if (cached) return JSON.parse(cached);
// Cache miss: query database
const product = await this.db.select().from(products)
.where(eq(products.id, id))
.limit(1);
// Cache for 5 minutes
await this.redis.setex(cacheKey, 300, JSON.stringify(product[0]));
return product[0];
}
キャッシュ無効化戦略: 書き込み時に無効化します。製品をアップデートした場合は、キャッシュキーを削除してください。ライトスルー (データベースがキャッシュを管理する) ではなく、キャッシュアサイド パターン (アプリケーションがキャッシュを管理する) を使用します。
ステップ 6: 水平シャーディング ($$$)
シャーディングは、シャード キーに基づいて複数のデータベース インスタンスにデータを分散します。
| シャーディング戦略 | 説明 | 最適な用途 |
|---|---|---|
| ハッシュベース | シャード キーをハッシュし、均等に分散します。均一なデータ分布 | |
| 範囲ベース | シャードに範囲を割り当てる (例: A-M、N-Z) | 時系列、地理データ |
| テナントベース | テナント/組織ごとに 1 つのシャード | マルチテナントSaaS |
シャードを行う場合:
- 単一データベースが 1 TB を超え、増加中
- 書き込みスループットが単一のプライマリで処理できる量を超えています
- 垂直スケーリングのコストが月額 2,000 ドルを超え、余裕がない
シャードしない場合:
- ステップ 1 ~ 5 をまだ完了していません
- データは単一の 500 GB データベースに収まります
- アプリケーションではクロスシャードクエリが一般的です
PostgreSQL 固有の最適化
パーティショニング (シャーディング前)
PostgreSQL のテーブル パーティショニングは、単一の論理テーブルを維持しながら、大きなテーブルを小さな物理テーブルに分割します。
-- Partition orders by month
CREATE TABLE orders (
id UUID PRIMARY KEY,
organization_id UUID NOT NULL,
created_at TIMESTAMP NOT NULL,
total DECIMAL(10,2)
) PARTITION BY RANGE (created_at);
CREATE TABLE orders_2026_01 PARTITION OF orders
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE orders_2026_02 PARTITION OF orders
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
CREATE TABLE orders_2026_03 PARTITION OF orders
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
PostgreSQL は関連するパーティションのみをスキャンするため、パーティショニングにより、大きなテーブルでの時間範囲クエリのパフォーマンスが 10 ~ 100 倍向上します。
掃除機とメンテナンス
-- Check table bloat
SELECT
schemaname,
relname,
n_live_tup,
n_dead_tup,
round(n_dead_tup::numeric / greatest(n_live_tup, 1) * 100, 2) AS dead_pct
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
ORDER BY n_dead_tup DESC;
書き込み量の多いテーブルに対して自動バキュームを積極的に構成します。
ALTER TABLE orders SET (
autovacuum_vacuum_threshold = 100,
autovacuum_vacuum_scale_factor = 0.05,
autovacuum_analyze_threshold = 50,
autovacuum_analyze_scale_factor = 0.02
);
データベースのパフォーマンスの監視
これらのメトリクスを追跡して、いつ、どのようにスケールするかを理解します。
| メトリック | ツール | アラートしきい値 |
|---|---|---|
| クエリレイテンシ (P95) | pg_stat_statements | >500ミリ秒 |
| アクティブな接続 | pg_stat_activity | 最大値の >80% |
| キャッシュ ヒット率 | pg_stat_データベース | <95% |
| レプリケーションの遅延 | pg_stat_replication | >1秒 |
| テーブルの肥大化 | pg_stat_user_tables | >20% のデッドタプル |
| ディスク I/O 待機 | iostat / CloudWatch | >20ms |
キャッシュ ヒット率が 95% を下回る場合は、より多くのメモリが必要であることを示す最も強力な指標です。 shared_buffers と effective_cache_size を増やすことは、多くの場合、リードレプリカを追加するよりも安価で高速です。
クエリパフォーマンスの追跡
-- Enable pg_stat_statements (postgresql.conf)
-- shared_preload_libraries = 'pg_stat_statements'
-- Find the top 10 most time-consuming queries
SELECT
queryid,
calls,
mean_exec_time::numeric(10,2) AS avg_ms,
total_exec_time::numeric(10,2) AS total_ms,
rows,
query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
上位 10 のクエリを毎週確認します。頻繁に実行される単一のクエリを最適化するだけでも、データベース全体の負荷を 10 ~ 30% 削減できます。
よくある質問
スケーリングのタイミングをどのように判断すればよいでしょうか?
クエリ遅延 P95 (500 ミリ秒でアラート)、接続使用率 (80% でアラート)、および CPU 使用率 (70% でアラートが継続) の 3 つのメトリクスを監視します。定期的にこれらのしきい値に達している場合は、スケーリングのはしごの次のステップに進んでください。事前に最適化しないでください --- データから指示されたときにスケーリングします。
リードレプリカとキャッシュ --- どちらが先ですか?
まずはキャッシングから始めます。 Redis キャッシュは実装が簡単で、より多くの負荷が排除され (キャッシュ ヒットがデータベースを完全にスキップします)、コストが低くなります。キャッシュ ヒット率がすでに 80% を超えているにもかかわらず、プライマリ データベースが依然としてキャッシュ ミスや書き込み操作による負荷を受けている場合は、リード レプリカを追加します。
データベースのスケーリングは Odoo でどのように機能しますか?
Odoo は PostgreSQL のみを使用します。クエリの最適化から始めます (Odoo はレポート用に複雑なクエリを生成します)。同時ユーザーが 50 人を超えた場合は、接続プール用に PgBouncer を追加します。クエリのレポートにはリードレプリカを使用します (Odoo の --db-replica オプションを構成します)。 ECOSIRE は、データベースのチューニングを含む Odoo パフォーマンスの最適化 を提供します。
マネージド データベース (RDS/Cloud SQL) にはプレミアムを利用する価値がありますか?
はい、ほとんどのビジネスに当てはまります。管理されたデータベースは、自動バックアップ、パッチ適用、フェイルオーバー、監視を処理します。セルフマネージド PostgreSQL に比べて 30 ~ 40% のコスト割増は、節約されたエンジニアリング時間によって相殺されます。例外は、大規模なインスタンスのコストプレミアムがパートタイムの DBA のコストを超える大規模なデプロイメントです。
次に何が起こるか
データベースのスケーリングは、より広範なインフラストラクチャのスケーリング戦略の 1 つのコンポーネントです。静的アセットの場合は CDN 最適化、アプリケーション ポッドの場合は Kubernetes 自動スケーリング、および現実的な条件下でスケーリングの決定を検証するには 負荷テスト と組み合わせます。
データベース最適化のコンサルティングについては ECOSIRE にお問い合わせ、完全なインフラストラクチャ ロードマップについては DevOps ガイド を参照してください。
ECOSIRE が発行 -- 企業が自信を持ってデータ インフラストラクチャを拡張できるように支援します。
執筆者
ECOSIRE Research and Development Team
ECOSIREでエンタープライズグレードのデジタル製品を開発。Odoo統合、eコマース自動化、AI搭載ビジネスソリューションに関するインサイトを共有しています。
関連記事
最新のアプリケーションの API ゲートウェイ パターンとベスト プラクティス
スケーラブルな Web アーキテクチャ向けに、レート制限、認証、リクエスト ルーティング、サーキット ブレーカー、API バージョン管理などの API ゲートウェイ パターンを実装します。
CDN パフォーマンスの最適化: グローバル配信を高速化するための完全ガイド
キャッシュ戦略、エッジ コンピューティング、画像の最適化、マルチ CDN アーキテクチャにより CDN パフォーマンスを最適化し、グローバル コンテンツ配信を高速化します。
CI/CD パイプラインのベスト プラクティス: 信頼性の高いデプロイメントへの方法を自動化する
実稼働ワークフローでのテスト、ステージング、デプロイの自動化、ロールバック戦略、セキュリティ スキャンのベスト プラクティスを使用して、信頼性の高い CI/CD パイプラインを構築します。