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