MongoServerError: $groupステージのメモリ制限超過を修正 — allowDiskUse:trueとその先へ

intermediate🍃 MongoDB2026-03-21| MongoDB 4.x / 5.x / 6.x / 7.x — 全OS対応(Linux、macOS、Windows)、全ドライバー対応(Node.js、Python、Go、Java)

Error Message

MongoServerError: Exceeded memory limit for $group stage, but didn't allow external sort. Pass allowDiskUse:true to opt in.
#mongodb#aggregation#メモリ#pipeline#allowDiskUse

TL;DR

MongoDBはパイプラインステージごとにRAMを100MBに制限しています。allowDiskUse: trueを追加すれば、クラッシュの代わりに中間データをディスクにスピルしてクエリをすぐに解除できます。本番環境で使用する場合は、パイプラインの早い段階に$matchを組み合わせて、重い処理ステージに到達するデータ量を減らしてください。

// クイックフィックス — Node.jsドライバー
db.collection('orders').aggregate(
  [
    { $group: { _id: '$customerId', total: { $sum: '$amount' } } },
    { $sort: { total: -1 } }
  ],
  { allowDiskUse: true }  // ← これを追加
);

発生する原因

$group$sort$bucketなどのステージはブロッキング処理です。出力を生成する前に、すべての入力を蓄積しなければなりません。MongoDBはステージごとにその蓄積量をRAM 100MBに制限しています。この上限に達すると、MongoDBは利用可能なメモリをすべて消費しないよう、クエリを停止します。

この問題が確実に発生する状況をいくつか挙げます:

  • フィルタリングされていない大規模なコレクション(数千万件のドキュメント)に対するグループ化
  • $limitの前に$sortを実行する — MongoDBは絞り込む前にすべてをソートしなければならない
  • 多数のドキュメントに対して$push$addToSetで配列を構築する
  • wiredTigerCacheSizeGBが低い共有クラスター(例:1GBインスタンスで0.5GB)

修正1:allowDiskUse(回避策)

ディスクスピルを使うと、MongoDBはすべてをRAMに保持する代わりに、ステージの中間状態を一時ファイルに書き込めます。クエリは完了しますが、処理は遅くなります。高速なNVMe SSDでは、インメモリ実行の5〜20倍のレイテンシが見込まれます。HDDやネットワークストレージではさらに悪化します。

mongosh / mongoシェル

db.orders.aggregate(
  [
    { $group: { _id: '$customerId', total: { $sum: '$amount' } } }
  ],
  { allowDiskUse: true }
);

Node.js(mongodbドライバー)

const cursor = collection.aggregate(pipeline, { allowDiskUse: true });
const results = await cursor.toArray();

Python(pymongo)

results = list(collection.aggregate(pipeline, allowDiskUse=True))

Mongoose

const results = await Order.aggregate(pipeline).allowDiskUse(true);

適している用途:単発レポート、管理者エクスポート、データ移行。適していない用途:ユーザーリクエストに応えるAPIや数秒ごとに更新されるダッシュボード。このパイプラインを頻繁に実行する場合は、次の修正方法がより重要になります。

修正2:早期フィルタリングで処理対象データを削減する

$matchを最初に配置してください。この一つの変更が、他のどの方法よりもメモリに効果をもたらします。MongoDBがその後に続くすべての重い処理ステージで処理するドキュメント数が減少するためです。5000万件の注文コレクションを先月の完了済み注文に絞り込むと、$groupに到達する前に20万件程度まで減少することがあります。

db.orders.aggregate([
  // ✅ グループ化の前にフィルタリング — インデックスを使用し、処理対象を縮小
  { $match: {
    createdAt: { $gte: new Date('2024-01-01') },
    status: 'completed'
  }},
  { $group: {
    _id: '$customerId',
    total: { $sum: '$amount' },
    count: { $sum: 1 }
  }}
]);

$matchフィールドにインデックスがない場合、MongoDBはコレクション全体をスキャンします。インデックスを作成してください:

db.orders.createIndex({ createdAt: 1, status: 1 });

修正3:必要なフィールドのみを射影する

$matchの直後にフィールドを削除してください。埋め込み配列や大きなテキストフィールドを持つドキュメントは1件あたり数キロバイトになることがあります。$groupで使用しないフィールドをすべて除外すると、処理対象のデータサイズを50〜80%削減できます。

db.orders.aggregate([
  { $match: { status: 'completed' } },
  // 重いフィールド(例:埋め込まれた明細行配列)を早期に削除
  { $project: { customerId: 1, amount: 1, _id: 0 } },
  { $group: {
    _id: '$customerId',
    total: { $sum: '$amount' }
  }}
]);

修正4:無制限の$push / $addToSetを避ける

$group内の$pushは、そのグループの各ドキュメントに対して1エントリを追加します。1万人の顧客に分散した100万件の注文では、平均で1配列あたり100アイテムになり、各アイテムにデータが含まれる場合は急速に増大します。代わりにカウントするか、グループ化後に配列サイズを制限してください。

// ❌ 巨大な配列が生成される可能性がある
{ $group: { _id: '$userId', items: { $push: '$productId' } } }

// ✅ 必要なのがそれだけなら、代わりにカウントする
{ $group: { _id: '$userId', itemCount: { $sum: 1 } } }

// ✅ またはグループ化後に配列サイズを制限する
{ $project: { items: { $slice: ['$items', 100] } } }

修正5:サーバーレベルの制限を引き上げる(MongoDB 6.0以降)

MongoDB 6.0以降、internalQueryMaxBlockingSortMemoryUsageBytesがステージごとの上限を制御します。デフォルトは104,857,600バイト(ちょうど100MB)です。引き上げることで余裕が生まれますが、暴走クエリが失敗する前により多くのRAMを消費する可能性があるため、このトレードオフを理解した上で調整してください。

// 制限を500MBに引き上げる(デフォルトは100MB = 104857600バイト)
db.adminCommand({
  setParameter: 1,
  internalQueryMaxBlockingSortMemoryUsageBytes: 524288000
});

MongoDB Atlasではこのパラメーターは公開されていません。Atlasでの選択肢は、パイプラインを最適化するかクラスタートップグレードのみです。

確認:修正が適用されたかを確認する

explainを使ってパイプラインを実行し、各ステージで何が起きたかを確認します:

const explain = await db.orders.aggregate(
  pipeline,
  { allowDiskUse: true, explain: true }
).toArray();

console.log(JSON.stringify(explain, null, 2));

ステージの出力でusedDisk: trueを探してください。これはそのステージがディスクにスピルしたことを確認します。usedDiskキーがない(またはfalse)場合はインメモリで実行されています。本番環境では、変更前後のoperationTimeを比較して実際の改善効果を測定してください。

クイックリファレンス

  • 即時修正:aggregateオプションにallowDiskUse: trueを追加
  • ベストプラクティス:最初に$match、次に$project、最後にグループ化
  • インデックス確認$matchフィールドにインデックスが設定されているか確認
  • 避けるべきこと:大規模コレクションでの無制限な$push/$addToSet
  • Atlasユーザー:クラスタートップグレードまたはパイプラインの最適化 — サーバーパラメーターへのアクセス不可

Related Error Notes