본문 바로가기
Infra/MongoDB

MongoDB - (5) - 인덱싱

by Inventer 2023. 4. 21.

인덱스를 사용하면 빠른 속도로 쿼리할 수 있다.

 

인덱스를 사용하지 않는 쿼리를 Collection scan이라 하며,

서버가 컬렉션 전체를 뒤져 결과를 가져오는 것을 의미한다.

 

몽고 DB가 쿼리에 효율적으로 응답하게 하려면

애플리케이션의 모든 쿼리 패턴에 인덱스를 사용하는 것이 좋다.

인덱스는 조회 영역에 한하여 놀라운 차이를 가져오지만,

 

인덱싱된 필드를 변경하는 CRUD 중 CUD 작업은

모든 인덱스를 갱신하기 때문에 오래걸린다.

 

몽고 DB 인덱스는 기존 RDBMS와 거의 동일하게 작동한다.

예제를 살펴보자.

 

아래와 같이 쿼리를 진행한다고 하자,

db.users.find().sort({"age" : 1, "username" : 1})

위 쿼리는 먼저 age로 정렬한 뒤, username으로 정리하는데

username으로 한 정렬은 별 도움이 되지 않는다,

 

이런 정렬을 최적화 하고자한다면 아래와 같은 코드로 인덱스를 만든다.

db.users.createIndex({"age" : 1, "username" : 1})

위는 age와 username을 인덱스로 만든 것이다.

 

위처럼 인덱스를 생성하면 각 인덱스의 항목은 나이와, 사용자 명을 포함하며

레코드 식별자(record indentifier)를 가르킨다. 레코드 식별자는 스토리지 엔진에 의해 사용되며

도큐먼트 데이터를 빠르게 찾을 수 있ㅇ는 역할을 수행한다.

 

age가 먼저 나왔으니 age는 완전한 오름차순으로 정렬되며

username은 각 정렬된 age 속에서 오름차순으로 정렬된다.

 

 

동등 쿼리

db.users.find({"age" : 21}).sort({"username" : -1})

단일 조건을 찾는 동등 쿼리이며, 결과 값은 여러개 일 수 있다.

여기서 몽고 DB는 age : 21과 일치하는 마지막 항목 부터 순서대로 인덱스를 탐색한다.

 

 

 

범위 쿼리

db.users.find({"age" : {$gte : 21, $lte : 30}})

인덱스 순서에 따라 도큐먼트 결과를 반환한다.

 

 

범위 쿼리 2

db.users.find({"age" : {$gte : 21, $lte : 30}}).sort({"username" : 1})

 

21 ~ 30세의 나이를 가진 유저 정보를 가져와서, username으로 정렬한다.

이는 비효율적인 쿼리인데, 그 이유는

 

이미 인덱싱됐기 때문에 그대로 받아오는게 효율적인데, 메모리에서 한번더 정렬을 진행했기 때문이다.

또한, 결과가 32MB 이상이면 몽고 DB는 오류를 뿜는다.

 

마지막으로 username을 먼저 놓은 인덱스이다.

db.users.createIndex({"username" : 1, "age" : 1})

이 떄 몽고 DB는 모든 인덱스 항목을 탐색하지만 원하는 순서로 되돌린다.

인덱스의 age 부분을 이용해서 일치하는 도큐먼트를 가져온다는 의미이다.

이는 mongoDB 내부에서 정렬하지 않아도 됨을 의미하긴 하지만,

 

일치하는 값을 찾으려면 어차피 전체 인덱스를 훑어봐야 한다는 단점이 있다.

따라서 복합적으로 인덱스를 구성할 때는 정렬 키를 첫번 째에 놓으면 좋다.

 

 

mongoDB는 어떻게 인덱스를 선택하는가?

 

5개의 쿼리가 있다고 가정해보자.

쿼리가 들어오면 몽고DB는 쿼리 모양을 확인한다.

모양은 검색할 필드와 정렬 여부 등 추가 정보와 관련이 있다.

시스템은 이 정보를 기반으로 쿼리를 충족하는데 사용할 인덱스 후보 집합을 식별한다.

 

(책의 필자는 위에 써놓은 것 처럼 써놨지만, 나는 이를 다른 표현으로 바꾸고싶다.)

더보기

db.users.find({"age" : 1, "username" : 1}) 에서, age와 username이라는 쿼리모양을 인식했고,

age와 username 등 여러 인덱스 중에서 어떤 것이 가장 좋을지 후보 집합을 식별한다.

 

 

이로써 5개 중 3개가 쿼리 후보로 선정됐다고 하자.

각 인덱스 마다 쿼리 플랜을 만들고 3개의 병렬 스레드에서 각각 쿼리를 실행한다.

어떤 스레드에서 가장 빨리 결과를 반환하는지 알기 위함이다.

 

쿼리 플랜은 일정 기간(Trial period) 동안 서로 경쟁하며

각 레이스의 결과로 전체 승리 플랜을 산출한다.

이로서, 향후 동일한 모양을 가진 쿼리가 들어온다면 승리한 인덱스를 지속적으로 사용한다.

 

이 때 쿼리스레드가 레이스에서 이기려면 모든 쿼리 결과를 가장 먼저 반환하거나

결과에 대한 시범 횟수를 정렬 순서로 가장 먼저 반환해야한다.

 

이로서 모양이 동일한 후속 쿼리가 있을 때 몽고 DB 서버에서 어떤 인덱스를 선택할지 알 수 있따.

쿼리 플랜엔 서버에 캐싱된다.

 

시간이 지나 컬렉션과 인덱스가 변경되면 쿼리 플랜이 캐시에서 제거되고

몽고 DB는 다시 가능한 쿼리 플랜을 실험하여 해당 컬렉션 및 인덱스 집합에 가장 적합한 플랜을 찾는다.

 

users collection에 다음과 같이 입력해보라.

db.users.find().explain("executionStats")

 

여기서 nReturned를 반환하기 위해 works 만큼 뒤져봤다는 결론이 나온다.

executionTimeMillis 필드에 표시됐듯 수행 시간이 ms 단위로 기재된다.

또한, 맨 위에 queryPlan이 기재된다.

여기서는 rejectedPlans이 없지만,

본 책에서는 아래와 같이 예시결과를 포함한다.

"rejectedPlans" : [
	{
    	"state" : "SORT",
        "sortPattern" : {
        	"student_id" : 1
            },
...

 

위는 실패한 쿼리 플랜이며, 

이는 DB 내에서 결과 셋을 정렬 할 때 인덱스를 사용할 수 없었으며

인메모리 정렬을 수행했다는 의미이다.

 

또한, 커서 중 hint라는 메소드를 사용하여 쿼리를 일부 강제할 수 있다.

 

복합 인덱스에서는 일반적으로 동등 필터를 사용할 필드가

다중 값 필터를 사용할 필드보다 앞에 오도록 복합 인덱스를 설계하는 것이 좋다.

 

Trade Off

보통 복합 인덱스를 설계할 때는 일반적으로 트레이드 오프가 있다.

 

 

Key 검사 vs 인메모리 정렬

복합 인덱스에서 자주 발생하는 문제로 인메모리 정렬을 피하려면

반환하는 도큐먼트 개수보다 더 많은 키를 검사해야한다.

 

따라서 정렬 구성 요소가 있다면

 

1. 동등필터

2. 정렬구성요소

3. 다중값 필터

 

순서로 인덱스를 구성하라.

 

아래 처럼 쿼리를 작성한경우

db.students.find({"student.id" : {$gt : 500000}, "class_id" : 54})
...		.sort({"final_grade" : 1})
...		.explain("executionStats")

 

아래처럼 인덱스를 생성해야한다.

 

db.students.createIndex({"class_id" : 1, "final_grade" : 1, "student_id" : 1})

 

요약하자면

  • 동등 필터에 대한 키를 맨 앞에 표시하라.
  • 정렬에 사용되는 키는 다중값 필드 앞에 표시해야한다.
  • 다중값 필터에 대한 키는 마지막에 표시해야한다.

이다.

'Infra > MongoDB' 카테고리의 다른 글

MongoDB - (7) - 집계 프레임워크  (0) 2023.04.22
MongoDB - (6) - 인덱싱2  (0) 2023.04.22
MongoDB - (4) - 쿼리  (0) 2023.04.20
MongoDB - (3) - CRUD 실습  (0) 2023.04.19
MongoDB - (2) - CRUD와 데이터타입  (0) 2023.04.18

댓글