최적의 쿼리 성능을 위한 MongoDB 인덱싱 마스터하기

인덱싱 기술을 마스터하여 MongoDB에서 최적의 쿼리 성능을 잠금 해제하십시오. 이 종합 가이드는 단일 필드, 복합 및 다중 키 인덱스를 다루고, 커버링 쿼리의 강력함을 설명하며, 성능 분석을 위해 `explain()`을 사용하는 방법을 안내합니다. 읽기 작업을 가속화하고, 데이터베이스 부하를 줄이며, 보다 반응성이 뛰어난 애플리케이션을 구축하기 위한 모범 사례를 알아보십시오. 성능 튜닝에 중점을 둔 모든 MongoDB 개발자에게 필수적인 자료입니다.

31 조회수

최적의 쿼리 성능을 위한 MongoDB 인덱싱 마스터하기

데이터베이스 관리 영역에서 성능은 매우 중요합니다. 인기 있는 NoSQL 문서 데이터베이스인 MongoDB의 경우, 쿼리 성능 최적화는 종종 반응적이고 확장 가능한 애플리케이션의 핵심입니다. 이를 달성하기 위한 가장 강력한 도구 중 하나는 인덱싱입니다. MongoDB의 인덱스는 컬렉션 데이터 세트의 작은 부분을 쉽게 탐색할 수 있는 형태로 저장하는 특수 데이터 구조입니다. 이를 통해 MongoDB는 전체 컬렉션을 스캔하지 않고도 문서를 빠르게 찾고 검색하여 읽기 작업을 극적으로 가속화할 수 있습니다.

이 글에서는 MongoDB에서 효율적인 인덱스를 생성하기 위한 필수 기술을 안내합니다. 인덱싱의 기본을 다루고, 복합 인덱스 및 커버링 쿼리와 같은 고급 개념을 탐색하며, 애플리케이션의 읽기 성능을 크게 향상시킬 수 있는 다양한 인덱스 유형에 대해 논의할 것입니다. MongoDB 인덱싱을 마스터함으로써 데이터베이스의 잠재력을 최대한 발휘하고 원활한 사용자 경험을 보장할 수 있습니다.

MongoDB 인덱스 이해하기

본질적으로 인덱스는 책의 색인과 같습니다. 특정 주제를 찾기 위해 책 전체를 읽는 대신 색인을 참조하여 관련 페이지로 빠르게 이동합니다. 마찬가지로 MongoDB 인덱스는 데이터베이스 엔진이 쿼리 조건과 일치하는 문서를 효율적으로 찾도록 돕습니다. 인덱스가 없으면 MongoDB는 컬렉션 스캔을 수행하여 모든 문서를 검사해야 쿼리를 충족하는 문서를 찾을 수 있습니다. 이는 특히 대규모 컬렉션의 경우 매우 느릴 수 있습니다.

인덱스 작동 방식

MongoDB는 일반적으로 인덱스를 위해 B-트리 구조를 사용합니다. B-트리는 정렬된 데이터를 유지하고 로그 시간 내에 검색, 순차 접근, 삽입 및 삭제를 허용하는 자체 균형 트리 데이터 구조입니다. 인덱싱된 필드로 컬렉션을 쿼리할 때 MongoDB는 B-트리를 탐색하여 일치하는 문서를 찾습니다. 이 과정은 전체 컬렉션을 스캔하는 것보다 훨씬 빠릅니다.

인덱스 사용 시기

인덱스는 다음 경우에 자주 사용되는 필드에 가장 유용합니다:

  • 쿼리 조건(find(), findOne()): 쿼리의 filter 문서에 사용되는 필드입니다.
  • 정렬 조건(sort()): 쿼리 결과의 순서를 지정하는 데 사용되는 필드입니다.
  • _id 필드: 기본적으로 MongoDB는 _id 필드에 인덱스를 생성하여 고유성과 ID를 통한 빠른 조회를 보장합니다.

그러나 인덱스에는 다음과 같은 비용도 따릅니다:

  • 저장 공간: 인덱스는 디스크 공간을 차지합니다.
  • 쓰기 성능: 문서가 삽입, 업데이트 또는 삭제될 때마다 인덱스를 업데이트해야 하므로 쓰기 작업 속도가 느려질 수 있습니다.

따라서 일반적인 읽기 작업에서 가장 큰 성능 향상을 가져올 필드에 중점을 두어 인덱스를 전략적으로 생성하는 것이 중요합니다.

인덱스 생성 및 관리

MongoDB는 인덱스를 생성하기 위해 createIndex() 메서드를, 기존 인덱스를 확인하기 위해 getIndexes()를 제공합니다. dropIndex() 메서드는 인덱스를 제거하는 데 사용됩니다.

기본 인덱스 생성

단일 필드 인덱스를 생성하려면 필드 이름과 인덱스 유형(일반적으로 오름차순은 1, 내림차순은 -1)을 지정합니다.

db.collection.createIndex( { fieldName: 1 } );

예시: username 필드를 오름차순으로 인덱싱:

db.users.createIndex( { username: 1 } );

인덱스 보기

컬렉션의 인덱스를 보려면:

db.collection.getIndexes();

예시: users 컬렉션의 인덱스 보기:

db.users.getIndexes();

이는 기본 _id 인덱스를 포함하여 인덱스 정의 배열을 반환합니다.

인덱스 삭제

인덱스를 제거하려면:

db.collection.dropIndex( "indexName" );

indexNamegetIndexes()의 출력에서 찾을 수 있습니다. 또는 createIndex()와 동일한 형식으로 인덱싱된 필드를 지정하여 인덱스를 삭제할 수 있습니다:

db.collection.dropIndex( { fieldName: 1 } );

예시: username 인덱스 삭제:

db.users.dropIndex( "username_1" ); // 인덱스 이름 사용
// 또는
db.users.dropIndex( { username: 1 } ); // 인덱스 정의 사용

복합 인덱스

복합 인덱스는 여러 필드를 포함합니다. 복합 인덱스의 필드 순서는 중요합니다. MongoDB는 filter 또는 sort 절에서 여러 필드를 포함하는 쿼리에 복합 인덱스를 사용합니다.

복합 인덱스 사용 시기

복합 인덱스는 쿼리가 여러 필드의 조합으로 자주 필터링하거나 정렬할 때 가장 효과적입니다. 인덱스는 인덱스에 정의된 순서와 동일한 순서로 필드를 일치시키거나 인덱스의 접두사와 일치하는 쿼리를 만족시킬 수 있습니다.

예시: userId, orderDate, status와 같은 필드를 가진 orders 컬렉션을 가정해 봅시다. 특정 사용자의 주문을 자주 쿼리하고 날짜별로 정렬하는 경우, { userId: 1, orderDate: 1 }에 대한 복합 인덱스는 매우 유용할 것입니다.

db.orders.createIndex( { userId: 1, orderDate: 1 } );

이 인덱스는 다음과 같은 쿼리를 효율적으로 지원할 수 있습니다:

  • db.orders.find( { userId: "user123" } ).sort( { orderDate: -1 } )
  • db.orders.find( { userId: "user123", orderDate: { $lt: ISODate() } } )

그러나 userId가 함께 지정되지 않거나 필드 순서가 다른 경우 orderDate로만 필터링하는 쿼리에는 효과적이지 않을 수 있습니다.

필드 순서의 중요성

복합 인덱스에서 필드의 순서는 다양한 쿼리 패턴에 대한 선택성을 결정합니다. 일반적으로 카디널리티(고유값 수)가 높거나 동등 일치에 가장 일반적으로 사용되는 필드를 인덱스 시작 부분에 배치합니다.

결과를 정렬하는 쿼리의 경우, 최적의 성능을 위해 인덱스의 필드 순서가 sort() 작업의 필드 순서와 일치해야 합니다. 쿼리에 필터와 정렬이 모두 포함되어 있고 인덱스가 필터 필드와 일치하는 경우, 정렬을 위한 별도의 컬렉션 스캔 없이 정렬에도 사용될 수 있습니다.

커버링 쿼리

커버링 쿼리란 MongoDB가 인덱스만을 사용하여 전체 쿼리를 만족시킬 수 있는 쿼리입니다. 이는 인덱스가 쿼리되고 프로젝션되는 모든 필드를 포함한다는 의미입니다. 커버링 쿼리는 컬렉션 자체에서 문서를 가져오는 것을 방지하여 매우 빠르게 만듭니다.

커버링 쿼리 달성 방법

커버링 쿼리를 달성하려면 다음을 확인해야 합니다:

  1. 쿼리의 필터에 사용된 모든 필드를 포함하는 인덱스가 있어야 합니다.
  2. 프로젝션에 해당 인덱싱된 필드(또는 그 일부)만 포함해야 합니다.

예시: name, age, city 필드를 가진 employees 컬렉션을 가정해 봅시다. { city: 1, age: 1 } 인덱스가 있고 특정 도시의 직원 이름과 나이를 검색하려는 경우, 커버링 쿼리를 생성할 수 있습니다:

db.employees.find( { city: "New York" }, { name: 1, age: 1, _id: 0 } ).explain()

이 쿼리에서 city는 인덱스에 있고 nameage는 프로젝션에 포함됩니다. 인덱스에 nameage도 포함되어 있다면 커버링 쿼리가 될 것입니다.

진정한 커버링 쿼리를 위해 인덱스와 쿼리를 다듬어 봅시다:

// 쿼리 및 프로젝션에 필요한 모든 필드를 포함하는 인덱스 생성
db.employees.createIndex( { city: 1, age: 1, name: 1 } );

// 이제 도시로 필터링하고 이름과 나이를 프로젝션하는 쿼리는 커버될 수 있습니다.
db.employees.find( { city: "New York" }, { name: 1, age: 1, _id: 0 } )

이 쿼리에서 explain("executionStats")를 실행하면 "totalDocsExamined"가 "totalKeysExamined"와 같고 "executionType"이 "_id_only" 또는 "covered_query"를 나타내는 것을 볼 수 있습니다. 이는 쿼리가 인덱스에 의해 완전히 만족되었음을 의미합니다.

기타 중요한 인덱스 유형

MongoDB는 특정 사용 사례를 위한 다양한 인덱스 유형을 제공합니다:

멀티키 인덱스

멀티키 인덱스는 배열 필드를 인덱싱할 때 자동으로 생성됩니다. 이를 통해 배열 내 요소를 쿼리할 수 있습니다.

예시: ["electronics", "gadgets"]와 같은 tags 배열 필드를 가진 products 컬렉션이 있는 경우:

db.products.createIndex( { tags: 1 } );

이 인덱스는 db.products.find( { tags: "electronics" } )와 같은 쿼리를 지원할 것입니다.

텍스트 인덱스

텍스트 인덱스는 문서의 문자열 콘텐츠를 효율적으로 검색하는 것을 지원합니다. $text 연산자를 사용하는 텍스트 검색 쿼리에 사용됩니다.

db.articles.createIndex( { content: "text" } );

이를 통해 다음과 같은 검색이 가능합니다: db.articles.find( { $text: { $search: "database performance" } } ).

지리 공간 인덱스

지리 공간 인덱스는 $near, $geoWithin, $geoIntersects 연산자를 사용하여 지리적 데이터를 효율적으로 쿼리하는 데 사용됩니다.

db.locations.createIndex( { loc: "2dsphere" } ); // 2dsphere 인덱스용

고유 인덱스

고유 인덱스는 필드 또는 필드 조합에 대한 고유성을 강제합니다. 중복 값이 삽입되거나 업데이트되면 MongoDB는 오류를 반환합니다.

db.users.createIndex( { email: 1 }, { unique: true } );

explain()을 사용한 성능 분석

MongoDB가 쿼리를 어떻게 실행하는지 이해하는 것은 쿼리를 최적화하는 데 매우 중요합니다. explain() 메서드는 인덱스가 사용되었는지 여부와 방법을 포함하여 쿼리 실행 계획에 대한 통찰력을 제공합니다.

db.collection.find( {...} ).explain( "executionStats" );

explain() 출력에서 찾아야 할 주요 필드:

  • winningPlan.stage: 실행 계획의 단계를 나타냅니다 (예: 컬렉션 스캔의 경우 COLLSCAN, 인덱스 스캔의 경우 IXSCAN).
  • executionStats.totalKeysExamined: 검사된 인덱스 키의 수입니다.
  • executionStats.totalDocsExamined: 검사된 문서의 수입니다.

좋은 실행 계획은 totalDocsExamined가 반환된 문서 수에 가깝거나 같고, totalKeysExamined가 컬렉션의 총 문서 수보다 훨씬 적어야 합니다. totalDocsExamined가 매우 높거나 COLLSCAN이 사용되는 경우, 인덱스가 없거나 효과적으로 사용되지 않고 있음을 시사합니다.

MongoDB 인덱싱 모범 사례

  • 필요한 것만 인덱싱: 거의 쿼리되거나 정렬되지 않는 필드에는 인덱스를 생성하지 마십시오. 각 인덱스는 오버헤드를 추가합니다.
  • 복합 인덱스를 현명하게 사용: 쿼리 패턴에 따라 필드를 올바르게 정렬하십시오. 가장 선택성이 높은 필드를 먼저 고려하십시오.
  • 커버링 쿼리 목표: 읽기 성능이 중요하다면 일반적인 읽기 작업을 커버하도록 인덱스를 설계하십시오.
  • 인덱스 사용량 모니터링: explain()db.collection.aggregate([{ $indexStats: {} }])를 사용하여 인덱스 사용량을 정기적으로 검토하여 사용되지 않거나 비효율적인 인덱스를 식별하십시오.
  • 인덱스 선택성 고려: 카디널리티가 낮은(고유값이 거의 없는) 필드의 인덱스는 카디널리티가 높은 필드의 인덱스만큼 효과적이지 않을 수 있습니다.
  • 인덱스를 작게 유지: 커버링 쿼리에 절대적으로 필요한 경우가 아니라면 인덱스에 큰 필드나 배열을 포함하지 마십시오.
  • 인덱스 테스트: 실제 부하 조건에서 새 인덱스가 읽기 및 쓰기 성능 모두에 미치는 영향을 항상 테스트하십시오.

결론

효과적인 MongoDB 인덱싱은 고성능 NoSQL 애플리케이션의 초석입니다. 기본 사항을 이해하고, 복합 인덱스를 마스터하고, 커버링 쿼리를 활용하고, explain() 메서드를 사용하여 분석함으로써 데이터베이스의 읽기 작업을 크게 최적화할 수 있습니다. 인덱싱의 이점과 비용의 균형을 맞추고, 애플리케이션의 특정 요구 사항을 충족하는지 확인하기 위해 항상 인덱싱 전략을 테스트하는 것을 기억하십시오. 전략적 인덱싱은 단순히 쿼리 속도를 높이는 것이 아니라, 확장 가능하고 반응적이며 효율적인 데이터베이스 시스템을 구축하는 것입니다.