-
MongoDB index 개념과 indexing 전략개발/MongoDB 2020. 6. 7. 17:52728x90
이번 시간에는 mongodb index에 대해 알아보려고 합니다. 어떤 조회 쿼리를 추가했는데 그 쿼리가 너무 느렸던 경험과 기존 프로젝트에 조회 쿼리가 데이터가 쌓일수록 느려져 문제가 되었던 적이 종종 있었습니다. 이런 경우 인덱스가 필요한데 없거나 인덱싱이 잘못되어 있어서 문제가 발생한 적이 있었습니다. 따라서 이번 시간에는 인덱스에 대한 개념과 종류 실습을 통해서 어떤 식으로 indexing을 해야하는지 전략에 대해 알아 보겠습니다. 😃
1. MongoDB index
인덱스는 DB의 검색을 빠르게 하기 위하여 데이터의 순서를 미리 정리해 두는 과정입니다. MongoDB 고정된 스키마는 없지만, 원하는 데이터 필드를 인덱스로 지정하여 검색 결과를 빠르게 하는 것이 가능합니다. 따라서 MongoDB 효율적으로 사용하고 하드웨이 자원을 최대로 사용하려면 인덱스에 대한 이해가 필요합니다.
MongoDB index에 대한 개념
- index는 한 쿼리에 한 index만 유효합니다. 따라서 두 개의 index 가 필요하다면 복합 index를 사용합니다.
- index는 어떤 데이터가 도큐먼트에 추가되거나 수정될 때( write 작업 ) 그 컬렉션에 생성되어 있는 index도 새로운 도큐먼트를 포함시켜 수정됩니다. 이로 인하여 index 추가 시 wirte 작업은 느려질 수 있습니다. 따라서 index는 read 작업 위주의 애플리케션에서 유용하고 읽기보다 쓰기 작업이 많으면 index를 추가하는 것은 고려해야 합니다.
2. 실습을 위한 mongoDB 설정
MongoDB를 설치하기 너무 귀찮아 docker로 Mongo 올려서 작업해 보겠습니다. 컴퓨터에 mongo가 설치되어 있다면 아래 "데이터 생성하기" 부분만 진행합니다.
docker mongodb 실행
아래의 명령어로 mongo를 실행합니다. mongo 기타 설정 방법은 mongo hub에 들어가면 알 수 있습니다.
$ docker run -d --name some-mongo \ -p 27017:27017 \ -v <로컬 볼륨 위치>:/data/db \ mongo --auth
mongo 설정하기
admin 생성하기
그 후 docker mongo 안으로 들어가서 설정을 하기 위해 아래 작업을 진행합니다.
$ docker exec -it some-mongo bash ------ 생략 ------ root@38a6bb030c5f:/# /usr/bin/mongo ------ 생략 ------ > use admin switched to db admin > db.createUser({user:"root", ... ... ... ... pwd:"root1234", ... ... ... ... roles:["root"]}) Successfully added user: { "user" : "root", "roles" : [ "root" ] } > exit
데이터 생성하기
인덱스 테스트를 위해 아래와 같이 많은 양의 데이터를 생성합니다. 이 작업은 오래 걸리니 귀찮으면 다른 sample 데이터를 임포트 하는 방법을 선택합니다.
데이터를 생성하기 위해서 admin 계정으로 접속해서 데이터를 생성합니다.
# mongo admin -u root -p 'root1234' --authenticationDatabase admin > use students > for (i =0; i<500000; i++){ db.user.insert({ "userid" : i, "name": "user"+i, "age": Math.floor(Math.random()*100), "score": Math.floor(Math.random()*100), "time": new Date() }); db.user.insert({ "userid" : i, "name": "user"+i, "age": Math.floor(Math.random()*100), "score": Math.floor(Math.random()*100), "time": new Date() }); }
mongodb client tool로 접속하기
편하게 접속하기 위해 mongodb client tool를 다운로드하여서 docker mongo에 접속하 겠습니다. 원하는 mongodb client tool를 사용하여 아래와 같이 host: localhost, port: 27017로 하고 authentication에는 이전에 설정한 admin 계정을 넣어 줍니다.
3. MongoDB index Types
MongoDB 인덱스 종류는 다양합니다. 종류는 단일 필드 인덱스, 복합 인덱스, 다중 키 인덱스 geospatial 인덱스, test 인덱스, 해쉬 인덱스가 존재합니다. 이번 시간에는 자주 사용하는 단일 필드 인덱스와 복합 인덱스에 대해만 알아보겠습니다.
Single Field Index(단일 필드 인덱스)
하나의 필드 인덱스를 사용하는 것을 단일 필드 인덱스라고 합니다. MongoDB에는 기본적으로 컬렉션에 _id라는 단일 필드 인덱스가 생성됩니다.
단일 필드 추가 방법은 아래와 같습니다. 원하는 feild를 입력합니다. 단일 필드 인덱스에서는 1은 오름차순 -1은 내림차순을 의미합니다. 하지만 단일 필드 인덱스에서는 오름차순인지 내림차순인지 중요하지 않습니다. 왜냐하면 어떤 방향으로 가도 동일하게 접근하기 때문입니다.
> db.user.createIndex({score:1})
아래 그림은 score에 인덱스를 주어 score 크기대로 오름차순(숫자 1이 오름차순, 숫자 -1은 내림차순입니다.) 정렬된 것을 확인할 수 있습니다.
인덱스 유무 속도 비교
인덱스 생성 전과 인덱스 생성 시 속도 비교해봅니다. 아래와 같은 explain 명령어를 사용해서 실행 속도를 파악해 봅니다. 저는 아래 명령어 입력 시 764밀리 초로 0.764초 정도 나왔습니다.
>db.user.find({score:"23"}).explain("executionStats").executionStats.executionTimeMillis 764
그러면 이제 score에 대한 인덱스를 생성합니다.
> db.user.createIndex({score:1})
인덱스 생성 후, 실행 속도를 확인해 봅니다. 저는 0이라는 값이 나왔습니다. 실습을 통해 인덱스로 매우 빠른 검색이 가능하다는 것을 알 수 있었습니다.
> db.user.find({score:"56"}).explain("executionStats").executionStats.executionTimeMillis 0
Compound Index(복합 인덱스)
두 개 이상의 필드를 사용하는 인덱스를 복합 인덱스라고 부릅니다.
아래와 같이 인덱스를 생성한다면, 아래 그림과 같이 userid는 오름차순으로 정렬됩니다. 그리고 같은 userid를 지니면 score로 내림차순 정렬하게 됩니다. 예를 들면 동일한 userid 인"ca2"는 score가 내림차순으로 정렬되어 있음을 그림에서 확인할 수 있습니다.
> db.user.createIndex({userid:1 ,score:-1})
복합 인덱스를 사용할 때는 아래의 특징을 고려하며 생성합시다. 이는 MongoDB 문서에서 확인할 수 있습니다.
- 특징 1. sort 연산 시 인덱스 순서를 고려하여 생성하자.
복합 인덱스에서는 키의 순서가 매우 중요합니다. 만일 복합 인덱스에서 순서가 userid-score가 아니라 score-userid로 생성하는 것은 다른 인덱스를 생성한 것입니다.
정렬 시 인덱스 순서와 조회 시 순서가 동일해야 합니다. 예를 들어 아래와 같이 인덱스 a-b순서로 생성하면 a-b 정렬은 지원을 하지만, b-a로 정렬은 지원하지 않습니다.
- 특징 2. 단일 인덱스와 다르게 복합 인덱스는 정렬 방향을 고려하자.
검색 쿼리에서 복합 인덱스를 사용하는 경우 지정된 정렬 방향은 index와 일치해야 합니다. 예를 들어 아래와 같이 두 필드 정렬이 역으로 생성된 경우라면 역으로 조회하는 쿼리는 지원하지만, 동일한 정렬을 하는 쿼리는 지원하지 않습니다.
- 생성된 인덱스: { a: 1, b: -1 } - 지원하는 조회 쿼리: { a: 1, b: -1 } - 지원하는 조회 쿼리: { a: -1, b: 1 } - 지원하지 않는 조회 쿼리: { b: 1, a: 1 } - 지원하지 않는 조회 쿼리: { b: -1, a: -1 }
아래 예제로 실습을 진행해 봅시다. 이전에 생성한 인덱스는 drop 하고 진행해 봅니다. {userid:1, score:-1} 인덱스를 생성합니다. 순서를 동일 시한 실행은 문제없이 실행됩니다. 반면 정렬 순서를 역으로 하지 않거나 순서를 다르게 한 경우는 정상 동작하지 않습니다.
> db.user.createIndex({userid:1, score:-1}) # 실행 시 문제 없이 진행 > db.user.find({}).sort({ userid:1,score:-1}) > db.user.find({}).sort({ userid:-1,score:1}) # RAM exceeded error 발생 > db.user.find({}).sort({ userid:1,score:1}) > db.user.find({}).sort({ score:-1,userid:1})
- 특징 3. Prefixes
Index prefixes란 조회 시 왼쪽 인덱스부터 적용되는 부분집합 인덱스를 말합니다. 정의가 이해하기 어려우니 아래의 예시로 이해해 봅시다. a, b, c, d 복합 인덱스를 생성할 때 { a: 1 }~{ a:1, b: 1, c: 1, d: 1 }까지 적용됩니다. 따라서 이런 인덱스를 생성할 시 {a:1} 인덱스가 정의되어 있다면, 삭제해도 상관없습니다. 왜냐면 { a:1, b: 1, c: 1, d: 1 } 인덱스로 인해 조회가 빠르게 이뤄질 것입니다.
- 생성된 인덱스: { a:1, b: 1, c: 1, d: 1 }
아래 예시로 어떤 index prefixes 가 적용되는지 확인해 봅니다.
- 생성된 인덱스: { "item": 1, "location": 1, "stock": 1 } - 지원되는 쿼리: { item: 1 } - 지원되는 쿼리: { item: 1, location: 1 } - 지원하지 않는 조회 쿼리: "item" 필드 없이 "location" 필드만 존재 혹은 "stock" 필드만 존재 - 지원하지 않는 조회 쿼리: "item" 필드 없이 "location", "stock" 필드만 존재
- 특징 4. sort 연산은 non-prefix를 지원한다.
sort 연산은 특징 3에서 배운 prefix 조건에 맞지 않아도 지원을 합니다. 그러나 이를 만족하기 위해서는 쿼리는 equality 조건은 prefix를 포함해야 합니다. 아래 예제로 이를 이해해 봅시다.
- 생성된 인덱스: { a:1, b: 1, c: 1, d: 1 }
아래 표 예제는 위에 생성된 인덱스에 만족하는 쿼리입니다. 앞에 equality 조건은 모두 prefix를 만족합니다. 그러나 sort 부분은 prfix 조건을 만족하지 않지만 올바르게 사용된 쿼리 입니다.
하지만 아래 쿼리는 위에 생성된 인덱스에 만족하지 못합니다. 첫 번째 쿼리는 equality 조건이 존재하지 않습니다. 두 번째 쿼리는 equality 조건이 prefix를 만족하지 못합니다.
> db.data.find( { a: { $gt: 2 } } ).sort( { c: 1 } ) > db.data.find( { c: 5 } ).sort( { c: 1 } )
아래 예제로 실습을 진행해 봅시다. 이전에 생성한 인덱스는 drop 하고 진행합니다.
> db.user.createIndex({userid:1, score:1, age:1})
아래 실습은 쿼리에 .explain("executionStats").executionStats.executionTimeMillis 를 넣어 실행 시간을 파악하였습니다. "userid" 없이 "score" 필드를 조건에 넣은 표 두 번째 쿼리는 실행 시간을 통해 적용이 안 된 것임을 알 수 있습니다. 또한, 6번째 쿼리도 prefix를 지키지 않아 적용이 안 됨을 알 수 있습니다.
쿼리 인덱스 생성 전 실행 시간(밀리 초) 인덱스 생성 후 실행 시간(밀리 초) 인덱스 적용 유무 db.user.find({userid:20300}) 922 1 적용 o db.user.find({score:53}) 1264 1109 적용 x db.user.find({userid:{gt:3333}}) 965 0 적용 o db.user.find({userid:11111}).sort({score:1}) 1090 0 적용 o db.user.find({userid:1222,score:{$gt:22}}) 1010 0 적용 o db.user.find({score:{$gt:22},age:22}) 1142 1082 적용 x db.user.find({userid:2222, score:{$gt:22},age:22}) 1131 0 적용 o - 특징 5. Index Intersection
인덱스 교차란 별개의 인덱스가 교차해서 쿼리에 자동으로 적용되는 것입니다. 예를 들어 아래 두 인덱스가 개별 생성될 시 아래의 쿼리는 인덱스 교차로 인해 자동으로 인덱스가 적용됩니다.
- 인덱스 1: { qty: 1 } - 인덱스 2: { item: 1 } > db.orders.find( { item: "abc123", qty: { $gt: 15 } } )
인덱스 교차로 동작했는지 확인하기 위해 explain() 명령어를 사용합니다. 해당 명령어 사용 시 AND_SORTED 나 AND_HASH 가 발견되면 인덱스 교차로 동작된 것입니다. 해당 특징에 대한 자세한 부분은 mongodb 문서에서 확인해 봅니다.
4. index 측정하기
생성한 index가 문제가 없는지 확인하기 위해서 index를 측정해야 합니다. 아래 3가지 명령어로 이를 확인하고 문제 있는 index는 수정하도록 합시다.
$indexStats: index 통계
각 인덱스에 대한 통계를 리턴합니다. 자세한 내용은 문서에서 확인 가능합니다.
우선 find 명령어로 score 인덱스를 한 번 사용합니다. 그리고 indexStats 명령어를 사용합니다. 결과 중 accesses 필드를 확인하면 since 시간 이후로 몇 번 해당 index를 사용했는지 확인할 수 있습니다. score_1 인덱스는 ops를 보면 1번 사용된 것으로 결과를 확인할 수 있습니다.
> db.user.createIndex({score:1}) > db.user.find({score:53}) > db.user.aggregate( [ { $indexStats: { } } ] ) { "name": "score_1", "key": { "score": 1.00 }, "host": "fc24a7b17b49:27017", "accesses": { "ops": NumberLong(1), "since": ISODate("2020-06-07T01:22:50.177+09:00") }, "spec": { "v": NumberInt(2), "key": { "score": 1.00 }, "name": "score_1", "ns": "student.user" } } { "name": "_id_", "key": { "_id": NumberInt(1) }, "host": "fc24a7b17b49:27017", "accesses": { "ops": NumberLong(0), "since": ISODate("2020-06-06T22:00:45.592+09:00") }, "spec": { "v": NumberInt(2), "key": { "_id": NumberInt(1) }, "name": "_id_", "ns": "student.user" } }
explain(): 특정 퀴리의 수행 통계
이는 특정 쿼리 수행 통계를 알 수 있습니다. 자세한 내용은 문서에서 확인 가능합니다.
- queryPlanner: 가장 효율적인 쿼리를 찾기 위해 쿼리 최적화를 제공합니다.
- executionStats mode: 특정 질의에서 실제로 실행한 결과의 세부사항을 제공합니다.
- allPlansExecution mode: queryPlanner + executionStats 내용 모두 포함
hint(): 특정 인덱스 테스트
이는 테스트하고 싶은 특정 인덱스가 원하는 쿼리에 효과 있는지 알아보기 위해 사용합니다.
자세한 내용은 문서에서 확인 가능합니다.
아래 예제는 score 조건으로 조회하지만 _id 인덱스를 사용하는 경우를 hint()로 테스트하며 explain으로 총 시간을 확인하는 명령어입니다.
> db.user.find({score:53}).hint({_id:1}).explain("executionStats").executionStats.executionTimeMillis 2367
5. index 사용 시 주의점
-
일반적으로 컬렉션에 2~3개 이상의 인덱스를 가지지 않는 것이 좋습니다.
- 이전에 말했듯이 모든 쓰기(읽기, 갱신, 삭제) 작업은 인덱스 때문에 더 오래 걸립니다. 그 이유는 데이터 변경될 시 인덱스의 순서도 재구성해야 합니다.
- 인덱스 구축이 완료될 때까지 데이터베이스의 모든 read/write 작업은 중단하면서 인덱스를 구축하게 됩니다. 따라서 background 옵션을 이용하여 인덱스를 구축하면서 다른 작업들도 가능하게 합니다.
- 하지만 포그라운드 인덱싱보다는 작업이 더 오래 걸립니다.
- 4.2 버전에서 이 옵션은 Deprecated 상태이고, 지정되어도 해당 옵션을 무시한다고 하니, 4.2 이전 버전에서만 사용할 것을 권장합니다.
> db.[컬렉션 명].ensureindex({"필드":1},{background:true})
6. 마무리
인덱스는 검색을 빠르게 하기 위해서 필요하고 학습해야 할 부분입니다. 프로젝트에 쿼리 성능이 문제가 없도록 하기 위해 index 측정 명령어로 꾸준하게 측정하며 많은 sample 데이터로 index를 생성하고 실습하는 것이 중요하다고 생각됩니다. 😃
'개발 > MongoDB' 카테고리의 다른 글
MongoDB의 기본 개념에 대해 알아보자. (1) 2020.06.21