エラーの内容
MongoServerError: operation exceeded time limit
アプリを暴走クエリから守るために maxTimeMS を設定しました。今、MongoDBはそれを活用しています — クエリが完了する前に強制終了しています。その部分は意図通りに動作しています。問題は、そもそもクエリが遅すぎることです。
十中八九、原因は三つのいずれかです。インデックスの欠如、数百万件のドキュメントに対するコレクション全体スキャン、またはそれほど大量のデータに対して実行するよう設計されていない集計パイプラインです。
まず遅いクエリを特定する
推測しないでください。何かを変更する前に、何が遅く動作しているのか、そしてその理由を正確に確認してください。
現在の操作を確認する
db.currentOp({ "active": true, "secs_running": { "$gt": 5 } })
これにより、5秒以上実行されているすべての操作が一覧表示されます。出力からクエリを見つけ、ns(名前空間)、command、planSummary フィールドを確認してください。
スロークエリログを確認する
MongoDBはスロークエリのしきい値(デフォルト100ms)を超えるクエリをすべてログに記録します:
db.adminCommand({ getLog: "global" })
またはログファイルを直接tail表示する:
tail -f /var/log/mongodb/mongod.log | grep -i "slow query"
問題のクエリにexplain()を実行する
これは最も重要な診断手順です。explain("executionStats") を使ってクエリを実行してください:
db.orders.find({ status: "pending", userId: ObjectId("...") }).explain("executionStats")
出力で確認すべき三つの項目:
"stage": "COLLSCAN"— MongoDBがコレクション全体をスキャンしており、インデックスが使用されていないdocsExaminedとnReturnedの比較 — 12件の結果を返すために200万件のドキュメントをスキャンするのは、典型的なインデックス問題executionTimeMillis— 実際の実行時間
修正1:不足しているインデックスを追加する(ほとんどのケースに対応)
explain() で COLLSCAN の結果が出た場合、MongoDBはコレクション内のすべてのドキュメントを読み込んでいます。大規模なデータセットでは、ハードウェアに関係なく遅くなります。クエリのフィルターとソートに合ったインデックスを作成してください:
// 単一フィールド
db.orders.createIndex({ status: 1 })
// クエリの形状に合致する複合インデックス
db.orders.createIndex({ status: 1, userId: 1 })
// ソートフィールドを含めてインメモリソートステップを回避する
db.orders.createIndex({ status: 1, userId: 1, createdAt: -1 })
インデックスを作成した後、explain() を再実行してください。ステージが COLLSCAN から IXSCAN に切り替わり、docsExamined が大幅に減少するはずです。
本番環境のコレクションでインデックスを構築する
MongoDB 4.2以前では、インデックスの構築中にコレクションのすべての読み書きがブロックされます。ダウンタイムを避けるために background: true を使用してください:
db.orders.createIndex({ status: 1, userId: 1 }, { background: true })
MongoDB 4.4以降では、デフォルトでブロックせずにインデックスを構築します — このオプションは必要ありません。
修正2:短期的な回避策としてmaxTimeMSを増やす
本番環境が停止していて、本質的な修正に取り組む時間が必要な場合は、タイムアウトを一時的に増やしてください。
Node.js / Mongoose
// MongoDB Node.js ドライバー
const result = await db.collection('orders')
.find({ status: 'pending' })
.maxTimeMS(30000) // 30秒
.toArray();
// Mongoose
const result = await Order.find({ status: 'pending' }).maxTimeMS(30000);
PyMongo
result = db.orders.find(
{"status": "pending"},
max_time_ms=30000
).to_list()
mongosh / mongo シェル
db.orders.find({ status: "pending" }).maxTimeMS(30000)
制限値を上げることで即座のエラーは解消されますが、根本的なクエリはまだすべてのドキュメントをスキャンします。負荷がかかると、接続が占有され、サーバー上で実行されている他のすべての処理が低下します。これはあくまで一時的なパッチであり、解決策ではありません。
修正3:重い集計パイプラインを最適化する
大規模なコレクションに対して $lookup、$group、または $unwind を含むパイプラインは頻繁な原因となります — 特に、早期にデータをフィルタリングするようにステージが順序付けられていない場合。
メモリ集約型の集計にallowDiskUseを追加する
db.orders.aggregate(
[
{ $match: { status: "pending" } },
{ $group: { _id: "$userId", total: { $sum: "$amount" } } },
{ $sort: { total: -1 } }
],
{ allowDiskUse: true, maxTimeMS: 60000 }
)
$matchと$limitをできる限り前に移動する
// 悪い例 — $lookupがコレクション全体で実行された後にフィルタリング
[
{ $lookup: { from: "users", ... } },
{ $match: { status: "active" } }
]
// 良い例 — 先にフィルタリングし、$lookupがより小さいセットで動作する
[
{ $match: { status: "active" } },
{ $lookup: { from: "users", ... } }
]
$match をステージ4からステージ1に移動するだけで、高コストな結合やグループ化が実行される前に、処理対象のデータセットを桁違いに削減できます。
修正4:スタックした操作を強制終了する
すでに実行中で他のクエリをブロックしている場合は、強制終了してください:
// opidを見つける
db.currentOp({ "active": true })
// opidで強制終了
db.killOp(12345)
修正を確認する
explain("executionStats")を再実行 — ステージがCOLLSCANではなくIXSCANになっていることを確認するdocsExaminedがnReturnedに近いことを確認する(例:14件を検査して12件を返す)- 元の
maxTimeMS値を使って実際のクエリを実行 — 正常に完了するはず - 数分間
db.currentOp()を監視して、長時間実行中の操作が残っていないことを確認する
インデックスが使用されていることを確認する
db.orders.aggregate([
{ $indexStats: {} }
])
クエリがヒットするにつれて、新しいインデックスの accesses.ops カウントが増加していくはずです。
今後の予防策
- 本番環境で問題が発生してからではなく、クエリを書いている段階でexplain()を実行する — 開発中にCOLLSCANを発見するコストはゼロ;深夜2時に発見するコストははるかに大きい
- スロークエリプロファイラーを有効にする:
db.setProfilingLevel(1, { slowms: 100 })— スロークエリはsystem.profileに記録され、問題が深刻化する前にレビューできる - 複合インデックスにはESRルールに従う — 等値フィールドを最初に、次にソート、次に範囲;インデックスのフィールド順序は、どのフィールドを含めるかと同様に重要
- 大きな結果セットはページネーションする — どれほど優れたインデックスがあっても、一度の呼び出しで50,000件のドキュメントを取得するのは遅い;代わりに
limit()とカーソルベースのページネーションを使用する - 未使用のインデックスを定期的に監査する — 未使用のインデックスはすべての書き込みを遅くする;アイドル状態であることが確認されたら
db.collection.dropIndex()で削除する

