[MongoDB] Ch5 - Indexing (인덱싱)
💡 컬렉션에 적합한 인덱스를 선택하면 성능에 큰 영향을 미친다
- 인덱싱의 정의와 사용하는 이유
- 인덱싱할 필드를 선정하는 방법
- 인덱스 사용을 평가하고 적용하는 방법
- 인덱스 생성 및 제거에 대한 관리 정보
5.1 인덱싱 소개
데이터베이스 인덱스는 책의 인덱스와 유사하다
전체 내용을 살펴보는 대신 지름길을 택해, 특정 내용을 가리키는 정렬된 리스트를 확인한다
인덱스를 사용하지 않는 쿼리를 컬렉션 스캔이라고 한다
(책의 내용을 1p부터 끝까지 전체를 읽는 방식과 같다)
💡 큰 컬렉션을 스캔할 때는 컬렉션 스캔이 매우 느려지니 이런 방식은 피하자
1. 인덱스 생성
# username 필드에 인덱스를 생성
db.indextest.createIndex({"username":1})
# 확인
db.indextest.find({"username" : "user101"}).explain("executionStats")
쿼리는 이제 거의 즉각적이며, 어떤 사용자명으로 쿼리하든 비슷하거나 더 나은 실행 시간을 보인다
💡 하지만, 인덱싱된 필드를 변경하는 쓰기(삽입, 갱신, 삭제) 작업은 더 오래 걸린다
데이터가 변경될 때마다 도큐먼트뿐 아니라 모든 인덱스를 갱신해야 하기 때문이다
따라서 어떤 필드가 인덱싱하기에 적합한지 신중히 파악해야 한다
2. 복합 인덱스 소개
인덱스는 가능한 한 효율적으로 쿼리하려는 목적으로 사용한다
상당 수의 쿼리 패턴은 2개 이상의 키를 기반으로 인덱스를 작성해야 한다
인덱스는 모든 값을 정렬된 순서로 보관하므로 인덱스 키로 정렬하는 작업이 있으면 훨씬 빠르다
하지만, 인덱스가 앞 부분에 놓일 때만 정렬에 도움이 된다
예를 들어 아래와 같은 코드가 있다고 해보자
db.user.find().sort({"age":1, "username" : 1})
age로 먼저 정렬한 후, username으로 정렬하므로 별로 도움이 되지 않는다
정렬을 최적화히려면 age, username에 인덱스를 만든다
# 복합 인덱스
db.users.name({"age" : 1, "username" : -1})
몽고DB가 실행하는 쿼리의 종류에 따라 인덱스를 사용하는 방법이 다르다
가장 많이 사용하는 3가지 방법을 알아보자
# 1. 단일 값을 찾는 동등 쿼리
db.users.find({"age":21}).sort({"username":-1})
## age:21이 일치하는 마지막 항목부터 순서대로 인덱스를 탐색
## 매우 효율적인 쿼리, 정확한 나이로 건너뛰고, 인덱스 탐색은 올바른 순서로 반환(정렬할 필요x)
# 2. 범위 쿼리이며 여러 값이 일치하는 도큐먼트를 찾아냄
db.users.find({"age":{"$gte" : 21, "$lte" : 30}})
# 3. 다중값 쿼리(정렬을 포함)
db.users.find({"age" : {"$gte" : 21, "$lte" : 30}}).sort({"username" : 1})
## 인덱스는 사용자명을 정렬된 순서로 반환하지 않으며, 정렬된 결과를 요청한다
## 결과를 반환하기 전에 메모리에서 정렬해야 한다 (따라서 이전 쿼리보다는 비효율적)
3. 몽고DB가 인덱스를 선택하는 방법
쿼리가 들어오면 몽고DB는 쿼리 모양을 확인한다
모양은 검색할 필드, 정렬 여부 등 추가 정보와 관련 있다
쿼리가 들어오고 인덱스 5개 중 3개가 쿼리 후보로 식별됐다고 가정해보자
몽고DB는 각 인덱스 후보에 하나씩 총 3개의 쿼리 플랜을 만들고, 각각 다른 인덱스를 사용하는 3개의 병렬 스레드에서 쿼리를 실행한다 (어떤 스레드에서 가장 빠른 결과를 반환하는지 확인)
이 과정은 레이스와 같고, 가장 먼저 목표 상태에 도달하는 쿼리 플랜이 승자이다
쿼리 스레드가 레이스에서 이기려면, 모든 쿼리 결과를 가장 먼저 반환하거나 결과에 대한 시범 횟수를 정렬 순서로 가장 먼저 반환해야 한다
승리한 플랜은 차후 모양이 같은 쿼리에 사용하기 위해 캐시에 저장된다
인덱스를 다시 작성하거나, 추가나 삭제를 하면 플랜이 캐시에서 제거된다
쿼리 플랜 캐시는 명시적으로 지울 수 있으며, mongod 프로세스를 다시 시작할 때도 삭제된다
4. 복합 인덱스 사용
복합 인덱스는 단일키 인덱스보다 좀 더 복잡하지만 매우 강력하다
복합 인덱스 설계하는 법을 알아보자
먼저 인덱스의 선택성을 고려한다
특정 쿼리 패턴에서 스캔할 레코드 개수를 인덱스가 얼마나 최소화하는지에 관심 있다
쿼리를 충족하는 데 필요한 모든 작업을 고려해야 하며 때로는 트레이드오프가 필요하다
예를 들어 정렬이 처리되는 방식을 고려해야 한다
executionStats 필드는 선정된 쿼리 플랜에 대해 완료된 쿼리 실행을 설명하는 통계를 포함한다
executionStats 필드에서 totalKeysExamined를 살펴보자
totalKeysExamined는 몽고DB가 결과 셋을 생성하기 위해 인덱스 내에서 몇 개의 키를 통과했는지 나타낸다
nReturns와 비교하면 몽고DB가 쿼리와 일치하는 도큐먼트를 찾으려고 얼마나 많은 인덱스를 통과했는지 알 수 있다
예를 들어 totalKeysExamined이 80만인데, nReturns가 9000개이면 쿼리를 충족하는 데 사용된 인덱스가 선택적이지 않았음을 의미한다
explain 출력 맨 위에는 선정된 쿼리 플랜이 있다 (winningPlan)
쿼리 플랜은 몽고DB가 쿼리를 충족하는 데 사용한 단계를 설명한다
서로 다른 쿼리 플랜이 경쟁한 결과로 JSON 형식이다
우리가 사용한 인덱스 종류와 몽고DB가 인메모리 정렬을 수행해야 하는지 여부에 특히 관심이 있다
선정된 플랜 아래에는 거부된 플랜이 있다
FETCH 단계는 도큐먼트를 검색하고 클라이언트가 요청하면 일괄적으로 반환한다
쿼리 플랜에 SORT 단계가 표시된다면, 인덱스를 사용할 수 없었으며 인메모리 정렬을 했다는 의미이다
커서 hint 메서드를 사용하면 모양이나 이름을 지정함으로써 사용할 인덱스를 지정할 수 있다
db.students.find({~}).sort({~}).hint({class_id:1})
💡 일반적으로 동등 필터를 사용할 필드가 다중값 필터를 사용할 필드보다 앞에 오도록 복합 인덱스를 설계해야 한다
복합 인덱스를 설계할 때,
- 동등 필터에 대한 키를 맨 앞에 표시해야 한다
- 정렬에 사용되는 키는 다중값 필드 앞에 표시해야 한다
- 다중값 필터에 대한 키는 마지막에 표시해야 한다
커버드 쿼리 사용하기
쿼리가 단지 인덱스에 포함된 필드를 찾는 중이라면 도큐먼트를 가져올 필요가 없다
인덱스가 쿼리가 요구하는 값을 모두 포함하면, 쿼리가 커버드된다고 한다
쿼리가 확실히 인덱스만 사용하게 하려면 _id 필드를 반환받지 않도록 반환받을 키를 지정해야 한다
explain을 실행하면 결과에 FETCH 단계의 하위 단계가 아닌 IXSCAN 단계가 있으며 totalDocsExamined 값이 0이 된다
5. $연산자의 인덱스 사용법
비효율적인 연산자
일반적으로 부정 조건은 비효율적이다
- $ne"$ne"로 지정된 항목을 제외한 모든 인덱스 항목을 살펴봐야 하므로 기본적으로 전체 인덱스를 살펴봐야 한다
- "$ne" 쿼리는 인덱스를 사용하긴 하지만 잘 활용하지는 못한다
- $not$not을 사용하는 쿼리 대부분은 테이블 스캔을 수행한다
- "$not"은 종종 인덱스를 사용하는데, 어떻게 사용해야 하는지 모를 때가 많다
- $nin
- 항상 테이블 스캔을 수행한다
범위
복합 인덱스는 몽고DB가 다중 절 쿼리를 더 효율적으로 실행하도록 돕는다
다중 필드로 인덱스를 설계할 때,
- 완전 일치가 사용될 필드를 첫 번째
- 범위가 사용될 필드를 마지막에 놓자
쿼리가 첫 번째 인덱스 키와 정확히 일치하는 값을 찾은 후, 두 번째 인덱스 범위 안에서 검색하게 해준다
OR 쿼리
몽고DB는 쿼리당 하나의 인덱스만 사용할 수 있다
유일한 예외는 $or다
$or는 두 개의 쿼리를 수행하고 그 결과를 합치므로 $or 절마다 하나씩 인덱스를 사용할 수 있다
💡 일반적으로 두 번 쿼리해서 결과를 병합하면 한 번 쿼리할 때보다 훨씬 비효율적이다
가능하면 $or보다는 $in을 사용하자
6. 객체 및 배열 인덱싱
몽고DB는 도큐먼트 내부에 도달해서 내장 필드와 배열에 인덱스를 생성하도록 허용한다
내장 도큐먼트 인덱싱하기
서브필드에 인덱스를 만들어 해당 필드를 이용하는 쿼리의 속도를 높일 수 있다
배열 인덱싱하기
인덱스를 활용하면 배열의 특정 요소를 효율적으로 찾을 수 있다
배열을 인덱싱하면 배열의 각 요소에 인덱스 항목을 생성한다
다중키 인덱스가 미치는 영향
다중키 인덱스가 사용됐다면 isMultiKey 필드는 true다
인덱스가 다중키로 표시되면 필드 내 배열을 포함하는 도큐먼트가 제거되더라도 비다중키가 될 수 없다
비다중키가 되게 하려면 인덱스를 삭제하고 생성해야만 한다
다중키 인덱스는 비다중키 인덱스보다 약간 느릴 수 있다
7. 인덱스 카디널리티
카디널리티는 컬렉션의 한 필드에 대해 고윳값이 얼마나 많은지 나타낸다
(gender는 2가지이므로 매우 낮은 카디널리티, username은 유일한 값이니 매우 높은 카디널리티)
일반적으로 필드의 카디널리티가 높을수록 인덱싱이 더욱 도움이 된다
인덱스 검색 범위를 빠르게 좁힐 수가 있기 때문이다
5.2 explain 출력
explain은 쿼리에 대한 많은 정보를 제공하며, 느린 쿼리를 위한 중요한 진단 도구다
explain 출력을 보면 어떤 인덱스가 어떻게 사용되는지 알 수 있다
어떤 쿼리든 마지막에 explain 호출을 추가할 수 있다
- nReturned : 쿼리에 의해 반환된 도큐먼트 개수
- totalKeysExamined : 검색한 인덱스 항목 개수
- totalDocsExamined : 검색한 도큐먼트 개수
- nscrannedObjects : 스캔한 도큐먼트 개수
- stage : 몽고DB가 인덱스를 사용해 쿼리할 수 있었는지 여부
- 쿼리가 "COLLSCAN"을 사용하면 인덱스를 사용하지 않음을 알 수 있다
5.3 인덱스를 생성하지 않는 경우
인덱스는 데이터의 일부를 조회할 때 가장 효율적이며 어떤 쿼리는 인덱스가 없는 게 더 빠르다
인덱스는 컬렉션에서 가져와야 하는 부분이 많을수록 비효율적이다
(인덱스를 하나 사용하려면 두 번의 조회를 해야 하기 때문이다)
인덱스가 도움이 될지 혹은 방해가 될지 알 수 있는 공식은 없다
대체로 쿼리가 컬렉션의 30% 이상을 반환하는 경우 인덱스는 종종 쿼리 속도를 높인다
인덱스가 적합한 경우
큰 컬렉션, 큰 도큐먼트, 선택적 쿼리
컬렉션 스캔이 적합한 경우
작은 컬렉션, 작은 도큐먼트, 비선택적 쿼리
5.4 인덱스 종류
인덱스를 구축할 때 인덱스 옵션을 지정해 동작 방식을 바꿀 수 있다
1. 고유 인덱스
고유 인덱스는 각 값이 인덱스에 한 번 이하 나타나도록 보장한다
username 키에 동일한 값을 가질 수 없도록 하려면 firstname 필드가 있는 도큐먼트에 대해서만 partialFilterExpression으로 고유 인덱스를 만들면 된다
이미 익숙한 고유 인덱스인 "_id"의 인덱스는 컬렉션을 생성하면 항상 자동으로 생성된다
다른 고유 인덱스와 달리 삭제할 수 없다는 점을 제외하면 일반적은 고유 인덱스이다
복합 고유 인덱스
개별 키는 같은 값을 가질 수 있지만 복합 키 조합은 인덱스에서 최대 한 번만 나타난다
중복 제거하기
기존 컬렉션에 고유 인덱스를 구축할 때 중복된 값이 있으면 실패한다
2. 부분 인덱스
고유 인덱스는 null을 값으로 취급하므로, 키가 없는 도큐먼트가 여러 개인 고유 인덱스를 만들 수 없다
고유한 필드가 존재하거나 필드가 아예 존재하지 않으면 "unique"와 "partial"을 결합할 수 있다
부분 인덱스는 반드시 고유할 필요는 없으며, 고유하지 않은 부분 인덱스를 만들려면 "unique" 옵션을 제외시키기만 하면 된다
5.5 인덱싱 관리
데이터베이스의 인덱스 정보는 모두 system.indexes 컬렉션에 저장된다
createIndex, createIndexes, dropIndexs와 같은 데이터베이스 명령어로만 조작할 수 있다
특정 컬렉션의 모든 인덱스 정보를 확인하려면 db.컬렉션.getIndexes()를 실행한다
1. 인덱스 식별
컬렉션 내 각 인덱스는 고유하게 식별하는 이름이 있다
인덱스명은 서버에서 인덱스를 삭제하거나 조작하는데 사용한다
createIndex 옵션으로 원하는 이름을 지정할 수 있다
db.soup.createIndex({"a":1,"b":1,"c":1...}, {"name" : "인덱스이름"})
2. 인덱스 변경
dropIndex 명령을 사용해 불필요한 인덱스를 삭제할 수 있다
새로운 인덱스를 구축하려면 시간이 오래 걸리고 리소스가 많이 필요하다
💡 데이터베이스 읽기와 쓰기에 어느 정도 응답하게 하려면 인덱스를 구축할 때 "background" 옵션을 사용하자
이는 인덱스 구축이 종종 다른 작업에 양보하도록 강제하지만 여전히 애플리케이션에 큰 영향을 준다