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 단계를 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 중 최소값인 10인 제한을 가진 단일 $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
}
}
}

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

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의 양을 증가시킵니다.

다음도 참조하세요.

돌아가기

집계 파이프라인