본문 바로가기

MongoDB

[MongoDB] 확장 검색 쿼리 - $facet 스테이지

쇼핑몰처럼 많은 상품을 판매하는 사이트를 경험해본 개발자라면 다양한 기준( 가격대별로 또는 제품의 특성 )별로

 

상품의 개수를 그룹핑해서 보여주고 있고,

 

특정 그룹을 선택하면 다시 하위의 다른 기준으로 상품을 그룹핑해서 보여주는 기능을 개발해본 적이 있을 것이다.

 

아마도 이 기능을 구현하기 위해서 여러개의 쿼리를 작성하고 그 결과를 응용 프로그램에서 조합해서

 

화면에 출력하도록 하는 작업을 많이 했을 것이다.

 

이런 기능을 "Facet Query" 라고 하는데, 일반적인 RDBMS에서는 하나의 쿼리로 다양한 기준의 그룹핑 쿼리를 수행할 수 없다.

 

하지만 MongoDB 3.4 버전부터는 $facet 스테이지를 이용해서 상품을 가격대별로 그리고 카테고리 별로 한번에 그룹핑해서 개수를 가져올 수 있다.

 

 

Facet Aggregation 쿼리는 다음과 같이 하나의 쿼리에 여러 개의 파이프라인을 나열해서 한 번에 여러 기준의 그룹핑 기능을 수행할 수 있다.

 

outputField1과 outputField2에는 기준별 그룹핑 결과를 담을 필드명을 명시하는데,

 

배열로 주어진 스테이지를 서브 파이프라인이라고 한다.

 

예시> db.collection.aggregate(
  [
    {
      $facet : {
          <outputField1> : [ <stage1>, <stage2>, ... ]
        , <outputField2> : [ <stage2>, <stage2>, ... ]
      }
    }
  ]
);

 

Facet Aggregation 쿼리의 각 서브 파이프라인은 다음 3개 중 하나의 Facet 서브 스테이지를 반드시 가져야 한다.

 

그리고 각 서브 스테이지는 서로의 결과를 다른 서브 스테이지와 공유하거나 참조할 수 없으며,

 

각 서브 스테이지는 Aggregation 쿼리에서 사용되던 다른 스테이지( $unwind, $project 등 )들을 같이 사용할 수도 있다.

 


 

$bucket

사용자가 지정한 범위( boundaries )별로 특정 필드( groupBy )값의 건수나 합계 산출

 

 

$bucketAuto

특정 필드( groupBy ) 값을 사용자가 지정한 개수( buckets )만큼 MongoDB 서버가 자동으로 범위를 나누어서 그룹핑하고,

그룹별로 건수나 합계를 산출

 

 

$sortByCount

특정 필드나 표현식( sortByCount )의 값으로 그룹을 만들어서 그룹별 건수를 산출

 


 

 

간단하게 3건의 영화 정보를 가진 movies 컬렉션에 대해서 $bucket과 $bucketAuto 그리고

 

$sortByCount 서브 파이프라인을 가지는 Facet 쿼리를 살펴보자.

 

// 예제 데이터 저장
mongo> db.movies.insertMany(
  [
      { _id : 1, title : "Terminator 2", year : 1991, price : 100, category : [ "SF", "ACTION" ] }
    , { _id : 2, title : "Salt", year : 2010, price : 150, category : [ "ACTION", "CRIME" ] }
    , { _id : 3, title : "Dirty Dancing", year : 1987, price : 70, category : [ "DRAMA", "MUSIC", "ROMANCE" ] }
  ]
);

// $facet Aggregation 쿼리
mongo> db.movies.aggregate(
  [
    {
      $facet : {
        "byCategory" : [
            { $unwind : "$category" }
          , { $sortByCount : "$category" }
        ]
        , "byPrice" : [
          {
            $bucket : {
                groupBy : "$price"
              , boundaries : [ 0, 50, 100, 150, 200]
              , default : "Other"
              , output : {
                  "count" : { $sum : 1 }
                , "titles" : { $push : "$title" }
              }
            }
          }
        ]
        , "byYear(Auto)" : [
          {
            $bucketAuto : {
                groupBy : "$year"
              , buckets : 4
            }
          }
        ]
      }
    }
  ]
);

 

위의 Facet 쿼리는 $sortByCountFacet 스테이지를 가지는 "byCategroy" 서브 파이프라인과

 

$bucket과 $bucketAuto Facet 스테이지를 각각 가지는 "byYear( Auto )" 서브 파이프라인 3개를 사용하고 있다.

 

첫 번째 byCategroy 서브 파이프라인은 movies 컬렉션의 category 필드의 배열값을

 

$unwind 스테이지를 이용해서 도큐먼트 단위로 풀고,

 

$sortByCount 스테이지를 이용해서 영화의 카테고리별로 정렬된 결과를 출력한다.

 

그리고 두 번째 byPrice 서브 파이프라인은 movies 컬렉션 디폴트 그룹( "Other" )으로 나눈 다음 개수를 결과로 출력한다.

 

마지막으로 세 번째 byYear 서브 파이프라인은 movies 컬렉션의 year 필드 값을 자동으로 4개의 그룹으로 나눠서 개수를 반환한다.

 

 

Aggregation 쿼리의 결과는 다음과 같이 byCategory와 byPirce 그리고 byYear 3개 서브 도큐먼트로 나눠진 결과를 반환한다.

 

// 쿼리 결과
{
  "byCategory" : [
      { "_id" : "ACTION", "count" : 2  }
    , { "_id" : "ROMANCE", "count" : 1 }
    , { "_id" : "DRAMA", "count" : 1 }
    , { "_id" : "CRIME", "count" : 1 }
    , { "_id" : "SF", "count" : 1 }
    , { "_id" : "MUSIC", "count" : 1 }
  ]
  , "byPrice" : [
      { "_id" : 50, "count" : 1, "titles" : [ "Dirty Dancing" ] }
    , { "_id" : 100, "count" : 1, "titles" : [ "Terminator 2" ] }
    , { "_id" : 150, "count" : 1, "titles" : [ "Salt" ] }
  ]
  , "byYear(Auto)" : [
      { "_id" : { "min" : 1987, "max" : 1991 }, "count" : 1 }
    , { "_id" : { "min" : 1991, "max" : 2010 }, "count" : 1 }
    , { "_id" : { "min" : 2010, "max" : 2010 }, "count" : 1 }
  ]
}

 

Facet 쿼리는 내부적으로 몇 개의 서브 스테이지를 가지고 있는지 없는지와 관계없이,

 

컬렉션의 도큐먼트는 단 1번만이라도 읽어서 처리된다.

 

그래서 서브 스테이지의 개수가 많아져도 컬렉션의 도큐먼트를 여러 번 참조하지는 않기 때문에 효율적으로 처리된다.