-
Mongodb 쿼리 성능 개선하기 - 인덱스 추가프로젝트/mongoDB 2025. 9. 28. 21:48반응형
개요
서비스를 운영하던 중 배치 서비스가 유독 느리게 수행되는 현상을 발견하게 되었습니다.
문제의 원인을 분석하고 쿼리 성능을 개선하기 위해 인덱스를 적용했던 사례를 기록해보고자 합니다.
문제 정의
배치는 특정일자 전 데이터를 읽고, 해당 데이터가 특정 기간보다 오래되었다면 삭제하는 역할을 수행합니다.
로그상 쿼리를 불러오고 삭제할 때 10초 정도의 시간이 소요되었음을 확인하여 해당 쿼리가 인덱스를 적절하게 활용하고 있지 않아서 데이터를 읽는 과정에서 느릴 것이라고 판단했습니다.
쿼리 파악
해당 쿼리는 requtestAt 기준으로 특정 날짜 이전의 데이터를 가져오고, type column으로 한번 더 equals 연산을 하여 데이터를 가져오고 있었습니다.
그리고 정렬은 수행하지 않은 채로 100개의 데이터를 페이징 형식으로 가져오고 있었습니다.
즉, 아래와 같은 쿼리로 데이터를 읽어오고 있었습니다.
db.my_collections.find({ requestedAt: { $lt: ISODate("2025-08-28T00:00:00Z")}, type: "A" }).limit(100)실행 계획 분석
db.my_collections.find({ requestedAt: { $lt: ISODate("2025-08-28T00:00:00Z")}, type: "A" }).limit(100).explain("executionStats")MongoDB의 explain() 메서드는 쿼리가 어떻게 실행되는지에 대한 실행 계획(Execution Plan) 을 확인할 수 있게 해주는 도구입니다. 쿼리가 어떤 인덱스를 사용하는지, 혹은 컬렉션 스캔(Collection Scan = Full Scan)을 하는지 보여줍니다.
이때 메서드 파라미터에 "executionStats" 값을 넣어 실제 실행 결과에 대한 통계(스캔한 도큐먼트 수, 실행 시간 등)를 포함한 결과를 확인할 수 있습니다.
executionStats: { "nReturned": 100, "executionTimeMillis": 7204, "totalKeysExamined": 1268657, "totalDocsExamined": 1268657, }executionStats 항목에서는 100건을 반환하려고 126만 건을 스캔해서 7초가 걸렸다는 사실을 알 수 있습니다.
winningPlan: { "stage": "LIMIT", "limitAmount": 100, "inputStage": { "stage": "FETCH", "filter": { "type": { "$eq": "A" } }, "inputStage": { "stage": "IXSCAN", "keyPattern": { "requestedAt": 1 }, "indexName": "requestedAt_1", "direction": "forward", "indexBounds": { "requestedAt": [ "[new Date(-9223372036854775808), new Date(1756166400000))" ] } } } }winningPlan 은 쿼리 옵티마이저(Query Optimizer)가 여러 실행 계획 중에서 최종적으로 선택한 실행 계획을 의미합니다.
winningPlan 구조는 트리 형태(계층 구조)로 표현되는데 안쪽(inputStage)에 있는 stage일수록 먼저 실행되고, 바깥쪽 stage는 그 결과를 받아 처리하는 흐름입니다.
먼저 IXSCAN 단계에서 특정 인덱스 필드(requestedAt)를 오름차순으로 전체 범위 스캔합니다. 이어서 FETCH 단계에서 인덱스로 찾은 문서들을 실제 읽어오며 type = "A" 조건을 적용합니다. 마지막으로 LIMIT 단계에서 그중 상위 100건만 반환합니다.
위 결과를 기반으로 requestedAt은 인덱스 스캔이 적절하게 동작했지만 해당 결과로 126만 건의 데이터가 후보군으로 나왔으며, 이후에 126만 건은 실제 문서를 읽어오면서 type이 A 인지 찾아오게 됩니다.
인덱스 추가
db.collection.createIndex({ type: 1, requestedAt: -1 })성능을 개선하기 위해서 type을 먼저 두고, 그 안에서 requestedAt 정렬이 가능한 구조인 복합 인덱스를 추가할 수 있습니다.
MongoDB에서 복합 인덱스를 생성할때는 ESR rule에 따라 생성하는 것이 권장됩니다.
ESR 규칙은 복합 인덱스를 설계할 때 필드 순서를 정하는 가이드라인으로, Equality(동등성), Sort(정렬), Range(범위)의 약자입니다.
동등성 조건으로 필터링한 후, 정렬 순서를 따르고 마지막으로 범위 연산을 적용하여 인덱스를 효율적으로 활용할 수 있도록 합니다.
MongoDB 4.2 이상은 기본적으로 백그라운드 인덱스 생성(background: true)을 지원해서 DB 전체 write는 막지 않습니다.
인덱스를 만드는 동안 기존 데이터, 새롭게 생성되는 데이터를 B-Tree에 넣어주어야 해서 CPU, 메모리, I/O를 많이 활용하여 latency가 늘어날 수 있어서 모니터링이 필요합니다.
인덱스 생성 이후
executionStats: { "nReturned": 100, "executionTimeMillis": 8, "totalKeysExamined": 100, "totalDocsExamined": 100, }totalKeysExamined는 스캔한 인덱스 항목수인데 백만건 -> 백건으로 줄어든 모습을 확인할 수 있습니다.
실행 시간도 7204ms에서 8ms로 감소하였습니다. (약 900배)
"winningPlan": { "stage": "LIMIT", "limitAmount": 100, "inputStage": { "stage": "FETCH", "inputStage": { "stage": "IXSCAN", "keyPattern": { "type": 1, "requestedAt": -1 }, "indexName": "type_1_requestedAt_-1", "isMultiKey": false, "multiKeyPaths": { "serviceType": [], "requestedAt": [] }, "isUnique": false, "isSparse": false, "isPartial": false, "indexVersion": 2, "direction": "forward", "indexBounds": { "type": [ "[\"A\", \"A\"]" ], "requestedAt": [ "(new Date(1758844800000), new Date(-9223372036854775808)]" ] } } } }type-1_requestedAt_-1 복합 인덱스를 활용하여 IXSCAN을 수행한 것을 확인할 수 있습니다.
참고자료
https://www.mongodb.com/ko-kr/docs/manual/reference/explain-results/
https://www.mongodb.com/ko-kr/docs/manual/reference/method/db.collection.explain/
'프로젝트 > mongoDB' 카테고리의 다른 글
MongoDB Replication이란? (0) 2024.03.02 MongoDB 인덱스 운영법 (0) 2024.02.22 MongoDB 인덱스란? (0) 2024.02.15 Spring Boot + Kotlin + MongoDB로 CRUD 해보기 (0) 2024.02.12 MongoDB 동시성 제어 (0) 2024.02.11