효율적인 MongoDB 쿼리 작성을 위한 5가지 모범 사례

더 나은 인덱스, 프로젝션, 스캔 회피, 정렬 계획 및 타겟 업데이트를 통해 MongoDB 쿼리 성능을 향상시키세요.

효율적인 MongoDB 쿼리 작성을 위한 5가지 모범 사례

MongoDB 쿼리는 개발 중에는 빠르게 느껴지지만 컬렉션이 커지면 성능이 크게 저하될 수 있습니다. 효율적인 MongoDB 쿼리는 실제 액세스 패턴에 맞게 인덱스를 구성하고, 필요한 필드만 반환하며, 대규모 스캔을 강제하는 작업을 피하는 데 달려 있습니다.

이 다섯 가지 사례는 읽기 작업을 예측 가능하게 유지하고 서버의 불필요한 작업을 줄이는 데 도움이 됩니다.

1. 쿼리를 지원하도록 전략적으로 인덱싱하기

쿼리 성능에서 가장 중요한 단일 요소는 인덱스의 존재와 올바른 사용입니다. 인덱스를 사용하면 쿼리 플래너가 컬렉션의 모든 문서를 스캔(COLLSCAN)하지 않고도 일치하는 문서를 빠르게 찾을 수 있습니다.

인덱싱 작동 방식

MongoDB는 인덱스를 사용하여 쿼리 조건(쿼리의 filter 부분)을 충족시킵니다. 쿼리가 인덱스의 일부인 필드를 사용하는 경우 MongoDB는 해당 인덱스를 사용하여 결과 집합을 빠르게 좁힐 수 있습니다.

모범 사례: 항상 일반적인 쿼리 패턴을 분석하세요. 필드 A, B, C를 자주 쿼리하거나 정렬하는 경우 { A: 1, B: 1, C: 1 } 복합 인덱스 생성을 고려하세요.

인덱스되지 않은 스캔 피하기

쿼리가 인덱스를 사용할 수 없는 경우 MongoDB는 기본적으로 컬렉션 스캔(COLLSCAN) 을 수행하여 컬렉션의 모든 문서를 읽습니다. 이는 대규모 데이터 세트에서 매우 느립니다.

팁: 쿼리에 explain('executionStats') 메서드를 사용하여 winningPlantotalKeysExaminedtotalDocsExamined를 확인하세요. 큰 차이는 일반적으로 인덱스 사용이 좋지 않거나 인덱스가 누락되었음을 나타냅니다.

// 예: 쿼리 성능 확인
db.users.find({ status: "active" }).explain('executionStats')

2. 프로젝션을 활용하여 반환되는 필드 제한하기

쿼리를 실행하면 MongoDB는 기본적으로 일치하는 전체 문서를 반환합니다. 많은 애플리케이션에서 몇 개의 필드(예: 이름 목록 표시)만 필요합니다. 불필요한 큰 필드(예: 포함된 배열 또는 큰 텍스트 블록)를 가져오면 네트워크 지연 시간, 데이터베이스 서버의 메모리 사용량 및 클라이언트 메모리 소비가 증가합니다.

프로젝션을 사용하면 반환할 필드를 정확히 지정할 수 있습니다.

프로젝션 구문

find() 메서드의 두 번째 인수를 사용하여 포함(1) 또는 제외(0)할 필드를 지정합니다.

  • _id는 명시적으로 제외(_id: 0)하지 않는 한 기본적으로 포함됩니다.
// 비효율적: 전체 사용자 문서 반환
db.users.find({ organizationId: "XYZ" })

// 효율적: 사용자의 이름과 이메일만 반환
db.users.find(
    { organizationId: "XYZ" },
    { name: 1, email: 1, _id: 0 } // 이름과 이메일 포함, _id 제외
)

경고: 프로젝션은 인덱스된 필드와 결합될 때 가장 효과적입니다. 쿼리가 여전히 전체 스캔을 필요로 하는 경우 필드 프로젝션은 네트워크 대역폭만 절약할 뿐 초기 검색 시간은 개선하지 않습니다.

3. 전체 컬렉션 스캔을 강제하는 작업 피하기

특정 쿼리 작업은 MongoDB가 표준 인덱스를 사용하여 처리하기 어렵거나 불가능하여 인덱스가 있어도 비용이 많이 드는 전체 컬렉션 스캔으로 이어지는 경우가 많습니다.

정규 표현식에서 선행 와일드카드 피하기

인덱스는 계층적으로 구성됩니다(알파벳순으로 정리된 책 인덱스와 유사). 와일드카드(.*)로 시작하는 정규 표현식은 검색어의 시작점을 알 수 없기 때문에 인덱스를 활용할 수 없습니다.

  • 일반적으로 인덱스 친화적: db.products.find({ sku: /^ABC/ })
  • 일반적으로 비용이 많이 듦: db.products.find({ sku: /.*CDE$/ })

팁: 문자열 값 내에서 검색해야 하는 경우 MongoDB의 텍스트 인덱스를 사용하여 전체 텍스트 검색 기능을 활용하거나 데이터 구조를 정규화하여 접두사 검색을 지원하는 것을 고려하세요.

인덱스되지 않은 필드 쿼리 시 주의

앞서 언급했듯이 인덱스되지 않은 필드를 쿼리하면 스캔이 강제됩니다. $where 절이나 JavaScript 함수 평가와 관련된 복잡한 쿼리는 거의 항상 모든 문서를 스캔하게 되므로 특히 주의하세요.

4. 정렬 작업 최적화 (커버드 쿼리)

.sort() 메서드를 사용하여 결과를 정렬하려면 MongoDB가 일치하는 모든 문서를 검색하여 메모리에서 정렬하거나(세트가 작은 경우) 정렬 순서를 지원하는 인덱스를 사용하는 인덱스 정렬 실행 계획을 사용해야 합니다.

MongoDB가 정렬에 인덱스를 사용할 수 없는 경우 차단 메모리 내 정렬이 필요할 수 있으며 정렬이 서버의 차단 정렬 작업 메모리 제한을 초과하면 실패할 수 있습니다.

모범 사례: 정렬에 커버드 쿼리 사용하기

커버드 쿼리는 쿼리 조건, 프로젝션 및 정렬 작업에 관련된 모든 필드가 단일 인덱스에 포함된 쿼리입니다. 쿼리가 커버되면 MongoDB는 실제 문서를 볼 필요가 없으며 인덱스 구조에서 직접 필요한 모든 것을 얻습니다.

// 인덱스 가정: { category: 1, price: -1 }

// 효율적인 커버드 쿼리:
db.inventory.find(
    { category: "Electronics" }, // 인덱스의 쿼리 필드
    { price: 1, _id: 0 }          // 인덱스의 프로젝션 필드
).sort({ price: -1 })            // 인덱스의 정렬 필드

5. 원자적 업데이트 및 쓰기 작업 선호하기

이 문서는 읽기 성능에 초점을 맞추고 있지만, 효율적인 쓰기는 잠금과 경합을 줄여 데이터베이스 전반적인 상태에 크게 기여합니다. 업데이트는 가능한 한 타겟화되어야 합니다.

전체 문서를 교체하는 대신 업데이트 연산자 사용하기

문서를 수정할 때 문서를 읽고, 클라이언트 측에서 수정한 후, 전체 문서를 다시 쓰는 대신 $set, $inc 또는 $push와 같은 특정 업데이트 연산자를 사용하세요.

비효율적: 전체 문서 읽기 -> 애플리케이션에서 수정 -> 전체 문서 다시 쓰기.

효율적: 원자적 연산자를 사용하여 필요한 필드만 변경합니다.

// 효율적인 업데이트: 다른 필드에 영향을 주지 않고 카운터를 원자적으로 증가
db.metrics.updateOne(
    { metricName: "login_attempts" },
    { $inc: { count: 1 } }
)

원자적 연산자를 사용하면 쓰기 충돌 가능성을 최소화하고 네트워크를 통해 전송되는 데이터 양을 줄일 수 있습니다.

핵심 요점

효율적인 MongoDB 쿼리 작성은 애플리케이션 로직과 데이터베이스 엔진의 인덱스 사용 간의 협력에 달려 있습니다. 이 다섯 가지 모범 사례를 준수하면 읽기 작업이 빠르고, 확장 가능하며, 리소스 친화적임을 보장할 수 있습니다:

  1. 전략적으로 인덱싱: 일반적인 쿼리 필터 및 정렬 기준에 대한 인덱스가 있는지 확인합니다.
  2. 프로젝션 사용: 절대적으로 필요한 필드만 검색합니다.
  3. 스캔 피하기: 정규 표현식의 선행 와일드카드와 $where 절을 피합니다.
  4. 정렬 최적화: 인덱스가 쿼리, 프로젝션 및 정렬에 필요한 모든 필드를 포함하는 커버드 쿼리를 목표로 합니다.
  5. 원자적 쓰기 선호: $set과 같은 연산자를 사용하여 업데이트 중 오버헤드를 최소화합니다.

느린 쿼리 로그를 정기적으로 검토하고 explain()을 사용하여 쿼리가 생성한 인덱스를 활용하고 있는지 확인하세요. 성능 튜닝은 지속적인 과정이지만 이러한 사례는 고성능 MongoDB 배포를 위한 강력한 기반을 형성합니다.