yuni02 2023. 11. 22. 23:03

 

본 내용은 패스트캠퍼스의 '백엔드 개발자를 위한 한 번에 끝내는 대용량 데이터 & 트래픽 처리 초격차 패키지 Online.'를 수강하면서 저의 방식대로 정리한 글입니다.

그에따라 틀린 내용이 있을 수 있습니다. 틀린 내용이 있으면 댓글로 알려주시면 감사하겠습니다.


compound index와 ESR Rule

이 강의를 들으면서 실제 참여중인 개발 프로젝트에서 활용할 수 있는 정보를 추려봤다. 핵심적인 기능은 explain 메서드를 통한 몽고DB쿼리문 성능 측정법이다. 이 방법을 활용해 쿼리문 성능 개선을 위한 인덱스 추가를 고려해볼 수 있다. 

 

MongoDB에서 .explain() 메소드는 쿼리 계획 및 실행에 대한 자세한 정보를 제공한다. 이 메소드의 주요 역할과 특징은 다음과 같다:

  1. 쿼리 실행 계획(Execution Plan) 제공: .explain()은 데이터베이스가 특정 쿼리를 어떻게 실행할 것인지에 대한 계획을 보여줍니다. 이는 인덱스 사용 여부, 쿼리가 스캔해야 하는 문서 수, 쿼리의 실행 경로 등을 포함한다.
  2. 성능 최적화 도구로 활용: 개발자들은 이 정보를 사용하여 쿼리의 성능을 분석하고 최적화할 수 있습니다. 예를 들어, 비효율적인 전체 컬렉션 스캔이 발생하는지, 적절한 인덱스가 사용되고 있는지 확인할 수 있다.
  3. 다양한 모드 제공: .explain() 메소드는 여러 모드("queryPlanner", "executionStats", "allPlansExecution")를 제공하여, 쿼리 계획만 보거나, 실행 통계 또는 모든 가능한 쿼리 계획의 실행 정보를 볼 수 있다.
  4. 디버깅 및 트러블슈팅: 데이터베이스의 성능 문제나 예상치 못한 쿼리 동작을 진단하는 데 유용합니다. 예를 들어, 쿼리가 예상보다 오래 걸릴 때 원인을 분석할 수 있다.

.explain() 메소드는 MongoDB에서 쿼리의 성능을 이해하고 최적화하는 데 필수적인 도구다.

  • 위의 그림처럼 explain method를 통해 쿼리 성능 개선이 가능하다. 이는 아래화면과 같은 성능에 대한 세부정보 확인을 통해 할 수 있다. 
  • explain method를 통해서 나온 실행결과가 길어서 두개의 캡처화면으로 이어붙였다. 결국, 두개의 그림이 하나의 결과다.

explain method 실행 결과 화면

  • 아래는 개발 프로젝트에서 사용한 쿼리문(Aggregate)의 성능을 조회해 본 지표다. 
  • MongoDB의 `.explain()` 메소드 실행 결과를 분석하여 성능 개선을 위한 조치를 고려하는 방법은 여러 단계로 나누어 볼 수 있다. 여기서 주요 포인트를 살펴보겠다:

    1. 쿼리 계획 분석
    - Namespace: 이는 쿼리가 실행되는 데이터베이스와 컬렉션을 나타낸다.
    - Query Planner:
      - `parsedQuery`: 쿼리는 `isUse`, `patientId` 필드와 `dcProcedure`, `opProcedure` 필드 내의 `isUse` 값을 기준으로 조건을 걸고 있다.
      - `winningPlan`: 쿼리 실행 계획에서 `PROJECTION_DEFAULT`와 `COLLSCAN` (Collection Scan, 컬렉션 전체를 스캔)을 사용하고 있음을 나타낸다.

     2. 성능 분석
    - Execution Stats:
      - `executionTimeMillis`: 쿼리 실행 시간은 1밀리초로, 매우 빠르다.
      - `totalDocsExamined`: 5개 문서를 검사했다. 이는 쿼리가 작업하는 문서의 양을 나타낸다.
      - `COLLSCAN`: 인덱스를 사용하지 않고 컬렉션을 전체 스캔했습니다. 대규모 데이터에서는 성능 저하의 원인이 될 수 있다.

    3. 성능 개선 방안
    - 인덱스 활용: 현재 쿼리는 `COLLSCAN`을 사용하고 있다. 이는 특히 데이터가 많은 경우 비효율적일 수 있다. 쿼리가 자주 사용하는 필드(`isUse`, `patientId`, `dcProcedure.isUse`, `opProcedure.isUse`)에 인덱스를 생성하여 성능을 개선할 수 있다.
    - 쿼리 최적화: 쿼리가 너무 많은 데이터를 반환하지 않도록 필요한 필드만 선택하거나, 조건을 더 명확히 해서 검색 범위를 좁히는 것도 성능 향상에 도움이 될 수 있다.

    4. 추가 고려 사항
    - 데이터 크기와 사용 빈도: 현재 쿼리는 소규모 데이터(5개 문서)를 대상으로 하고 있으며, 실행 시간도 매우 짧다. 따라서 현재 상태에서 성능 문제가 없다면 굳이 복잡한 최적화를 진행할 필요는 없을 수 있다. 하지만 데이터 크기가 증가하거나 쿼리 빈도가 높아질 경우 성능 문제가 발생할 가능성이 있다.

    최종적으로, 쿼리의 성능 최적화는 데이터 크기, 사용 빈도, 애플리케이션의 특정 요구사항 등 다양한 요소를 고려해야 한다. 따라서 `.explain()`의 결과를 바탕으로 문맥에 맞는 조치를 취하는 것이 중요하다.
{
  explainVersion: "1",
  queryPlanner: {
    namespace: "d1-op.implant",
    indexFilterSet: false,
    parsedQuery: {
      $and: [
        {
          $or: [
            { dcProcedure: { $elemMatch: { isUse: { $eq: true } } } },
            { opProcedure: { $elemMatch: { isUse: { $eq: true } } } },
          ],
        },
        { isUse: { $eq: true } },
        { patientId: { $eq: "6551d1b85b4897748047d46b" } },
      ],
    },
    queryHash: "37095531",
    planCacheKey: "37095531",
    optimizedPipeline: true,
    maxIndexedOrSolutionsReached: false,
    maxIndexedAndSolutionsReached: false,
    maxScansToExplodeReached: false,
    winningPlan: {
      stage: "PROJECTION_DEFAULT",
      transformBy: {
        _id: true,
        dentalCode: true,
        isUse: true,
        modId: true,
        regDt: true,
        modDt: true,
        site: true,
        patientId: true,
        provNum: true,
        examDate: true,
        providerId: true,
        provAbbr: true,
        regId: true,
        opProcedure: {
          $filter: {
            input: "$opProcedure",
            as: "op",
            cond: { $eq: ["$$op.isUse", { $const: true }] },
          },
        },
        dcProcedure: {
          $filter: {
            input: "$dcProcedure",
            as: "dc",
            cond: { $eq: ["$$dc.isUse", { $const: true }] },
          },
        },
      },
      inputStage: {
        stage: "COLLSCAN",
        filter: {
          $and: [
            {
              $or: [
                { dcProcedure: { $elemMatch: { isUse: { $eq: true } } } },
                { opProcedure: { $elemMatch: { isUse: { $eq: true } } } },
              ],
            },
            { isUse: { $eq: true } },
            { patientId: { $eq: "6551d1b85b4897748047d46b" } },
          ],
        },
        direction: "forward",
      },
    },
    rejectedPlans: [],
  },
  executionStats: {
    executionSuccess: true,
    nReturned: 0,
    executionTimeMillis: 1,
    totalKeysExamined: 0,
    totalDocsExamined: 5,
    executionStages: {
      stage: "PROJECTION_DEFAULT",
      nReturned: 0,
      executionTimeMillisEstimate: 1,
      works: 6,
      advanced: 0,
      needTime: 5,
      needYield: 0,
      saveState: 0,
      restoreState: 0,
      isEOF: 1,
      transformBy: {
        _id: true,
        dentalCode: true,
        isUse: true,
        modId: true,
        regDt: true,
        modDt: true,
        site: true,
        patientId: true,
        provNum: true,
        examDate: true,
        providerId: true,
        provAbbr: true,
        regId: true,
        opProcedure: {
          $filter: {
            input: "$opProcedure",
            as: "op",
            cond: { $eq: ["$$op.isUse", { $const: true }] },
          },
        },
        dcProcedure: {
          $filter: {
            input: "$dcProcedure",
            as: "dc",
            cond: { $eq: ["$$dc.isUse", { $const: true }] },
          },
        },
      },
      inputStage: {
        stage: "COLLSCAN",
        filter: {
          $and: [
            {
              $or: [
                { dcProcedure: { $elemMatch: { isUse: { $eq: true } } } },
                { opProcedure: { $elemMatch: { isUse: { $eq: true } } } },
              ],
            },
            { isUse: { $eq: true } },
            { patientId: { $eq: "6551d1b85b4897748047d46b" } },
          ],
        },
        nReturned: 0,
        executionTimeMillisEstimate: 1,
        works: 6,
        advanced: 0,
        needTime: 5,
        needYield: 0,
        saveState: 0,
        restoreState: 0,
        isEOF: 1,
        direction: "forward",
        docsExamined: 5,
      },
    },
  },
  command: {
    aggregate: "implant",
    pipeline: [
      {
        $match: {
          isUse: true,
          patientId: "6551d1b85b4897748047d46b",
          $or: [
            { opProcedure: { $elemMatch: { isUse: true } } },
            { dcProcedure: { $elemMatch: { isUse: true } } },
          ],
        },
      },
      {
        $project: {
          opProcedure: {
            $filter: {
              input: "$opProcedure",
              as: "op",
              cond: { $eq: ["$$op.isUse", true] },
            },
          },
          dcProcedure: {
            $filter: {
              input: "$dcProcedure",
              as: "dc",
              cond: { $eq: ["$$dc.isUse", true] },
            },
          },
          dentalCode: 1,
          site: 1,
          examDate: 1,
          providerId: 1,
          provNum: 1,
          provAbbr: 1,
          patientId: 1,
          isUse: 1,
          regId: 1,
          regDt: 1,
          modId: 1,
          modDt: 1,
        },
      },
    ],
    cursor: {},
    $db: "d1-op",
  },
  serverInfo: {
    host: "ac-izgs2dd-shard-00-01.mvapd2m.mongodb.net",
    port: 27017,
    version: "6.0.11",
    gitVersion: "f797f841eaf1759c770271ae00c88b92b2766eed",
  },
  serverParameters: {
    internalQueryFacetBufferSizeBytes: 104857600,
    internalQueryFacetMaxOutputDocSizeBytes: 104857600,
    internalLookupStageIntermediateDocumentMaxSizeBytes: 16793600,
    internalDocumentSourceGroupMaxMemoryBytes: 104857600,
    internalQueryMaxBlockingSortMemoryUsageBytes: 33554432,
    internalQueryProhibitBlockingMergeOnMongoS: 0,
    internalQueryMaxAddToSetBytes: 104857600,
    internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600,
  },
  ok: 1,
  $clusterTime: {
    clusterTime: Timestamp({ t: 1700663692, i: 27 }),
    signature: {
      hash: Binary.createFromBase64("VbuGB3LK+4w5zoo3Cg6Au19+w9k=", 0),
      keyId: 7241868336311566000,
    },
  },
  operationTime: Timestamp({ t: 1700663692, i: 27 }),
};