MongoDB 스터디 8주차(aggregation)

어그리게이션(Aggregation)

어그리게이션 작업은 데이터를 처리하여 계산된 결과를 반환합니다. 어그리게이션은 여러 도큐먼트의 값을 그룹화하고, 데이터에 다양한 작업을 수행한 후 단일 결과를 반환할 수 있습니다. MongoDB는 세 가지 어그리게이션을 제공합니다.

  • Aggregation Pipeline
  • Single Purpose Aggregation Operations
  • Map-reduce

어그리게이션 파이프라인(Aggregation Pipeline)

Aggregation pipeline 은 파이프라인 이용해 데이터의 집계를 처리하는 프레임워크입니다. 여러 스테이지에 걸쳐 도큐먼트들을 집계된 결과로 변경합니다.

아래 예를 살펴봅시다:

db.orders.aggregate([
	{ $match: { status: "A" } },
	{ $group: { _id: "$custmor_id", total: { $sum: "$amount" } } }
])

  1. $match 스테이지는 도큐먼트들을 status 필드가 "A" 인 데이터만 다음 스테이지로 보냅니다.
  2. $group 스테이지는 custmor_id 로 도큐먼트를 그룹화 한 다음 $amount 의 합을 계산합니다.

파이프라인

파이프라인은 스테이지들로 구성됩니다. 각 스테이지는 도큐먼트들 처리해 변환합니다. 스테이지는 하나의 입력된 도큐먼트보다 많거나 적은 도큐먼트를 반환 할 수도 있습니다. 예를 들어 새 문서를 생성하거나 기존 문서를 필터링 할 수 있습니다.

$out, $merge, $geoNear 를 제외한 스테이지는 파이프라인에 여러 번 사용될 수 있습니다.

고려사항

어그리게이션 파이프라인 제약사항

  • 결과 크기 제한
    • 반환되는 도큐먼트 크기는 16 메가바이트로 제한됩니다
    • 이 제한은 마지막으로 반환되는 도큐먼트에만 적용되고, 파이프라인 안에서는 더 클 수 있습니다.
  • 메모리 제한
    • 스테이지는 100 메가바이트 이상의 메모리를 쓸 수 없습니다.
    • 그보다 큰 데이터를 처리하려면 allowDiskUse 옵션을 사용해 데이터를 임시 파일에 쓸 수 있습니다.

어그리게이션 파이프라인 최적화

최적화 단계를 거쳐 성능이 향상됩니다. explain 최적화된 결과를 볼 수 있습니다.

프로젝션 최적화:

결과를 얻기 위해 도큐먼트의 일부 필드만 필요하다면, 나머지 필드를 제외하여 파이프라인을 통과하는 데이터 양을 줄입니다.

파이프라인 시퀀스 최적화:

($project or $unset or $addFields or $set) + $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: { 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 } } }

$sort + $match 시퀀스 최적화

전:

{ $sort: { age : -1 } },
{ $match: { status: 'A' } }

후:

{ $match: { status: 'A' } },
{ $sort: { age : -1 } }

$redact + $match 시퀀스 최적화

전:

{ $redact: { $cond: { if: { $eq: [ "$level", 5 ] }, then: "$$PRUNE", else: "$$DESCEND" } } },
{ $match: { year: 2014, category: { $ne: "Z" } } }

후:

{ $match: { year: 2014 } },
{ $redact: { $cond: { if: { $eq: [ "$level", 5 ] }, then: "$$PRUNE", else: "$$DESCEND" } } },
{ $match: { year: 2014, category: { $ne: "Z" } } }

$project/$unset + $skip 시퀀스 최적화

전:

{ $sort: { age : -1 } },
{ $project: { status: 1, name: 1 } },
{ $skip: 5 }

후:

{ $sort: { age : -1 } },
{ $skip: 5 },
{ $project: { status: 1, name: 1 } }

파이프라인 병합(Coalescence) 최적화

$sort + $limit 병합

전:

{ $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
  }
}

$limit + $limit 병합

전:

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

후:

{ $limit: 10 }

$skip + $skip 병합

전:

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

후:

{ $skip: 7 }

$match + $match 병합

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

$lookup + $unwind 병합

{
  $lookup: {
    from: "otherCollection",
    as: "resultingArray",
    localField: "x",
    foreignField: "y"
  }
},
{ $unwind: "$resultingArray"}
{
  $lookup: {
    from: "otherCollection",
    as: "resultingArray",
    localField: "x",
    foreignField: "y",
    unwinding: { preserveNullAndEmptyArrays: false }
  }
}

$sort + $skip + $limit 시퀀스

{ $sort: { age : -1 } },
{ $skip: 10 },
{ $limit: 5 }
{
   "$sort" : {
      "sortKey" : {
         "age" : -1
      },
      "limit" : NumberLong(15)
   }
},
{
   "$skip" : NumberLong(10)
}

샤딩된 컬렉션에 대한 어그리게이션

동작방식

만약 파이프라인이 샤드 키로 구분 가능한 $match 로 시작하면 해당 샤드에서만 파이프라인이 실행됩니다.

작업이 여러 샤드에서 실행되면 결과는 mongos 로 라우팅 되어 합쳐집니다.(아래 경우를 제외하고)

  • 파이프라인이 $out 또는 $lookup 스테이지를 가지고 있으면 프라이머리 샤드에서 머지됩니다.
  • 파이프라인이 sort 또는 grouping 스테이지를 가지고 있고, allowDiskUse 설정이 켜저 있으면 머지는 임의로 선택된 샤드에서 실행됩니다.

최적화

파이프라인을 두 파트로 나눌 수 있다면, 가능한 한 많은 단계로 나누어 실행되게 최적화합니다.

예: 우편번호 데이터 어그리게이션

데이터 모델

{
  "_id": "10280",     // 우편번호
  "city": "NEW YORK", // 도시
  "state": "NY",      // 주(약자)
  "pop": 5574,        // 인구
  "loc": [            // 위치(위경도 좌표)
    -74.016323,
    40.710537
  ]
}

천만 명 이상의 인구가 있는 주 반환

db.zipcodes.aggregate( [
   { $group: { _id: "$state", totalPop: { $sum: "$pop" } } },
   { $match: { totalPop: { $gte: 10*1000*1000 } } }
] )

결과

{
  "_id" : "AK",
  "totalPop" : 550043
}

주 내 도시의 평균 인구 반환

db.zipcodes.aggregate( [
   { $group: { _id: { state: "$state", city: "$city" }, pop: { $sum: "$pop" } } },
   { $group: { _id: "$_id.state", avgCityPop: { $avg: "$pop" } } }
] )

결과

{
  "_id" : "MN",
  "avgCityPop" : 5335
}

주 내 가장 큰 도시와 가장 작은 도시 반환

db.zipcodes.aggregate( [
   { $group:
      {
        _id: { state: "$state", city: "$city" },
        pop: { $sum: "$pop" }
      }
   },
   { $sort: { pop: 1 } },
   { $group:
      {
        _id : "$_id.state",
        biggestCity:  { $last: "$_id.city" },
        biggestPop:   { $last: "$pop" },
        smallestCity: { $first: "$_id.city" },
        smallestPop:  { $first: "$pop" }
      }
   },

  // the following $project is optional, and
  // modifies the output format.

  { $project:
    { _id: 0,
      state: "$_id",
      biggestCity:  { name: "$biggestCity",  pop: "$biggestPop" },
      smallestCity: { name: "$smallestCity", pop: "$smallestPop" }
    }
  }
] )

결과

{
  "state" : "RI",
  "biggestCity" : {
    "name" : "CRANSTON",
    "pop" : 176404
  },
  "smallestCity" : {
    "name" : "CLAYVILLE",
    "pop" : 45
  }
}

참고자료