集計パイプラインの最適化
項目一覧
集計パイプラインの操作には、パフォーマンスを向上させるためにパイプラインを再構築しようとする最適化フェーズがあります。
オプティマイザーが特定の集計パイプラインをどのように変換するかを確認するには、explain
オプションをdb.collection.aggregate()
メソッドに含めます。
最適化はリリースに応じて変更される場合があります。
最適化フェーズ中に実行される集計パイプラインの最適化について学習するだけでなく、 インデックスとドキュメント フィルター を使用して集計パイプラインのパフォーマンスを向上させる方法についても学習します。 「インデックスとドキュメント フィルターによるパフォーマンスの向上を参照してください。
MongoDB Atlas でホストされている配置の UI で集計パイプラインを実行できます。
プロジェクションの最適化
集計パイプラインは、結果を取得するためにドキュメント内のフィールドのサブセットのみが必要かどうかを判断できます。 その場合、パイプラインはそれらの必須フィールドのみを使用するため、パイプラインを通過するデータ量が減ります。
$project
ステージ配置
$project
ステージを使用する場合、通常はパイプラインの最後のステージにして、クライアントに返すフィールドを指定するために使用します。
パイプラインの最初や途中に $project
ステージを使用して、その後のパイプラインステージに渡されるフィールドの数を減らすことは、パフォーマンスの向上にはつながりません。なぜなら、データベースはこの最適化を自動的に行うからです。
パイプラインシーケンスの最適化
($project
または$unset
または$addFields
または$set
) +$match
シーケンス最適化
プロジェクション ステージ($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
シーケンス最適化
$sort
の後に $match
が続くシーケンスがある場合、ソートするオブジェクトの数を最小限に抑えるために、$match
が $sort
の前に移動されます。たとえば、パイプラインが以下のステージで構成されているとします。
{ $sort: { age : -1 } }, { $match: { status: 'A' } }
最適化フェーズでは、オプティマイザはシーケンスを次のように変換します。
{ $match: { status: 'A' } }, { $sort: { age : -1 } }
$redact
+$match
シーケンス最適化
可能であれば、パイプラインに$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
シーケンス最適化
$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
の統合
$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 を使用したシーケンスの最適化
[1] | allowDiskUse が true に設定されており、 n 個のアイテムが集計メモリの制限を超える場合でも、最適化は適用されます。 |
$limit
+$limit
の統合
$limit
が、別の$limit
の直後に続く場合、2 つのステージは 1 つの $limit
に統合され、制限値は 2 つの初期制限値のうち小さい方になります。たとえば、パイプラインには次のシーケンスが含まれるとします。
{ $limit: 100 }, { $limit: 10 }
次に、2 番目の $limit
ステージが 1 番目の $limit
ステージに統合され、単一の $limit
ステージになります。ここでの制限値は、2 つの初期制限値である 100
と 10
のうち小さい方の 10
になります。
{ $limit: 10 }
$skip
+$skip
の統合
$skip
が、別の$skip
の直後に続く場合、2 つのステージは 1 つの $skip
に統合され、スキップ値は 2 つの初期スキップ値の合計になります。たとえば、パイプラインには次のシーケンスが含まれるとします。
{ $skip: 5 }, { $skip: 2 }
次に、2 番目の $skip
ステージが最初の $skip
ステージに統合され、単一の $skip
ステージになります。ここでのスキップ値は、2 つの初期制限値である 5
と 2
の合計の 7
になります。
{ $skip: 7 }
$match
+$match
の統合
$match
が、別の $match
の直後に続く場合、2 つのステージは、それぞれの条件を $and
で組み合わせた単一の $match
に統合されます。たとえば、パイプラインには次のシーケンスが含まれるとします。
{ $match: { year: 2014 } }, { $match: { status: "A" } }
次に、2番目の $match
ステージは最初の $match
ステージに統合され、単一の $match
ステージになります。
{ $match: { $and: [ { "year" : 2014 }, { "status" : "A" } ] } }
$lookup
、$unwind
、$match
の統合
$unwind
が$lookup
} の直後に続き、 $unwind
が$lookup
のas
フィールドで操作される場合、オプティマイザは$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
を使用する方法の詳細については、「集計パイプライン操作に関する情報の返却 」を参照してください。
次のセクションでは、次の内容について説明します。
スロットベースのエンジンを集計に使用する条件。
スロットベースのエンジンが使用されたかどうかを確認する方法。
$group
最適化
バージョン 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
に含まれます。
$lookup
最適化
バージョン 6.0 で追加。
バージョン6.0以降、 MongoDB は、パイプライン内の先行する$lookup
すべて の ステージもスロット ベースのエンジンで実行でき、次の条件のいずれも当てはまらない場合、 スロット ベースの実行クエリ エンジンを使用して ステージを実行できます。
$lookup
操作は、結合されたコレクション上でパイプラインを実行します。この種の操作の例については、「結合されたコレクションの結合条件とサブクエリ」を参照してください。$lookup
のlocalField
またはforeignField
は数値コンポーネントを指定します。例:{ localField: "restaurant.0.review" }
。パイプラインに含まれる任意の
$lookup
のfrom
フィールドには、ビューまたはシャーディングされたコレクションが明示されます。
スロットベースのクエリ実行エンジンが $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
に含まれます。
インデックスとドキュメント フィルターによるパフォーマンスの向上
次のセクションでは、インデックスとドキュメント フィルターを使用して集計パフォーマンスを向上させる方法を示します。
Indexes
集計パイプラインでは、入力コレクションのインデックスを使用してパフォーマンスを向上させることができます。 インデックスを使用すると、1つのステージで処理されるドキュメントの量が制限されます。 理想的には、インデックスでステージ クエリをカバーできます。 カバード クエリは、インデックスが一致するすべてのドキュメントを返すため、特にパフォーマンスが高くなります。
たとえば、$match
、$sort
、$group
で構成されるパイプラインでは、各ステージで以下のようなインデックスのメリットが得られます。
$match
クエリフィールドのインデックスは、関連するデータを効率的に識別できますソートフィールドのインデックスは、
$sort
ステージのデータをソートされた順序で返すことができます$sort
の順序と一致するグループ化フィールドのインデックスは、$group
ステージ(カバード クエリ)の実行に必要なすべてのフィールド値を返すことができます
パイプラインがインデックスを使用しているかどうかを判断するには、クエリプランを確認して IXSCAN
プランまたは DISTINCT_SCAN
プランを探します。
注意
場合によっては、クエリ プランナーは、インデックスキー値ごとに 1 つのドキュメントを返す DISTINCT_SCAN
インデックスプランを使用することもあります。キー値ごとに複数のドキュメントがある場合、DISTINCT_SCAN
は IXSCAN
よりも速く実行されます。ただし、インデックス スキャン パラメーターは、DISTINCT_SCAN
と IXSCAN
の時間の比較に影響する可能性があります。
集計パイプラインの初期段階では、クエリ フィールドのインデックス作成を検討します。インデックスのメリットが得られるステージは次のとおりです。
$match
ステージ$match
は、パイプラインの最初のステージである場合、クエリ プランナー による最適化の後、インデックスを使用してドキュメントをフィルタリングできます。$sort
ステージ$sort
は、その前に$project
、$unwind
、または$group
ステージがない限り、インデックスのメリットを得ることができます。$group
ステージ$group
は、次の条件をすべて満たす場合、インデックスを使用して各グループ内の最初のドキュメントを検索できます。グループ化されたフィールドのソート順序に一致するインデックスが存在する
例については、「$group パフォーマンスの最適化」を参照してください。
$geoNear
ステージ$geoNear
は常にインデックスを使用します。これはパイプラインの最初のステージである必要があり、地理空間インデックスが必要なためです。
さらに、パイプラインの後半のステージで、変更されていない他のコレクションからデータを取得する場合は、それらのコレクションのインデックスを使用して最適化を行うことができます。これらのステージには、次のものが含まれます。
ドキュメント フィルター
集計操作でコレクション内のドキュメントのサブセットのみが必要な場合は、まず以下のようにドキュメントをフィルタリングします。
例
$sort
+$skip
+$limit
シーケンス
パイプラインには、以下のように $sort
、$skip
、$limit
の順に続くシーケンスが含まれます。
{ $sort: { age : -1 } }, { $skip: 10 }, { $limit: 5 }
オプティマイザーは$sort
+ $limit
統合を実行してシーケンスを次のように変換します。
{ "$sort" : { "sortKey" : { "age" : -1 }, "limit" : NumberLong(15) } }, { "$skip" : NumberLong(10) }
MongoDB は並べ替えによって $limit
の値を増やします。