집계 파이프라인 최적화
집계 파이프라인 작업에는 성능 향상을 위해 파이프라인을 재구성하는 최적화 단계가 있습니다.
옵티마이저가 특정 집계 파이프라인을 어떻게 변환하는지 확인하려면 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
는 쿼리 플래너의 최적화 후 파이프라인의 첫 번째 단계인 경우 인덱스를 사용하여 문서를 필터링할 수 있습니다.$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
의 양을 증가시킵니다.