TT Lab
블로그 시작하기
배우기 러닝패스 코스

MongoDB — 문서 데이터베이스의 판단

인덱스는 순서와 범위로 설계한다

TT Lab 에서 이어서 보기

한 줄 요약

인덱스는 「어느 필드에 걸까」 가 아니라 「어떤 순서로, 무엇을 담을까」 의 문제다. 같은 세 필드라도 순서에 따라 훑는 키가 수천 배 달라지고, 담는 범위(부분·TTL)에 따라 크기와 수명이 달라진다. 판단은 실행 계획의 숫자로 한다.

왜 필요했나 — 인덱스가 있는데도 느리다

「인덱스를 걸었는데 왜 느리냐」 는 신고의 대부분은 인덱스가 없는 경우가 아니라 맞지 않는 경우다. 목록 화면의 흔한 쿼리를 보자.

db.orders.find({status: "shipped", amount: {$gte: 150000}}).sort({createdAt: -1}).limit(20)

쿼리에 나온 순서대로 {status, amount, createdAt} 인덱스를 만들면 status 로 좁힌 뒤 amount 범위를 훑는다. 그런데 범위를 훑은 결과는 createdAt 순서가 아니다. 그래서 계획에 SORT 단계가 생기고, 조건에 맞는 수천 건을 전부 메모리에 올려 정렬한 뒤 20건을 자른다. limit 20 은 아무 도움이 안 된다.

어떻게 동작하나 — ESR

복합 인덱스는 앞 필드부터 값으로 정렬된 하나의 목록이다. 그래서 순서 규칙이 나온다.

순서 필드 이유
E 같음(status: "shipped") 한 구간으로 좁힌다. 그 구간 안은 다음 필드 순서로 정렬돼 있다
S 정렬(createdAt: -1) 같음 구간 안에서 이미 정렬된 순서로 걸으면 정렬이 공짜다
R 범위(amount >= …) 걸으면서 키에서 바로 거른다. 20건이 차면 멈춘다

{status, createdAt, amount} 로 만들면 계획은 LIMIT>FETCH>IXSCAN 이 되고, 훑은 키가 20건을 채울 만큼으로 줄어든다. 범위 필드 뒤에 둔 필드는 탐색이 아니라 거르기에만 쓰인다 — 이것이 ERS 와 ESR 의 차이 전부다.

세 가지가 더 있다.

계획을 읽는 순서

실행 계획은 나무다. 바깥(winningPlan)이 마지막에 하는 일이고, inputStage 를 따라 내려가면 처음 하는 일이 나온다. LIMIT>FETCH>IXSCAN 은 「인덱스를 훑어 → 문서를 가져와 → 20건에서 자른다」 이고, SORT>COLLSCAN 은 「전부 훑어 → 메모리에서 정렬한다」 이다. 읽을 때는 세 숫자를 함께 본다.

비교 뜻
totalKeysExamined ≫ nReturned 인덱스를 탔지만 구간이 넓다 — 순서나 정규식을 의심한다
totalDocsExamined ≫ nReturned 문서를 가져와서 거른다 — 거르는 필드가 인덱스에 없다
totalDocsExamined = 0 인덱스만으로 답했다(커버드)

플래너는 후보 계획을 잠깐씩 같이 돌려 보고 먼저 결과를 많이 낸 쪽을 고른 뒤, 그 선택을 쿼리 모양별로 캐시한다. 진 계획은 rejectedPlans 에 남는다. 그래서 인덱스를 새로 만들면 캐시가 비워지고, 같은 쿼리의 계획이 바뀔 수 있다 — 고친 뒤에 다시 재는 이유다. 특정 인덱스의 성적을 따로 재고 싶으면 hint 로 그 인덱스를 강제하고, 인덱스 없이 재려면 {$natural: 1} 을 준다.

현장에서 만나는 모습

다음 실습에서 할 것

주문 60,000건 위에서 같은 목록 쿼리를 인덱스 없이·ERS·ESR 로 재어 숫자를 비교하고, 커버드 쿼리·부분 인덱스·TTL·정규식 접두어를 차례로 잰다. 마지막에는 화면 셋이 느려진 사건을 받아, 같은 증상 뒤의 서로 다른 원인을 가른다.