(몽고DB 완벽 가이드) Chapter 5. 인덱싱

이재문·2023년 11월 27일

5.1 - 인덱싱

MongoDB 인덱스에 대해

효과적인 인덱싱 전략을 위해서 가장 중요한 것은 “Selectivity(선택성)“를 높이는 것이다.

즉, 최대한 좁은 범위를 탐색할 수 있도록 인덱스를 만들어야 한다. 그리고 읽기보다 쓰기 작업이 많은 컬렉션에는 인덱스를 복잡하게 설계하지 않아야 한다.

DB의 인덱스는 책의 목차와 비슷

db는 전체 내용을 확인하지 않고, 특정 내용을 가르키는 정렬된 리스트를 확인한다.

  • 인덱스를 사용하지 않는 쿼리를 collection scan
  • explain
    • 몽고DB가 무엇을 하는지 확인 가능

    • // 100만 유저 생성
      > for (i=0; i < 1000000; i++) {
          db.User.insertOne(
      		{
              "i": i,
              "username": "user" + i,
              "created": new Date()
      		}
      	);
      }
      
      --------------------------------------------------------------------------------
      
      // 
      > db.User.find({"username":"user101"}).explain("executionStats")
      
      >> { explainVersion: '1',
        queryPlanner: 
         { namespace: 'practice.User',
           indexFilterSet: false,
           parsedQuery: { username: { '$eq': 'user101' } },
           queryHash: '7D9BB680',
           planCacheKey: '7D9BB680',
           maxIndexedOrSolutionsReached: false,
           maxIndexedAndSolutionsReached: false,
           maxScansToExplodeReached: false,
           winningPlan: 
            { stage: 'COLLSCAN',
              filter: { username: { '$eq': 'user101' } },
              direction: 'forward' },
           rejectedPlans: [] },
        executionStats: 
         { executionSuccess: true,
           nReturned: 1, // return 받은 개수
           executionTimeMillis: 8,
           totalKeysExamined: 0,
           totalDocsExamined: 10558, // 살펴본 document 개수
           executionStages: 
            { stage: 'COLLSCAN',
              filter: { username: { '$eq': 'user101' } },
              nReturned: 1,
              executionTimeMillisEstimate: 2,
              works: 10559,
              advanced: 1,
              needTime: 10557,
              needYield: 0,
              saveState: 10,
              restoreState: 10,
              isEOF: 1,
              direction: 'forward',
              docsExamined: 10558 } },
        command: 
         { find: 'User',
           filter: { username: 'user101' },
           '$db': 'practice' },
        serverInfo: 
         { host: 'ac-hs55kvl-shard-00-01.o9ycmpz.mongodb.net',
           port: 27017,
           version: '6.0.10',
           gitVersion: '8e4b5670df9b9fe814e57cb5f3f8ee9407237b5a' },
        serverParameters: 
         { internalQueryFacetBufferSizeBytes: 104857600,
           internalQueryFacetMaxOutputDocSizeBytes: 104857600,
           internalLookupStageIntermediateDocumentMaxSizeBytes: 16793600,
           internalDocumentSourceGroupMaxMemoryBytes: 104857600,
           internalQueryMaxBlockingSortMemoryUsageBytes: 33554432,
           internalQueryProhibitBlockingMergeOnMongoS: 0,
           internalQueryMaxAddToSetBytes: 104857600,
           internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600 },
        ok: 1,
        '$clusterTime': 
         { clusterTime: Timestamp({ t: 1695037335, i: 14 }),
           signature: 
            { hash: Binary(Buffer.from("a21048e472375d56e19d63153edd582cc7313ad1", "hex"), 0),
              keyId: 7221939211316756000 } },
        operationTime: Timestamp({ t: 1695037335, i: 14 }) }
      
    • totalDocsExamined : 모든 doc을 조회 한 이유 - 중복 된 값이 있을 수 있기 때문

  • 쿼리를 효율적으로 사용하려면 애플리케이션의 모든 쿼리 패턴에 인덱스 사용.
  • 단일 인덱스가 여러 쿼리 패턴을 지원

5.1.1 - 인덱스 생성

  • 인덱스 생성

    > db.User.createIndex({"username":1})
    >> 'username_1'
    • db.currentOp() 로 로그를 통해 인덱스 구축 진행률 확인 가능
  • 인덱스 쿼리

    > db.User.find({"username":"user101"}).explain("executionStats")
    >> {
      explainVersion: '1',
      queryPlanner: {
        namespace: 'practice.User',
        indexFilterSet: false,
        parsedQuery: { username: { '$eq': 'user101' } },
        queryHash: '7D9BB680',
        planCacheKey: '24069050',
        maxIndexedOrSolutionsReached: false,
        maxIndexedAndSolutionsReached: false,
        maxScansToExplodeReached: false,
        winningPlan: {
          stage: 'FETCH',
          inputStage: {
            stage: 'IXSCAN',
            keyPattern: { username: 1 },
            indexName: 'username_1',
            isMultiKey: false,
            multiKeyPaths: { username: [] },
            isUnique: false,
            isSparse: false,
            isPartial: false,
            indexVersion: 2,
            direction: 'forward',
            indexBounds: { username: [ '["user101", "user101"]' ] }
          }
        },
        rejectedPlans: []
      },
      executionStats: {
        executionSuccess: true,
        nReturned: 1,
        executionTimeMillis: 0,
        totalKeysExamined: 1,
        totalDocsExamined: 1, // 1개 조회
        executionStages: {
          stage: 'FETCH',
          nReturned: 1, // 1개 return
          executionTimeMillisEstimate: 0, // 0mil초
          works: 2,
          advanced: 1,
          needTime: 0,
          needYield: 0,
          saveState: 0,
          restoreState: 0,
          isEOF: 1,
          docsExamined: 1,
          alreadyHasObj: 0,
          inputStage: {
            stage: 'IXSCAN',
            nReturned: 1, 
            executionTimeMillisEstimate: 0,
            works: 2,
            advanced: 1,
            needTime: 0,
            needYield: 0,
            saveState: 0,
            restoreState: 0,
            isEOF: 1,
            keyPattern: { username: 1 },
            indexName: 'username_1',
            isMultiKey: false,
            multiKeyPaths: { username: [] },
            isUnique: false,
            isSparse: false,
            isPartial: false,
            indexVersion: 2,
            direction: 'forward',
            indexBounds: { username: [ '["user101", "user101"]' ] },
            keysExamined: 1,
            seeks: 1,
            dupsTested: 0,
            dupsDropped: 0
          }
        }
      },
      command: { find: 'User', filter: { username: 'user101' }, '$db': 'practice' },
      serverInfo: {
        host: 'ac-hs55kvl-shard-00-01.o9ycmpz.mongodb.net',
        port: 27017,
        version: '6.0.10',
        gitVersion: '8e4b5670df9b9fe814e57cb5f3f8ee9407237b5a'
      },
      serverParameters: {
        internalQueryFacetBufferSizeBytes: 104857600,
        internalQueryFacetMaxOutputDocSizeBytes: 104857600,
        internalLookupStageIntermediateDocumentMaxSizeBytes: 16793600,
        internalDocumentSourceGroupMaxMemoryBytes: 104857600,
        internalQueryMaxBlockingSortMemoryUsageBytes: 33554432,
        internalQueryProhibitBlockingMergeOnMongoS: 0,
        internalQueryMaxAddToSetBytes: 104857600,
        internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600
      },
      ok: 1,
      '$clusterTime': {
        clusterTime: Timestamp({ t: 1695038170, i: 7 }),
        signature: {
          hash: Binary(Buffer.from("c6c180ca513554054ad17bdf94d91f2031f89230", "hex"), 0),
          keyId: Long("7221939211316756482")
        }
      },
      operationTime: Timestamp({ t: 1695038170, i: 7 })
    }
    • 인덱싱된 필드를 변경하는 쓰기 작업(삽입, 갱신, 삭제)은 더 오래 걸린다. - 데이터가 변경될 때마다 도큐먼트뿐아니라 모든 인덱스 갱신 필요
    • Force 쿼리 같은 소요 시간에 큰 영향이 없다면 인덱싱 할 필요 X

5.1.2 - 복합 인덱스 소개

  • 상당수의 쿼리 패턴은 두개 이상의 키를 기반으로 인덱스를 작성해야 한다.
  • 인덱스는 모든 값을 정렬된 순서로 보관하므로 인덱스 키로 도큐먼트를 정렬하는 작업이 훨씬 빨라지게 한다.
  • ex
    > db.User.find().sort({'created': 1, 'username': 1}) // created 정렬 이후 username 정렬
    • 위 쿼리는 created 정렬 이후 username을 정렬하기 때문에 index로 username 만을 생성 했을 때, 도움이 되지 않는다.
    • 정렬을 최적화하려면 created와 username을 함께 인덱싱한다. (복합인덱스 - 2개 이상의 필드)
      > db.User.createIndex({'age': 1, 'username': 1})
      >> 'age_1_username_1'
      • 정렬 방향이 여러 개, 검색 조건이 여러 개 일 때 유용

인덱스 사용 방법

> db.User.find({'age': 21}).sort({"username":-1})
  • 단일 값을 찾는 동등 쿼리
  • username 이 인덱싱이 되어 있기 때문에 age 21의 마지막 항목부터 순서대로 인덱스 탐색
> db.user.find({"age": {"$gte": 21, "$lte": 30}})
  • 범위 쿼리
  • 인덱스에 있는 첫번째 키인 age를 사용하여 도큐먼트 반환
  • 인덱스를 사용해 쿼리하면 일반적으로 인덱스 순서에 따라 반환
> db.user.find({"age": {"$gte": 21, "$lte": 30}}).sort({"username": 1})
  • 정렬이 포함 된 다중값 쿼리
  • 사용자명을 정렬된 순서대로 반환하지 않는다.
  • 사용자명에 따라 정렬된 결과 요청
  • 정렬된 인덱스를 통과 X, 결과 변환 전 메모리에서 정렬
  • 비효율

같은 키를 역순으로 한 인덱스 또한 사용 가능

  • 모든 인덱스 항목을 탐색하지만 원하는 순서로 바꿈

5.1.3 몽고DB가 인덱스를 선택하는 방법

  • 5개의 인덱스가 존재한다고 가정 - Race와 유사
  1. 쿼리가 들어오면 쿼리 모양(query shape) 확인
    • query shape - 검색 필드, 정렬 여부 등 추가 정보와 관련
  2. 5개의 인덱스 중 3개의 인덱스가 쿼리 후보로 선별
  3. 각 인덱스 후보당 1개씩 쿼리 플랜 생성 후 각각의 인덱스로 3개의 쿼리를 병렬 스레드로 실행
  4. 어떤 스레드(인덱스)가 가장 빠른 결과를 반환하는지 확인
  • 차후 같은 모양의 쿼리에 사용하기 위해 캐시에 저장.
  • 시간이 지나거나 인덱스가 변경되고, 캐시가 제거되면 다시 Race를 함

5.1.4 복합 인덱스 사용

5.2 explain 출력

  • explain은 쿼리에 대한 정보 제공, 느린 쿼리 진단 도구
  • 모든 쿼리에서 .explain() 호출
  • 샤딩은 쿼리를 여러 서버에서 수행하므로 explain들의 집합체를 반환
  • 인덱스를 사용X 쿼리에서 explain 흔히 볼 수 있고, 가장 일반적이다.
  • 쿼리에서 “COLLSCAN”을 사용하면 인덱스를 사용 X
  • .exlain() 호출 시 반환 값
    • nReturned: 쿼리에 의해 결과로 반환된 실제 도큐먼트 개수
    • totalKeysExamined: 검색한 인덱스 항목 개수
      • 인덱스가 사용됐다면 확인된 인덱스 항목 개수, 테이블 스캔을 했다면 스캔한 도큐먼트 개수
    • totalDocsExamined: 검색한 도큐먼트 개수
      • 실제 도큐먼트를 가르키는 인덱스 포인터를 따라간 횟수, 쿼리가 인덱스의 일부가 아닌 검색 조건을 포함하거나, 인덱스에 포함되지 않은 필드를 반환하도록 요청하면 각 인덱스 항목이 가르키는 도큐먼트를 확인 해 봐야한다.
    • nscannedObjects: 스캔한 도큐먼트 개수
    • executionTimeMillis: 쿼리 실행 시간 (서버가 요청을 받고 응답을 보낸 시점, 가장 빠른 플랜이 아닌 모든 플랜이 실행되기까지 걸린 시간)
    • isMultiKey: 다중키 인덱스 사용 여부
    • stage: “IXSCAN”
      • 인덱스를 사용해 쿼리할 수 있었는지 여부. “COLSCAN”은 인덱스로 쿼리할 수 없어 컬렉션 스캔을 사용했다고 뜻함
    • needYields: 0
      • 쓰기 요청을 처리하도록 쿼리가 일시정지(yield)한 횟수
      • 대기 중인 쓰기가 있으면 쿼리가 일시적으로 락을 해제하고 쓰기가 처리되게 한다.
    • executionTimeMillis
      • 데이터베이스가 쿼리하는 데 걸린 시간
    • indexBounds
      • 인덱스가 어떻게 사용됐는지 설명. 탐색한 인덱스의 범위를 제공
      • 이해 못함.. 173p

5.3 인덱스를 생성하지 않는 경우

  • 인덱스가 없는 게 더 빠른 쿼리도 있다.
  • 인덱스는 컬렉션에서 가져와야 하는 부분이 많을수록 비효율적이다.
    • find(all)의 경우 인덱스를 사용하면 인덱스 스캔과 인덱스 포인터를 모두 조회함
  • 쿼리가 컬렉션의 30% 이상 반환하는 경우 인덱스는 쿼리 속도를 높인다.

5.4 엔덱스 종류

5.4.1 고유 인덱스

  • 필드에 고유한 값을 넣기 위해 partialFileterExpression으로 고유 인덱스를 만들 수 있다.
    • ex
      db.users.craeteIndex(
      		{"firstname":1}, 
      			{
      				"unique": true, 
      				"partialFileterExpression": {"firstname": {$exists:true}
      			}
      		}
      )
      • 중복키 예외 발생 시킴
      • 중복을 필터링하기보다는 고유 조건을 사용.
      • _id는 컬렉션 생성과 동시에 자동 인덱싱

복합 고유 인덱스

  • 복합 고유 인덱스를 만들 수 있다.
  • 복합 고유 인덱스가 뭔데

중복 제거하기

  • 기존 컬렉션에 고유 인덱스를 생성할 때 중복된 값이 있으면 실패.
  • 중복이 발생하는 부분 어떻게 처리할지 고민

5.4.2 부분 인덱스

  • 고유한 필드가 있거나, 아무 데이터도 없다면 “unique”와 “partial”를 결합 가능
  • partialFilterExpression 옵션으로 부분인덱스 생성가능
  • 부분 인덱스는 고유할 필요없다.

5.5 인덱스 관리

  • 동일한 인덱스는 생성X’
  • system.indexes 컬렉션에 저장 - createIndex, creatIndexes, dropIndexes 로 제어
  • system.indexes 로 메타 정보 확인 가능
  • db.{CollectionName}.getIndexes()로 정보 확인 가능
  • ex
    db.users.getIndexes()
    [
      { v: 2, key: { _id: 1 }, name: '_id_' },
      { v: 2, key: { username: 1 }, name: 'username_1' },
      { v: 2, key: { age: 1, created: 1 }, name: 'age_1_created_1' },
      { v: 2, key: { created: 1, age: 1 }, name: 'created_1_age_1' }
    ]
    • 같은 필드를 가져도 순서에 따라 다른 인덱스가 된다.

    • v: 1 이 없는 필드는 오래되고 비효율적인 형식으로 저장된 상태이다.

      5.5.1 인덱스 식별

    • 인덱스명은 서버에서 컨트롤 용도로 사용

    • indexName1_indexDirection1_indexName2_indexDirection2…

      db.users.createIndex({'a':1, 'b':1....}, {'name': myIndex})
    • name을 넣어 인덱스명 지정 가능

      5.5.2 인덱스 변경

    • db.user.dropIndex(”IndexName”)

    • mongoDB 4.2 이후 버전에서는 인덱스를 빨리 생성하기 위해, 완료될 때까지 읽기, 쓰기를 중단.

    • 읽기, 쓰기가 작동하게 하려면 생성할 때 background 옵션을 사용 - foreground indexing 보다 느려짐

    • mongoDB 4.2 시작과 끝에만 락을 가짐, interleaving 으로 작업

    • 속도 : 기존 도큐먼트에 인덱스 넣기 > 인덱싱 후 도큐먼트 넣기

profile
이제부터 백엔드 개발자

0개의 댓글