Mongo 7 - 집계
7.1 파이프라인
모든 단계의 입력과 출력은 도큐먼트다.
한 번에 입력 도큐먼트 스트림 하나씩 가져와서 각 도큐먼트 하나씩 처리하고 출력 도큐먼트 스트림 생성
각 단계는 knobs, tunables 셋 제공 이 항목 조정해서 각 단계 매개변수로 지정해 원하는 작업을 수행한다. mongod.conf 파일이나 명령줄 옵션을 통해 조정할 수 있다. 예를 들면 캐시 크기 4GB 설정한다고 하면 mongod.conf 파일에 아래 내용을 추가한다.
*knobs, tunables → 데이터베이스 성능 최적화 하기 위해 조정할 수 있는 설정 및 매개변수이다.
storage:
wiredTiger:
engineConfig:
cacheSizeGB: 4
7.2 익숙한 작업들
일치 → 선출 → 정렬 → 건너뛰기 → 제한 단계가 있다.
match → projection → sort → skip → limit
db.companies.aggregate([
{$match: {founded_year: 2004}},
])
//위 작업은 find 랑 동일하다.
db.companies.find({founded_year: 2004})
도큐먼트당 몇 개의 필드만 나타내도록 수정한다면 다음과 같다.
id 필드는 제외하고 name과 founded_year는 포함한다.
db.companies.aggregate([
{$match: {founded_year: 2004}},
{$project: {
_id: 0,
name: 1,
founded_year: 1
}}
])
예상되는 결과
[
{ "name": "회사1", "founded_year": 2004 },
{ "name": "회사2", "founded_year": 2004 },
...
]
aggregate는 집계 쿼리를 실행할 때 호출하는 메서드이다.
필터링을 위한 일치 단계 + 출력을 위한 도큐먼트
제한
→ 다음은 결과 셋을 5개로 제한을 해보자.
db.companies.aggregate([
{$match: {founded_year: 2004}},
{$limit: 5},
{$project: {
_id: 0,
name: 1}}
])
/*
이 쿼리는 $match 스테이지에서 조건에 맞는 문서를 먼저 필터링하고,
그 중에서 상위 5개만 즉시 $limit로 제한합니다.
이렇게 하면 필요한 문서의 수가 적기 때문에 데이터베이스는 더 적은 양의 데이터를 처리하게 됩니다.
이후 $project 스테이지는 이미 제한된 수의 문서에만 적용되기 때문에 빠르게 실행됩니다.
*/
db.companies.aggregate([
{$match: {founded_year: 2004}},
{$project: {
_id: 0,
name: 1}},
{$limit: 5},
])
/*
이 쿼리는 모든 매칭되는 문서에 대해 먼저 프로젝션을 적용하고,
그 결과로 나온 문서들 중에서 5개만을 최종적으로 제한합니다.
만약 매칭되는 문서가 많다면, $project 스테이지에서 많은 연산이 이루어질 것이고,
이후에 이 중에서 5개만 선택하게 됩니다. 따라서 이 방식은 처리해야 할 데이터 양이 더 많아져 비효율적일 수 있습니다.
*/
결과를 5개로 제한하기 전에 선출 단계에서 수백개의 도큐먼트를 전달해야 한다.
*몽고DB 버전에 쿼리 플래너가 수행할 수 있는 최적화 유형에 관계없이 항상 집계 파이프라인의 효율성을 고려해야 한다.
순서가 중요하다면 제한 단계 전에 정렬을 수행한다.
db.companies.aggregate([
{$match: {founded_year: 2004}},
{$sort : { name : 1}},
{$limit: 5},
{$project: {
_id: 0,
name : 1 }}
])
db.companies.aggregate([
{$match: {founded_year: 2004}},
{$sort : { name : 1}},
{$skip: 10},
{$limit: 5},
{$project: {
_id: 0,
name : 1 }}
])
표현식
불리언 표현식
AND, OR, NOT 표현식을 사용할 수 있다.
7.4
$project
중첩 필드(nested field), 승격(promothing) 에 대해 확인해보자.
db.companies.aggregate([
{$match: {"funding rounds.investments.financial.org.
permalink": "greylock"}},
{$project: { -> 선출 단계
_id: 0, // id를 숨김
name: 1, //포함
ipo: "$ipo.pub_year", //승격
valuation: "$ipo.valuation_amount", //승격
funders: "$funding_rounds.investments.financial_org.permalink" //승격
}}
]).pretty() //정렬
집계 파이프라인에서 정의한 선출 단계는 id 를 숨기고 name을 포함한다.
또한 일부 중첩 필드를 승격한다. 선출 단계는 점 표기법으로 ipo 필드와 funding_rounds 필드에 도달할 필드 경로를 표현해 중첩된 도큐먼트 및 배열에서 값을 선택한다.
7.5 $unwind
배열 필드 작업할 때 종종 하나 이상의 전개 unwined 를 포함해야 한다.
db.collection.aggregate([
{ $unwind: "$fruits" }
]);
{
"_id": 1,
"name": "John",
"fruits": ["apple", "banana", "orange"]
//배열로 묶여있는것을 하나씩 품
//도큐먼트 배열이 아니라 각 fruits에 해당하는 단일 도큐먼트이다.
}
{ "_id": 1, "name": "John", "fruits": "apple" }
{ "_id": 1, "name": "John", "fruits": "banana" }
{ "_id": 1, "name": "John", "fruits": "orange" }
작은 이슈
db.companies.aggregate([
{$match: {"funding_rounds.investments.financial_org.permalink" : "greylock"} },
{$unwind: "$funding_rounds" },
{$project : {
_id: 0,
name: 1,
funder: "$funding_rounds.investments.financial_org.permalink",
amount: "$funding_rounds.raised_amount",
year: "$funding_rounds.funded_year"
}}
])
//input
"funding_rounds": [{
"id": 4,
"round_code" : "b",
"raised_amount": 27500000,
"raised_currency_code": "USD",
"funded_year": 2006,
"investments": [
{
"company": null,
"financial_org": {
"name": "Greylock Partners",
"permalink": "greylock"
},
"person": null
},
{
"company": null,
"financial_org": {
"name": "Meritech Capital Partners",
"permalink": "meritech-capital-partners"
},
"person": null
},
//output
{
"name": "Farecast",
"funder" : [
"madrona-ventrue-group",
"wrf-capital"
], -> 제대로 안풀림 (unwind)
"amount": 1500000,
"year" : 2004
}
첫번째 문제
funding_rounds.investments 필드는 그 자체 배열이고
funding_rounds.investments.financial_org 도 배열이기 때문에 한번 더 unwind를 해줘야 한다.
db.companies.aggregate([
{$match: {"funding_rounds.investments.financial_org.permalink": "greylock"}},
{$unwind: "$funding_rounds"},
{$unwind: "$funding_rounds.investments"},
{$match: {"funding_rounds.investments.financial_org.permalink": "greylock"}},
//investment 항목들 중에서 financial_org.permalink가 "greylock"인 항목만을 다시 한 번 필터링하여, 정확히 "greylock"에 의해 투자된 경우만을 선택
{$project: {
_id: 0,
name: 1,
funder: "$funding_rounds.investments.financial_org.permalink",
amount: "$funding_rounds.raised_amount",
year: "$funding_rounds.funded_year"
}}
]);
//$match를 한번 더 하는 이유는 $unwind가
두번째 문제
//output
{
"name": "Farecast",
"funder" : [
"madrona-ventrue-group",
"wrf-capital"
],
"amount": 1500000,
"year" : 2004
}
그레이록이 참여하지 않은 펀딩 라운드를 나타내는 많은 도큐먼트가 선출 단계로 전달
db.companies.aggregate([
{$match: {"funding_rounds.investments.financial_org.permalink" : "greylock"} },
{$unwind: "$funding_rounds" },
{$match: {"funding_rounds.investments.financial_org.permalink" : "greylock"} },
{$project : {
_id: 0,
name: 1,
funder: "$funding_rounds.investments.financial_org.permalink",
amount: "$funding_rounds.raised_amount",
year: "$funding_rounds.funded_year"
}}
])
그레이록이 한 번이라도 펀딩 라운드에 참여한 회사 먼저 필터링 → 펀딩 라운드 전개 다시 필터링해서 그레이록이 실제로 참여한 펀딩 라운드를 나타내는 도큐먼트만 선출 단계로 전달
7.6 배열 표현식
db.companies.aggregate([
{$match: {"funding_rounds.investments.financial_org.permalink" : "greylock"} },
{$project: {
_id: 0,
name: 1,
founded_year: 1,
rounds: { $filter: {
input: "$funding_rounds",
as: "round",
cond: {$gte: ["$$round.raised_amount", 1000000] // 1억 달러 이상
}}}
}},
{ $match: {"rounds.investments.financial_org.permalink": "greylock" }},
]).pretty()
$$는 작업 중인 표현식 내에서 정의된 변수를 참조하는데 사용
//output
{
"name" : "Dropbox",
"founded_year" : 2007,
"rounds" : [
{
"id": 25090,
"round_code": "b",
"rased_amount" : 25000000
}],
"investments" : [
{
"financial_org": {
"name": "Index Ventures",
"permalink": "index-ventures"
}
}]
$arrayElemAt 연산자를 사용하면 배열 내 특정 슬록을 선택할 수 있다.
db.companies.aggregate([
{$match: {"founded_year": 2010}},
{$project: {
_id: 0,
name: 1,
founded_year: 1,
first_round: {$arrayElemAt: ["$funding_rounds", 0]},
last_round: {$arrayElemAt: ["$funding_rounds", -1]}}}
]).pretty()
$slice
db.companies.aggregate([
{$match: {"founded_year": 2010}},
{$project: {
_id: 0,
name: 1,
founded_year: 1,
early_rounds: {$slice: ["$funding_rounds", 1, 3]}
}
]).pretty()
$size
db.companies.aggregate([
{$match: {"founded_year": 2010}},
{$project: {
_id: 0,
name: 1,
founded_year: 1,
total_rounds: {$size: "$funding_rounds" }
}}
]).pretty()
7.7 누산기
db.companies.aggregate([
{$match: {"funding_rounds": { $exists: true,
$ne: []} }},
{$project: {
_id: 0,
name: 1,
largest_round: {$max: "$funding_rounds.raised_amount"}
}}
])
선출 단계에서 누산기는 배열값 필드에서 작동해야 된다.
db.companies.aggregate([
{$match: {"funding_rounds":
{ $exists: true, $ne: []}}},
{$project: {
_id: 0,
name: 1,
total_funding: {$sum: "$funding_rounds.raised_amount" }
}}
])
7.8 그룹화
그룹 단계는 SQL Group By 명령과 유사한 기능을 수행한다.
db.companies.aggregate([
{$group : {
_id: {funded_year: "$founded_year" },
average_number_of_employees:
{$avg: "$number_of_employees"}
}},
{$sort: {average_number_of_employees: -1 }}
])
//output
- companies 컬렉션의 모든 도큐먼트를 그룹 단계에 전달
- 그룹 단계에서는 founded_year 에 대해 동일한 값을 갖는 모든 도큐먼트를 단일 그룹으로 취급
- $avg 누산기를 사용해 founded_year가 동일한 모든 회사의 평균 직원 수를 계산
{"_id" : {"founded_year" : 1887},
"average_number_of_employees" 35000 }
db.companies.aggregate([
{$match: {"relationshops.persion": {$ne:null}}},
{$project: {relationshops: 1, _id: 0}},
{$unwind: "$relationshops" },
{$group: {
_id: "$relationshops.person",
count: {$sum: 1}
}},
{$sort: {count: -1}}
]).pretty()
- person이 null이 아닌 관계는 모두 필터링
- 도큐먼트 관계 모두 선출
- 전개 단계에서 배열의 모든 관계가 그룹 단계를 통과하도록 관계를 전개
동일한 person 값을 갖는 모든 도큐먼트는 그룹화된다.
마지막은 내림차순~
//output
{
"_id" : {
"first_name": "Tim",
"last_name" : "Hanlon",
"permalink" : "tim-hanlon"
},
"count" : 28
}
7.8.1 그룹 단계의 _id 필드
도큐먼트를 그룹화 하는 일반적인 방법
db.companies.aggregate([
{ $match: {founded_year: {$gte: 2013}}},
{ $group: {
_id: {founded_year: "$founded_year"},
companies: { $push : "$name"}
}},
{$sort: {"_id.founded_year": 1}}
]).pretty()
//output
{
"_id": {
"founded_year": 2013
},
"companies": [
"Fixya",
"Wamba",
"Advaliant",
"Fluc",
"iBazar",
...
]
}
*비교 (_id가 2013인것을 보고 설립 연도를 나타내는것을 유추가능하지만 문서의 구조에서 이것이 무엇을 의미하는지 직접적으로 보이지 않음)
{ "_id": 2013, "companies": [...] }
db.companies.aggregate([
{$group: {
_id: {ipo_year: "$ipo.pub_year"},
companies: {$push: "$name"}
}},
{$sort: {"_id.ipo_year": 1}}
]).pretty()
7.8.2 그룹 vs 선출
db.companies.aggregate([
{$match: {funding_rounds: {$ne: [ ]}}},
{$unwind: "$funding_rounds" },
{$sort: {"funding_rounds.funded_year":1,
"funding_rounds.funded_month": 1,
"funding_rounds.funded_day": 1}},
{$group: {
_id: {company: "$name"},
funding: {
$push: {
amount: "$funding_rounds.raised_amount",
year: "$funding_rounds.funded_year"
}}
}},
]).pretty()
//output
{
"_id": {
"company" : "Green Apple Media"
},
"funding": [
{
"amount": 300000,
"year": 2013
},
]
}
$push 연산자는 그룹단계에서만 작동한다.
생성된 도큐먼트를 컬렉션에 쓸 수 있는 $out, $merge → 집계 파이프라인의 마지막이어야 한다.
*$out 의 제약사항
- 동일한 데이터베이스에 쓸 수 있고
- 기존 컬렉션이 있으면 덮어쓰고
- 샤딩된 컬렉션에는 쓸 수 없다.
db.orders.aggregate([
{ $match: { status: "A" } },
{ $group: { _id: "$cust_id", total: { $sum: "$amount" } } },
{ $out: "order_totals" } //order_totals에 덮어씀
])
*$merge
- 샤딩 여부와 관계없이 모든 데이터베이스와 컬렉션에 쓸 수 있다.
db.orders.aggregate([
{ $match: { status: "A" } },
{ $group: { _id: "$cust_id", total: { $sum: "$amount" } } },
{ $merge: {
into: "order_totals",
on: "_id",
whenMatched: "replace",
//만약 일치하는 문서가 없다면
whenNotMatched: "insert"
//새로운 문서가 삽입된다.
}}
])
*$merg
구체화된 뷰(Materialized View)란, 데이터베이스에서 쿼리의 결과를 물리적으로 저장하고, 이를 마치 하나의 테이블이나 컬렉션처럼 사용할 수 있는 데이터 구조를 말합니다. 이는 데이터를 빠르게 읽을 수 있게 해주지만, 기본 데이터에 변경이 있을 때 마다 갱신해야 하는 단점
'IT' 카테고리의 다른 글
몽고디비 11장 - 복제 셋 구성요소 (0) | 2024.04.14 |
---|---|
몽고 디비 9장 (0) | 2024.04.01 |
6장 - 키-값 저장소 설계 (0) | 2024.02.12 |
kafka 학습 내용 정리 - 추가중 ... (0) | 2024.02.10 |
대규모 시스템 설계 기초 - 5장 (0) | 2024.02.04 |
댓글
이 글 공유하기
다른 글
-
몽고디비 11장 - 복제 셋 구성요소
몽고디비 11장 - 복제 셋 구성요소
2024.04.14 -
몽고 디비 9장
몽고 디비 9장
2024.04.01 -
6장 - 키-값 저장소 설계
6장 - 키-값 저장소 설계
2024.02.12 -
kafka 학습 내용 정리 - 추가중 ...
kafka 학습 내용 정리 - 추가중 ...
2024.02.10