1. 인덱싱 소개
- 데이터베이스 인덱스는 책의 인덱스와 유사함
- 전체 내용을 살펴보는 대신 인덱스를 통해 특정 내용을 가리키는 정렬된 리스트를 확인하여 엄청난 양의 명령을 더 빠르게 쿼리 가능
- 인덱스를 사용하지 않는 쿼리를 컬렉션 스캔이라 하며, 서버가 쿼리 결과를 찾으려면 `전체 내용을 살펴봐야 함`을 의미
- 큰 컬렉션을 스캔할 때는 컬렉션 스캔이 매우 느려지니 이런 방식은 지양해야 함
- explain 함수를 이용해 쿼리가 실행될 때 몽고DB가 무엇을 하는지 확인 가능
- explain은 명령을 감싸는 커서 보조자 메서드와 사용하면 좋음
- explain 커서 메서드는 다양한 CRUD 작업의 실행 정보를 제공하며 해당 메서드는 여러 가지 verbosity 모드에서 실행 가능
- executionStats 모드는 인덱스를 이용한 쿼리의 효과를 이용하는 데 도움이 됨
부연 설명
- "executionStats" 필드의 값인 중첩된 도큐먼트에서 "totalDocsExamined"는 몽고DB가 쿼리를 실행하면서 살펴본 도큐먼트 개수이며, 보다시피 컬렉션에 들어 있는 모든 도큐먼트 개수와 같음
- 몽고DB는 모든 도큐먼트 안에 있는 모든 필드를 살펴보며 해당 작업을 완료하는 데 약 0.5초가 걸렸음 (executionTimeMillis)
- "executionStats" 도큐먼트의 "nReturned" 필드는 반환받은 결과의 개수를 보여줌
- 사용자명이 "user101"인 사용자는 단 한 명이므로 값은 1
- 몽고DB는 사용자명이 고유함을 알지 못하므로 일치하는 항목을 조회하기 위해 컬렉션 내 모든 도큐먼트를 살펴봐야 함
- 몽고DB가 쿼리에 효율적으로 응답하게 하려면 애플리케이션의 모든 쿼리 패턴에 인덱스를 사용해야 함
- 쿼리 패턴이란 단순히 애플리케이션이 데이터베이스에 요구하는 다양한 유형의 질문을 의미 i.g. 사용자명으로 사용자 컬렉션을 쿼리
- 많은 애플리케이션에서 단일 인덱스가 여러 쿼리 패턴을 지원함
1.1 인덱스 생성
- 인덱스를 생성하려면 createIndex 컬렉션 메서드를 사용해야 함
- 컬렉션을 특히 크게 만들지 않는 한 인덱스를 생성하는 데는 몇 초면 충분
- "createIndex" 호출이 몇 초 이내 반환되지 않으면, 다른 셸에서 db.currentOp()를 실행하거나 mongod의 로그를 확인해 인덱스 구축의 진행률을 확인하는 것을 권장
- 인덱스는 쿼리 조회 시간에 놀라운 차이를 만듦
- 하지만 인덱싱된 필드를 변경하는 쓰기 (CREATE, UPDATE, DELTE) 작업은 데이터가 변경될 때마다 도큐먼트뿐만 아니라 모든 인덱스를 갱신해야 하기 때문에 더 오래 걸린다는 단점 존재
- 그러므로 어떤 필드가 인덱싱 하기에 적합한지 신중히 파악해야 함
- 인덱스를 생성할 대상 필드를 선택하려면, 자주 쓰는 쿼리와 빨리 수행해야 하는 쿼리를 조사해 공통적인 키 셋을 조회하는 것을 권장
- 해당 쿼리가 특히 자주 쓰이거나 병목 현상이 발생한다면 해당 키를 인덱싱 하면 좋음
- 하지만 독특한 쿼리이거나, 관리자에 의해 실행되는 쿼리와 같이 소요 시간에 구애받지 않을 경우 인덱싱하지 않는 것을 권장
1.2 복합 인덱스 소개
- 인덱스는 가능한 한 효율적으로 쿼리 하려는 목적으로 사용
- 상당수의 쿼리 패턴은 두 개 이상의 키를 기반으로 인덱스를 작성해야 함
- i.g. 인덱스는 모든 값을 정렬된 순서로 보관하므로 인덱스 키로 도큐먼트를 정렬하는 작업이 훨씬 빨라지게 지원하지만 인덱스가 앞 부분에 놓일 때만 정렬에 도움이 됨
- 복합 인덱스는 두 개 이상의 필드로 구성된 인덱스이며 정렬 방향이 여러 개이거나 검색 조건에 여러 개의 키가 있을 때 유용함
- 아래 예제에서 각 인덱스 항목은 나이와 사용자명을 포함하고 레코드 식별자를 가리킴
- 레코드 식별자는 내부에서 스토리지 엔진에 의해 사용되며 도큐먼트 데이터를 조회
- "age" 필드는 완전한 오름차순으로 정렬되며, 각 나이에서 "username" 역시 오름차순으로 정렬됨
몽고DB가 실행하는 쿼리의 종류에 따라 인덱스를 사용하는 방법이 상이하며 가장 많이 사용하는 세 가지 방법은 다음과 같습니다.
- db.users.find({"age": 21}).sort({"username": -1})
- db.users.find({"age: {"$gte": 21, "$lte": 30}})
- db.users.find({"age": {"$gte": 21, "$lte": 30}}).sort({"username": 1})
db.users.find({"age": 21}).sort({"username": -1})
- 단일 값을 조회하는 동등 쿼리이며 결괏값으로 여러 도큐먼트가 있을 수 있음
- 인덱스의 두 번째 필드로 인해 결과는 이미 적절한 순서로 정렬됨
- 몽고DB는 {"age": 21}과 일치하는 마지막 항목부터 순서대로 인덱스 탐색
- 몽고DB는 곧바로 정확한 나이로 건너뛸 수 있으며 인덱스 탐색은 데이터를 올바른 순서로 반환하므로 결과를 정렬할 필요가 없으므로 매우 효율적
- 몽고DB는 인덱스를 어느 방향으로도 쉽게 탐색하므로 정렬 방향은 문제가 되지 않음
db.users.find({"age": {"$gte": 21, "$lte": 30}})
- 범위 쿼리이며 여러 값이 일치하는 도큐먼트를 조회함 i.g. 나이 값이 21과 30 사이인 모든 도큐먼트
- 몽고DB는 인덱스에 있는 첫 번째 키인 "age"를 사용해 일치하는 도큐먼트를 반환받음
- 몽고DB가 인덱스를 사용해 쿼리 하면 일반적으로 인덱스 순서에 따라 도큐먼트 결과를 반환함
db.users.find({"age": {"$gte": 21, "$lte": 30}}).sort({"username": 1})
- 마찬가지로 다중값 쿼리이지만 이번에는 정렬을 포함
- 몽고DB는 검색 조건에 맞는 인덱스를 사용하지만 인덱스는 사용자명을 정렬된 순서로 반환하지 않으며, 쿼리는 사용자명에 따라 정렬된 결과를 요청
- 몽고DB가 이미 원하는 순서대로 도큐먼트가 정렬된 인덱스를 단순히 통과하지 않고, 결과를 반환하기 전에 메모리에서 정렬해야 함을 의미 (이전 쿼리보다 비효율적)
예제에는 같은 키를 역순으로 한 {"username": 1, "age": 1} 인덱스 또한 사용할 수 있으며 이때 몽고DB는 모든 인덱스 항목을 탐색하지만 원하는 순서로 되돌립니다.
- 인덱스의 "age" 부분을 이용해서 일치하는 도큐먼트를 가져옴
- 이는 거대한 인메모리 정렬이 필요하지 않다는 장점이 있지만 일치하는 값을 모두 찾으려면 전체 인덱스를 훑어야 함
- 따라서 복합 인덱스를 구성할 때는 정렬 키를 첫 번째에 놓는 것을 권장하며 해당 방법은 동등 쿼리, 다중값 쿼리, 정렬을 고려해 복합 인덱스를 구성할 때의 모법 사례
1.3 몽고DB가 인덱스를 선택하는 방법
- 쿼리가 들어오면 몽고DB는 쿼리 모양을 확인함
- 모양은 검색 필드와 정렬 여부 등 추가 정보와 관련 있음
- 시스템은 해당 정보를 기반으로 쿼리를 충족하는 데 사용할 인덱스 후보 집합을 식별
- 쿼리가 들어오고 인덱스 5개 중 3개가 쿼리 후보로 식별됐다고 가정하면 몽고DB는 각 인덱스 후보에 하나씩 총 3개의 쿼리 플랜을 만들고, 어떤 쓰레드에서 가장 빨리 결과를 반환하는지 확인하기 위해 각각 다른 인덱스를 사용하는 3개의 병렬 쓰레드에서 쿼리를 실행함
- 위 과정은 레이스와 같으며 가장 먼저 목표 상태에 도달하는 쿼리 플랜이 승자가 됨
- 또한 앞으로 동일한 모양을 가진 쿼리에 사용할 인덱스로 선택된다는 점이 중요함
- 플랜은 일정 기간 동안 서로 경쟁하며, 각 레이스의 결과로 전체 승리 플랜을 산출함
- 여러 쿼리 플랜이 서로 경쟁함으로써, 모양이 동일한 후속 쿼리가 있을 때 몽고DB 서버에서 어떤 인덱스를 선택할지 알 수 있음
- 쿼리 쓰레드가 레이스에서 이기려면, 모든 쿼리 결과를 가장 먼저 반환하거나 결과에 대한 시범 횟수를 정렬 순서로 가장 먼저 반환해야 함
- 인메모리 정렬을 하면 비용이 많이 들기 때문에 정렬 순서는 중요한 부분
- 서버는 쿼리 플랜의 캐시를 유지하는데, 승리한 플랜은 차후 모양이 같은 쿼리에 사용하기 위해 캐시에 저장됨
- 시간이 지나 컬렉션과 인덱스가 변경되면 쿼리 플랜이 캐시에서 제거되고, 몽고DB는 다시 가능한 쿼리 플랜을 실험해 해당 컬렉션 및 인덱스 집합에 가장 적합한 플랜을 찾음
- 또한 인덱스를 다시 작성하거나, 인덱스를 추가하거나 삭제하면 플랜이 캐시에서 제거됨
- 쿼리 플랜 캐시는 명시적으로 지울 수 있으며, mongod 프로세스를 재시작할 때도 삭제됨
1.4 복합 인덱스 사용
- 먼저 인덱스의 선택성을 고려해야함
- 쿼리 패턴에서 스캔할 레코드 개수를 인덱스가 얼마나 최소화하는지 중요
- 쿼리를 충족하는 데 필요한 모든 작업을 고려해야 하며 때로는 트레이드오프가 필요
- i.g. 정렬 처리되는 방식을 고려해야 함
이해를 돕기 위해 약 백만 개의 레코드가 포함된 학생 데이터셋을 통해 설명하겠습니다.
인덱스 두 개로 시작해서 몽고DB가 쿼리를 충족시키기 위해 인덱스를 다음과 같이 구성했습니다.
다음 쿼리를 통해 인덱스 설계 시 고려해야 할 몇 가지 문제점을 살펴볼 수 있습니다.
- 해당 쿼리에서 ID 50000 보다 큰 모든 레코드를 요청
- 전체 레코드의 절반 정도, 또한 검색을 ID가 54인 클래스에 대한 레코드로 제한
- 데이터셋에는 약 500개의 클래스가 있음
- 마지막으로 student_id를 기준으로 오름차순으로 정렬
쿼리를 실행한 뒤 explain 메서드의 출력을 통해 몽고DB가 인덱스를 어떻게 사용해서 쿼리를 충족했는지 알 수 있습니다.
- executionStats에서 totalKeysExamined는 몽고DB가 결과셋을 생성하기 위해 인덱스 내에서 몇 개의 키를 통과 했는지 나타냄
- totalKeysExamined nReturned와 비교하면 몽고DB가 쿼리와 일치하는 도큐먼트를 찾을려고 얼마나 많은 인덱스를 통과했는지 알 수 있음
- 예제에서는 일치하는 도큐먼트 9903개를 찾으려고 인덱스키 85만 477개를 검사했으며 쿼리를 충족하는 데 사용된 인덱스가 선택적이지 않았음을 의미
- executionTimeMillis를 보면 4.3초가 넘게 걸렸음
선택성은 인덱스 설계 시 핵심 목표이므로 예제 쿼리에서 잘못된 부분을 파악해야 합니다.
- 문제는 선택성
- 실행 중인 다중값 쿼리는 "student_id"가 50만보다 큰 레코드를 요청하므로 광범위한 "student_id" 값을 지정하며 이는 컬렉션에 있는 레코드의 약 절반
class_id가 54인 도큐먼트는 500개 정도 이므로 선택적인 기준으로 class_id 기반으로 인덱스를 사용하는 것이 좋습니다.
몽고DB가 데이터베이스가 특정 인덱스를 사용하도록 강제하는 두 가지 방법을 제공하지만 쿼리 플래너의 결과를 재정의하는 방법은 매우 신중히 사용되어야 하기 때문에 운영 환경에서는 사용하기는 어려움
- 커서 hint 메서드를 사용하면 모양이나 이름을 지정함으로써 사용할 인덱스를 지정할 수 있으며 인덱스 필터는 쿼리, 정렬, 프로젝션 사양의 조합인 쿼리 모양을 사용
- planCacheSetFilter 함수를 인덱스 필터와 함께 사용하면, 쿼리 옵티마이저가 인덱스 필터에 지정된 인덱스만 고려하도록 제한할 수 있음
다음 예제와 같이 hint를 사용하도록 쿼리를 약간 변경하면 explain 출력이 상당히 달라진 것을 확인 가능합니다.
- 출력된 결과를 살펴보면, 이전에 다른 인덱스를 사용한 쿼리 플랜에서 약 85만 개의 인덱스 키를 스캔한 것에서 약 2만 개로 줄어들어 1만 개 미만의 인덱스 키를 얻은 것을 확인 가능
- 또한 실행 시간은 4.3초가 아닌 272ms 밖에 되지 않음
그러나 궁극적으로 원하는 것은 "totalKeysExamined"와 매우 유사한 "nReturned"이며 hint를 사용하지 않고 쿼리를 효율적으로 실행하기를 원합니다.
더 나은 인덱스를 설계함에 따라 위에서 열거한 문제를 모두 해결 가능합니다.
- 문제의 쿼리 패턴에 더 나은 인덱스는 순서대로 "class_id"와 "student_id"를 기반으로 하는 인덱스
- "class_id"를 접두사로 사용하면 쿼리에서 equality 필터를 사용해 인덱스에서 고려되는 키를 제한함
- 이는 쿼리에서 가장 선택적인 구성 요소이며 몽고 DB가 쿼리를 충족하기 위해 고려할 키의 개수를 효과적으로 제한함
위 내용을 고려하여 인덱스는 다음과 같이 작성합니다.
- 모든 데이터셋에 해당되지는 않지만, 일반적으로 동등 필터를 사용할 필드가 다중값 필터를 사용할 필드보다 앞에 오도록 복합 인덱스를 설계하는 것을 권장
- 새로운 인덱스가 준비된 상태에서 쿼리를 다시 실행하면 hint가 필요 없는 것을 확인 가능하며 속도가 37ms로 매우 빠른 것을 확인 가능
- 이는 선정된 쿼리 플랜을 반영하는 "executionStages"에 새로 생성한 인덱스를 사용하는 인덱스 스캔이 포함됐기 때문
복합 인덱스를 설계할 때는 인덱스를 사용할 공통 쿼리 패턴의 동등 필터, 다중값 필터, 정렬 구성 요소를 처리하는 방법을 알아야 합니다.
- 이러한 세 가지 요소는 모든 복합 인덱스 설계 시 고려해야 하며, 인덱스를 올바르게 설계하면 몽고DB에서 최상의 쿼리 성능을 얻을 수 있음
- 예제 쿼리에서 [class_id, student_id] 인덱스를 사용해 세 가지 요소를 모두 해결했음
- 그러나 예제 쿼리는 필터링 대상 필드 중 하나를 기준으로 정렬하므로 특수한 복합 인덱스 문제를 나타냄
다음 예제 쿼리는 필터링 대상 필드 중 하나를 기준으로 정렬하므로 특수한 복합 인덱스 문제를 나타냅니다.
- explain을 확인해 보면 쿼리 플랜에 "SORT" 단계가 포함되며, 이제 인메모리 정렬을 수행함을 알 수 있음
- 쿼리는 136ms로 여전히 빠르지만, 인메모리 정렬을 하므로 "student_id"로 정렬할 때보다는 훨씬 느림
인덱스를 더 잘 설계하면 인메모리 정렬을 피할 수 있으며 이를 통해 데이터셋 크기와 시스템 부화의 관련해 보다 쉽게 확장할 수 있습니다.
- 하지만 복합 인덱스를 설게 할 때는 일반적으로 트레이드오프가 있음
복합 인덱스에서 자주 발생하는 문제로, 인메모리 정렬을 피하려면 반환하는 도큐먼트 개수보다 더 많은 키를 검사해야 합니다.
인덱스를 사용해 정렬하려면 몽고DB가 인덱스 키를 순서대로 살펴볼 수 있어야 하므로 복합 인덱스 키 사이에 정렬 필드를 포함해야 합니다.
- 새로운 복합 인덱스의 키는 [class_id, final_grade, student_id]와 같이 정렬돼야 함
- 정렬 구성 요소는 동등 필더 바로 뒤, 다중값 필터 앞에 포함
- 해당 인덱스는 쿼리에서 고려하는 키 집합을 매우 선택적으로 좁힘
- 그런 다움 몽고DB는 인덱스의 동등 필터와 일치하 키 3개를 통해 필터와 일치하는 레코드를 식별
- 해당 복합 인덱스는 몽고DB가 결과 셋에 포함될 도큐먼트보다 더 많은 도큐먼트의 키를 검사 수행
- 그러나 인덱스를 사용해 도큐먼트를 정렬함으로써 실행 시간을 절약 가능
- expain을 통해 인메모리 정렬 대신 방금 만든 인덱스를 사용하는 것을 확인 가능
정리하면 복합 인덱스를 설계할 때 다음과 같은 사항을 고려해야 합니다.
- 동등 필터에 대한 키를 맨 앞에 표시
- 정렬에 사용되는 키는 다중값 필드 앞에 표시
- 다중값 필터에 대한 키는 마지막에 표시
가. 키 방향 선택하기
- 지금까지 인덱스 항목은 모두 오름차순으로 정렬됐지만 두 개 이상의 검색 조건으로 정렬할 때는 인덱스 키의 방향이 서로 달라야 함
- i.g. 이전의 users 컬렉션 예제로 돌아가서 나이가 적은 사용자부터 많은 사용자 순으로, 사용자명은 Z부터 A로 컬렉션을 정렬한다고 가정했을 때 이전 인덱스는 나이 집단 내에서 "username"이 A -> Z 순서로 정렬됐기 때문에 효율적이지 않음
- 복합 정렬을 서로 다른 방향으로 최적화하려면 방향이 맞는 인덱스를 사용해야 함
- i.g. 예제에서는 {"age": 1, "username": -1}을 사용해 나이가 적응 사용자부터 많은 사용자 순으로 정렬되며, 각 나이 집단 내에서 사용자명은 Z부터 A로 정렬
- 애플리케이션이 {"age": 1, "username": 1}을 이용해 정렬을 최적화해야 한다면 해당 방향으로 두 번째 인덱스를 생성
- 인덱스 방향은 다중 조건에 따라 정렬할 때만 문제가 됨
- 단일 키로 정렬하면 몽고DB는 인덱스를 쉽게 역순으로 읽을 수 있음
- i.g. {"age": -1}로 정렬해야 하는데 {"age": 1}로 인덱스를 가질 때, 몽고DB는 {"age": -1}로 인덱스를 가질 때처럼 정렬을 최적화할 수 있으므로 둘 다 생성하지 않는 것을 권장
- 방향은 다중키로 정렬할 때만 문제가 됨
나. 커버드 쿼리 사용하기
- 인덱스가 쿼리가 요구하는 값을 모두 포함하면, 쿼리가 커버드 된다고 함
- 실무에서는 도큐먼트로 되돌아가지 말고 항상 커버드 쿼리를 사용하는 것을 권장
- 앞선 예제에서 인덱스는 항상 적합한 도큐먼트를 조회하는 데 사용되고, 실제 도큐먼트를 가져오기 위해 곧바로 포인터를 따라갔지만
- 쿼리가 단지 인덱스에 포함된 필드를 찾는 중이라면 도큐먼트를 가져올 필요가 없음 (해당 방법으로 작업 셋을 훨씬 작게 만들 수 있음)
- 쿼리가 확실히 인덱스만 사용하게 하려면 "_id" 필드를 반환받지 않도록 반환 키를 지정해야 함
- 쿼리 하지 않는 필드에 인덱스를 생성해야 할 수도 있으므로, 쓰기 때문에 늘어날 부하와 쿼리 속도를 잘 조율해야 함
다. 암시적 인덱스
- 복합 인덱스는 '이중 임무'를 수행할 수 있으며 쿼리마다 다른 인덱스처럼 동작할 수 있음
- {"age": 1, "username":1}로 인덱스를 가지면 age 필드는 {"age": 1}로만 인덱스를 가질 때와 동일한 방법으로 정렬되기 때문에 복합 인덱스는 자체적으로 {"age": 1} 인덱스를 가질 때처럼 사용 됨
- 인덱스가 N개의 키를 가진다면 키들의 앞부분은 '공짜' 인덱스가 됨
- i.g. {"a": 1, "b": 1, "c": 1, ..., "z": 1}과 같은 인덱스가 있을 때 사실상 {"a": 1}, {"a": 1, "b": 1}, {"a": 1, "b": 1, "c": 1} 등으로 인덱스를 가짐
1.5 $ 연산자의 인덱스 사용법
가. 비효율적인 연산자
- 일반적으로 부정 조건은 비효율적
- $ne 쿼리는 인덱스를 사용하긴 하지만 잘 활용하지 못함
- "$ne"로 지정된 항목을 제외한 모든 인덱스 항목을 살펴봐야 하므로 기본적으로 전체 인덱스를 살펴봐야 함
- "$not"은 종종 인덱스를 사용하는데, 어떻게 사용해야 하는지 모를 때가 많음
- "$not"은 기초적인 범위와 정규 표현식을 반대로 뒤집을 수 있음
- 하지만 "$not"을 사용하는 쿼리 대부분은 테이블 스캔을 수행
- "$nin"은 항상 테이블 스캔을 수행
이런 종류의 쿼리를 신속하게 실행해야 한다면, 몽고DB가 인덱스를 사용하지 않는 일치를 시도하기 전에, 결과 셋이 적은 수의 도큐먼트를 반환하게끔 필터링하는 인덱스를 사용하도록 쿼리에 추가할 만한 절이 있는지 파악하는 것을 권장합니다.
나. 범위
- 복합 인덱스는 몽고DB가 다중 절 쿠리를 더 효율적으로 실행하도록 도움
이해를 돕기 위해 다중 필드로 인덱스를 설계할 때는 완전 일치가 사용된 필드를 첫 번째에, 범위가 사용될 필드를 마지막에 배치했습니다.
- 이는 쿼리가 첫 번째 인덱스 키와 정확히 일치하는 값을 조회한 뒤
- 두 번째 인덱스 범위 안에서 검색하게 진원
예를 들어 특정한 나이와 사용자명의 범위에 {"age": 1, "username": 1} 인덱스를 사용해 쿼리 한다고 가정했을 때 꽤 정확한 인덱스의 한계를 얻을 수 있습니다.
- 쿼리는 곧장 "age": 47로 건너뛰고 곧이어 "user5"와 "user8" 사이의 사용자명 범위 내에서 검색
반대로 {"username": 1, "age": 1}로 인덱스를 사용한다고 가정했을 때는 쿼리가 "user5"와 "user8" 사이의 사용자를 모두 살펴보고 "age": 47인 사용자를 뽑아내야 하므로 쿼리 플랜을 변경합니다.
- 이는 몽고DB가 이전 인덱스를 사용할 때의 100개 되는 인덱스 항목을 살펴보도록 강제하기 때문에 하나의 쿼리에 두 개의 범위를 사용하면 비효율적인 쿼리가 됨
다. OR 쿼리
- 현재 몽고DB는 쿼리당 하나의 인덱스만 사용 가능
- i.g. {"x": 1}로 인덱스를 하나 생성하고 {"y": 1}로 또 다른 인덱스를 생성한 뒤 {"x": 123, "y": 456}으로 쿼리를 실행하면 몽고DB는 생성한 인덱스 두 개 중 하나만 사용 가능
- 유일한 예외는 "$or"이며 "$or"는 두 개의 쿼리를 수행하고 결과를 합치므로 "$or"은 "$or" 절마다 하나씩 인덱스를 사용 가능
부연 설명
- explain은 두 "IXSCAN" 단계에서 나타난 대로 두 개의 인덱스상에 있는 분리된 두 쿼리의 집합체
- 일반적으로 두 번 쿼리해서 결과를 병합하면 한 번 쿼리할 때보다 훨씬 비효율적이므로 가능하면 "$or"보다는 "$in"을 사용하는 것을 권장
- "$or"을 사용해야 한다면 몽고DB가 두 쿼리의 결과를 조사하고 중복을 모두 제거해야 함
- 일반적으로 두 번 쿼리해서 결과를 병합하면 한 번 쿼리할 때보다 훨씬 비효율적이므로 가능하면 "$or"보다는 "$in"을 사용하는 것을 권장
- "$in" 쿼리를 실행할 때, 정렬을 제외하면 반환되는 도큐먼트의 순서를 제어하는 방법은 없음 i.g. {"x": {"$in": [1, 2, 3]}}은 {"x": {"$in": [3, 2, 1]})과 동일한 순서로 반환함
1.6 객체 및 배열 인덱싱
- 몽고DB는 도큐먼트 내부에 도달해서 내장 필드와 배열에 인덱스를 생성하도록 허용
- 내장 객체와 배열 필드는 복합 인덱스에서 최상위 필드와 결합될 수 있으며, 다소 특수한 경우를 제외하면 대부분 '일반적인' 인덱스 필드와 같은 방식으로 동작함
가. 내장 도큐먼트 인덱싱하기
- 인덱스는 일반적인 키에 생성될 때와 동일한 방식으로 내장 도큐먼트 키에 생성될 수 있음
부연 설명
- "loc.city"와 같은 "loc"의 서브필드에 인덱스를 생성해 해당 필드를 이용하는 쿼리의 속도를 높일 수 있음
- 내장 도큐먼트 자체 ("loc")를 인덱싱하면 내장 도큐먼트의 필드 ("loc.city")를 인덱싱할 때와는 매우 다르게 동작함
- 서브도큐먼트 전체를 인덱싱하면, 서브도큐먼트 전체에 쿼리할 때만 도움이 됨
- i.g. 쿼리 옵티마이저는 도큐먼트 전체가 올바른 필드 순서로 기술된 쿼리에만 "loc" 인덱스를 사용 가능
- ex) db.uesrs.find({"loc.city": "Shelbyville"})와 같은 쿼리에는 인덱스를 사용할 수 없음
나. 배열 인덱싱하기
- 배열에도 인덱스를 생성할 수 있으며 인덱스를 사용하면 배열의 특정 요소를 효율적으로 찾을 수 있음
- 각 도큐먼트가 하나의 게시물인 블로그 게시물 컬렉션이 있고 각 게시물은 "comments" 필드를 갖는데 이는 "comment" 서브도큐먼트들로 구성된 배열이라고 가정
- 가장 최근에 댓글이 달린 블로그 게시물을 찾으려면, 컬렉션에 내장된 "comments" 도큐먼트 배열 내 "date" 키에 인덱스를 생성해야 함
- 배열을 인덱싱하면 배열의 각 요소에 인덱스 항목을 생성하므로, 한 게시물에 20개의 댓글이 달렸다면 도큐먼트는 20개의 인덱스 항목을 가짐
- 따라서 입력, 갱신 및 제거 작업을 하려면 모든 배열 요소가 갱신돼야 하므로 배열 인덱스는 단일 값 인덱스보다 더 부담스러운 존재
- 배열 필드 인덱싱은 배열 자체가 아니라 배열의 각 요소를 인덱싱하기 때문에 배열 전체를 단일 개체처럼 인덱싱할 수 없음
- 배열 요소에 대한 인덱스에는 위치 개념이 없기 때문에 "comments.4"와 같이 특정 배열 요소를 찾는 쿼리에는 인덱슬르 사용할 수 없음
- 배열의 특정 항목에 인덱스를 생성할 수는 있지만 해당 인덱스는 정확히 N번째 배열 요소를 쿼리할 때만 유용함
- i.g. db.blog.createIndex({"comments.10.votes": 1})
- 인덱스 항목의 한 필드만 배열로부터 가져올 수 있음
- 복합 인덱스를 구성할 때는 한 개 이상의 필드가 배열인 경우 인덱스 내부에서 배열 필드의 값들이 결합되어 인덱스 항목으로 생성되는 것을 막기 위한 정책
- 인덱스 항목이 불필요하게 폭발적으로 많아지는 상황을 피함으로써, 쿼리 성능 및 쓰기 작업 시 성능 저하 예방 가능
다. 다중키 인덱스가 미치는 영향
- 어떤 도큐먼트가 배열 필드를 인덱스 키로 가지면 인덱스는 즉시 다중키 인덱스로 표시됨
- 인덱스는 일단 다중키로 표시되면 필드 내 배열을 포함하는 도큐먼트가 모두 제거되더라도 비다중키가 될 수 없음
- 비다중키가 되게 하려면 인덱스를 삭제하고 재생성해야 함
- 다중키 인덱스는 비다중키 인덱스보다 약간 느릴 수 있음
- 하나의 도큐먼트를 여러 개의 인덱스 항목이 가리킬 수 있으므로 몽공DB는 결과를 반환하기 전에 중복을 제거해야 함
1.7 인덱스 카디널리티
- 카디널리티는 컬렉션의 한 필드에 대해 고유값이 얼마나 많은지 나타냄
- i.g. "gender"와 같은 필드는 가질 수 있는 값이 두 가지 뿐이며, 이는 매우 낮은 카디널리티로 간주되고 "username"과 같은 필드는 컬렉션의 각 도큐먼트마다 유일한 값을 가지며 매우 높은 카디널리티로 간주됨
- 인덱스가 검색 범위를 훨씬 작은 결과 셋으로 빠르게 좁힐 수 있기 때문에 일반적으로 필드의 카디널리티가 높을수록 인덱싱이 더욱 도움이 됨
2. explain 출력
- explain은 쿼리에 대한 많은 정보를 제공하며, 느린 쿼리를 특정하기 위한 중요한 진단 도구
- 쿼리의 explain 출력을 보면 어떤 인덱스가 어떻게 사용되는지 알 수 있으며 어떤 쿼리든 마지막에 explain 호출을 추가 가능
- 인덱스를 사용하지 않는 쿼리의 explain이 가장 일반적
- 쿼리가 "COLSCAN"을 사용하면 인덱스를 사용하지 않음을 알 수 있음
- 인덱스를 사용하는 쿼리의 explain 출력은 다양함
- i.g. imdb.rating에 대한 인덱스를 추가했을 때 출력은 다음과 같음
부연 설명
- 출력은 어떤 인덱스가 사용했는지 먼저 나타냄 (imdb.rating)
- "nReturned"는 실제로 반환된 도큐먼트 개수
- 이는 몽고DB가 쿼리에 답변하기 위해 얼마나 많은 작업을 했는지 나타내지 않음
- 즉 검색한 인덱스와 도큐먼트의 개수를 반드시 반영하지 않는다는 점에 유의해야 함
- "totalKeysExamined"는 검색한 인덱스 항목 개수를 보고하며 "totalDocsExamined"는 검색한 도큐먼트의 개수를 나타냄
- 스캔한 도큐먼트 개수는 "nscannedObjects"에 반영됨
- "executionTimeMillis"는 서버가 요청을 받고 응답을 보낸 시점까지 쿼리가 얼마나 빨리 실행됐는지 보고하지만 해당 값이 우리가 찾는 숫자가 아닐 수도 있음
- 몽고DB가 여러 개의 쿼리 플랜을 시도했다면 "executionTimeMillis"는 최고로 뽑힌 플랜이 아니라 모든 플랜이 실행되기까지 걸린 시간을 반영함
- "stage": "IXSCAN"은 몽고DB가 인덱스를 사용해 쿼리할 수 있었는지 여부를 나타냄
- "COLSCAN"은 인덱스로 쿼리할 수 없어 컬렉션 스캔을 수행했음을 뜻함
- "needYields"는 쓰기 요청을 처리하도록 쿼리가 양보한 횟수
- 대기 중인 쓰기가 있다면 쿼리는 일시적으로 락을 해제하고 쓰기가 처리되게 함
- "indexBounds"는 인덱스가 어떦게 사용됐는지 설명하며 탐색한 인덱스의 범위를 제공
- 쿼리에서 첫 번째 절은 완전 일치이므로 인덱스는 단지 42만 찾으면 됐음
- 쿼리가 키에 어떤 제약 사항도 명시하지 않았기 때문에 두 번째 인덱스 키는 자유 변수
- 따라서 데이터베이스는 "age": 42 내에서 사용자명이 음의 무한대와 양의 무한대 사이에 있는 값을 조회
3. 인덱스를 생성하지 않는 경우
- 인덱스는 데이터의 일부를 조회할 때 가장 효율적이지만 어떤 쿼리는 인덱스가 없는 게 더 빠름
- 인덱스는 컬렉션에서 가져와야 하는 부분이 많을수록 비효율적인데, 인덱스를 한 번 사용하려면 두 번의 조회를 해야 하기 때문
- 한 번은 인덱스 항목을 살펴보고, 또 한 번은 도큐먼트를 가리키는 인덱스의 포인터를 따라감
- 반면에 컬렉션 스캔을 할 때는 도큐먼트만 살펴보면 됨
- 최악의 경우 (컬렉션의 모든 도큐먼트를 반환해야 할 때) 인덱스를 사용할 경우 두 배나 많은 조회를 수행하며 이는 대체로 컬렉션 스캔보다 느림
- 인덱스는 컬렉션에서 가져와야 하는 부분이 많을수록 비효율적인데, 인덱스를 한 번 사용하려면 두 번의 조회를 해야 하기 때문
- 아쉽게도 인덱스를 부여할지 여부에 대한 공식은 없음
- 실제 데이터 크기, 인덱스 크기, 도큐먼트 크기, 결과 셋의 평균 크기 등에 따라 상황이 다르기 때문
- 대체로 쿼리가 컬렉션의 30% 이상을 반환하는 경우 인덱스는 종종 쿼리 속도를 향상시키지만 해당 수치는 2%부터 60%가지 다양함
인덱스가 적합한 경우 | 컬렉션 스캔이 적합한 경우 |
큰 컬렉션 | 작은 컬렉션 |
큰 도큐먼트 | 작은 도큐먼트 |
선택적 쿼리 | 비선택적 쿼리 |
4. 인덱스 종류
4.1 고유 인덱스
- 고유 인덱스는 각 값이 인덱스에 최대 한 번 나타나도록 보장함
- i.g. 여러 도큐먼트에서 "firstname" 키에 동일한 값을 가질 수 없도록 하려면 "firstname" 필드가 있는 도큐먼트에 대해서만 "partialFilterExpression"으로 고유 인덱스를 생성하면 됨
- 이미 익숙한 고유 인덱스인 "_id"의 인덱스는 컬렉션을 생성할 때 항상 자동으로 생성됨
- 다른 고유 인덱스와 달리 삭제할 수 없다는 점만 제외하면 일반적인 고유 인덱스
가. 복합 고유 인덱스
- 복합 고유 인덱스 또한 생성할 수 있으며 이때 개별 키는 같은 값을 가질 수 있지만 인덱스 항목의 모든 키에 걸친 값의 조합은 인덱스에서 최대 한 번만 나타남
나. 중복 제거하기
- 기존 컬렉션에 고유 인덱스를 구축할 때 중복된 값이 있으면 실패함
- E11000 duplicate key error collection 에러 발생
4.2 부분 인덱스
- 고유 인덱스는 null을 값으로 취급하므로, 키가 없는 도큐먼트가 여러 개인 고유 인덱스를 만들 수 없음
- 하지만 오직 키가 존재할 때만 고유 인덱스가 적용되도록 할 때가 많음
- 고유한 필드가 존재하거나 필드가 아예 존재하지 않을 경우 "unique"와 "partial"을 결합할 수 있음
- 부분 인덱스를 만들려면 "partialFilterExpression" 옵션을 포함시킴
- 부분 인덱스는 생성하려는 필터 표현식을 나타내는 도큐먼트와 함께 최소 인덱스가 제공하는 기능의 슈퍼셋을 나타냄
- i.g. 이메일 주소는 선택 항목이지만 입력할 경우 고유해야 한다면 다음과 같이 실행
db.users.ensureIndex({"email": 1}, {"unique": true, "partialFilterExpression": {email: {$exists: true}}})
- 부분 인덱스는 반드시 고유할 필요가 없으므로 고유하지 않은 부분 인덱스를 만들려면 "unique" 옵션을 제외시키기만 하면 됨
5.5 인덱스 관리
- 앞서 설명했다시피 createIndex 함수를 사용해 인덱스 생성 가능하며 인덱스는 컬렉션당 한 번만 만들어야 함
- 동일한 인덱스를 재생성해도 아무 일도 일어나지 않음
- 데이터베이스와 인덱스 정보는 모두 system.indexes 컬렉션에 저장됨
- 이는 에약된 컬렉션이므로 안에 있는 도큐먼트를 수정하거나 제거할 수 없으며 createIndex, createIndexes, dropIndexes와 같은 데이터베이스 명령으로만 조작할 수 있음
- 인덱스 생성 시 system.indexes에서 인덱스의 메타 정보를 확인할 수 있음
- 특정 컬렉션의 모든 인덱스 정보를 확인하려면 db.컬렉션명.getIndexes()를 실행하면 됨
부연 설명
- 중요한 필드는 "key"와 "name"
- 키는 힌트에 사용하거나, 인덱스가 명시돼야 하는 위치에 사용 가능
- 인덱스는 필드 순서에 상관 있음 i.g. {"class_id": 1, "student_id": 1}인 인덱스는 {"student_id": 1, "class_id": 1}인 인덱스와 다름
- 인덱스명은 dropIndexes와 같은 관리적인 인덱스 작업에서 식별자로 사용됨
- "v" 필드는 내부적으로 인덱스 버저닝에 사용됨
- "v": 1인 필드조차 없는 인덱스는 오래되고 비효율적인 형식으로 저장된 상태
가. 인덱스 식별
- 인덱스명은 서버에서 인덱스를 삭제하거나 조작하는 데 사용
- 인덱스 키가 두 개 이상이면 인덱스명이 길어질 수 있으므로 craeteIndex의 옵션으로 원하는 이름을 지정할 수 있음
나. 인덱스 변경
- 애플리케이션이 커지고 변경됨에 따라 데이터와 쿼리가 변경되고 잘 작동하던 인덱스가 더는 작동하지 않을 수 있음
- 이때 dropIndex 명령을 사용해 불필요한 인덱스를 제거할 수 있음
- 새로운 인덱스를 구축하는 작업은 오래 걸리고 리소스를 많이 소모함
- 몽고DB 4.2 버전에서는 인덱스를 최대한 빨리 구축하도록 구축이 완료될 때까지 데이터베이스의 모든 읽기와 쓰기를 중단시킴
- 데이터베이스가 읽기와 쓰기에 어느 정도 응답하게 하렴녀 인덱스를 구축할 때 "background" 옵션을 사용하는 것을 권장
- 포그라운드 인덱싱보다는 느리지만 인덱스 구축이 종종 다른 작업에 양보하도록 강제함
- 몽고DB 4.2는 하이브리드 인덱스 구축이라는 새로운 접근 방식을 도입함
- 인덱스 구축 프로세스의 시작과 끝에만 락을 가짐
- 따라서 프로세스의 나머지 부분은 읽기 및 쓰기 작업을 인터리빙함
- 이는 몽고DB 4.2에서 포그라운드, 백그라운드 인덱싱을 모두 대체했음
참고
몽고DB 완벽 가이드 3판 - 한빛미디어
'DB > 몽고DB 완벽 가이드 3판' 카테고리의 다른 글
[6장] 특수 인덱스와 컬렉션 유형 (0) | 2025.04.10 |
---|---|
[4장] 쿼리 (0) | 2025.03.29 |
[3장] 도큐먼트 생성, 갱신, 삭제 (1) | 2025.03.28 |
[2장] 몽고DB 기본 (1) | 2025.03.27 |
[1장] 몽고DB 소개 (1) | 2025.03.26 |