데이터베이스 인덱스 완전 정리: B-Tree 인덱스 vs 역인덱스
데이터베이스 인덱스 완전 정리: B-Tree 인덱스 vs 역인덱스
개요
데이터베이스에서 빠른 검색을 구현하려면 인덱스가 필수입니다. 하지만 인덱스에도 여러 종류가 있고, 각각의 특성과 사용 목적이 다릅니다. 이 글에서는 전통적인 인덱스(B-Tree 인덱스)와 역인덱스(Inverted Index)의 차이를 이해하고, 언제 어떤 것을 사용해야 하는지 알아봅니다.
이 글을 읽고 나면:
- 인덱스와 역인덱스의 기본 원리를 이해할 수 있습니다
- 각 인덱스의 장단점과 사용 사례를 파악할 수 있습니다
- MongoDB Atlas Search에서 역인덱스가 어떻게 활용되는지 알 수 있습니다
문제 상황: 왜 인덱스가 필요한가?
인덱스 없이 검색할 때
수백만 개의 문서가 있는 컬렉션에서 특정 이름을 가진 문서를 찾는다고 가정해봅시다. 인덱스가 없다면 데이터베이스는 모든 문서를 하나씩 확인해야 합니다.
문서 1: { name: "김철수", ... } 문서 2: { name: "이영희", ... } 문서 3: { name: "박민수", ... } ... 문서 1,000,000: { name: "안유진", ... }
“안유진”을 찾기 위해 1번부터 1,000,000번까지 순차적으로 검색해야 합니다. 이는 O(n) 시간 복잡도로, 데이터가 많아질수록 검색 시간이 선형적으로 증가합니다.
인덱스가 있을 때
인덱스가 있으면 데이터베이스는 인덱스 구조를 먼저 확인하여 해당 문서의 위치를 빠르게 찾을 수 있습니다. B-Tree 인덱스의 경우 O(log n) 시간 복잡도로 검색할 수 있어 훨씬 빠릅니다.
전통적인 인덱스(B-Tree 인덱스)
기본 원리
전통적인 인덱스는 책의 색인과 비슷합니다. 책에서 특정 단어를 찾을 때 처음부터 끝까지 읽지 않고, 색인 페이지를 먼저 확인하여 해당 단어가 나오는 페이지 번호를 찾는 것과 같습니다.
구조 예시
다음과 같은 문서들이 있다고 가정합니다:
문서 ID: 1, name: "안유진" 문서 ID: 2, name: "김민지" 문서 ID: 3, name: "이해인" 문서 ID: 4, name: "안유진" 문서 ID: 5, name: "박지은"
B-Tree 인덱스는 다음과 같은 구조를 가집니다:
[김민지] / \ [이해인] [안유진] / \ / \ [박지은] [null] [null] [null]
각 노드는 값과 해당 문서 ID를 저장합니다. “안유진”을 검색하면 트리를 탐색하여 문서 ID 1과 4를 빠르게 찾을 수 있습니다.
장점
- 정확한 값 검색에 효율적:
name = "안유진"과 같은 정확한 일치 검색이 빠릅니다 - 범위 검색에 유리:
age >= 20 AND age <= 30과 같은 범위 쿼리에 최적화되어 있습니다 - 정렬된 결과: 인덱스가 정렬된 상태를 유지하므로 ORDER BY가 빠릅니다
단점
- 부분 문자열 검색에 비효율적:
name LIKE "%유진%"과 같은 검색은 인덱스를 활용하기 어렵습니다 - 전체 텍스트 검색 부적합: 여러 단어를 동시에 검색하거나 관련도 순으로 정렬하기 어렵습니다
- 복합 검색의 한계: 여러 필드를 조합한 복잡한 검색 쿼리에 제약이 있습니다
역인덱스(Inverted Index)
기본 원리
역인덱스는 검색 엔진의 핵심 구조입니다. 각 단어(토큰)를 키로 하고, 해당 단어가 포함된 문서들의 목록을 값으로 저장합니다.
구조 예시
다음 문서들이 있다고 가정합니다:
문서 1: { name: "안유진", introduction: "아이브의 리더" } 문서 2: { name: "김민지", introduction: "뉴진스의 리더" } 문서 3: { name: "이해인", introduction: "아이브의 멤버" }
역인덱스는 다음과 같은 구조를 가집니다:
"안유진" → [문서 1] "김민지" → [문서 2] "이해인" → [문서 3] "아이브" → [문서 1, 문서 3] "리더" → [문서 1, 문서 2] "뉴진스" → [문서 2] "멤버" → [문서 3]
“아이브”를 검색하면 문서 1과 문서 3을 즉시 찾을 수 있습니다. “아이브”와 “리더”를 동시에 검색하면 두 리스트의 교집합을 구하여 문서 1을 찾을 수 있습니다.
분석 과정
역인덱스를 만들기 위해서는 텍스트를 토큰으로 분리하는 과정이 필요합니다. 이를 텍스트 분석(Text Analysis)이라고 합니다.
예를 들어, “아이브의 리더”라는 텍스트는 다음과 같이 분석됩니다:
원본 텍스트: "아이브의 리더" ↓ (토큰화) ["아이브", "의", "리더"] ↓ (불용어 제거 - 선택적) ["아이브", "리더"] ↓ (어간 추출 - 선택적) ["아이브", "리더"]
한국어의 경우 형태소 분석이 필요하므로 lucene.nori 같은 분석기를 사용합니다.
장점
- 전체 텍스트 검색에 최적화: 여러 단어를 동시에 검색하고 관련도 순으로 정렬할 수 있습니다
- 부분 문자열 검색 지원: 단어 단위로 검색하므로 유연한 검색이 가능합니다
- 복합 검색 효율적: 여러 필드를 조합한 복잡한 검색 쿼리를 빠르게 처리합니다
- 관련도 점수 계산: 각 문서의 검색어 관련도를 계산하여 정확한 순서로 결과를 제공합니다
단점
- 정확한 값 검색에 비효율적: 단일 값 검색은 B-Tree 인덱스보다 느릴 수 있습니다
- 범위 검색 부적합: 숫자나 날짜 범위 검색에는 적합하지 않습니다
- 저장 공간: 각 단어마다 문서 리스트를 저장하므로 더 많은 저장 공간이 필요합니다
- 인덱스 구축 시간: 텍스트 분석 과정이 필요하므로 인덱스 구축에 시간이 걸립니다
비교: 언제 무엇을 사용할까?
B-Tree 인덱스가 적합한 경우
- 정확한 값 검색
// 사용자 ID로 조회 db.users.find({ userId: 'user123' });
- 범위 검색
// 나이 범위로 검색 db.users.find({ age: { $gte: 20, $lte: 30 } });
- 정렬된 결과
// 가입일 순으로 정렬 db.users.find().sort({ createdAt: -1 });
역인덱스가 적합한 경우
- 전체 텍스트 검색
// 제목이나 내용에서 검색 db.articles.find({ $text: { $search: 'MongoDB Atlas Search' } });
- 복합 검색
// 여러 필드에서 동시에 검색 { compound: { should: [ { text: { query: '안유진', path: 'name' } }, { text: { query: '안유진', path: 'introduction' } }, ]; } }
- 관련도 순 정렬
// 검색어와의 관련도에 따라 자동 정렬 // 점수가 높은 문서가 먼저 반환됨
MongoDB Atlas Search에서의 역인덱스
MongoDB Atlas Search는 역인덱스를 기반으로 작동합니다. Atlas Search 인덱스를 생성하면 자동으로 역인덱스가 구축됩니다.
인덱스 정의 예시
{ "mappings": { "dynamic": false, "fields": { "name": { "type": "string", "analyzer": "lucene.nori", "searchAnalyzer": "lucene.nori" }, "introduction": { "type": "string", "analyzer": "lucene.nori", "searchAnalyzer": "lucene.nori" } } } }
이 인덱스는 다음과 같은 역인덱스를 생성합니다:
"안유진" → [문서 ID: 1, 4] "아이브" → [문서 ID: 1, 3, 5] "리더" → [문서 ID: 1, 2] ...
검색 쿼리 예시
db.idols.aggregate([ { $search: { index: 'korean_text_index', compound: { should: [ { text: { query: '안유진', path: 'name', score: { boost: { value: 10 } }, }, }, { text: { query: '안유진', path: 'introduction', score: { boost: { value: 3 } }, }, }, ], }, }, }, ]);
이 쿼리는 다음과 같이 처리됩니다:
- “안유진”이라는 토큰을 역인덱스에서 찾습니다
name필드에서 찾은 문서는 10점,introduction필드에서 찾은 문서는 3점을 받습니다- 점수가 높은 순서로 결과를 정렬하여 반환합니다
실제 사용 사례
사례 1: 전자상거래 상품 검색
전자상거래 사이트에서 상품을 검색할 때는 역인덱스가 필수입니다.
사용자 검색어: "무선 이어폰 블루투스"
B-Tree 인덱스로는 이 검색을 효율적으로 처리하기 어렵습니다. 하지만 역인덱스를 사용하면:
- “무선”, “이어폰”, “블루투스” 각각의 문서 리스트를 찾습니다
- 세 리스트의 교집합을 구합니다
- 관련도 점수를 계산하여 정렬합니다
사례 2: 사용자 프로필 검색
사용자 ID로 정확히 조회할 때는 B-Tree 인덱스가 적합합니다.
사용자 ID: "user_12345"
B-Tree 인덱스는 O(log n) 시간에 정확한 문서를 찾을 수 있습니다.
사례 3: 블로그 포스트 검색
블로그 포스트의 제목과 내용을 검색할 때는 역인덱스가 필요합니다.
검색어: "NestJS MongoDB 연동"
여러 필드에서 동시에 검색하고, 관련도 순으로 정렬하여 사용자에게 가장 관련성 높은 포스트를 먼저 보여줄 수 있습니다.
성능 비교
검색 속도
| 검색 유형 | B-Tree 인덱스 | 역인덱스 |
|---|---|---|
| 정확한 값 검색 | 매우 빠름 (O(log n)) | 보통 (O(1) ~ O(n)) |
| 범위 검색 | 빠름 (O(log n)) | 느림 (전체 스캔) |
| 전체 텍스트 검색 | 느림 (전체 스캔) | 매우 빠름 (O(1)) |
| 복합 검색 | 느림 | 빠름 |
저장 공간
- B-Tree 인덱스: 원본 데이터의 약 10-20% 추가 공간 필요
- 역인덱스: 원본 데이터의 약 30-50% 추가 공간 필요 (텍스트 분석 결과 저장)
요약
B-Tree 인덱스
- 용도: 정확한 값 검색, 범위 검색, 정렬
- 장점: 빠른 정확한 검색, 효율적인 범위 쿼리
- 단점: 전체 텍스트 검색에 부적합
역인덱스
- 용도: 전체 텍스트 검색, 복합 검색, 관련도 순 정렬
- 장점: 유연한 텍스트 검색, 관련도 점수 계산
- 단점: 저장 공간이 많이 필요, 인덱스 구축 시간 소요
선택 가이드
- 정확한 값이나 범위로 검색 → B-Tree 인덱스
- 텍스트 내용을 검색 → 역인덱스
- 둘 다 필요 → 두 인덱스를 모두 생성
결론
인덱스와 역인덱스는 각각 다른 목적을 위해 설계되었습니다. B-Tree 인덱스는 정확한 값 검색과 범위 쿼리에 최적화되어 있고, 역인덱스는 전체 텍스트 검색과 복합 검색에 최적화되어 있습니다.
MongoDB에서는 일반 쿼리에는 B-Tree 인덱스를, Atlas Search를 통한 텍스트 검색에는 역인덱스를 사용합니다. 프로젝트의 요구사항에 맞는 인덱스를 선택하여 검색 성능을 최적화하세요.
FAQ
Q1: B-Tree 인덱스와 역인덱스를 동시에 사용할 수 있나요?
네, 가능합니다. MongoDB에서는 일반 쿼리용 B-Tree 인덱스와 Atlas Search용 역인덱스를 별도로 생성할 수 있습니다. 각각의 목적에 맞게 사용하면 됩니다.
Q2: 역인덱스는 언제 구축되나요?
MongoDB Atlas Search의 경우, 인덱스를 생성하면 백그라운드에서 자동으로 역인덱스가 구축됩니다. 데이터가 많을 경우 시간이 걸릴 수 있으므로 background: true 옵션을 사용하는 것이 좋습니다.
Q3: 역인덱스의 크기는 어떻게 확인하나요?
MongoDB Atlas 콘솔의 Search 인덱스 페이지에서 인덱스 크기를 확인할 수 있습니다. 일반적으로 원본 데이터의 30-50% 정도의 크기를 가집니다.
Q4: 한국어 검색을 위해 특별한 설정이 필요한가요?
한국어는 형태소 분석이 필요하므로 lucene.nori 분석기를 사용해야 합니다. 이 분석기는 한국어 텍스트를 형태소 단위로 분리하여 역인덱스를 구축합니다.
Q5: 인덱스가 성능에 미치는 영향은?
인덱스는 검색 속도를 크게 향상시키지만, 데이터를 추가하거나 수정할 때 인덱스도 함께 업데이트해야 하므로 쓰기 성능에 약간의 영향을 줄 수 있습니다. 대부분의 경우 읽기 성능 향상이 쓰기 성능 저하보다 훨씬 큽니다.