MongoDB - Index 인덱스 사용하기 - (2)

Share

Last Updated on 8월 30, 2023 by Jade(정현호)

안녕하세요 
이번 포스팅글에서는 MongoDB 의 Index(인덱스) 의 종류 와 사용에 관한 전반적인 내용에 대해서 확인해보려고 하며, 아래의 이전 포스팅에서 이어지는 연재 포스팅 글입니다.  

Compound Index

MongoDB 도 그렇고 RDBMS도 그렇고 인덱스의 구성 형태는 여러가지 의미로 다양하게 있을 수 있습니다.

그러한 여러가지 의미의 인덱스의 분류에서 단일 컬럼(필드) 인덱스와 복합 컬럼(필드) 인덱스로도 구분을 해볼 수 있습니다.

이전 포스팅 에서는 단일 필드를 가진 인덱스에 대해서 주로 설명과 내용을 확인해보았고, 이번 포스팅에서는 2개 이상의 필드를 가지는 복합 인덱스에 대해서 확인해보려고 합니다.

사실 보통 실무적으로 많이 사용되는 유형은 단일 컬럼(필드) 의 인덱스도 있겠지만, 앞에서 여러 번 확인해본 내용과 같이 인덱스가 많이 있다면 여러 WHERE 절 조건에 대응할 수 있는 또는 조회에서 조금 더 효과 적인 인덱스를 사용할 수는 있겠지만, trade-off 관계로 Index 가 많아질수록 데이터의 입력이나 갱신에서는 적은 인덱스를 사용하는 것에 비해 상대적으로 성능이 불리해질 수 있습니다.

그래서 가급적 최대한으로 여러 조회 조건을 만족할 수 있는 복합 컬럼(필드) 인덱스를 생성하여 주로 사용을 하게 됩니다.

MongoDB 에서는 이러한 2개 이상의 필드로 구성된 인덱스를 Compound Index(컴파운드 인덱스) 라고 하며, 필드가 연결 되어있다는 의미로 Concatenate Index 라고도 불립니다.

[mongodb.com/core/index-compound]


Compound Index 는 위의 이미지와 같이 2개 이상의 필드로 구성한 인덱스를 의미하며, 인덱스를 구성하는 각 필드가 서로 다른 정렬 방식을 설정해서 사용할 수 있습니다. 

위의 이미지에서는 userid 는 오름차순을 의미하는 1로 설정하였고, score 는 내림차순을 의미하는 -1 로 설정된 인덱스를 의미합니다.

이렇게 필드별로 다른 정렬 순서로 해서 인덱스를 생성할 경우, 쿼리에서 각 필드의 정렬 순서나 기준이 다른 요청이 와도 MongoDB 는 별도의 정렬 없이 인덱스를 읽는 것 만만으로 정렬 효과를 볼 수 있게 됩니다.
          

단일 필드와 복합 필드

MongoDB 에서도 단일 필드 인덱스와 복합 필드 인덱스에 대해서 조금 더 명확하게 확인을 해보도록 하겠습니다.

> db.test.insert ({
    field1 :{ sub_field1: 123, sub_field2: "abc"},
    field2 : "서울 강남구 테헤란로 518"
})


위의 예제를 에서 보면 test 라는 도큐먼트에는 2개의 필드가 있습니다.  field1, field2


그리고 field1 에는 서브 도큐먼트를 가지고 있습니다. 

아래에서 3개의 인덱스 생성 구문을 통해 더 자세한 내용을 확인해보도록 하겠습니다.

# 1
> db.test.createIndex({ field1 : 1})

# 2
> db.test.createIndex({ field1 : 1, field2:1 })

# 3
> db.test.createIndex({ "field1.sub_field1" : 1, field2:1 })


#1
과 같이 생성하게 되면 리프 블럭에는 서브 도큐먼트의 sub_field1 , sub_field2 필드명 과 데이터 모두를 포함하게 됩니다. 2개 이상의 필드명이 리프 블럭에 포함되지만 해당 인덱스는 단일 필드 인덱스입니다.


#2 는 포스팅에서 설명하고 있는 Compound Index 로 2개 이상의 필드를 포함해서 생성하고 있습니다.

#3 의 경우는 복합 필드 인덱스 생성시 서브 도큐먼트의 필드를 포함할 수 있고, 그러한 조합으로 생성을 하고 있습니다.


컬렉션에 생성된 인덱스에 대한 정보는 다음과 같이 getIndexes() 을 사용하여 확인할 수 있습니다.

> db.컬렉션명.getIndexes()

## 출력 결과 예시
[
  { v: 2, key: { _id: 1 }, name: '_id_' },
  { v: 2, key: { name: 1 }, name: 'name_1' }
]

         

서브 도큐먼트의 각 필드 구성 인덱스

위의 인덱스 생성 예재 #3 의 구문과 같이 서브 도큐먼트의 각 필드의 조합으로 컴파운드 인덱스를 생성하는 것과 메인 필드(level 1) 로 생성하는 것의 차이에 대해서 확인해보도록 하겠습니다.

#1
> db.test.createIndex({ "field1.sub_field1" : 1, "field1.sub_field2" : 1})

#2
> db.test.createIndex({ field1 : 1 })


field1 : 1 으로 생성한 인덱스의 경우에는 field1 필드에 저장되는 값이 어떤 값이던지 BSON 으로 전환하여 하나의 바이트 배열 값으로 저장 및 판단하게 됩니다. 그래서 field1 필드의 서브 도큐먼트에서 sub_field1 와 sub_field2 가 있을 경우 2개의 순서가 변경된다면, 다른 바이트 배열이 되기 때문에 다른 값으로 인식하게 됩니다.

그러므로 db.test.createIndex({ field1 : 1 }) 으로 생성된 인덱스에서는 아래와 같이 입력된 2개의 도큐먼트가 서로 다른 값으로 인식하기 때문에 동일한 검색 조건으로는 값이 검색되지 않을 수 있습니다.


아래와 같이 collection 과 인덱스를 새로 만들어보겠습니다.

-- 테스트를 위해서 삭제
> db.test.drop()

-- 도큐먼트 입력
> db.test.insert({
    field1: { sub_field1 : 123, sub_field2: "abc" },
    field2 : "서울 강남구 테헤란로 518"
})

> db.test.insert({
    field1: { sub_field2: "abc", sub_field1 : 123 },
    field2 : "서울 강남구 테헤란로 518"
})

-- 인덱스 생성
> db.test.createIndex({ field1 : 1 });


조회를 해보도록 하겠습니다.

-- 먼저 전체 도큐먼트 확인
> db.test.find().pretty()
{
    "_id" : ObjectId("636845d37e5267769a1e196c"),
    "field1" : {
        "sub_field1" : 123,
        "sub_field2" : "abc"
    },
    "field2" : "서울 강남구 테헤란로 518"
}
{
    "_id" : ObjectId("636845d37e5267769a1e196d"),
    "field1" : {
        "sub_field2" : "abc",
        "sub_field1" : 123
    },
    "field2" : "서울 강남구 테헤란로 518"
}


-- 조회 조건으로 조회
db.test.find({ 
    field1: { sub_field1 : 123, sub_field2: "abc" }
}).pretty()

{
    "_id" : ObjectId("6368411e7e5267769a1e196b"),
    "field1" : {
        "sub_field1" : 123,
        "sub_field2" : "abc"
    },
    "field2" : "서울 강남구 테헤란로 518"
}

입력된 전체 도큐먼트는 2건 입니다. 조회를 하면 만족하는 1개만 출력이 됩니다. 즉 서브 도큐먼트의 순서가 달라진 2개의 도큐먼트에 대해서 다른 값으로 보고(저장) 있다는 것을 의미합니다.

그럼 명시적으로 각각의 서브 도큐먼트의 필드를 선택하여 조회 및 플랜을 확인해보도록 하겠습니다.

-- 조회
db.test.find({ 
    "field1.sub_field1" : 123, "field1.sub_field2" : "abc" }
).pretty()

{
    "_id" : ObjectId("636845d37e5267769a1e196c"),
    "field1" : {
        "sub_field1" : 123,
        "sub_field2" : "abc"
    },
    "field2" : "서울 강남구 테헤란로 518"
}
{
    "_id" : ObjectId("636845d37e5267769a1e196d"),
    "field1" : {
        "sub_field2" : "abc",
        "sub_field1" : 123
    },
    "field2" : "서울 강남구 테헤란로 518"
}


-- 실행 계획
db.test.find({ 
    "field1.sub_field1" : 123, "field1.sub_field2" : "abc" }
).explain('executionStats')

{
    "queryPlanner" : {
        "plannerVersion" : 1,
        "namespace" : "test.test",
        "indexFilterSet" : false,
        "parsedQuery" : {

            < .. 중략 .. >

    "executionStats" : {
        "executionSuccess" : true,
        "nReturned" : 2,
        "executionTimeMillis" : 0,
        "totalKeysExamined" : 0,
        "totalDocsExamined" : 2,
        "executionStages" : {
        ---!!>	"stage" : "COLLSCAN",
            "filter" : {
        < .. 중략 .. >

명시적으로 서브 도큐먼트와 필드 를 선택해서 조회하면 조회는 가능 합니다. 하지만 여전히 PLAN 으로 확인하였을 때도 인덱스를 사용(검색하지)하지 못하고(stage: COLLSCAN) 있는 것을 확인할 수 있습니다.


인덱스를 삭제 후 다시 생성해서 조회를 해보도록 하겠습니다.

-- 기존 인덱스 삭제
db.test.dropIndex({ field1 : 1 });

-- 신규 인덱스 생성
db.test.createIndex({ "field1.sub_field1" : 1, "field1.sub_field2" : 1})

-- 조회
db.test.find({ 
    "field1.sub_field1" : 123, "field1.sub_field2" : "abc" }
).pretty()

{
    "_id" : ObjectId("636845d37e5267769a1e196c"),
    "field1" : {
        "sub_field1" : 123,
        "sub_field2" : "abc"
    },
    "field2" : "서울 강남구 테헤란로 518"
}
{
    "_id" : ObjectId("636845d37e5267769a1e196d"),
    "field1" : {
        "sub_field2" : "abc",
        "sub_field1" : 123
    },
    "field2" : "서울 강남구 테헤란로 518"
}


-- 실행 계획
db.test.find({ 
    "field1.sub_field1" : 123, "field1.sub_field2" : "abc" }
).explain('executionStats')

{
    "queryPlanner" : {
        "plannerVersion" : 1,
        "namespace" : "test.test",
        "indexFilterSet" : false,
        "parsedQuery" : {
            "$and" : [
        < ...중략 ... >

        "docsExamined" : 2,
        "alreadyHasObj" : 0,
        "inputStage" : {
    ---!!>>	"stage" : "IXSCAN",
            "nReturned" : 2,

이번에도 각 서브 도큐먼트의 필드를 명시적으로 선언하여 조회하였고 2건이 조회 및 결과 출력이 되는 것은 동일하나 실행 계획을 살펴보면 이번 조회에서는 인덱스를 사용할 수 있다는 것을 확인할 수 있습니다.
("stage" : "IXSCAN")


field1 필드 단일로 인덱스를 만들게 되면 어떤 서브 도큐먼트가 저장되더라도 그 값을 BSON 으로 변환한 다음 전체 BSON을 인덱스 키 엔트리로 사용합니다.

createIndex({ "field1.sub_field1" : 1, "field1.sub_field2" : 1}) 와 같이 각각의 서브 도큐먼트의 필드의 조합으로 생성시에는 서브 도큐먼트가 어떤 필드를 가지고 있는지 상관없이 2개의 필드의 조합으로 컴파운드 인덱스를 생성되게 되며, 그로 인해서 인덱스 SCAN 이 가능해진다는 점입니다.


일반적인 RDBMS에서의 결합 인덱스는 단일 컬럼을 여러개 결합하여 생성한다는 것을 의미합니다.

하지만 MongoDB 에서의 컴파운드 인덱스는 여러 타입의 인덱스를 혼합해서 결합하는 형태이며, 위의 예시와 같은 메인필드와, 서브 도큐먼트의 필드의 결합 , 또는 서브 도큐먼트의 필드끼리의 조합, 전문검색 인덱스 와 일반 단일 값을 가지는 필드의 결합, 공간 데이터 필드와 단일 값의 결합 등 여러가지 조합으로 결합이 가능 합니다.

또한 MongoDB 의 컴파운드 인덱스는 결합된 필드 단위로 정렬 순서를 지정할 수 있습니다.
                 

인덱스 정렬 과 스캔(서치) 방향

MongoDB 인덱스에서 각각의 필드를 어떻게 정렬할 지는 생성시에 결정하게 되며, 인덱스를 어떤 방향으로 읽을지는 쿼리가 원하는 값에 따라 옵티마이져가 실시간으로 만드는 실행 계획에 의해 결정됩니다.

이러한 정렬을 가지는 것은 B-Tree 인덱스만 가능하며, 생성시 인덱스 대상 필드를 1 또는 -1 로 설정하여 오름차순(1) 과 내림차순(-1) 설정을 할 수 있습니다.

         

인덱스 정렬

우선 인덱스 정렬에 대한 부분은 위에서 언급한 내용처럼 생성 시점에 오름차순, 내림차순으로 설정할 수 있으며, 오퍼레이션과 쿼리에 따라서 정렬 순서를 결정하는 것은 매우 중요한 부분이 되게 됩니다.

먼저 단일 필드로 구성된 인덱스의 경우 아래와 같이 인덱스를 생성할 수 있습니다.

> db.users.createIndex( { name : -1 } )

or

> db.users.createIndex( { address : 1 } )

위와 같이 단일 필드로 구성된 인덱스는 오름차순이나 내림차순의 정렬의 차이는 작은 값이 왼쪽에서 부터 시작하는지, 오른쪽에서 부터 시작하는 지의 차이 뿐입니다.

그래서 오름차순(1) 으로 정렬된 인덱스에서 가장 작은 값부터 순서대로 가져오려면 왼쪽인 시작 부분 부터 스캔하면 되는 것이며, 큰 값 부터 가져오려면 마지막 부분인 가장 오른쪽 부터 스캔을 하면 됩니다.

내림차순의 경우 위의 내용와 반대가 되게 됩니다.

MongoDB 옵티마이저는 필요에 따라서 인덱스를 오름차순으로 읽을지 내림차순으로 읽을지를 판단도 하기 때문에 이러한 단일 필드 구성 인덱스의 경우 오름차순 과 내림차순이 사실상 차이가 없게 됩니다.


2개 이상이 필드로 구성된 컴파운드 인덱스에서는 아래와 같은 경우 오름차순과 내림차순을 혼합해서 생성해서 사용하는 것을 고려 해볼 수 있을 것 같습니다.

> db.users.find().sort( {name : 1, address:-1} )

> db.users.find().( {name : {$gt : "haley", $lt: "jake" }})
                .sort( { name:1, address:-1})


이 두 쿼리의 특징은 name 필드의 값이 상수가 아니라는 것이며(= 동등 조건이 아니므로) 과 결과를 name 필드로는 오름차순으로 하고, address 는 내림차순으로 정렬하는 조건(.sort) 을 가지고 있다는 점입니다.
           

인덱스 스캔 방향

MongoDB 에서는 인덱스를 생성시에 오름차순 또는 내림차순으로 혼합하여 생성할 수 있지만, 이것과 무관 하게 쿼리를 실행할 때 인덱스 자체를 오름차순 또는 내림차순으로 스캔할 수 있습니다.

이유는 B-Tree 인덱스의 구조상 기본 정렬은 항상 오름차순으로 구현되어 있지만, 이러한 인덱스를 읽는 방향에 따라서 내림차순의 효과를 얻을 수 있기 때문입니다.(반대의 경우도 마찬가지) 

즉 인덱스를 오름차순으로 읽으면 출력결과가 오름차순이 되며, 내림차순으로 읽게 되면 결과가 내림차순으로 정렬된 결과값을 받게 되는 것입니다.

읽기의 방향이 결정되는 또는 달라지는 것은 보통 쿼리에서 정렬 처리나 최대, 최소값을 구하는 쿼리일 경우입니다.
               

인덱스 효율성과 가용성

쿼리의 조건이나 정렬 또는 그룹핑 작업에 대한 요구사항에 따라, 또는 FIND에서 가져와야 하는 필드에 따라서 해당 쿼리가 인덱스를 사용할 수 있을지, 사용할 수 있다면 어떠한 비교 조건으로 사용 가능하게 된 것인지 식별을 해야 하거나 식별이 필요할 수 있습니다.

그에 따라서 현재의 상태가 최적화가 되어서 인덱스를 사용 것이 맞는지 또는 아닌지도 판단할 수 있기 때문입니다.
(최적화되어 있지 않다면 최적화를 위한 인덱스 생성 등을 고려하기 위해)


여러 필드로 구성된 컴파운드 인덱스에서는 각 필드의 순서와 그 필드에 사용되는 조건이 동등 비교("$eq") 인지, 범위 비교인 크다($gt) 와 작다($lt) 와 같은 조건 인지에 따라서 각 인덱스 필드의 비교 형태가 달라지게 됩니다.

RDBMS 에서 처럼 유사하게 컴파운드 인덱스의 필드의 구성 순서는 조회하는 조건이 점 조건이 주로 들어오는지 아니면 주로 선분조건이 들어오는지에 따라서 결정이나 고려하면 됩니다.

  • 점 조건은 =($eq) 나 IN 과 같은 연산자를 통해서 조회
  • 선분 조건은 LIKE 나 BETWEEN , 크다 작다의 < > 연산자를 통해서 조회


인덱스 처음 필드가 선분조건, 두번째가 점 조건 일 경우 두 번 째 필드의 조회 조건은 비교 작업의 범위를 좁히는데 도움을 주지 못하며 필터링 조건으로 사용되고, 첫번째 필드의 조건의 검색 범위가 크면 클수록 더욱더 성능적 불리함이 나타나게 됩니다.



그래서 컴파운드 인덱스에서는 인덱스 구성하는 필드의 순서와 그 순서에 맞추어서 어디까지 동등조건("$eq") 로 검색되었는지에 따라서 인덱스의 스캔 대상 범위가 결정되게 됩니다.


B-Tree 인덱스의 특징은 왼쪽 값을 기준(Left-most) 으로 오른쪽 값이 정렬되어 있다는 것 입니다.(기본 오름차순)

왼쪽이라고 표현한 것은 하나의 필드 뿐만 아니라 다중 필드 인덱스의 필드에서도 동일하게 적용됩니다.

B-tree 에서의 정렬이 검색이 되는 방향의 기준이 되게 되고, 어떠한 왼쪽 값에서 부터 스캔을 하게 되고 그 말은 즉, 왼쪽 값을 알아야 한다는 것입니다.

> db.users.find( {name : {$regex: 'jade'}})

> db.users.find( {name : /jade/})


위의 쿼리는 name 필드에서 "jade" 로 시작하는 유저를 검색하지만, "jade" 라는 문자열이 포함된 경우에도 결과가 반환되게 됩니다.
RDBMS 에서는 LIKE '%jade%' 와 같이 양쪽의 % 이 있는 형태입니다.

그래서 이 쿼리는 인덱스를 사용한 검색은 불가능 합니다. 위에서 설명한 내용과 같이 왼쪽 값을 알아야 하지만 name 필드 값에 대해서 왼쪽 값을 알 수 없기 때문입니다.


기본적으로 B-Tree 인덱스의 특성상 다음과 같은 조건에서는 인덱스를 보통 사용할 수 없습니다. 경우에 따라서 체크 조건으로 사용될 수는 있습니다.
(사용할 수 없다는 부분은 작업 결정 조건으로 사용할 수 없다는 의미)

  • NOT-EQUAL 로 비교된 경우 ("$ne","$nin")
  • 문자열 패턴 검색에서 프리릭스가 일치하지 않은 경우
    • db.users.find( {name : /jade/})
  • 문자열 데이터 타입의 콜레이션(Collations) 이나 컬렉션이나 인덱스의 콜레이션이 다를 경우



여기까지 해서 MongoDB에서의 B-Tree 인덱스 사용 및 인덱스 스캔에 대해서 확인해보았고 글을 마무리하도록 하겠습니다.
           

Reference

Reference URL
• mongodb.com/indexes
• mongodb.com/query-comparison


Reference Book
• Real MongoDB


연관된 다른 글

 

           

0
글에 대한 당신의 생각을 기다립니다. 댓글 의견 주세요!x