MongoDB: パフォーマンスチューニングと監視
最終更新:2026-08-26
パフォーマンス監視は本番システムの「健康診断」—マスターすれば9割のパフォーマンス問題を予防・解決できます。
1. 学習内容
- explain()実行計画分析
- インデックス戦略(ESRルール)
- スロークエリログとプロファイラ
- サーバーメトリクス監視(serverStatus)
- メモリと接続管理
- パフォーマンストラブルシューティングフロー
graph TB
A[パフォーマンス問題発見] --> B{スロークエリ?}
B -->|はい| C[explain分析]
B -->|いいえ| D[リソース監視]
C --> E[インデックス不足?]
E -->|はい| F[インデックス作成]
E -->|いいえ| G[クエリ最適化]
D --> H[メモリ不足?]
H -->|はい| I[WiredTiger設定調整]
H -->|いいえ| J[接続プール/ネットワーク]
F --> K[✅ 解決]
G --> K
I --> K
J --> K
style K fill:#d4edda
2. explain()実行計画分析
explain()とは? explain()はMongoDBクエリオプティマイザがどのようにクエリを実行するかを示す「実行計画」を返します。どのインデックスが使用されたか、どれだけのドキュメントがスキャンされたか、実行時間などを知ることができます。これはMongoDBパフォーマンス最適化で最も重要なツールです。
explainの出力構造:
JAVASCRIPT
db.products.find({ category: 'Electronics', price: { $lt: 1000 } })
.sort({ price: -1 })
.explain('executionStats');
出力:
TEXT 📖 参照専用{ executionStats: { executionSuccess: true, nReturned: 125, // 返却されたドキュメント数 executionTimeMillis: 5, // 実行時間(ミリ秒) totalKeysExamined: 125, // インデックスでスキャンしたキー数 totalDocsExamined: 125, // スキャンしたドキュメント数 indexUsed: 'category_1_price_-1', // 使用されたインデックス ... } }
重要な統計の解釈:
| メトリクス | 説明 | 健全値 | 問題ある値 |
|---|---|---|---|
nReturned |
返却ドキュメント数 | — | — |
totalDocsExamined |
スキャンしたドキュメント総数 | ≈ nReturned |
nReturnedの10倍以上 |
totalKeysExamined |
インデックスキーでスキャンした数 | ≈ nReturned |
nReturnedの10倍以上 |
executionTimeMillis |
実行時間 | < 10ms | > 100ms |
indexUsed |
使用されたインデックス名 | インデックス名あり | null(COLLSCAN) |
stage |
クエリステージ | IXSCAN |
COLLSCAN |
explainの3つのモード:
| モード | 説明 | 使用シーン |
|---|---|---|
queryPlanner |
実行計画情報のみ | 最適化の初期診断 |
executionStats |
実行統計を含む | パフォーマンス分析(推奨) |
allPlansExecution |
全候補プランの実行統計 | 最適化の比較 |
JAVASCRIPT
// === 実行統計を確認 ===
db.orders.find({ userId: 'user_001', status: 'paid' })
.explain('executionStats');
// === 結果の分析 ===
// totalDocsExamined: 50000(5万件スキャン)
// nReturned: 50(50件返却)
// 結論:インデックス不足、1000倍の過剰スキャン
スキャン比率分析:
理想:totalDocsExamined ≈ nReturned(1対1のスキャン)許容:totalDocsExamined < 10 × nReturned(10倍以下)問題:totalDocsExamined > 100 × nReturned(100倍以上の過剰スキャン)
3. インデックス戦略(ESRルール)
ESRルールとは? ESRルール(Equality-Sort-Range)は複合インデックスのフィールド順序を決定するガイドラインです。正しいフィールド順序はインデックス効率を劇的に向上させ、間違った順序はインデックスが全く使用されない可能性があります。
ESRルールの原則:
graph LR
E[Equality<br/>等価条件] --> S[Sort<br/>ソート条件]
S --> R[Range<br/>範囲条件]
style E fill:#d4edda
style S fill:#cce5ff
style R fill:#fff3cd
| 優先順位 | カテゴリ | 説明 | 例 |
|---|---|---|---|
| 1 | Equality(等価) | 完全一致条件 | { category: 'Electronics' } |
| 2 | Sort(ソート) | 並び替え条件 | .sort({ price: -1 }) |
| 3 | Range(範囲) | 範囲条件 | { price: { $lt: 1000 } } |
ESRルールの実践:
JAVASCRIPT
// クエリ:
db.products.find({
category: 'Electronics', // Equality(等価)
price: { $lt: 1000 } // Range(範囲)
}).sort({
price: -1 // Sort(ソート)
});
// 最適なインデックス(ESRルールに従う):
// 1. category(等価)
// 2. price(ソート + 範囲は同じフィールド)
db.products.createIndex({ category: 1, price: -1 });
インデックスが使用される条件:
| クエリ条件 | 使用されるインデックス | 理由 |
|---|---|---|
{a: 1} |
{a: 1, b: 1} ✅ |
先頭フィールドが一致 |
{a: 1, b: 1} |
{a: 1, b: 1} ✅ |
完全一致 |
{b: 1} |
{a: 1, b: 1} ❌ |
先頭フィールドが一致しない |
{a: 1, c: 1} |
{a: 1, b: 1, c: 1} ✅ |
a使用、bスキップ、c使用 |
{a: {$gt: 5}, b: 1} |
{a: 1, b: 1} ⚠️ |
a範囲条件後、b使用不可 |
複合インデックス設計のポイント:
- 等価条件フィールドを先頭に配置
- ソートフィールドを2番目に配置
- 範囲条件フィールドを最後に配置
- 複数の範囲条件がある場合、より選択性が高い(カーディナリティが高い)フィールドを先に
4. スロークエリログとプロファイラ
スロークエリの特定: MongoDB Profilerは設定された閾値を超えるクエリをsystem.profileコレクションに記録します。本番環境では、パフォーマンス影響を最小限にするため、レベル1(スロークエリのみ)を使用します。
JAVASCRIPT
// === スロークエリログを有効化(100ms以上)===
db.setProfilingLevel(1, { slowms: 100 });
// === スロークエリを確認 ===
db.system.profile.find({ millis: { $gt: 100 } })
.sort({ ts: -1 })
.limit(10)
.pretty();
// === プロファイラ無効化 ===
db.setProfilingLevel(0);
プロファイラレベル:
| レベル | 説明 | パフォーマンス影響 | 使用シーン |
|---|---|---|---|
| 0 | オフ | なし | 通常運用 |
| 1 | スロークエリのみ記録 | 低い | 本番環境推奨 |
| 2 | 全操作を記録 | 高い | 開発・デバッグ |
5. サーバーメトリクス監視
主要な監視メトリクス:
| メトリクス | コマンド | 健全範囲 | アラート閾値 |
|---|---|---|---|
| 接続数 | db.serverStatus().connections |
< 1000 | > 8000 |
| メモリ使用量 | db.serverStatus().mem |
< 総メモリの80% | > 95% |
| ページフォルト | db.serverStatus().extra_info.page_faults |
低い | 急増 |
| Opcounters | db.serverStatus().opcounters |
ベースライン | 10倍急増 |
| レプリケーション遅延 | rs.status() |
< 1秒 | > 10秒 |
JAVASCRIPT
// === サーバーステータス確認 ===
db.serverStatus();
// === 主要メトリクス抽出 ===
const status = db.serverStatus();
print(`接続数: ${status.connections.current}/${status.connections.available}`);
print(`メモリ: ${status.mem.resident}MB`);
print(`クエリ/秒: ${status.opcounters.query}`);
6. メモリと接続管理
WiredTigerメモリ設定:
JAVASCRIPT
// WiredTigerキャッシュサイズを設定(デフォルトは総メモリの50%)
// 4GBメモリサーバーで2GBをキャッシュに割り当てる例
mongod --wiredTigerCacheSizeGB 2
// または設定ファイルで:
// storage:
// wiredTiger:
// engineConfig:
// cacheSizeGB: 2
接続プール設定:
JAVASCRIPT
// Mongoose接続プール
mongoose.connect(uri, {
maxPoolSize: 50, // 最大接続数
minPoolSize: 5, // 最小接続数
maxIdleTimeMS: 30000, // アイドル接続タイムアウト
waitQueueTimeoutMS: 10000 // 接続待機タイムアウト
});
7. パフォーマンストラブルシューティングフロー
トラブルシューティング手順:
graph TB
A[パフォーマンス問題] --> B{スロークエリログに記録?}
B -->|はい| C[explain分析]
B -->|いいえ| D[リソース監視確認]
C --> E{インデックス使用?}
E -->|IXSCAN| F[クエリ条件見直し]
E -->|COLLSCAN| G[インデックス作成]
D --> H{CPU/メモリ/IO?}
H -->|CPU高負荷| I[クエリ最適化/インデックス見直し]
H -->|メモリ不足| J[WiredTigerキャッシュ調整]
H -->|IO高負荷| K[インデックスサイズ削減/データ圧縮]
F --> L[解決]
G --> L
I --> L
J --> L
K --> L
style L fill:#d4edda
| 手順 | コマンド/ツール | 確認事項 |
|---|---|---|
| 1 | スロークエリログ | 閾値超えクエリの特定 |
| 2 | explain('executionStats') |
インデックス使用状況、スキャン数 |
| 3 | インデックス確認 | db.collection.getIndexes() |
| 4 | 使用状況確認 | db.collection.aggregate([{$indexStats: {}}]) |
| 5 | サーバーリソース | db.serverStatus() |
▶ サンプル:スロークエリの診断と解決のハンズオンガイド
JAVASCRIPT
// === シナリオ:ShopHub商品検索が遅い(3秒)===
// 1. 現在のクエリを確認
db.products.find({
category: 'Electronics',
price: { $lt: 1000 }
}).sort({ rating: -1 });
// 2. explainで分析
db.products.find({
category: 'Electronics',
price: { $lt: 1000 }
}).sort({ rating: -1 }).explain('executionStats');
// 結果:
// winningPlan: { stage: 'COLLSCAN', ... } // 全件スキャン!
// totalDocsExamined: 100000
// nReturned: 125
// executionTimeMillis: 3000
// 3. 既存インデックスを確認
db.products.getIndexes();
// [ { _id: 1 }, { sku: 1 } ] // category, priceインデックスなし
// 4. ESRルールでインデックスを設計
// Equality: category
// Sort: rating
// Range: price
db.products.createIndex({ category: 1, rating: -1, price: 1 });
// 5. 再度explainで確認
db.products.find({
category: 'Electronics',
price: { $lt: 1000 }
}).sort({ rating: -1 }).explain('executionStats');
// 結果:
// winningPlan: {
// stage: 'IXSCAN',
// indexName: 'category_1_rating_-1_price_1'
// }
// totalDocsExamined: 125
// nReturned: 125
// executionTimeMillis: 5 // 3秒 → 5ms(600倍高速化)
// 6. 未使用インデックスをクリーンアップ
db.products.aggregate([{ $indexStats: {} }]);
// 使用されていないインデックスを特定 → 削除
出力:
TEXT 📖 参照専用explain分析でCOLLSCAN(全件スキャン)を発見;ESRルールでインデックス作成;3秒→5ms(600倍高速化)。
▶ サンプル 2:本番環境パフォーマンス監視スクリプト(難易度 ⭐⭐)
JAVASCRIPT
// TechCorp本番環境パフォーマンス監視スクリプト
function monitorPerformance() {
const status = db.serverStatus();
console.log('=== MongoDB パフォーマンス監視 ===');
console.log(`時刻: ${new Date().toISOString()}`);
// 接続監視
const conn = status.connections;
console.log(`接続: ${conn.current}/${conn.available} (使用率: ${(conn.current/conn.available*100).toFixed(1)}%)`);
if (conn.current > 1000) {
console.log('⚠️ 警告: 接続数が1000を超えています');
}
// メモリ監視
const mem = status.mem;
console.log(`メモリ: ${mem.resident}MB / ${mem.virtual}MB`);
if (mem.resident > 4096) {
console.log('⚠️ 警告: メモリ使用量が4GBを超えています');
}
// Opcounters(1分あたり操作数)
const ops = status.opcounters;
console.log(`操作/秒: query=${ops.query}, insert=${ops.insert}, update=${ops.update}, delete=${ops.delete}`);
// レプリケーション遅延(レプリカセットのみ)
try {
const rsStatus = rs.status();
const primary = rsStatus.members.find(m => m.stateStr === 'PRIMARY');
if (primary) {
const lag = (Date.now() - primary.optimeDate.getTime()) / 1000;
console.log(`レプリケーション遅延: ${lag.toFixed(1)}秒`);
if (lag > 10) {
console.log('⚠️ 警告: レプリケーション遅延が10秒を超えています');
}
}
} catch (e) {
// レプリカセットではない場合はスキップ
}
// スロークエリ確認(過去5分)
const slowQueries = db.system.profile.find({
ts: { $gt: new Date(Date.now() - 5*60*1000) },
millis: { $gt: 100 }
}).count();
if (slowQueries > 0) {
console.log(`⚠️ 過去5分間に${slowQueries}件のスロークエリ(>100ms)を検出`);
}
console.log('========================');
}
// 1分ごとに監視実行
setInterval(monitorPerformance, 60000);
monitorPerformance();
出力:
TEXT 📖 参照専用=== MongoDB パフォーマンス監視 === 時刻: 2026-07-01T10:30:00.000Z 接続: 150/83860 (使用率: 0.2%) メモリ: 1024MB / 4096MB 操作/秒: query=1234, insert=567, update=89, delete=12 レプリケーション遅延: 0.5秒 ========================
▶ サンプル 3:インデックス戦略と最適化(難易度 ⭐⭐⭐)
JAVASCRIPT
// TechCorp:インデックス戦略の設計と最適化
// === 1. 既存インデックスを確認 ===
db.products.getIndexes();
// [
// { "_id": 1 },
// { "sku": 1 },
// { "category_1_price_-1": 1 }
// ]
// === 2. インデックス使用統計を確認 ===
db.products.aggregate([{ $indexStats: {} }]);
// どのインデックスが使用されているか、いつ最後に使用されたかを表示
// === 3. クエリパターン分析 ===
// クエリ1:カテゴリでフィルタ + 価格でソート
db.products.find({ category: 'Electronics' }).sort({ price: -1 });
// → category_1_price_-1 インデックスが有効
// クエリ2:カテゴリ + 価格範囲 + レーティングでソート
db.products.find({
category: 'Electronics',
price: { $lt: 1000 }
}).sort({ rating: -1 });
// → ESRルールに従い、新インデックスが必要
// === 4. ESRルールで新インデックス設計 ===
// E: category(等価)
// S: rating(ソート)
// R: price(範囲)
db.products.createIndex(
{ category: 1, rating: -1, price: 1 },
{
name: 'category_rating_price',
background: true, // バックグラウンド作成(本番推奨)
partialFilterExpression: { // 部分インデックス(条件を満たすドキュメントのみ)
isActive: true
}
}
);
// === 5. explainで最適化確認 ===
db.products.find({
category: 'Electronics',
price: { $lt: 1000 }
}).sort({ rating: -1 }).explain('executionStats');
// 結果:
// winningPlan: {
// stage: 'IXSCAN',
// indexName: 'category_rating_price'
// }
// totalDocsExamined: 125
// nReturned: 125
// executionTimeMillis: 5
// === 6. 未使用インデックスを特定して削除 ===
db.products.aggregate([{ $indexStats: {} }]).forEach(index => {
if (index.accesses.ops === 0 && index.name !== '_id_') {
print(`未使用インデックス: ${index.name}`);
// db.products.dropIndex(index.name); // 削除(確認後)
}
});
// === 7. インデックスサイズ確認 ===
db.products.stats().indexSizes;
// category_1_price_-1: 1048576 (1MB)
// category_rating_price: 2097152 (2MB)
// === 8. 複合インデックスの選択性分析 ===
// 高い選択性(カーディナリティが高い)のフィールドを前に配置
db.products.aggregate([
{ $group: { _id: '$category', count: { $sum: 1 } } },
{ $sort: { count: -1 } }
]);
// Electronics: 50000, Books: 30000, Clothing: 20000...
// → categoryの選択性は高い(4つの値に分散)
// === 9. テキストインデックス(検索用)===
db.products.createIndex(
{ title: 'text', description: 'text' },
{
name: 'product_text',
weights: { title: 10, description: 5 }, // タイトルを優先
default_language: 'none' // 言語指定なし(多言語対応)
}
);
// テキスト検索
db.products.find(
{ $text: { $search: 'smartphone case' } },
{ score: { $meta: 'textScore' } }
).sort({ score: { $meta: 'textScore' } });
// === 10. インデックス戦略まとめ ===
// - ESRルールに従って複合インデックスを設計
// - explain()で使用状況を確認
// - 未使用インデックスを定期的にクリーンアップ
// - バックグラウンド作成で本番影響を最小化
// - 部分インデックスでインデックスサイズを削減
出力:
TEXT 📖 参照専用インデックス戦略:ESRルールで複合インデックス設計→explain検証→未使用インデックス削除。バックグラウンド作成と部分インデックスで本番影響を最小化。
❓ よくある質問
Q
explain()はいつ使うべき?A クエリが遅い場合、または新規クエリのパフォーマンスを設計する場合に使用。
Q インデックスを作成すればするほど良い?
A いいえ。インデックス過多は書き込みパフォーマンスを低下させ、ストレージを消費します。必要なインデックスのみ作成。
Q プロファイラは本番で使って良い?
A レベル1(スロークエリのみ)であれば、本番で使用可能。レベル2はデバッグのみに使用。
📖 まとめ
- explain():実行計画分析、COLLSCAN vs IXSCAN
- ESRルール:等価 → ソート → 範囲の順序でインデックス設計
- スロークエリログ:プロファイラで特定
- サーバー監視:接続数、メモリ、Opcounters、レプリケーション遅延
- メモリ管理:WiredTigerキャッシュ設定
- トラブルシューティングフロー
📝 練習問題
- 基礎問題(⭐): 商品検索クエリを
explain()で分析し、実行統計を確認。 - 基礎問題(⭐): ESRルールに従って複合インデックスを作成。
- 応用問題(⭐⭐): スロークエリを特定し、インデックスで最適化(パフォーマンスを10倍以上向上)。
- 応用問題(⭐⭐): 未使用インデックスを
$indexStatsで特定し、クリーンアップ。 - チャレンジ(⭐⭐⭐): パフォーマンス監視スクリプトを作成(接続、メモリ、スロークエリ、レプリケーション遅延を監視)。