Database/MongoDB 실습

[MongoDB] Partial 인덱스 생성과 주의사항

꽁담 2024. 8. 12. 18:30

1. MongoDB Partial 인덱스

쿼리를 효율적으로 사용하기 위해 MongoDB 는 인덱스를 제공한다.

MongoDB 에서 인덱스 유형은 매우 다양하다.

1-1. MongoDB 인덱스 유형

유형 설명
단일 필드 인덱스 특정 필드에 대한 인덱스
복합 인덱스 여러 필드를 조합한 인덱스
해시 인덱스 해시된 값으로 인덱스
텍스트 인덱스 텍스트 검색을 위한 인덱스
2dsphere 인덱스 지리공간 데이터에 대한 인덱스
유일 인덱스 유일한 값만 허용
- 로컬서버 기준에 한정, 샤드서버로 데이터가 분산되는 경우에는 어플리케이션에서 처리

 

이러한 인덱스의 옵션에서 Partial 을 제공하는데 , 이 때는 이 필드에 포함된 값이 특정 값 이상일 때만 인덱스에 포함시킨다.

 

2. MongoDB 테스트 버전

유형 버전 구성
mongosh 2.2.10  
mongodb 7.0.12 Config : 1개, 포트 20000
Route : 1개, 포트 20001
Shard1 : 1개, 포트 30001
Shard2 : 1개, 포트 40001

 

 

3. Partial 인덱스 생성

3-1. Partial 인덱스 생성

partialFilterExpression 에서 특정 필드가 특정 값 이상일 때 인덱스 구성에 포함되도록 설정한다.

Score 인덱스를 만드는데 이 때 50 이상인 경우에 대해서만 인덱스를 생성한다.

따라서 50 이상인 값을 조회할 때만 인덱스를 사용할 수 있다.

[direct: mongos] indexDB> db.partialIndex.createIndex ( { Score : 1 } , { partialFilterExpression : { Score : { $gt : 50 } } } )
Score_1


[direct: mongos] indexDB> db.partialIndex.insert ( { "Name" : "N1" , "Score" : 39 } )
DeprecationWarning: Collection.insert() is deprecated. Use insertOne, insertMany, or bulkWrite.
{
  acknowledged: true,
  insertedIds: { '0': ObjectId('66b9d3ade51e7f66d81b157d') }
}

[direct: mongos] indexDB> db.partialIndex.insert ( { "Name" : "N2" , "Score" : 51 } )
{
  acknowledged: true,
  insertedIds: { '0': ObjectId('66b9d3b3e51e7f66d81b157e') }
}

[direct: mongos] indexDB> db.partialIndex.insert ( { "Name" : "N3" , "Score" : 72 } )
{
  acknowledged: true,
  insertedIds: { '0': ObjectId('66b9d3b8e51e7f66d81b157f') }
}

[direct: mongos] indexDB> db.partialIndex.insert ( { "Name" : "N4" , "Score" : 49 } )
{
  acknowledged: true,
  insertedIds: { '0': ObjectId('66b9d3bee51e7f66d81b1580') }
}

[direct: mongos] indexDB> db.partialIndex.insert ( { "Name" : "N5" } )
{
  acknowledged: true,
  insertedIds: { '0': ObjectId('66b9d3ebe51e7f66d81b1581') }
}

 

 

4. Partial 인덱스 조회

위에서 만들어 놓은 Partial 인덱스를 사용하는지 실행계획으로 확인해본다.

 

4-1. Partial 범위에 포함된 데이터 조회

50 이상인 데이터를 조회하면 winningPlan.stage 에 FETCH 로 나온다.

[direct: mongos] indexDB> db.partialIndex.find({"Score": { $gt : 50 } } ).explain()
{
  queryPlanner: {
    mongosPlannerVersion: 1,
    winningPlan: {
      stage: 'SINGLE_SHARD',
      shards: [
        {
          shardName: 'rs1',
          connectionString: 'rs1/127.0.0.1:30001',
          serverInfo: {
            host: 'ip-172-31-7-169.ap-northeast-2.compute.internal',
            port: 30001,
            version: '7.0.12',
            gitVersion: 'b6513ce0781db6818e24619e8a461eae90bc94fc'
          },
          namespace: 'indexDB.partialIndex',
          indexFilterSet: false,
          parsedQuery: { Score: { '$gt': 50 } },
          queryHash: '14DDA81F',
          planCacheKey: '8FF5F16D',
          maxIndexedOrSolutionsReached: false,
          maxIndexedAndSolutionsReached: false,
          maxScansToExplodeReached: false,
          winningPlan: {
            stage: 'FETCH',
            inputStage: {
              stage: 'IXSCAN',
              keyPattern: { Score: 1 },
              indexName: 'Score_1',
              isMultiKey: false,
              multiKeyPaths: { Score: [] },
              isUnique: false,
              isSparse: false,
              isPartial: true,
              indexVersion: 2,
              direction: 'forward',
              indexBounds: { Score: [ '(50, inf.0]' ] }
            }
          },
          rejectedPlans: []
        }
      ]
    }
  },
  serverInfo: {
    host: 'ip-172-31-7-169.ap-northeast-2.compute.internal',
    port: 20001,
    version: '7.0.12',
    gitVersion: 'b6513ce0781db6818e24619e8a461eae90bc94fc'
  },
  serverParameters: {
    internalQueryFacetBufferSizeBytes: 104857600,
    internalQueryFacetMaxOutputDocSizeBytes: 104857600,
    internalLookupStageIntermediateDocumentMaxSizeBytes: 104857600,
    internalDocumentSourceGroupMaxMemoryBytes: 104857600,
    internalQueryMaxBlockingSortMemoryUsageBytes: 104857600,
    internalQueryProhibitBlockingMergeOnMongoS: 0,
    internalQueryMaxAddToSetBytes: 104857600,
    internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600,
    internalQueryFrameworkControl: 'trySbeRestricted'
  },
  command: {
    find: 'partialIndex',
    filter: { Score: { '$gt': 50 } },
    lsid: { id: UUID('84e46ccb-a44d-48ca-817b-2a41d457dbf5') },
    '$clusterTime': {
      clusterTime: Timestamp({ t: 1723514261, i: 2 }),
      signature: {
        hash: Binary.createFromBase64('AAAAAAAAAAAAAAAAAAAAAAAAAAA=', 0),
        keyId: 0
      }
    },
    '$db': 'indexDB'
  },
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1723514406, i: 2 }),
    signature: {
      hash: Binary.createFromBase64('AAAAAAAAAAAAAAAAAAAAAAAAAAA=', 0),
      keyId: Long('0')
    }
  },
  operationTime: Timestamp({ t: 1723514401, i: 1 })
}

 

4-2. Partial 범위에 포함되지 않는 데이터 조회

49 이상인 데이터를 조회하면 winningPlan.stage 에 COLLSCAN (테이블스캔) 으로 나온다.

[direct: mongos] indexDB> db.partialIndex.find({"Score": { $gt : 49 } } ).explain()
{
  queryPlanner: {
    mongosPlannerVersion: 1,
    winningPlan: {
      stage: 'SINGLE_SHARD',
      shards: [
        {
          shardName: 'rs1',
          connectionString: 'rs1/127.0.0.1:30001',
          serverInfo: {
            host: 'ip-172-31-7-169.ap-northeast-2.compute.internal',
            port: 30001,
            version: '7.0.12',
            gitVersion: 'b6513ce0781db6818e24619e8a461eae90bc94fc'
          },
          namespace: 'indexDB.partialIndex',
          indexFilterSet: false,
          parsedQuery: { Score: { '$gt': 49 } },
          queryHash: '14DDA81F',
          planCacheKey: '81864C8B',
          maxIndexedOrSolutionsReached: false,
          maxIndexedAndSolutionsReached: false,
          maxScansToExplodeReached: false,
          winningPlan: {
            stage: 'COLLSCAN',
            filter: { Score: { '$gt': 49 } },
            direction: 'forward'
          },
          rejectedPlans: []
        }
      ]
    }
  },
  serverInfo: {
    host: 'ip-172-31-7-169.ap-northeast-2.compute.internal',
    port: 20001,
    version: '7.0.12',
    gitVersion: 'b6513ce0781db6818e24619e8a461eae90bc94fc'
  },
  serverParameters: {
    internalQueryFacetBufferSizeBytes: 104857600,
    internalQueryFacetMaxOutputDocSizeBytes: 104857600,
    internalLookupStageIntermediateDocumentMaxSizeBytes: 104857600,
    internalDocumentSourceGroupMaxMemoryBytes: 104857600,
    internalQueryMaxBlockingSortMemoryUsageBytes: 104857600,
    internalQueryProhibitBlockingMergeOnMongoS: 0,
    internalQueryMaxAddToSetBytes: 104857600,
    internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600,
    internalQueryFrameworkControl: 'trySbeRestricted'
  },
  command: {
    find: 'partialIndex',
    filter: { Score: { '$gt': 49 } },
    lsid: { id: UUID('84e46ccb-a44d-48ca-817b-2a41d457dbf5') },
    '$clusterTime': {
      clusterTime: Timestamp({ t: 1723514406, i: 2 }),
      signature: {
        hash: Binary.createFromBase64('AAAAAAAAAAAAAAAAAAAAAAAAAAA=', 0),
        keyId: 0
      }
    },
    '$db': 'indexDB'
  },
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1723514456, i: 2 }),
    signature: {
      hash: Binary.createFromBase64('AAAAAAAAAAAAAAAAAAAAAAAAAAA=', 0),
      keyId: Long('0')
    }
  },
  operationTime: Timestamp({ t: 1723514451, i: 1 })
}

 

 

5. Partial 인덱스 주의사항

Partial 인덱스는 NULL 비교가 상황에 따라 때로는 다르게 떄로는 같게 취급된다.

따라서 조회되어야 하는 데이터가 조회되지 않을수도 있다.

 

위에서 Name 이 N5 인 도큐먼트를 적재할 때 Score 를 명시하지 않았다.

그리고 N6 에서 Score 는 null 이라고 추가로 명시해 적재했다.

 

이 후 Partial 인덱스 힌트를 사용하지 않을때와 사용할때 조회해본 결과를 비교해본다.

[direct: mongos] indexDB> db.partialIndex.insert( { "Name" : "N6", "Score":null } )
{
  acknowledged: true,
  insertedIds: { '0': ObjectId('66babf0001027809e11b157d') }
}


# 일반적인 조회로는 N5 와 N6 이 출력된다.
[direct: mongos] indexDB> db.partialIndex.find({"Score": null } )
[
  { _id: ObjectId('66b9d3ebe51e7f66d81b1581'), Name: 'N5' },
  {
    _id: ObjectId('66babf0001027809e11b157d'),
    Name: 'N6',
    Score: null
  }
]

# Partial 인덱스 힌트를 사용하면 조회되지 않는것을 확인할 수 있다.
# 결과 빈 값
[direct: mongos] indexDB> db.partialIndex.find({"Score": null } ).hint( { "Score" : 1 } )

 

힌트 없을 때
힌트 있을 때