집계 파이프라인 최적화
집계 파이프라인 작업에는 성능 향상을 위해 파이프라인을 재구성하는 최적화 단계가 있습니다.
옵티마이저가 특정 집계 파이프라인을 어떻게 변환하는지 확인하려면 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
단계를 4개의 개별 필터( $match
쿼리 문서의 각 키에 대해 하나씩)로 나눕니다. 그런 다음 옵티마이저는 각 필터를 가능한 많은 프로젝션 단계 앞으로 이동시켜, 필요에 따라 새로운 $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
단계가 있는 경우, 집계는 때때로 $redact
단계 앞에 $match
단계의 일부를 추가할 수 있습니다. 추가된 $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
). $sort
단계와 $limit
단계 사이에 문서 수를 변경하는 파이프라인 단계가 있는 경우 MongoDB는 $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는 메모리[1]에 n
개의 항목만 저장하면 됩니다. 자세한 내용은 $sort
연산자 및 메모리를 참조하십시오.
참고
$skip을 사용한 시퀀스 최적화
[1] | allowDiskUse 이 true 이고 n 항목이 집계 메모리 제한을 초과하는 경우에도 최적화는 계속 적용됩니다. |
$limit
+ $limit
병합
$limit
이 다른 $limit
바로 뒤에 오는 경우, 두 단계는 두 초기 제한 수량 중 더 작은 제한 값을 가진 단일 $limit
으로 합쳐질 수 있습니다. 예를 들어 파이프라인은 다음과 같은 시퀀스를 포함합니다.
{ $limit: 100 }, { $limit: 10 }
그런 다음 두 번째 $limit
단계는 첫 번째 $limit
단계로 합쳐지고, 제한 값이 두 초기 제한 값 100
과 10
중 최소값인 10
인 제한을 가진 단일 $limit
단계를 결과로 갖게 됩니다.
{ $limit: 10 }
$skip
+ $skip
병합
$skip
이 다른 $skip
바로 뒤에 오는 경우, 두 단계는 초기 스킵 수량의 합을 스킵 값으로 하는 단일 $skip
으로 합쳐질 수 있습니다. 예를 들어 파이프라인은 다음과 같은 시퀀스를 포함합니다.
{ $skip: 5 }, { $skip: 2 }
그런 다음 두 번째 $skip
단계가 첫 번째 $skip
단계로 합쳐져 스킵 수량이 두 초기 스킵 5
와 2
의 합인 7
을 가진 단일 $skip
단계를 결과로 낳게 됩니다.
{ $skip: 7 }
$match
+ $match
병합
$match
가 다른 $match
바로 뒤에 오는 경우, 두 단계는 조건을 $and
을 사용하여 결합하는 단일 $match
로 합쳐질 수 있습니다. 예를 들어 파이프라인은 다음과 같은 시퀀스를 포함합니다.
{ $match: { year: 2014 } }, { $match: { status: "A" } }
그런 다음 두 번째 $match
단계가 첫 번째 $match
단계로 합쳐져 단일 $match
단계가 될 수 있습니다.
{ $match: { $and: [ { "year" : 2014 }, { "status" : "A" } ] } }
$lookup
, $unwind
및 $match
병합
$unwind
가 다른 $lookup
바로 뒤에 오고, $unwind
가 $lookup
의 as
필드에서 작동하는 경우, 옵티마이저는 $unwind
를 $lookup
단계와 병합합니다. 이렇게 하면 큰 중간 문서가 생성되는 것을 방지할 수 있습니다. 또한 $unwind
의 뒤에 $lookup
의 as
하위 필드에 있는 $match
가 오는 경우, 옵티마이저는 $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 } } }
계획 설명에서 이 최적화된 파이프라인을 확인할 수 있습니다.
슬롯 기반 쿼리 실행 엔진 파이프라인 최적화
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
은 '동등성 조회 (equality lookup)'를 의미합니다.
queryPlanner
객체의 위치는 파이프라인이 $lookup
단계 이후에 슬롯 기반 실행 엔진을 사용하여 실행할 수 없는 단계를 포함하고 있는지 여부에 따라 달라집니다.
만약
$lookup
이 마지막 단계이거나$lookup
이후의 모든 단계가 슬롯 기반 실행 엔진을 사용하여 실행될 수 있다면,queryPlanner
객체는 최상위explain
출력 객체 (explain.queryPlanner
)에 위치합니다.파이프라인에
$lookup
이후에 슬롯 기반 엔진을 사용하여 실행할 수 없는 단계가 포함되어 있는 경우,queryPlanner
객체는explain.stages[0].$cursor.queryPlanner
에 위치합니다.
인덱스 및 문서 필터로 성능 향상
다음 섹션에서는 인덱스와 문서 필터를 사용하여 집계 성능을 향상할 수 있는 방법을 보여줍니다.
Indexes
집계 파이프라인은 입력 컬렉션의 인덱스를 사용하여 성능을 개선할 수 있습니다. 인덱스를 사용하면 단계가 처리하는 문서의 양이 제한됩니다. 이상적으로는 인덱스가 단계 쿼리를 커버할 수 있습니다. 커버된 쿼리는 특히 성능이 높은데, 이는 인덱스가 일치하는 모든 문서를 반환하기 때문입니다.
예를 들어 $match
, $sort
, $group
로 구성된 파이프라인은 모든 단계에서 인덱스의 이점을 누릴 수 있습니다.
$match
쿼리 필드의 인덱스는 관련 데이터를 효율적으로 식별합니다.정렬 필드의 인덱스는
$sort
단계에 대해 데이터를 정렬된 순서로 반환합니다.$sort
순서와 일치하는 그룹화 필드의 인덱스는$group
단계에 필요한 모든 필드 값을 반환하며, 이를 커버된 쿼리를 만듭니다.
파이프라인이 인덱스를 사용하는 여부를 확인하려면 쿼리 계획을 검토하고 IXSCAN
또는 DISTINCT_SCAN
계획이 있는지 찾아보아야 합니다.
참고
경우에 따라 쿼리 플래너는 인덱스 키 값당 하나의 문서를 반환하는 DISTINCT_SCAN
인덱스 계획을 사용합니다. 키 값당 여러 문서가 있는 경우 DISTINCT_SCAN
이 IXSCAN
보다 빠르게 실행됩니다. 그러나 인덱스 스캔 매개변수는 DISTINCT_SCAN
과 IXSCAN
의 시간 비교에 영향을 줄 수 있습니다.
집계 파이프라인의 초기 단계에서는 쿼리 필드를 인덱싱하는 것을 고려하세요. 인덱스를 활용할 수 있는 단계는 다음과 같습니다.
$match
단계$match
단계 동안, 서버는 쿼리 플래너의 최적화 이후 파이프라인의 첫 번째 단계가$match
일 경우 인덱스를 사용할 수 있습니다.$sort
단계$sort
단계 동안, 해당 단계가$project
,$unwind
, 또는$group
단계를 앞서지 않는 경우 서버는 인덱스를 사용할 수 있습니다.$group
단계$group
단계 동안, 해당 단계가 다음 두 조건을 모두 충족하는 경우 서버는 인덱스를 사용하여 각 그룹의$first
또는$last
문서를 빠르게 찾을 수 있습니다.예를 보려면 $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
의 양을 증가시킵니다.