Docs Menu

집계 파이프라인 최적화

집계 파이프라인 작업에는 성능 향상을 위해 파이프라인을 재구성하는 최적화 단계가 있습니다.

옵티마이저가 특정 집계 파이프라인을 변환하는 방법을 보려면 explain 옵션을 db.collection.aggregate() 메서드에 추가합니다.

최적화는 릴리스 간에 변경될 수 있습니다.

최적화 단계에서 수행되는 집계 파이프라인 최적화에 대해 학습하는 것 외에도 인덱스 및 문서 필터를 사용하여 집계 파이프라인 성능을 개선하는 방법도 확인할 수 있습니다.

MongoDB Atlas에서 호스팅되는 배포에 대해 UI에서 집계 파이프라인을 실행할 수 있습니다.

집계 파이프라인은 결과를 얻기 위해 문서 필드의 하위 집합만 필요한지 여부를 결정할 수 있습니다. 이 경우 파이프라인은 해당 필드만 사용하므로 파이프라인을 통과하는 데이터 양이 줄어듭니다.

$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 단계를 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 필터는 이동될 수 없습니다.

maxTimeminTime 필드는 $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 단계가 있는 경우, 집계는 때때로 $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이 오는 시퀀스가 있는 경우 $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). $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을 사용한 시퀀스 최적화

$sort$limit 단계 사이에 $skip 단계가 있는 경우 MongoDB는 $limit$sort 단계로 통합하고 $limit 값을 $skip 양만큼 늘립니다. 예를 보려면 $sort + $skip + $limit 시퀀스를 참조하세요.

[1] allowDiskUsetrue이고 n 항목이 집계 메모리 제한을 초과하는 경우에도 최적화는 계속 적용됩니다.

$limit이 다른 $limit 바로 뒤에 오는 경우, 두 단계는 두 초기 제한 수량 중 더 작은 제한 값을 가진 단일 $limit으로 합쳐질 수 있습니다. 예를 들어 파이프라인은 다음과 같은 시퀀스를 포함합니다.

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

그런 다음 두 번째 $limit 단계는 첫 번째 $limit 단계로 합쳐지고, 제한 값이 두 초기 제한 값 10010 중 최소값인 {11}인 제한을 가진 단일 $limit 단계를 결과로 갖게 됩니다.

{ $limit: 10 }

$skip이 다른 $skip 바로 뒤에 오는 경우, 두 단계는 초기 스킵 수량의 을 스킵 값으로 하는 단일 $skip으로 합쳐질 수 있습니다. 예를 들어 파이프라인은 다음과 같은 시퀀스를 포함합니다.

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

그런 다음 두 번째 $skip 단계가 첫 번째 $skip 단계로 합쳐져 스킵 수량이 두 초기 스킵 52의 합인 7을 가진 단일 $skip 단계를 결과로 낳게 됩니다.

{ $skip: 7 }

$match가 다른 $match 바로 뒤에 오는 경우, 두 단계는 조건을 $and을 사용하여 결합하는 단일 $match로 합쳐질 수 있습니다. 예를 들어 파이프라인은 다음과 같은 시퀀스를 포함합니다.

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

그런 다음 두 번째 $match 단계가 첫 번째 $match 단계로 합쳐져 단일 $match 단계가 될 수 있습니다.

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

$unwind가 다른 $lookup 바로 뒤에 오고, $unwind$lookupas 필드에서 작동하는 경우, 옵티마이저는 $unwind$lookup 단계와 병합합니다. 이렇게 하면 큰 중간 문서가 생성되는 것을 방지할 수 있습니다. 또한 $unwind의 뒤에 $lookupas 하위 필드에 있는 $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
}
}
}

계획 설명에서 이 최적화된 파이프라인을 확인할 수 있습니다.

이전 explain 출력에 표시된 unwinding 필드는 $unwind 단계와 다릅니다. unwinding 필드는 파이프라인이 내부적으로 최적화되는 방식을 보여줍니다. $unwind 단계는 입력 문서에서 배열 필드를 분해하고 각 요소에 대한 문서를 출력합니다.

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은 '동등성 조회 (equality lookup)'를 의미합니다.

queryPlanner 객체의 위치는 파이프라인이 $lookup 단계 이후에 슬롯 기반 실행 엔진을 사용하여 실행할 수 없는 단계를 포함하고 있는지 여부에 따라 달라집니다.

  • 만약 $lookup이 마지막 단계이거나 $lookup 이후의 모든 단계가 슬롯 기반 실행 엔진을 사용하여 실행될 수 있다면, queryPlanner 객체는 최상위 explain 출력 객체 (explain.queryPlanner)에 위치합니다.

  • 파이프라인에 $lookup 이후에 슬롯 기반 엔진을 사용하여 실행할 수 없는 단계가 포함되어 있는 경우, queryPlanner 객체는 explain.stages[0].$cursor.queryPlanner에 위치합니다.

다음 섹션에서는 인덱스와 문서 필터를 사용하여 집계 성능을 향상할 수 있는 방법을 보여줍니다.

집계 파이프라인은 입력 컬렉션의 인덱스를 사용하여 성능을 개선할 수 있습니다. 인덱스를 사용하면 단계가 처리하는 문서의 양이 제한됩니다. 이상적으로는 인덱스가 단계 쿼리를 커버할 수 있습니다. 커버된 쿼리는 특히 성능이 높은데, 이는 인덱스가 일치하는 모든 문서를 반환하기 때문입니다.

예를 들어 $match, $sort, $group로 구성된 파이프라인은 모든 단계에서 인덱스의 이점을 누릴 수 있습니다.

  • $match 쿼리 필드의 인덱스는 관련 데이터를 효율적으로 식별합니다.

  • 정렬 필드의 인덱스는 $sort 단계에 대해 데이터를 정렬된 순서로 반환합니다.

  • $sort 순서와 일치하는 그룹화 필드의 인덱스는 $group 단계에 필요한 모든 필드 값을 반환하며, 이를 커버된 쿼리를 만듭니다.

파이프라인이 인덱스를 사용하는 여부를 확인하려면 쿼리 계획을 검토하고 IXSCAN 또는 DISTINCT_SCAN 계획이 있는지 찾아보아야 합니다.

참고

경우에 따라 쿼리 플래너는 인덱스 키 값당 하나의 문서를 반환하는 DISTINCT_SCAN 인덱스 계획을 사용합니다. 키 값당 여러 문서가 있는 경우 DISTINCT_SCANIXSCAN보다 빠르게 실행됩니다. 그러나 인덱스 스캔 매개변수는 DISTINCT_SCANIXSCAN의 시간 비교에 영향을 줄 수 있습니다.

집계 파이프라인의 초기 단계에서는 쿼리 필드를 인덱싱하는 것을 고려하세요. 인덱스를 활용할 수 있는 단계는 다음과 같습니다.

$match 단계
$match 단계 동안, 서버는 쿼리 플래너의 최적화 이후 파이프라인의 첫 번째 단계가 $match일 경우 인덱스를 사용할 수 있습니다.
$sort 단계
$sort 단계 동안, 해당 단계가 $project, $unwind, 또는 $group 단계를 앞서지 않는 경우 서버는 인덱스를 사용할 수 있습니다.
$group 단계

$group 단계 동안, 해당 단계가 다음 두 조건을 모두 충족하는 경우 서버는 인덱스를 사용하여 각 그룹의 $first 또는 $last 문서를 빠르게 찾을 수 있습니다.

  • 파이프라인이 동일한 필드에 의해 sorts하고 groups합니다.

  • $group 단계에서는 $first 또는 $last 누산기 (accumulator) 연산자만 사용합니다.

예를 보려면 $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의 양을 증가시킵니다.

다음도 참조하세요.