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

5가지 필수 쿼리 최적화 기술을 마스터하여 MongoDB 애플리케이션 속도를 높이세요. 색인 효과적으로 활용하는 방법, 전략적 프로젝션을 통한 문서 스캔 최소화, 비용이 많이 드는 전체 컬렉션 스캔 방지, 그리고 NoSQL 데이터베이스에서 탁월한 읽기 성능을 위한 정렬 작업 최적화 방법을 알아보세요.

29 조회수

고효율 MongoDB 쿼리를 작성하기 위한 다섯 가지 모범 사례

MongoDB는 선도적인 NoSQL 문서 데이터베이스로서 엄청난 유연성과 확장성을 제공합니다. 그러나 통제되지 않은 성장과 제대로 작성되지 않은 쿼리는 특히 데이터 볼륨이 증가함에 따라 심각한 성능 병목 현상을 빠르게 초래할 수 있습니다. 빠르고 응답성이 뛰어난 애플리케이션을 유지하기 위해서는 읽기 성능을 최적화하는 것이 중요합니다. 이 글은 디스크 I/O를 최소화하고, 인덱스를 효과적으로 활용하며, 데이터 검색을 능률화하는 데 초점을 맞춰 고효율 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는 기본적으로 일치하는 전체 문서를 반환합니다. 많은 애플리케이션에서는 몇 가지 필드(예: 이름 목록 표시)만 필요합니다. 불필요하게 큰 필드(예: 내장된 배열 또는 큰 텍스트 블록)를 가져오는 것은 네트워크 대기 시간, 데이터베이스 서버의 메모리 사용량 및 클라이언트 메모리 소모를 증가시킵니다.

프로젝션(Projection)을 사용하면 어떤 필드를 반환해야 하는지 정확히 지정할 수 있습니다.

프로젝션 구문

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

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

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

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

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

특정 쿼리 작업은 MongoDB가 표준 인덱스를 사용하여 충족하기가 본질적으로 어렵거나 불가능하며, 인덱스가 존재하더라도 비용이 많이 드는 전체 컬렉션 스캔을 유발하는 경우가 많습니다.

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

인덱스는 계층적으로 구조화되어 있습니다(알파벳순으로 정리된 책의 색인처럼). 와일드카드(.*)로 시작하는 정규 표현식은 검색어의 시작 지점을 알 수 없으므로 인덱스를 활용할 수 없습니다.

  • 비효율적 (스캔 강제): db.products.find({ sku: /^ABC/ }) (인덱스 사용 가능)
  • 매우 비효율적 (스캔 강제): db.products.find({ sku: /.*CDE$/ }) (인덱스를 효율적으로 사용할 수 없음)

팁: 문자열 값 내에서 검색해야 하는 경우, 전체 텍스트 검색 기능을 위해 MongoDB의 텍스트 인덱스(Text Indexes) 사용을 고려하거나, 접두사 검색을 지원하도록 데이터 구조를 정규화하십시오.

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

앞서 언급했듯이, 인덱싱되지 않은 필드를 쿼리하면 스캔이 강제됩니다. $where 절이 포함되거나 JavaScript 함수를 평가하는 복잡한 쿼리는 거의 항상 모든 문서의 스캔을 초래하므로 특히 주의해야 합니다.

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

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

MongoDB가 정렬에 인덱스를 사용할 수 없는 경우, 결과 집합이 메모리 내 정렬(기본 100MB 메모리 제한)에 너무 클 경우 오류를 반환할 수 있습니다.

모범 사례: 정렬을 위해 커버드 쿼리 사용

커버드 쿼리(Covered Query)는 쿼리 프레디케이트, 프로젝션 및 정렬 작업에 관련된 모든 필드가 단일 인덱스 내에 포함되는 쿼리입니다. 쿼리가 커버되면 MongoDB는 실제 문서를 전혀 볼 필요가 없습니다. 필요한 모든 것을 인덱스 구조 자체에서 직접 가져옵니다.

// 인덱스: { category: 1, price: -1 } 이 있다고 가정

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

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

이 글은 읽기 성능에 초점을 맞추고 있지만, 효율적인 쓰기는 잠금(locking) 및 경합(contention)을 줄여 전반적인 데이터베이스 건전성에 크게 기여합니다. 업데이트는 가능한 한 목표 지향적이어야 합니다.

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

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

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

효율적: 필요한 필드만 변경하기 위해 원자적 연산자 사용.

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

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

요약 및 다음 단계

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

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

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