エラーについて
シャーディングされたMongoDBクラスター上で$lookupを使った集計パイプラインを実行すると、次のエラーが発生します:
MongoServerError: $lookup from a sharded collection is not allowed
原因は外部コレクション — fromで指定されたコレクション — がシャーディングされていることです。MongoDBはこれについて厳格なルールを持っており、古いバージョンではジョインの実行を単純に拒否します。
根本原因
MongoDBの$lookupは、外部コレクションを単一ノード上でローカルに解決する必要があります。そのコレクションがシャーディングされている場合、データは複数のシャードに分散されています。ターゲットを絞ったルーティング戦略がなければ、MongoDBはジョインを完了するためにすべてのシャードにクエリを送る必要があり — クラスター内のN個すべてのシャードにアクセスするスキャッタ・ギャザーが発生します。
その高コストな操作をサイレントに実行する代わりに、MongoDBは完全にブロックします。
具体的な制限はバージョンによって異なります:
- MongoDB < 5.1:シャーディングされた外部コレクションへの
$lookupは一切許可されません。 - MongoDB 5.1以降:許可されますが、特定の条件下のみ — 同じシャード上でのデータの同居、またはパフォーマンスのトレードオフを伴うクロスシャード。
よくある発生条件:
- MongoDB < 5.1でシャーディングされた
fromコレクションを持つmongos経由の集計実行 - ソースコレクションはシャーディングされていないが、
fromコレクションはシャーディングされている - どちらかの側がシャーディングされている
$facet内での$lookupの使用
修正方法:複数のアプローチ
オプション1 — MongoDB 5.1以降へのアップグレード(長期的な推奨策)
MongoDB 5.1ではシャーディングされたコレクションへの$lookupのネイティブサポートが追加されました。4.xまたは初期の5.xを使用している場合、アップグレードが最もクリーンな方法です — クエリの書き直しもスキーマ変更も不要です。
まず現在のバージョンを確認します:
mongosh --eval "db.version()"
今すぐ古いバージョンから移行できない場合でも、以下のオプションがアップグレードなしで機能します。
オプション2 — シャードに直接接続する(mongosをバイパス)
mongosを完全にスキップして、シャードのプライマリに直接接続します。集計はそのシャード上でローカルに実行されるため、$lookupの制限が適用されません。
// シャードのプライマリに直接接続
mongosh "mongodb://shard1-primary:27017/mydb"
db.orders.aggregate([
{
$lookup: {
from: "products",
localField: "product_id",
foreignField: "_id",
as: "product_info"
}
}
])
**注意点:**これはfromコレクションのデータが実際にそのシャード上に存在する場合にのみ機能します。3つのシャードがあり、productsがすべてに分散している場合、サイレントに部分的な結果が得られます。本番環境で使用する前に、シャードの分散状況を確認してください。
オプション3 — 外部コレクションのシャーディングを解除する
シャーディングされたデータベース内のすべてのコレクションをシャーディングする必要はありません。fromコレクションが小さな参照テーブル — 商品カタログ、ユーザーロール、設定データなど — であれば、シャーディングしないままにしておきます。制限はシャーディングされた外部コレクションにのみ適用されます。
// コレクションがシャーディングされているか確認
use config
db.collections.find({ _id: "mydb.products" })
シャーディングを解除するには、バージョンによって方法が異なります。7.0より前では、シャードキーなしでダンプおよびリストアします。MongoDB 7.0からは専用のコマンドがあります:
// MongoDB 7.0以降のみ
db.adminCommand({ unshardCollection: "mydb.products" })
オプション4 — $lookupのパイプライン形式を使用する(MongoDB 5.1以降)
パイプライン構文はMongoDBのクエリプランナーにより多くの情報を提供します。5.1以降では、これを使ってシャーディングされた外部コレクションに対するジョインを最適化できます:
db.orders.aggregate(
[
{
$lookup: {
from: "products",
let: { pid: "$product_id" },
pipeline: [
{ $match: { $expr: { $eq: ["$_id", "$$pid"] } } }
],
as: "product_info"
}
}
],
{ allowDiskUse: true }
)
ジョイン条件を明示することで、MongoDBはすべてのシャードにクエリをブロードキャストするのではなく、適切なシャードにルーティングできます。
オプション5 — 書き込み時に非正規化して埋め込む
パフォーマンスが重要なパイプラインでは、ジョインを完全にスキップします。必要なフィールドを書き込み時にソースドキュメントに直接埋め込みます — 読み取り時にはクロスコレクションのルックアップが一切不要になります。
// 読み取り時にジョインする代わりに、挿入時に商品データを埋め込む
db.orders.insertOne({
_id: ObjectId(),
product_id: "abc123",
product_snapshot: {
name: "Widget Pro",
price: 29.99,
sku: "WP-001"
}
})
確かにデータが重複します。しかし、数百万件の注文を持つシャーディングされたクラスターでは、すべての読み取りでスキャッタ・ギャザージョインを回避することは、そのトレードオフに値することが多いです。
修正の確認
$limit: 1を付けて集計を実行し、素早くテストします:
db.orders.aggregate([
{
$lookup: {
from: "products",
localField: "product_id",
foreignField: "_id",
as: "product_info"
}
},
{ $limit: 1 }
])
MongoServerErrorが出ない?良いですね。空の結果([])でも、パイプラインが正常に実行されたことを意味します。
次に、explainプランを確認します。外部側に高コストなクロスシャードブロードキャストを示すSHARD_MERGEではなく、"stage": "EQ_LOOKUP"が表示されるのが理想です:
db.orders.explain("executionStats").aggregate([
{
$lookup: {
from: "products",
localField: "product_id",
foreignField: "_id",
as: "product_info"
}
}
])
予防策
- シャーディング前にシャーディング戦略を決定する。
$lookupのfromターゲットとして使用されるコレクションは、本当に大規模(数千万ドキュメント)でない限り、シャーディングしないままにしておく方が通常は適切です。 - **ステージング環境で本番のトポロジーを再現する。**シングルノードの開発環境ではシャーディングの制限を検出できません。本番のシャードレイアウトと一致するクラスターに対して集計をテストしてください。
- **新しいシャーディングデプロイメントではMongoDB 5.1以降をターゲットにする。**クロスシャード
$lookupのサポートだけでも、アップグレードする価値があります。 - 参照/ルックアップテーブルは、データウェアハウスのディメンションテーブルのように考えてください — 小さく、安定していて、集中管理する方が適切です。データベースの残りの部分がシャーディングされているからといって、反射的にシャーディングしないようにしましょう。
ヒント
開発環境と本番環境をまたいで集計パイプラインをデバッグする際、パイプラインJSONへの誤った編集やコピー&ペーストのミスが、追跡しにくい動作の違いを引き起こすことがあります。私は環境間でパイプラインファイルのハッシュを比較するためにToolCraftのHash Generatorを使用しています — 深く調査する前に「このファイルは実際に変更されたのか?」を素早く確認できる方法です。

