기존의 인덱스

MongoDB 4.2에 새로 추가되는 와일드카드 인덱스에 대해 알아봅시다. MongoDB에서 인덱스를 만드는 과정을 떠올려보면, 자주 검색에 활용되거나 부하가 클 걸로 예상되는 필드를 찾아서 걸어줘야 합니다. 만약 인덱스가 제대로 성능을 발휘하지 못하면 explain()으로 점검하고 hint()로 인덱스를 걸어보면서 가장 좋은 조합을 찾아봅니다. 시간이 좀 걸리기는 하지만, 검색 조건이 명확하고 부하 예측이 어느 정도 된다면 인덱스를 만들어 줄 필드를 찾아내는 것은 그렇게 어려운 일은 아닙니다. 하지만 다음과 같은 경우는 어떤가요?

{
   "type":"book",
   "title":"The Red Book",
   "attributes": {
       "color":"red",
       "size":"large",
       "inside": {
           "bookmark":1,
           "postitnote":2
       },
       "outside": {
           "dustcover": "worn"
       }
   }
}
{
   "type":"book",
   "title":"The Blue Book",
   "attributes": {
       "color":"blue",
       "size":"small",
       "inside": {
           "map":1
       },
       "outside": {
           "librarystamp": "Local Library"
       }
   }
}
{
   "type":"book",
   "title":"The Green Book",
   "attributes": {
       "color":"green",
       "size":"small",
       "inside": {
           "map":1,
           "bookmark":2
       },
       "outside": {
           "librarystamp": "Faraway Library",
           "dustcover": "good"
       }
   }
}

우리는 책을 검색하는 기능을 구현하려 합니다. 상세 검색을 할 때에는 attributes 하위의 속성들로 책을 찾을 수 있어야 합니다. 예를 들어, bookmark2인 책을 찾고 싶습니다.

db.example.find({ "attributes.inside.bookmark": 2} })

어떤 상황에서는 sizelarge인 책을 찾고 싶습니다.

db.example.find({ "attributes.size": "large"} })

이런 경우, attributes 하위의 모든 요소들에 인덱스를 만들어줘야 합니다. 이런 상황에서 와일드카드 인덱스가 효과적으로 사용 될 수 있습니다.

와일드카드 인덱스

윗 단락에서 예를 들었던 bookmark 검색을 MongoDB가 어떻게 처리하는지 살펴보기 위해 explain()을 사용해봅시다.

> db.example.find({ "attributes.inside.bookmark": 2 }).explain()
{
    "queryPlanner" : {
        "plannerVersion" : 1,
        "namespace" : "test.example",
        "indexFilterSet" : false,
        "parsedQuery" : {
            "attributes.inside.bookmark" : {
                "$eq" : 2
            }
        },
        "queryHash" : "F33E15E9",
        "planCacheKey" : "F33E15E9",
        "winningPlan" : {
            "stage" : "COLLSCAN",
            "filter" : {
                "attributes.inside.bookmark" : {
                    "$eq" : 2
                }
            },
            "direction" : "forward"
        },
        "rejectedPlans" : [ ]
    },
    "ok" : 1
}

Query Planner가 선택한 최종 계획은 콜렉션 스캔(COLLSCAN)입니다. 다시 말해, 이 필드를 검색하기 위해 모든 문서를 다 살펴본 것입니다. 와일드카드 인덱스를 만들어 줄 때입니다.

> db.example.createIndex({ "attributes.$**": 1 });
{
    "createdCollectionAutomatically" : false,
    "numIndexesBefore" : 1,
    "numIndexesAfter" : 2,
    "ok" : 1
}

처음 보는 방식의 인덱스 생성입니다. 필드 하위에 $**를 붙여서 이 필드의 하위 모든 문서에 와일드카드 색인을 만드는 것입니다. 이렇게 하면, 내부적으로 해당 필드의 하위 모든 필드에 인덱스를 만듭니다. 하위 필드가 문서라면 그 하위 요소들을 순회하면서 인덱스를 만드는 작업을 반복합니다.

다시 한번 동일한 쿼리를 날려보면 이번에는 콜렉션 스캔(COLLSCAN)이 아니라 인덱스 스캔(IXSCAN)으로 바뀐 것을 확인 할 수 있습니다.

> db.example.find({ "attributes.inside.bookmark": 2 }).explain()
{
    "queryPlanner" : {
        "plannerVersion" : 1,
        "namespace" : "test.example",
        "indexFilterSet" : false,
        "parsedQuery" : {
            "attributes.inside.bookmark" : {
                "$eq" : 2
            }
        },
        "queryHash" : "F33E15E9",
        "planCacheKey" : "92EE47A6",
        "winningPlan" : {
            "stage" : "FETCH",
            "inputStage" : {
                "stage" : "IXSCAN",
                "keyPattern" : {
                    "$_path" : 1,
                    "attributes.inside.bookmark" : 1
                },
                "indexName" : "attributes.$**_1",
                "isMultiKey" : false,
                "multiKeyPaths" : {
                    "$_path" : [ ],
                    "attributes.inside.bookmark" : [ ]
                },
                "isUnique" : false,
                "isSparse" : false,
                "isPartial" : false,
                "indexVersion" : 2,
                "direction" : "forward",
                "indexBounds" : {
                    "$_path" : [
                        "[\"attributes.inside.bookmark\", \"attributes.inside.bookmark\"]"
                    ],
                    "attributes.inside.bookmark" : [
                        "[2.0, 2.0]"
                    ]
                }
            }
        },
        "rejectedPlans" : [ ]
    },
    "ok" : 1
}

마치며

MongoDB와 같은 문서형 데이터베이스는 관계형 데이터베이스와 달리 고정 된 스키마가 필요하지 않습니다. 이로 인해 데이터셋을 유연하게 설계 할 수 있다는 장점을 가지고 있습니다. 하지만, 빠른 검색을 위해 인덱스를 지정하려고 할 때 고정된 스키마가 없다는 점이 난감 할 때가 있습니다. 특히 특정 필드가 단순히 하나의 값이 아닌 구조가 각각 다른 하위 문서처럼 다형(polymorphic)적인 데이터 일 때 더욱 그렇습니다. 와일드카드 인덱스는 이런 문서형 데이터베이스가 가지고 있던 단점을 극복하기 위해 좋은 해결책이 될 것으로 보입니다.

참고: https://www.mongodb.com/blog/post/coming-in-mongodb-42–1-wildcard-indexes