도큐먼트 재구성
MongoDB 집계 파이프라인은 도큐먼트를 변형하여 출력 도큐먼트를 생성하는 데 사용할 수 있는 많은 함수를 가지고 있다.
일반적으로 $project 연산자와 사용되지만 $group 연산자에 대한 _id를 정의할 때 사용할 수도 있음.
(집계 프레임워크 재형성 함수 목록 : https://docs.mongodb.com/manual/reference/operator/aggregation/group/)
db.users.aggregate([
{$match: {username: 'kbanker'}},
//first, last 두개의 필드가 있는 name 하위 객체 생성
{$project: {name: {first:'$first_name',
last:'$last_name'}}
} ])
{ "_id" : ObjectId("4c4b1476238d3b4dd5000001"),
"name" : { "first" : "Kyle",
"last" : "Banker" }
}
- 문자열함수
- $concat : 두 개 이상의 문자열을 단일 문자열로 연결
- $strcasecmp : 대/소문자를 구분하지 않는 문자열 비교를 하며, 숫자 반환
- $substr : 문자열의 부분 문자열 생성 (v3.4 deprecated, $substrBytes 대체)
- $toLower : 소문자 변환
- $toUpper : 대문자 변환
db.users.aggregate([
{$match: {username: 'kbanker'}},
{$project:
//first_name과 last_name을 공백으로 연결
{name: {$concat:['$first_name', ' ', '$last_name']},
//firstInital은 이름의 첫 번째 문자로 설정
firstInitial: {$substr: ['$first_name',0,1]},
//사용자 이름은 대문자로 변경
usernameUpperCase: {$toUpper: '$username'}
}
}
])
{ "_id" : ObjectId("4c4b1476238d3b4dd5000001"),
"name" : "Kyle Banker",
"firstInitial" : "K",
"usernameUpperCase" : "KBANKER"
}
- 산술 함수
- $add: 배열번호를 추가
- $divide: 첫 번째 숫자를 두 번째 숫자로 나눔
- $mod: 첫 번째 숫자의 나머지를 두 번째 숫자로 나눔
- $multiply: 숫자 배열을 곱함
- $subtract: 첫 번째 숫자에서 두 번째 숫자를 뺌
- 날짜/시간 함수
- $dayOfYear: 연 중의 일로서 1 ~ 366
- $dayOfMonth : 월 중의 일로서 1 ~ 31
- $dayOfWeek : 주 중의 일로서 1 ~ 7 (1: 일요일)
- $year: 날짜의 연도
- $month: 날짜의 달 (1 ~ 12)
- $week: 날짜의 주 (0 ~ 53)
- $hour: 시간 (0 ~ 23)
- $minute: 분 (0 ~ 59)
- $secont: 초 (0 ~ 59)
- $millisecond: 시간 중 밀리초 (0~ 999)
- 논리 함수
- $and: 모든 값이 true일 경우 true
- $cmp: 두 개 값을 비교하여 결과 반환, 두 값이 동일하면 0
- $cond: if .. then .. else 조건부 논리
- $eq: 두 값이 동일하면 true
- $gt: >
- $gte: >=
- $ifNull: null일 경우 true
- $lt: <
- $lte: <=
- $ne: 두 값이 동일하지 않으면 true
- $not: 주어진 값의 받대 조건 반환, true면 false, false면 true
- $or: 어떤 하나라도 true면 true
- 집함 함수
- $setEquals: 두 개의 집합이 완전히 같은 요소를 가지면 true
- $setIntersection: 두 개의 집합에서 공통적으로 존재하는 요소 배열 반환
- $setDifference: 두 번째 집합에 없는 첫 번째 집합의 요소 반환
- $setUnion: 두 집합의 합집합
- $setIsSubset: 두 번째 집합이 첫 번째 집합의 부분집합이면 true
- $anyElementTrue: 집합의 요소 중 하나라도 true면 true
- $allElementsTrue: 집합의 모든 요소가 true면 true
{ "_id" : ObjectId("4c4b1476238d3b4dd5003981"),
"productName" : "Extra Large Wheel Barrow",
"tags" : [ "tools", "gardening", "soil" ]}
{ "_id" : ObjectId("4c4b1476238d3b4dd5003982"),
"productName" : "Rubberized Work Glove, Black",
"tags" : [ "gardening" ]}
testSet1 = ['tools']
db.products.aggregate([
{$project:
{productName: '$name',
tags:1,
setUnion: {$setUnion:['$tags',testSet1]}, // $setUnion 으로 tags(=tools) 결합
}
}
])
{ "_id" : ObjectId("4c4b1476238d3b4dd5003981"),
"productName" : "Extra Large Wheel Barrow",
"tags" : ["tools", "gardening", "soil"], //tools, gardening, soil 조합
"setUnion" : ["gardening","tools","soil"]
}
{ "_id" : ObjectId("4c4b1476238d3b4dd5003982"),
"productName" : "Rubberized Work Glove, Black",
"tags" : ["gardening"],
"setUnion" : ["tools", "gardening"] //tolls,gardening 조합
}
- 기타 함수
- $meta: 텍스트 검색 관련 정보에 접근
- $size: 배열의 크기 반환 => 배열이 비어있는지 여부 확인에 유용
- $map: 배열의 각 멤버에 표현식(expression) 적용 => $unwind를 사용하지 않고 배열의 내용을 바꾸고 싶을 때 유용.
- $let: 표현식의 범위 내에서 사용되는 변수 정의 => 여러 개의 $project 단계를 사용하지 않고 임시 정의 변수 사용 가능.
- $literal: 표현식의 값을 평가하지 않고 반환 => 필드값을 0,1 또는 $로 초기화할 때 발생하는 문제를 피할 수 있음
집계 파이프라인 성능에 대한 이해
집계 파이프라인 성능에 중요한 영향을 미칠 수 있는 사항
- 파이프라인에서 가능한 한 빨리 도큐먼트 수와 크기를 줄인다.
- 인덱스는 $match, $sort 에서만 사용 가능
- sharding을 사용하는 경우(매우 큰 컬렉션의 경우) $match 및 $project 연산자는 개별 샤드에서 실행된다. 다른 연산자를 사용하면 남아있는 파이프라인이 프라이머리 샤드에서 실행된다.
집계 파이프라인의 옵션
aggregate() 함수에 전달할 수 있는 두 번째 매개변수 옵션
- explain() – 파이프라인을 실행하고 오직 파이프라인 프로세스 세부 정보만 반환
- allowDiskUse – 중간 결과를 위해 디스크를 사용
- cursor – 초기 배치 크기 지정
//옵션 사용 형식
db.collection.aggregate(pipeline,additionalOptions) // additionalOptions : aggregate()에 전달할 수 있는 선택적 JSON 객체
//additionalOptions 형식
{explain:true, allowDiskUse:true, cursor: {batchSize: n} }
집계 파이프라인의 explain() 함수
SQL에서의 explain과 유사하다. 개발자가 쿼리에서 사용한 인덱스를 밝혀 냄으로써 느린 연산을 진단할 수 있다.
> db.numbers.find({num: {"$gt": 19995 }}).explain("executionStats")
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "tutorial.numbers",
"indexFilterSet" : false,
"parsedQuery" : {
"num" : {
"$gt" : 19995
}
},
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"num" : 1
},
"indexName" : "num_1", //num_1 인덱스를 사용
"isMultiKey" : false,
"direction" : "forward",
"indexBounds" : {
"num" : [
"(19995.0, inf.0]"
]
}
}
},
"rejectedPlans" : [ ]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 4, //4개의 도큐먼트가 반환
"executionTimeMillis" : 0, // 훨씬 더 빠름
"totalKeysExamined" : 4,
"totalDocsExamined" : 4, //4개의 도큐먼트만 스캔
"executionStages" : {
"stage" : "FETCH",
"nReturned" : 4,
"executionTimeMillisEstimate" : 0, //훨씬 더 빠름
"works" : 5,
"advanced" : 4,
"needTime" : 0,
"needFetch" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"docsExamined" : 4,
"alreadyHasObj" : 0,
"inputStage" : {
"stage" : "IXSCAN",
"nReturned" : 4,
"executionTimeMillisEstimate" : 0, //훨씬 더 빠름
"works" : 4,
"advanced" : 4,
"needTime" : 0,
"needFetch" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"keyPattern" : {
"num" : 1
},
"indexName" : "num_1", //num_1 인덱스 사용
"isMultiKey" : false,
"direction" : "forward",
"indexBounds" : {
"num" : [
"(19995.0, inf.0]"
]
},
"keysExamined" : 4,
"dupsTested" : 0,
"dupsDropped" : 0,
"seenInvalidated" : 0,
"matchTested" : 0
}
}
},
"serverInfo" : {
"host" : "rMacBook.local",
"port" : 27017,
"version" : "3.0.6",
"gitVersion" : "nogitversion"
},
"ok" : 1
}
> countsByRating = db.reviews.aggregate([
...
{$match : {'product_id': product['_id']}}, // $match 먼저 수행
... {$group : { _id:'$rating',
... count:{$sum:1}}}
... ],{explain:true}) //explain 옵션 true
{
"stages" : [
{
"$cursor" : {
"query" : {
"product_id" : ObjectId("4c4b1476238d3b4dd5003981")
},
"fields" : {
"rating" : 1,
"_id" : 0
},
"plan" : {
"cursor" : "BtreeCursor ", //인덱스 기반 커서인 BTreeCursor 사용
"isMultiKey" : false,
"scanAndOrder" : false,
"indexBounds" : {
"product_id" : [ //단일 제품에 사용되는 범위
[
ObjectId("4c4b1476238d3b4dd5003981"),
ObjectId("4c4b1476238d3b4dd5003981")
]
]
},
"allPlans" : [
...
]
"$group" : {
"_id" : "$rating",
"count" : {
"$sum" : {
"$const" : 1
}
}
}
}
],
"ok" : 1 }
인덱스가 사용되었는지 그리고 인덱스 내에서 범위 스캔이 되었는지 여부 확인
allowDiskUse 옵션
일반적으로 allowDiskUse 옵션을 사용하면 파이프라인 속도가 느려질 수 있다.
assert: command failed: {
"errmsg" : "exception: Exceeded memory limit for $group,
but didn't allow external sort. Pass allowDiskUse:true to opt in.",
"code" : 16945,
"ok" : 0
} : aggregate failed
파이프라인 단계에서 MongoDB가 허용하는 100MB 램 제한을 초과할 때 오류 메시지.
db.orders.aggregate([
{$match: {purchase_data: {$gte: new Date(2010, 0, 1)}}}, //처리할 도큐먼트를 줄이기 위해 $match 먼저 사용
{$group: {
_id: {year : {$year :'$purchase_data'},
month: {$month :'$purchase_data'}},
count: {$sum:1},
total: {$sum:'$sub_total'}}},
{$sort: {_id:-1}}
], {allowDiskUse:true}); //MongoDB가 중간 저장을 위해 디스크를 사용하게 해줌
집계 커서 옵션
V2.6이전에는 파이프라인의 결과가 16MB 제한된 한 개의 도큐먼트였으나 V2.6 이후 기본값은 커서를 반환하는 것이다.
많은 양의 데이터를 스트리밍할 수 있게 해줌 (toArray(), pretty() 메서드 사용 지양)
$group 파이프라인 연산자를 사용하여 각각을 프로그램에 보내지 않고 출력 도큐먼트를 계산할 수 있다.
countsByRating = db.reviews.aggregate([
{$match : {'product_id': product['_id']}},
{$group : { _id:'$rating',
count:{$sum:1}}}
],{cursor:{}}) // 커서를 반환
- cursor.hasNext(): 결과에 다음 도큐먼트가 존재하는지 확인
- cursor.next(): 결과에서 다음 도큐먼트 반환
- cursor.toArray(): 전체 결과를 배열로 반환
- cursor.forEach(): 결과의 각 행에 대해 함수 실행
- cursor.map(): 결과의 각 행에 대해 함수 실행하고, 함수 반환값의 배열을 반환
- cursor.itcount(): 항목수를 반환 (테스트 전용)
- cursor.pretty(): 형식을 갖춘 결과의 배열 표시
기타 집계 기능
- .count()
product = db.products.findOne({‘slug’: ‘wheelbarrow-9092’})
reviews_count = db.reviews.count({‘product_id’: product[‘_id’]}) //제품에 대한 리뷰 집계 - .distinct() : 도큐먼트 최대 크기인 16MB로 제한 됨
db.orders.distinct(‘shipping_address.zip’)
- 맵리듀스 : 일반적으로 집계 프레임워크보다 훨씬 느림. 그러나 유연함.
// 판매 요약 정보 제공 집계 파이프라인
db.orders.aggregate([
{"$match": {"purchase_data":{"$gte" : new Date(2010, 0, 1)}}},
{"$group": {
"_id": {"year" : {"$year" :"$purchase_data"},
"month" : {"$month" : "$purchase_data"}},
"count": {"$sum":1},
"total": {"$sum":"$sub_total"}}},
{"$sort": {"_id":-1}}]);
맵리듀스를 이용하여 아래와 같이 표현
// 1. map 함수를 작성하여 그룹화 중인 키를 정의하고 계산에 필요한 모든 데이터를 패키지화함
map = function() {
var shipping_month = (this.purchase_data.getMonth()+1) + //주문 생성된 월
'-' + this.purchase_data.getFullYear();
var tmpItems = 0;
this.line_items.forEach(function(item) {
tmpItems += item.quantity;
});
//emit() 함수 이용, 첫 번째 인수는 그룹화 기준, 두 번째 인수는 일반적으로 값을 줄이는 도큐먼트
emit(shipping_month, {order_total: this.sub_total,
items_total: tmpItems});
};
// 2. 대응하는 reduce 함수는 이를 더 명확하게 만들어야함
// reduce 작업은 해당 값이 원하는 방식으로 함계 집계된 다음 단일 값으로 반환되는 지 확인.
// 두번 이상 호출될 수 있음.
reduce = function(key, values) { //리듀스 함수에는 키와 하나 이상의 값의 배열이 전달
var result = { order_total: 0, items_total: 0 };
values.forEach(function(value){
result.order_total += value.order_total;
result.items_total += value.items_total;
});
return ( result );
};
쿼리 필터를 추가하고 결과를 저장하기
// filter는 특정 주문만 선택
// 맵은 키-값 쌍을 내보내며, 일반적으로 각각 하나씩 출력
// 리듀스는 맵에 의해 생성된 값 배열을 전달받고 출력
filter = {purchase_data: {$gte: new Date(2010, 0, 1)}};
db.orders.mapReduce(map, reduce, {query: filter, out: 'totals'}); //totals라는 컬렉션에 결과 저장
// 맵리듀스 결과 컬렉션 조회
> db.totals.find()
{ "_id" : "11-2014", "value" : {"order_total" : 4897, "items_total" : 1 } }
{ "_id" : "4-2014", "value" : {"order_total" : 4897, "items_total" : 1 } }
{ "_id" : "8-2014", "value" : {"order_total" : 11093, "items_total" : 4 } }