エラーの理解
$merge 演算子は、データのゴールライン(終着点)だと考えてください。MongoDBでは、ほとんどの集計ステージはドキュメントを変換して次のステージへ渡しますが、$merge は出力アクションです。これは最終結果をコレクションに直接書き込みます。そのため、MongoDBでは厳格なルールが適用されます:$merge は集計配列の絶対的な最終ステージでなければなりません。
もしその後に $sort、$project、あるいは単純な $limit さえも追加しようとすると、データベースエンジンは処理を停止します。その際、以下の特定のエラーメッセージが表示されます:
MongoServerError: The $merge stage must be the last stage in the pipeline
発生原因
集計フレームワーク(Aggregation Framework)はベルトコンベアのように機能します。各ステージがデータを処理し、次へと渡します。$merge ステージがユニークなのは、ストレージエンジンと対話して、アップサート(upsert)やドキュメントの置換といった重い処理を実行するためです。
データが宛先コレクションに到達した時点で、パイプラインの役割は終了します。MongoDBは、すでにディスクにコミットされたドキュメントの処理を継続することを許可していません。このエラーは通常、以下の3つの理由で発生します:
- 偶発的な追加: JavaScriptのループ内で
.push()を使用してパイプラインを構築しており、マージロジックの後にステージを追加してしまった。 - テンプレートのミス: 他のクエリから
$projectブロックをコピーして、コードの一番下に貼り付けてしまった。 - 書き込み後のフィルタリング: データベースに書き込んだ後、UI用に結果をフィルタリングできると誤解していた。
修正方法
1. 順序が正しくないステージを特定する
集計配列の中で、$merge ブロックの後にある波括弧 {} を探してください。最後に小さな $count ステージがあるだけでも、クエリは失敗します。以下は誤ったパイプラインの例です:
db.orders.aggregate([
{ $match: { status: "shipped" } },
{ $group: { _id: "$product_id", unitsSold: { $sum: "$quantity" } } },
{
$merge: {
into: "sales_report",
whenMatched: "replace"
}
},
{ $sort: { unitsSold: -1 } } // エラー: マージ後にソートすることはできません
])
2. パイプラインの順序を入れ替える
この問題の修正は、通常、置き間違えたステージを移動するだけで済みます。保存前に10,000件のドキュメントをソートする必要がある場合は、$sort ステージを $merge ステージの前に配置します。アプリケーションで結果を確認する必要がある場合は、集計の完了後にターゲットコレクションに対して別途 find() クエリを実行してください。
以下が修正済みのバージョンです:
db.orders.aggregate([
{ $match: { status: "shipped" } },
{ $group: { _id: "$product_id", unitsSold: { $sum: "$quantity" } } },
{ $sort: { unitsSold: -1 } }, // 正解: 書き込みの前にデータがソートされます
{
$merge: {
into: "sales_report",
whenMatched: "replace",
whenNotMatched: "insert"
}
}
])
3. 動的なロジックに注意する (Node.js)
プログラムでパイプラインを構築する場合、コード内での実行順序が重要になります。Node.js でよくある間違いは以下の通りです:
const pipeline = [
{ $match: { year: 2023 } }
];
// ロジックが merge ステージを追加
pipeline.push({ $merge: { into: "yearly_archive" } });
// 別の関数が後から projection を追加
pipeline.push({ $project: { _id: 0 } }); // これがクラッシュの原因となります
// 修正策: merge ロジックが常に配列の最後の要素になるようにしてください。
ソリューションのテスト
修正した集計を実行します。$merge はコンソールにドキュメントのリストを返さないことに注意してください。通常、空の結果セットまたは成功のメタデータを返します。データを確認するには、宛先コレクションを直接クエリします:
// 1. 最初に集計を実行する
db.orders.aggregate([...]);
// 2. 新しいコレクションのデータを確認する
db.sales_report.find().limit(10);
プロのヒント
$merge vs $out
どちらのステージもパイプラインの最後にある必要があります。ただし、$merge の方がはるかに柔軟です。$out が既存のコレクションを削除して完全に置き換えるのに対し、$merge は既存ドキュメントの特定のフィールドを更新できます。テーブル全体を再構築するのではなく、変更内容のみを同期する必要がある大規模なデータセットには $merge を使用してください。
パフォーマンスとメモリ
$merge の前に $sort を配置することは完全に有効ですが、大規模なデータセットには注意が必要です。ソートが100MBのRAM制限を超え、フィールドにインデックスが作成されていない場合、パイプラインは失敗します。データが最終的なマージステージに到達する前に、早い段階で $match ステージを使用してドキュメント数を減らすように常に心がけてください。

