DataBase/MongoDB

MongoDB 인덱스와 인덱싱 전략 이해하기

누구세연 2024. 10. 31. 00:45

데이터가 많아질수록 찾고자 하는 특정 데이터를 빠르게 조회하는 것이 중요해집니다.

만약 인덱스가 없다면, 데이터베이스는 모든 문서를 처음부터 끝까지 읽어가며 일일이 비교해야 합니다. 이렇게 되면 데이터가 많을 때는 조회 속도가 매우 느려지겠죠?🤔 인덱스가 있으면 지정한 필드에 대한 위치를 미리 알고 있기 때문에 빠르게 찾을 수 있습니다.

이 글에서 MongoDB의 인덱스에 대해 알아보겠습니다.

 

 

동작 원리

MongoDB의 인덱스는 B-트리 구조로 만들어집니다.

이는 데이터가 오름차순이나 내림차순으로 정렬된 트리 구조로 특정 값에 접근할 때 트리의 높이에 비례하여 빠르게 이동할 수 있습니다. 이렇게 만들어진 인덱스 덕분에 특정 값을 탐색하거나 범위 조회가 가능해지며 이 과정에서 많은 시간을 절약할 수 있습니다.

  1. 데이터 삽입
    새로운 데이터가 삽입될 때, B-트리는 항상 정렬된 상태를 유지합니다. 이를 위해 새로운 키가 추가될 위치를 찾아 해당 노드에 삽입하고, 노드가 가득 차면 분할하여 새로운 중간 노드를 생성합니다.
  2. 데이터 검색
    특정 값을 검색할 때는 루트 노드부터 시작하여 다음 단계로 이동합니다. 각 노드에서 키 값을 비교하고, 해당 키보다 작은 값이 포함된 자식 노드로 이동합니다. 이러한 과정을 반복하여 리프 노드에 도달하면, 원하는 데이터를 찾을 수 있습니다.
  3. 범위 조회
    범위 조회 시, 시작 키 값을 검색하여 해당 키가 있는 리프 노드에 도달한 후, 그다음 노드들을 순차적으로 방문하여 원하는 범위의 데이터를 가져옵니다. B-트리의 특성 덕분에 범위 조회도 빠르게 수행할 수 있습니다.

 

 

인덱스 예제

예를 들어, 직원 정보를 담은 컬렉션 `employees`가 있다고 가정해 봅시다.

여기에는 직원의 이름, 부서, 나이 등 다양한 필드가 있을 것입니다. 이제 이 컬렉션에서 특정 부서에 소속된 직원들을 빠르게 찾기 위해 `department` 필드에 인덱스를 추가해 보겠습니다.

// department 필드에 인덱스를 추가하는 방법
db.employees.createIndex({ department: 1 });

여기서 1은 오름차순 정렬을 의미합니다. (-1이라면 내림차순 정렬입니다!)

이 인덱스를 추가하면 `department`필드를 기준으로 정렬된 구조가 만들어지기 때문에 특정 부서명을 찾을 때 훨씬 빠르게 접근할 수 있게 됩니다.

 

인덱스 장단점

장점

  • 인덱스가 생성된 필드는 데이터가 많아져도 검색 속도가 크게 저하되지 않습니다. 특히 대용량 데이터베이스에서 특정 조건을 빠르게 조회할 수 있도록 돕습니다.

 

단점

  • 추가 저장 공간 필요
    인덱스를 생성하면 데이터 외에도 별도의 저장 공간을 차지하게 됩니다. 데이터가 많을수록 필요한 공간이 커지기 때문에, 특히 다중 인덱스 사용 시에는 디스크 사용량이 급증할 수 있습니다.
  • 쓰기 성능 저하
    데이터 삽입, 업데이트, 삭제 시 인덱스도 함께 업데이트되어야 하므로, 쓰기 성능이 저하될 수 있습니다. 불필요한 인덱스를 줄여 성능을 최적화하는 것이 중요합니다.

 

 

인덱싱 전략

1. 싱글 필드 인덱스(Single Field Index)

가장 기본적인 인덱스 전략으로 한 개의 필드에만 인덱스를 생성하는 방법입니다.

예를 들어, 이름으로만 자주 검색되는 데이터라면 이름 필드에만 인덱스를 걸면 됩니다.

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

 

싱글 필드 인덱스는 단일 필드를 대상으로 하는 간단한 검색 쿼리에서 좋은 성능을 발휘합니다.

 

2. 복합 인덱스(Compound Index)

복합 인덱스는 여러 필드를 함께 인덱싱하여 다중 조건을 효율적으로 처리하는 전략입니다.

예를 들어, `name`과 `age`를 함께 검색하는 경우가 잦다면 복합 인덱스를 사용하면 좋습니다.

db.users.createIndex({ name: 1, age: -1 });

복합 인덱스는 필드 순서가 중요합니다. 위의 예에서 `{ name: 1, age: -1}`로 인덱스를 걸면 `name`만으로 조회할 때도 인덱스가 사용되지만 `age`만으로 조회할 때는 사용할 수 없습니다.

 

3. 다중 키 인덱스(Multikey Index)

MongoDB에서 배열 필드에 인덱스를 생성할 때는 다중 키 인덱스가 만들어집니다.

이는 배열의 각 요소마다 인덱스를 생성해 배열 내부의 값을 기반으로 빠르게 조회할 수 있게 합니다.

다중 키 인덱스는 하나의 필드가 여러 값을 가질 수 있는 경우에 유용합니다.

 

아래와 같은 `posts` 컬렉션이 있다고 가정해 보겠습니다.

{
  "_id": 1,
  "title": "MongoDB Indexing Guide",
  "tags": ["database", "NoSQL", "MongoDB"]
}

위 문서에서 tags 필드는 여러 태그(문자열)를 담고 있는 배열입니다. 여기서 tags 필드를 인덱스로 지정할 때, MongoDB는 배열의 각 값(database, NoSQL, MongoDB)을 각각 인덱싱하여 검색 속도를 높여줍니다.

즉, tags 필드에 다중 키 인덱스를 생성한 상태에서는 "database"라는 태그나 "MongoDB"라는 태그를 가진 모든 문서를 빠르게 찾을 수 있게 되는 것이죠.

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

이 인덱스로 tags 필드에 인덱스를 설정하면, tags 배열에 포함된 각각의 요소를 독립적으로 인덱싱하여 검색 성능을 높입니다.

 

4. 해시 인덱스(Hashed Index)

해시 인덱스는 특정 값의 정확한 일치를 빠르게 찾아야 할 때 사용하는 인덱스 유형입니다.

이 인덱스는 데이터의 해시 값을 기반으로 생성되기 때문에, 주로 특정 값과 일치하는 문서를 찾는 데 최적화되어 있습니다.

 

작동 방식

  1. 정확한 값 일치에 유리함
    해시 인덱스는 데이터의 값을 해시 알고리즘을 통해 일정한 길이의 해시 값으로 변환하여 저장합니다.
    이렇게 저장된 해시 값으로 문서를 찾으므로 특정 값과 정확히 일치하는 데이터를 빠르게 찾을 수 있습니다.
  2. 범위 쿼리에 부적합
    해시 인덱스는 해시 값의 정렬 순서가 데이터의 실제 순서를 반영하지 않기 때문에 범위 쿼리에는 적합하지 않습니다.
    예를 들어, age 필드가 20~30 사이인 문서를 찾으려 할 때 해시 인덱스 범위 내 값을 찾아내지 못합니다.
    대신 age가 특정 값(예: 25)과 정확히 일치하는 문서를 찾는 경우에 적합합니다.
  3. 해시 충돌 최소화
    해시 인덱스는 특정 값에 대해 유일한 해시 값을 생성하므로 데이터 값이 같다면 해시 값도 동일하게 매핑되지만 다른 값은 고유의 해시 값으로 저장됩니다. MongoDB는 해시 충돌을 최소화하도록 설계되어 있습니다.

예를 들어, 사용자의 ID가 주어졌을 때, 해당 ID와 일치하는 사용자를 빠르게 찾고자 한다면 해시 인덱스가 효과적입니다. 아래는 `user_id` 필드에 해시 인덱스를 생성하는 코드입니다.

db.users.createIndex({ userId: "hashed" });

이제 `user_id`가 특정 값과 정확히 일치하는 사용자를 찾을 때, 해시 인덱스를 통해 빠르게 검색할 수 있게 됩니다.

 

5. 텍스트 인덱스(Text Index)

텍스트 인덱스는 문서에서 특정 키워드를 빠르게 검색하는 용도로 만들어진 인덱스입니다.

일반적으로 기사 본문, 상품 설명과 긴 문자열 필드에서 특정 단어를 찾고자 할 때 사용됩니다.

  • 다중 언어 지원
    MongoDB는 영어 외에도 여러 언어로 텍스트 인덱스를 지원합니다.
  • 일부 단어는 제외됨
    영어의 'the', 'and'와 같이 흔히 쓰이는 단어는 텍스트 인덱스에서 제외하여 검색 성능을 높입니다.

아래 예시는 `content`필드에 텍스트 인덱스를 추가하여 특정 키워드가 포함된 문서를 빠르게 검색할 수 있도록 합니다.

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

위와 같이 인덱스를 설정한 후 특정 단어를 포함한 문서를 검색할 때는 `$text`쿼리를 사용합니다.

db.articles.find({ $text: { $search: "MongoDB 인덱스" } });

 

6. 부분 인덱스(Partial Index)

부분 인덱스는 문서의 일부 조건에 맞는 필드에만 인덱스를 적용하는 방식입니다.

이렇게 하면 불필요한 데이터까지 인덱싱하는 것을 방지하여 저장 공간을 절약하고 조회 성능을 높일 수 있습니다.

  • 필터 조건을 설정하여 해당 조건을 만족하는 문서에만 인덱스를 생성합니다.
  • 불필요한 인덱싱을 줄여 성능을 높이고 저장 공간을 절감할 수 있습니다.
  • 자주 조회되지만 특정 조건만 자주 검색되는 필드에 유리합니다.

아래 예시는 `status`가 “stop” 상태인 문서에만 인덱스를 적용하여, 정지된 상태 주문만 빠르게 조회할 수 있게 만듭니다.

db.orders.createIndex({ status: 1 }, { partialFilterExpression: { status: "shipped" } });
위와 같이 부분 인덱스를 설정한 후, 배송된 주문만 찾고자 할 때는 아래와 같이 간단히 조회할 수 있습니다.
db.orders.find({ status: "shipped" });

 

7. TTL 인덱스(TTL Index)

TTL(Time To Live) 인덱스는 특정 시간이 지나면 자동으로 데이터를 삭제하도록 설정할 수 있는 인덱스입니다.

주로 로그나 일회성 데이터에 사용됩니다.

db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 });

위의 인덱스는 `createdAt`필드를 기준으로 한 시간(3600초) 후에 문서가 자동 삭제 됩니다.

 

 

각 인덱스 전략의 선택 기준

 

  • 싱글 필드 인덱스: 간단한 필드 조회가 많을 때.
  • 복합 인덱스: 여러 필드를 동시에 자주 조회할 때.
  • 다중 키 인덱스: 배열 필드에서 값을 자주 검색할 때.
  • 해시 인덱스: 특정 값의 일치 여부만 확인할 때.
  • 텍스트 인덱스: 텍스트 기반 검색이 필요할 때.
  • 부분 인덱스: 특정 조건에만 인덱스를 적용해 성능을 높이고 싶을 때.
  • TTL 인덱스: 일정 시간이 지나면 자동으로 삭제가 필요한 데이터일 때.