본문 바로가기
개발 Story/몽고디비

08. Mongodb - 쿼리와 집계

by niee 2016. 11. 26.

스터디 github : https://github.com/KWSStudy

몽고디비 정리 wiki : https://github.com/KWSStudy/Mongodb/wiki


목표

  • Mongodb 질의 연산자를 자세히 다뤄보자.
  • 맵-리듀스 함수를 중심으로 데이터에 대해 집계를 실행하는 방법을 살펴 보자.

상품, 카테고리, 리뷰 쿼리

  • 상품 페이지를 가져오는 쿼리 예
//1. slug가 whell-barrow-9092인 상품을 찾는다.
db.products.findOne({'slug':'whell-barrow-9092'}) 

//2. 해당 상품의 카테고리 정보를 가져온다.
db.categories.findOne({'_id':product['main_cat_id']}) 

//3. 상품 리뷰를 불러온다.
db.reviews.find({'product_id':product['_id']}) 
  • findOne()과 find()의 차이점

    • findOne()은 도큐먼트를 리턴한다.하나의 도큐먼트를 얻고자 할 때 사용한다.
    • find()는 커서객체를 리턴한다. 여러개의 도큐먼트를 리턴 할 때 사용한다.
    • findOne()은 db.collection.find().limit(1)과 동일하다
  • 페이지를 나누기위해 Mongodb는 skip()과 limit() 옵션을 제공한다.

//1. 0~12번째 상품 리뷰를 불러온다.
db.reviews.find({'product_id':product['_id']}).skip(0).limit(12)
  • 정렬은 sort() 함수를 사용한다.
  • 1은 오름차순 -1은 내림차순 정렬.
//1. 0~12번째 상품 리뷰를 추천수가 많은 순서로 불러온다.
db.reviews.find({'product_id':product['_id']}).sort({helpful_votes:-1}).skip(0).limit(12)
  • 특정 키정보가 없는 도큐먼트만 가져오고 싶을때는 null(책은 nil이라고 되어있는데 지금 버전에서는 null인듯)을 사용한다.
//1. parent_id정보가 없는 카테고리 가져오기
db.category.find({'parent_id':null})

유저와 오더

  • findOne()을 사용하면 도큐먼트를 결과값으로 받고, 결과가 없으면 아무것도 리턴하지 않는다.
  • 필요하지 않은 필드까지 가져옴으로써 성능에 문제가 생길수 있다.
  • 도큐먼트에서 가져올 필드를 제한하는 방법으로, 두번째 파라미터로 필요한 필드는 값을1 필요없는 필드는 값을0으로 지정해준다.
//1. _id정보만 가져오기
db.category.findOne({slug:'gardening-tools'},{_id:1})
//{ "_id" : ObjectId("6a5b1476238d3b4dd5000048") }

//2. _id정보만 제외하고 가져오기
db.category.findOne({slug:'gardening-tools'},{_id:0})
/*
{
    "slug" : "gardening-tools",
    "ancestors" : [
        {
            "name" : "Home",
            "_id" : ObjectId("8b87fb1476238d3b4dd50003"),
            "slug" : "home"
        },
        {
            "name" : "Outdoors",
            "_id" : ObjectId("9a9fb1476238d3b4dd500001"),
            "slug" : "outdoors"
        }
    ],
    "parent_id" : ObjectId("9a9fb1476238d3b4dd500001"),
    "name" : "Gardening Tools",
    "description" : "Gardening gadgets galore!"
}
*/
  • RDBMS의 like와 같은 쿼리는 정규식을 이용한다
//1. 이름이 ba로 시작하는 사용자 찾기
db.users.find({last_name:/^ba/})

MongoDB 질의어

  • RDBMS의 AND 절과 같은 질의는 find({name:'park', age:33})
  • RDBMS의 <, <=, >, >= 는 각 $gt, $gte, $lt, $lte이다
  • $in은 검색 키워드와 일치하는 값이 하나라도 있을경우 해당 도큐먼트를 리턴한다.
  • $nin은 검색 키워드와 일치하지 않은 도큐먼트를 리턴해준다.
  • $all은 검색키워드가 모두 일치하는 도큐먼트를 리턴해준다.
  • $in, $all은 인덱스를 사용하지만, $nin은 인덱스를 사용하지 않는다.
  • $ne 단일,배열 값에 상관없이 지정한 값이 아닌 도큐먼트를 찾아준다.
  • $ne는 인덱스를 사용하지 않는다.
  • $not은 정규 표현식 쿼리로부터 얻은 결과의 여집합을 리턴한다.
  • $not을 사용 할 때는 연산자나 정규표현식에 부정형이 존재하지 않을때 사용한다.
  • $or는 2개의 서로다른 키에대한 논리합을 표현한다.
  • 결과값이 가능한 같은 키에대한것이라면 $in을 이용하자.
  • Mongodb는 고정된 스키마가 없기때문에 특정키를 가지고 있는지 확인해야 할 경우 $exists를 이용한다.

서브도큐먼트 매칭

  • 아래와 같이 도큐먼트 안에 도큐먼트가 포함된 경우 서브도큐먼트에 접근하기 위해 닷(.)을 이용한다.
// address는 서브 도큐먼트로 zipcode에 접근할 때는 address.zipcode 처럼 접근한다. 
{
_id : ObjectId("5c32cji23cij3c"),
name : "niee",
  address : {
    zipcode : "12345",
    home : "Inchone"
  }
}

배열

  • 배열로 이루어진 키에도 인덱스를 이용할 수 있다.
  • 닷 표기법을 이용하여 특정 원소에 접근 가능하다. ex)arrayKey.0// 0번째 원소
  • 같은 키를 갖는 오브젝트 배열에서 key.0.name을 사용하면 첫번째 오브젝트의 name에 접근하고 key.name을 사용하면 모든 오브젝트 배열에 접근하여 name을 가져온다.

자바스크립트

  • 쿼리 툴로도 표현할 수 없다면, 자바스크립트를 작성하여 사용할 수 있다.
  • 자바스크립트는 $where연산자와 함께 함수를 작성하는데 함수내부의 this는 현재 도큐먼트를 가리킨다.
//1. helpful_votes > 3 도큐먼트 찾기
db.reviews.find({$where : "function(){return this.helpful_votes > 3;}"})

//2. 더 간단하게 아래처럼 가능
db.reviews.find({$where : "this.helpful_votes > 3"})
  • 자바스크립트를 이용할 경우 인덱스를 사용하지 못해 발생하는 성능 저하 문제와 함께, SQLInjection에 취약해진다.

정규 표현식, 그밖의 쿼리 연산자, 프로젝션, 정렬은 책을 찹조 121~124

스킵과 리미트

  • skip을 사용시 지정한 값만큼 도큐먼트를 스캔해야 하기 때문에 큰 값이 넘어가면 쿼리가 비효율 적일 수 있다.
  • 좀더 나은 쿼리를 위해 skip은 생략하고, 다음 결과 값이 시작되는 범위 조건을 추가 하는 것이다.

group

  • 대부분의 RDBMS는 합계, 평균, 편차와같은 내장 집계함수를 많이 제공하지만, Mongodb에는 구현될 때까지 group,map-reduce를 사용해야한다.
  • group()은 최소한 3개의 파라미터를 받는다.
  • 첫번째 파라미터 key는 데이터를 어떻게 그룹으로 묶을것인지를 지정한다.
  • 두번째 파라미터는 리듀스(reduce funciton) 함수로, 결과 값에 대해 그룹으로 묶는 자바스크립트 함수다.
  • 세번짜 파라미터는 각 키의 값에 대해 처음 반복할 때 리듀스 함수에게 제공하는 초기 도큐먼트다.
  • group은 20,000개 까지 허용되는듯?(group이 20,000개 넘어가니 group() can't handle more than 20000 unique keys 에러 발생)
  • group은 많은 경우 맵-리듀스보다 빠르기 때문에 좋은 선택일 수 있다.
  • key : 그룹으로 나누는 기준이 되는 키를 표현한 도큐먼트. 복합키도 가능.
  • keyf : 그룹을 위한 키를 계산을 통해 만들어야 하는경우 필요한 자바스크립트 함수. key를 지정하지 않으면 반드시 필요.
  • initial : 집계 결과의 초기값으로 사용되는 도큐먼트.(필수)
  • reduce : 집계 기능을 수행하는 함수.현재 도큐먼트와, 집계결과를 저장하는 도큐먼트 2개의 파라미터를 받는다.리턴할 필요가 없다.(필수)
  • cond : 집계를 수행하기 위한 도큐먼트를 필터링 하는 쿼리 셀렉터.
  • finalize : 결과값을 리턴하기전 각 도큐먼트에 적용되는 함수.
//1. 사용자별 리뷰 수와 리뷰 평점
db.reviews.group({
  key : {user_id : true},
  initial : {reviews : 0, votes : 0.0},
  reduce : function(doc, aggregator){
    aggregator.reviews +=1;
    aggregator.votes += doc.votes;
  },
  finalize : function(doc){
    doc.average_votes = doc.votes / doc.reviews;
  }
})

맵-리듀스(map-reduce)

  • 맵-리듀스는 group의 좀더 유연한 버전으로 생각할 수 있다.
  • 그룹 키에 대한 세밀한 제어를 할수 있다.
  • 새 컬렉션에 결과를 저장하는 것 같은 다양한 출력 옵션을 가질 수 있다.
  • 맵-리듀스는 샤드 구성에서 대량의 데이터를 반복 처리하기 위해 필요한 분산된 어그리 게이터를 지원한다.
  • map : 각 도큐먼트에 적용하는 자바스크립트 함수. 집계에 사용 할 키와 값을 선택하기 위해 emit()을 의무적으로 호출 해야함.
  • reduce : 키와 값의 리스트를 받는 자바스크립트 함수. 반드시 받은 값과 같은 구조의 값을 리턴 해야함.
  • query : 매핑될 컬렉션을 필터하는 퀄리 셀렉터, group의 cond와 같은 기능.
  • sort : 쿼리에 적용할 정렬 조건. limit옵션과 사용할때 유용
  • limit : 쿼리와 정렬에 적용되어 결과 값의 개수를 제한하는 정수.
  • out : 결과가 어떻게 리턴되는지를 결정.(자세한 설명은 134p참조)
  • finalize : 리듀스가 완료된 후 각 결과 도큐먼트에 적용될 자바스크립트 함수.
  • scope : map, reduce, finalize 함수에 의해 액세스 되는 전역 변서의 값을 지정하는 도큐먼트.
  • verbose : true일 경우 맵-리듀스 실행 시간에 대한 통계 데이터를 리턴.
//1. 2010년 이후 월 판매액을 구하여 total컬렉션에 저장하기
map = function(){
  var shipping_month = this.purchase_date.getMonth() + '-' + this.purchase_date.getFullYear();
  var tmpItems = 0;
  this.line_items.forEach(function(item){
    tmpItems += item.quantity;
  });

  emit(shipping_month, {order_total : this.sub_total, items_total : tmpItems });
}

reduce = function(key, values){
  var tmpTotal = 0;
  var tmpItems = 0;

  tmpTotal += doc.order_total;
  tmpItems += doc.items_total;

  return({total : tmpTotal, items : tmpItems});
}

filter = {purchase_date : {$gte : new Date(2010, 0, 1)}}

db.orders.mapReduce(map, reduce, {query : filter, out : 'totals'});

distinct

  • 특정 키에대한 고유 값을 얻기 위한 가장 간단한 툴.
  • 단일 키와 배열 값을 갖는 키에 모두 적용된다.
  • 전체 컬렉션에 수행되지만, 두번째 파라메터로 쿼리 셀렉터를 이용하여 제한가능.
//1. product 컬렉션의 tags 키의 값만 얻기
db.products.distinct("tags")

//2. product 컬렉션의 name이 car인 tags 키의 값만 얻기
db.products.distinct("tags",{name:'car'})

group과 distinct는 16MB가 넘는 결과 값은 리턴하지 못한다. 이럴경우 맵-리듀스를 이용하여 컬렉션에 저장한다.

group과 맵-리듀스의 한계는 속도다. 자바스크립트 인터프리터 때문에 대량의 데이터에 대해 속도가 보장되지 못한다.


맵-리듀스 설명 잘된 곳