Docs Menu
Docs Home
/
MongoDBマニュアル
/ /

集計パイプラインの最適化

項目一覧

  • プロジェクションの最適化
  • パイプラインシーケンスの最適化
  • パイプラインの統合最適化
  • スロットベースのクエリ実行エンジン パイプラインの最適化
  • インデックスとドキュメント フィルターによるパフォーマンスの向上

集計パイプラインの操作には、パフォーマンスを向上させるためにパイプラインを再構築しようとする最適化フェーズがあります。

オプティマイザーが特定の集計パイプラインをどのように変換するかを確認するには、explain オプションをdb.collection.aggregate()メソッドに含めます。

最適化はリリースに応じて変更される場合があります。

最適化フェーズ中に実行される集計パイプラインの最適化について学習するだけでなく、インデックスとドキュメント フィルターを使用して集計パイプラインのパフォーマンスを向上させる方法についても学習します。

集計パイプラインは、結果を取得するためにドキュメント内のフィールドのサブセットのみが必要かどうかを判断できます。その場合、パイプラインはそれらのフィールドのみを使用するため、パイプラインを通過するデータ量が減ります。

$project ステージを使用する場合、通常はパイプラインの最後のステージにして、クライアントに返すフィールドを指定するために使用します。

パイプラインの最初や途中に $project ステージを使用して、その後のパイプラインステージに渡されるフィールドの数を減らすことは、パフォーマンスの向上にはつながりません。なぜなら、データベースはこの最適化を自動的に行うからです。

プロジェクション ステージ($addFields$project$set 、または$unset ) の後に$matchステージが続く集計パイプラインの場合、MongoDB ではプロジェクションステージで計算された値を必要としない$matchステージのフィルターが、プロジェクションの前の新しい$matchステージに移動します。

集約パイプラインに複数のプロジェクション ステージまたは $match ステージが含まれている場合、MongoDB は $match ステージごとにこの最適化を実行し、フィルターが依存していないすべてのプロジェクション ステージの前に各 $match フィルターを移動します。

次のようなステージを持つパイプラインを考えてみます。

{
$addFields: {
maxTime: { $max: "$times" },
minTime: { $min: "$times" }
}
},
{
$project: {
_id: 1,
name: 1,
times: 1,
maxTime: 1,
minTime: 1,
avgTime: { $avg: ["$maxTime", "$minTime"] }
}
},
{
$match: {
name: "Joe Schmoe",
maxTime: { $lt: 20 },
minTime: { $gt: 5 },
avgTime: { $gt: 7 }
}
}

オプティマイザーにより、$match ステージは$match クエリドキュメントのキーごとに 1 つずつ、合計 4 つの個別のフィルターに分割されます。その後、オプティマイザーは各フィルターをできるだけ多くのプロジェクション ステージの前に移動させ、必要に応じて新しい $match ステージを作成します。

この例の場合、オプティマイザーにより次の最適化されたパイプラインが自動的に生成されます。

{ $match: { name: "Joe Schmoe" } },
{ $addFields: {
maxTime: { $max: "$times" },
minTime: { $min: "$times" }
} },
{ $match: { maxTime: { $lt: 20 }, minTime: { $gt: 5 } } },
{ $project: {
_id: 1, name: 1, times: 1, maxTime: 1, minTime: 1,
avgTime: { $avg: ["$maxTime", "$minTime"] }
} },
{ $match: { avgTime: { $gt: 7 } } }

注意

最適化されたパイプラインは手動で実行することを意図したものではありません。元のパイプラインと最適化されたパイプラインは同じ結果を返します。

最適化されたパイプラインは説明プランで確認できます。

$match フィルターの { avgTime: { $gt: 7 } } は、avgTime フィールドを計算するために $project ステージに依存します。$project ステージはこのパイプラインの最後のプロジェクション ステージであるため、 avgTime$match フィルターを移動できませんでした。

maxTime フィールドと minTime フィールドは $addFields ステージ内で計算されますが、$project ステージには依存しません。オプティマイザは、これらのフィールドのフィルター用に新しい $match ステージを作成し、それを $project ステージの前に配置しました。

$matchフィルター{ name: "Joe Schmoe" }は、$project ステージまたは $addFields ステージで計算された値を使用しないため、両方のプロジェクション ステージの前に新しい $match ステージとして移動されました。

最適化後は、フィルター{ name: "Joe Schmoe" }がパイプラインの先頭の$matchステージにあります。これには、最初にコレクションをクエリするときに、集計で name フィールドのインデックスを使用できるという利点もあります。

$sort の後に $match が続くシーケンスがある場合、ソートするオブジェクトの数を最小限に抑えるために、$match$sort の前に移動されます。たとえば、パイプラインが以下のステージで構成されているとします。

{ $sort: { age : -1 } },
{ $match: { status: 'A' } }

最適化フェーズでは、オプティマイザはシーケンスを次のように変換します。

{ $match: { status: 'A' } },
{ $sort: { age : -1 } }

可能であれば、パイプラインに$redact ステージの直後に$match ステージがある場合、集計によって$match ステージの一部が$redact ステージの前に追加されることがあります。追加された $match ステージがパイプラインの開始時である場合、集計はインデックスを使用するだけでなく、コレクションにクエリを実行してパイプラインに入るドキュメントの数を制限できます。詳細については、 「インデックスとドキュメント フィルターによるパフォーマンスの向上」を参照してください。

たとえば、パイプラインが以下のステージで構成されているとします。

{ $redact: { $cond: { if: { $eq: [ "$level", 5 ] }, then: "$$PRUNE", else: "$$DESCEND" } } },
{ $match: { year: 2014, category: { $ne: "Z" } } }

オプティマイザは、次のように $redact ステージの前に同じ $match ステージを追加できます。

{ $match: { year: 2014 } },
{ $redact: { $cond: { if: { $eq: [ "$level", 5 ] }, then: "$$PRUNE", else: "$$DESCEND" } } },
{ $match: { year: 2014, category: { $ne: "Z" } } }

$project または $unset の後に $skip が続くシーケンスがある場合、$skip$project の前に移動されます。たとえば、パイプラインが以下のステージで構成されているとします。

{ $sort: { age : -1 } },
{ $project: { status: 1, name: 1 } },
{ $skip: 5 }

最適化フェーズでは、オプティマイザはシーケンスを次のように変換します。

{ $sort: { age : -1 } },
{ $skip: 5 },
{ $project: { status: 1, name: 1 } }

可能な場合、最適化フェーズでは、パイプライン ステージをその前のステージに統合します。一般的に、統合はシーケンスの並べ替えの最適化の後に発生します。

$sort$limit の前にある場合、途中のステージでドキュメント数が変更されない限り、オプティマイザは $limit$sort に統合することができます(例: $unwind$group)。MongoDBは、$sort ステージと $limit ステージの間にドキュメント数を変更するパイプライン ステージがある場合、$limit$sort に統合しません。

たとえば、パイプラインが以下のステージで構成されているとします。

{ $sort : { age : -1 } },
{ $project : { age : 1, status : 1, name : 1 } },
{ $limit: 5 }

最適化フェーズでは、オプティマイザはシーケンスを次のように統合します。

{
"$sort" : {
"sortKey" : {
"age" : -1
},
"limit" : NumberLong(5)
}
},
{ "$project" : {
"age" : 1,
"status" : 1,
"name" : 1
}
}

これにより、ソート操作の進行中に上位 n 個の結果のみが保持できます。ここでは、n は指定された制限であり、MongoDB はメモリに n 個のアイテムのみを保存するだけで済みます [1] 。詳細については、「$sort 演算子とメモリ」を参照してください。

注意

$skip を使用したシーケンスの最適化

ステージと$skip $sort$limitステージの間に ステージがある場合、MongoDB は$limit $sortを ステージに統合し、$limit の値を$skip の量ずつ増加させます。例については、「 $sort + $skip + $limitシーケンス」を参照してください。

[1] allowDiskUsetrue に設定されており、 n 個のアイテムが集計メモリの制限を超える場合でも、最適化は適用されます。

$limitが、別の$limit の直後に続く場合、2 つのステージは 1 つの $limit に統合され、制限値は 2 つの初期制限値のうち小さい方になります。たとえば、パイプラインには次のシーケンスが含まれるとします。

{ $limit: 100 },
{ $limit: 10 }

次に、2 番目の $limit ステージが 1 番目の $limit ステージに統合され、単一の $limit ステージになります。ここでの制限値は、2 つの初期制限値である 10010 のうち小さい方の 10 になります。

{ $limit: 10 }

$skipが、別の$skip の直後に続く場合、2 つのステージは 1 つの $skip に統合され、スキップ値は 2 つの初期スキップ値の合計になります。たとえば、パイプラインには次のシーケンスが含まれるとします。

{ $skip: 5 },
{ $skip: 2 }

次に、2 番目の $skip ステージが最初の $skip ステージに統合され、単一の $skip ステージになります。ここでのスキップ値は、2 つの初期制限値である 52 の合計の 7 になります。

{ $skip: 7 }

$match が、別の $match の直後に続く場合、2 つのステージは、それぞれの条件を $and で組み合わせた単一の $match に統合されます。たとえば、パイプラインには次のシーケンスが含まれるとします。

{ $match: { year: 2014 } },
{ $match: { status: "A" } }

次に、2番目の $match ステージは最初の $match ステージに統合され、単一の $match ステージになります。

{ $match: { $and: [ { "year" : 2014 }, { "status" : "A" } ] } }

$unwind$lookup } の直後に続き、 $unwind$lookupasフィールドで操作される場合、オプティマイザは$unwind$lookupステージに統合します。 これにより、大規模な中間ドキュメントの作成を回避できます。 さらに、$unwind の後に$match の任意のas サブフィールドで$lookup が続く場合、オプティマイザは$match も統合します。

たとえば、パイプラインには次のシーケンスが含まれるとします。

{
$lookup: {
from: "otherCollection",
as: "resultingArray",
localField: "x",
foreignField: "y"
}
},
{ $unwind: "$resultingArray" },
{ $match: {
"resultingArray.foo": "bar"
}
}

オプティマイザーは、 $unwindステージと$matchステージを$lookupステージに統合します。 explainオプションを使用して集計を実行すると、 explainの出力には統合された ステージが表示されます。

{
$lookup: {
from: "otherCollection",
as: "resultingArray",
localField: "x",
foreignField: "y",
let: {},
pipeline: [
{
$match: {
"foo": {
"$eq": "bar"
}
}
}
],
unwinding: {
"preserveNullAndEmptyArrays": false
}
}
}

この最適化されたパイプラインはexplain プランで確認できます。

MongoDB はスロットベースのクエリ実行エンジンを使用して、特定の条件が満たされたときに特定のパイプライン ステージを実行できます。ほとんどの場合、スロットベースの実行エンジンは、従来のクエリ エンジンと比較してパフォーマンスが向上し、CPU とメモリのコストが削減されます。

スロットベースの実行エンジンが使用されていることを確認するには、explain オプションを使用して集計を実行します。このオプションは、集計のクエリプランに関する情報を出力します。集計で explain を使用する方法の詳細については、「集計パイプライン操作に関する情報の返却」を参照してください。

次のセクションでは、次の内容について説明します。

  • スロットベースの実行エンジンを集計に使用する条件。

  • スロットベースの実行エンジンが使用されたかどうかを確認する方法。

バージョン 5.2 で追加

バージョン 5.2 以降、MongoDB は次のいずれかの場合にスロットベースの実行クエリ エンジンを使用して$groupステージを実行します。

  • $group はパイプラインの第一ステージです。

  • パイプラインの先行ステージもすべて、スロットベースの実行エンジンで実行できます。

スロットベースのクエリ実行エンジンが $group に使用される場合、explain の結果にはqueryPlanner.winningPlan.queryPlan.stage: "GROUP" が含まれます。

queryPlanner オブジェクトのロケーションは、パイプラインが $group ステージの後にスロットベースの実行エンジンでは実行できないステージを含んでいるかどうかによって異なります。

  • $group が最後のステージであるか、$group 以降のすべてのステージがスロットベースの実行エンジンで実行できる場合、queryPlanner オブジェクトは最上位の explain 出力オブジェクト(explain.queryPlanner)内に含まれます。

  • パイプラインが $group のあとに、スロットベースの実行エンジンでは実行できないステージを含んでいる場合、queryPlanner オブジェクトは explain.stages[0].$cursor.queryPlanner に含まれます。

バージョン 6.0 で追加。

バージョン 6.0 以降、パイプライン内の先行するすべてのステージがスロットベースの実行エンジンで実行でき、以下の条件のいずれも当てはまらない場合、MongoDB は スロットベースの実行クエリ エンジンを使用して $lookup ステージを実行できます。

  • $lookup 操作は、結合されたコレクション上でパイプラインを実行します。この種の操作の例については、「結合されたコレクションの結合条件とサブクエリ」を参照してください。

  • $lookuplocalField または foreignField は数値コンポーネントを指定します。例: { localField: "restaurant.0.review" }

  • パイプラインに含まれる任意の $lookupfrom フィールドには、ビューまたはシャーディングされたコレクションが明示されます。

スロットベースのクエリ実行エンジンが $lookup に使用される場合、explain の結果にはqueryPlanner.winningPlan.queryPlan.stage: "EQ_LOOKUP" が含まれます。EQ_LOOKUP は「等価検索」を意味します。

queryPlanner オブジェクトのロケーションは、パイプラインが $lookup ステージの後にスロットベースの実行エンジンでは実行できないステージを含んでいるかどうかによって異なります。

  • $lookup が最後のステージであるか、$lookup 以降のすべてのステージがスロットベースの実行エンジンで実行できる場合、queryPlanner オブジェクトは最上位の explain 出力オブジェクト(explain.queryPlanner)内に含まれます。

  • パイプラインが $lookup のあとに、スロットベースの実行エンジンでは実行できないステージを含んでいる場合、queryPlanner オブジェクトは explain.stages[0].$cursor.queryPlanner に含まれます。

次のセクションでは、インデックスとドキュメント フィルターを使用して集計パフォーマンスを向上させる方法を示します。

集計パイプラインでは、入力コレクションのインデックスを使用してパフォーマンスを向上させることができます。インデックスを使用すると、1つのステージで処理されるドキュメントの量が制限されます。理想的には、インデックスでステージ クエリをカバーできます。カバード クエリは、一致するすべてのドキュメントをインデックスが返すため、特に高いパフォーマンスを発揮します。

たとえば、$match$sort$group で構成されるパイプラインでは、各ステージで以下のようなインデックスのメリットが得られます。

  • $match クエリフィールドのインデックスは、関連するデータを効率的に識別します

  • ソートフィールドのインデックスは、$sort ステージのデータをソートされた順序で返します。

  • $sort の順序と一致するグループ化フィールドのインデックスは、$group ステージに必要なすべてのフィールド値を返すため、カバード クエリになります。

パイプラインがインデックスを使用しているかどうかを判断するには、クエリプランを確認して IXSCAN プランまたは DISTINCT_SCAN プランを探します。

注意

場合によっては、クエリ プランナーは、インデックスキー値ごとに 1 つのドキュメントを返す DISTINCT_SCAN インデックスプランを使用することもあります。キー値ごとに複数のドキュメントがある場合、DISTINCT_SCANIXSCAN よりも速く実行されます。ただし、インデックス スキャン パラメーターは、DISTINCT_SCANIXSCAN の時間の比較に影響する可能性があります。

集計パイプラインの初期段階では、クエリ フィールドのインデックス作成を検討します。インデックスのメリットが得られるステージは次のとおりです。

$match ステージ
$match ステージでは、$match がパイプラインの最初のステージであれば、クエリ プランナーによる最適化の後、サーバーはインデックスを使用できます。
$sort ステージ
$sort ステージでは、そのステージの前に $project$unwind、または $group ステージがない場合、サーバーはインデックスを使用できます。
$group ステージ

$group ステージでは、ステージが以下の条件の両方を満たしている場合、サーバーはインデックスを使用して各グループ内の $first ドキュメントまたは $last ドキュメントを迅速に見つけることができます。

  • パイプラインは同じフィールドで sortsgroups を実行します。

  • $group ステージでは、$first または $last のアキュムレータ演算子のみが使用されます。

例については、「$group パフォーマンスの最適化」を参照してください。

$geoNear ステージ
サーバーは、地理空間インデックスを必要とするため、常に $geoNear ステージのインデックスを使用します。

さらに、パイプラインの後半のステージで、変更されていない他のコレクションからデータを取得する場合は、それらのコレクションのインデックスを使用して最適化を行うことができます。これらのステージには、次のものが含まれます。

集計操作でコレクション内のドキュメントのサブセットのみが必要な場合は、まず以下のようにドキュメントをフィルタリングします。

  • パイプラインに入力するドキュメントを制限するには、$match$limit、および $skip のステージを使用します。

  • 可能であれば、パイプラインの先頭に $match を配置して、コレクション内の一致するドキュメントをスキャンするインデックスを使用します。

  • パイプラインの先頭で $match の後に $sort が続く場合は、ソートを含む単一のクエリと同等であり、インデックスを使用できます。

パイプラインには、以下のように $sort$skip$limit の順に続くシーケンスが含まれます。

{ $sort: { age : -1 } },
{ $skip: 10 },
{ $limit: 5 }

オプティマイザーは$sort + $limit統合を実行してシーケンスを次のように変換します。

{
"$sort" : {
"sortKey" : {
"age" : -1
},
"limit" : NumberLong(15)
}
},
{
"$skip" : NumberLong(10)
}

MongoDB は並べ替えによって $limit の値を増やします。

Tip

以下も参照してください。

explain オプションを含む db.collection.aggregate()

戻る

フィールドパス