일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- console.log
- node.js설치
- mongo-native
- [Object]
- Installation
- node.js 연동
- Projection
- 파이썬3
- nodejs mongodb
- Windows10
- query
- util.inspect
- MYSQL
- mongoDB [Object]
- homebrew
- mongodb
- pip jupyter
- mongodb nodejs driver
- python3
- 맥
- collection.find
- MacOS
- Node.js
- Jupyter notebook
- 맥에 파이썬 설치
- Today
- Total
Bon Voyage
'MongoDB in Action'으로 정리해보는 MongoDB의 인덱스 개념 본문
MongoDB in Action https://book.naver.com/bookdb/book_detail.nhn?bid=6876243
8장 인덱스 파트를 요약 정리한 내용입니다.
인덱스 이론
1. 인덱싱 규칙
- 도큐먼트를 가져오기 위해 필요한 작업량을 많이 줄인다 → 없으면 컬렉션 전체 스캔해야 함
- 한 쿼리를 실행할 때 하나의 단일 키 인덱스만 사용 가능 → 여러개는 복합 인덱스로
- a-b의 복합 인덱스에서는 a에 대한 단일 인덱스의 존재는 중복이나 b에 대한 인덱스는 중복이 아님
- 복합 인덱스에서는 키의 순서가 중요함
2. 단순 인덱스Simple Index
하나의 필드 키를 사용하는 인덱스
하나의 쿼리에서 단일 키 인덱스를 두 개 사용하면, 쿼리 옵티마이저가 그 중 제일 효율적인 인덱스를 선택함
→ 그러나 그 결과가 항상 최선인 것은 아니다.
3. 복합 인덱스 Compound Index
하나 이상의 필드를 키로 사용하는 인덱스
복합 인덱스에서는 순서가 중요함:
— 첫 번째로 오는 인덱싱 조건은 '일치Equality' 이어야 하고
— 범위를 지정한 키는 그 다음에 와야 한다
3-1. 참고: 인덱스 교차 Index Intersection
— 각각의 필드에 대한 인덱스에서 검색어와 일치하는 페이지 번호를 찾은 후, 페이지 번호의 교차 지점 리스트를 스캔하며 두 검색어와 일치하는 도큐먼트를 찾는다
— MongoDB에서 교차점 계산하는 방식은 지원하지 않음
4. 인덱스 효율
- 각 인덱스는 유지 비용이 든다:
도큐먼트 CUD할 때마다 생성된 인덱스도 새로운 도큐먼트를 포함하도록 수정해야 함
→ 읽기 위주의 App에서 인덱스 비용은 상쇄할 수 있지만, 불필요한 인덱스는 없어야 함 - 인덱스 & 작업 중인 데이터를 RAM에서 다 처리하지 못하는 경우,
모든 인덱스가 적합하더라도 쿼리 처리 성능이 저하될 수 있음- WiredTiger 스토리지 엔진에서는 모든 도큐먼트, 컬렉션, 인덱스 포함 데이터 파일이
페이지page라는 4KB의 청크로 OS에 의해 RAM으로 스왑된다 - 해당 페이지의 데이터가 요청될 때마다 OS는 그 페이지가 RAM에 있는지 확인한다
— CUD 연산은 RAM에서 발생하고 수정사항은 OS가 비동지적으로 Disk에 반영한다 - 없다면 페이지 폴트 Page Fault 예외를 발생시키고, 메모리 관리자가 Disk로부터 페이지를 RAM으로 불러들인다
- RAM이 충분하면 필요한 모든 데이터파일이 메모리에 로드된다
- 데이터를 다 수용하지 못하면 점점 페이지 폴트가 많이 발생하게 되고
OS가 Disk를 액세스하는 횟수가 많아지면서 CRUD연산이 매우 느려지게 된다
최악의 경우 Thrashing이 발생한다:
데이터 크기가 가용한 RAM 크기보다 훨씬 커서 모든 CRUD에 대해 Disk Access가 필요해지는 상황이 발생할 수도 있음
- WiredTiger 스토리지 엔진에서는 모든 도큐먼트, 컬렉션, 인덱스 포함 데이터 파일이
- 위와 같은 이유로, 정말 필요한 인덱스만 유지해야 할 필요성이 생긴다
- 자주 발생하는 쿼리에 대한 커버링 인덱스 Covering Index를 생성하여 유용하게 사용 가능
- 인덱스는 인덱스에 포함된 데이터와 함께 RAM에 별도로 저장되며 Clustered되지 않는다
MongoDB에서는 클러스터 인덱스를 사용하지 않는다
(인덱스 순서대로 데이터 순서를 정렬하지 않는다)\ 인덱스 키의 순서는 데이터의 실제 순서와 관계가 없다:
모든 도큐먼트가 데이터 세트의 어느 위치에나 있을 수 있다, 지역성Locality을 보장하지 않는다\
5. B-트리 (Balanced Tree)
70년대 후반부터 DB의 레코드, 인덱스로 자주 이용되고 있는 B-Tree를 MongoDB도 인덱스 생성에 사용함
특징
- 다양한 쿼리를 용이하게 함:
일치 조건, 범위 조건, 정렬, Prefix 일치, Index만의 쿼리 등 - 키가 추가되거나 삭제되더라도 Balanced 상태를 계속 유지한다
- 각 키는 인덱스된 필드값을 나타내는 BSON 객체 형태 (Binary JSON)
- 각 노드에 포함된 키는 두개의 포인터를 포함하고 있음:
1) 자신이 속한 데이터 파일의 포인터
2) 자식 노드에 대한 포인터 - 노드 자체는 노드의 최솟값보다 작은 값을 갖는 노드를 가리키고 있다
인덱스 크기
-
MongoDB에서 구현된 B-Tree는 새 노드에 8KB(8192Byte)를 할당함
-
(노드 할당크기 - 노드 당 오버헤드) / (키의 크기 - 키 당 오버헤드) = 노드 당 가질 수 있는 키의 개수
e.g. 키의 평균적인 크기가 30Byte 안팎이라면, (8192 - 40) / (30 - 18) = 169.8
(노드 당 대략 170개의 키를 가질 수 있음) -
B-Tree 노드는 일반적으로 60%만을 사용하도록 의도적으로 기본 설정되어 있다
MongoDB에 실제 구현된 인덱스
1. 인덱스 타입
1. 고유 인덱스 Unique Index
db.collection.createIndex({username: 1}, {unique: true})
- 해당 도큐먼트에서 인덱스 필드 값의 고유한지 확인함
실제로 _id 필드가 고유 기본 키라는 것을 보장하기 위해 MongoDB가 사용하고 있음 - 인덱스의 모든 엔트리가 고유해야 함
이미 존재하는 키 값을 삽입하려고 하면 예외 발생, 삽입이 실패함 - 컬렉션에 데이터가 존재하지 않을 때 생성하는 것이 좋음: 처음부터 고유 제약 조건을 보장함
- 기존에 있는 컬렉션에 생성해야 할 때는
1) 반복적으로 수행하면서 중복되는 키 삭제
2)dropDups: true
옵션으로 중복 키 자동 임의 삭제
3) 새 컬렉션을 만들고, 고유 인덱스를 만든 다음, 도큐먼트를 복사하면서 중복 데이터 삭제
2. 희소 인덱스 Sparse Index
db.collection.createIndex({user_id: 1}, {sparse: true, unique: true})
- 인덱스는 기본 설정이 밀집 dense
→ 컬랙션 내 한 도큐먼트가 인덱스 키 필드 값을 가지고 있지 않으면 null 값을 갖는 엔트리로 처리함
문제점
모든 도큐먼트가 다 가지고 있지 않은 필드에 고유 인덱스를 생성하게 되면,
첫 번째 삽입은 null로 가능하지만 그 다음부터는 null 자체가 중복이라고 생각해서 삽입 연산 실패함 - 희소 인덱스는 Sparse Index
→ 옵션sparse: true
: 인덱스의 키가 null이 아닌 값을 가지고 있는 도큐먼트만 존재하게 함 - 많은 도큐먼트가 인덱스 키를 가지고 있지 않은데 키를 가진 대상으로 질의를 하는 경우
e.g. 익명 상품평이 가능한 사이트에서 회원의 상품평을 대상으로 질의가 필요한 경우
3. 다중키 인덱스 Multikey Index
- 여러 개의 엔트리가 동일한 도큐먼트를 지시함
e.g. 한 도큐먼트가 여러 개의 태그를 가지고 있을 때 - 배열을 가지는 필드에 인덱스를 생성하면: 배열의 어느 값으로도 다 해당 도큐먼트를 찾을 수 있다
- 몇가지 예외를 제외하면 MongoDB에서 항상 사용할 수 있는 상태
- 인덱스 필드가 배열을 포함하면, 배열의 각 값은 인덱스 내에서 각자 자신만의 엔트리를 가진다
- 물론 CUD는 더 많은 비용이 든다
4. 해시 인덱스 Hashed Index
db.collection.createIndex({recipe_name: 'hased'})
- 해시 값이 도큐먼트 엔트리 순서를 결정함: 이름 순으로 서로 가까이 있지 않을 가능성이 있음
- Indexing 값은 원본 필드 값의 해시값이 됨
제한 사항
- 동등 쿼리 Equality Queries는 거의 동일하게 작동하지만, 범위로 쿼리를 처리할 수는 없다
- 다중 키 해시 인덱스가 허용되지 않음
- 부동 소수점 값은 해싱 전에 정수로 변환되어, 예를 들어 4.2와 4.4는 같은 인덱스 해시 값을 가지게 됨
그럼 왜 쓰느냐?
- 해시 인덱스의 엔트리가 균등하게 분배되기 때문
- 키 데이터가 균일하지 않게 분포할 경우, 해시 인덱스 값은 일관성을 유지한다
- _id의 가장 중요한 비트는 생성 시간을 기반으로 하는데,
_id를 사용하는 인덱스로 샤드를 결정하게 되면 동 시간대에 대량의 쓰기 load가 생기면 문제 발생 가능성 - 샤딩과 연관된 개념이므로 일단 간단하게 말하면 해시된 인덱스 키 값은 인덱스 엔트리의 지역성을 변경한다는 뜻, Sharded Collection에서 유용하게 사용될 수 있다
5. 지리공간적 인덱스 Geospatial Index
- 저장된 위도값, 경도값에 따라 도큐먼트를 특정 위치에 '가까이' 배치하는 것이다
- 지구의 곡률을 포함해 지리적 거리르 효율적으로 계산할 수 있는 인덱스
2. 인덱스 관리
1. 생성 및 삭제
생성createIndex()
헬퍼 메서드를 직접 구현 가능: ns, key, name을 system.indexes 컬렉션에 넣으면 됨
ns: namespace, key: index field, name: index name
use db
idx = {ns: 'db.collection', key: {'field_name': 1}, name: 'index_name'}
db.system.indexes.insert(idx, true)
//생성된 인덱스 확인
db.system.indexes.find().pretty()
삭제
system.indexes에서 인덱스 도큐먼트를 직접 삭제하는 연산은 금지되어 있음
deleteIndexes
로 삭제 가능함runCommand
사용
db.runCommand({deleteIndexes: 'collection_name', index:'idx_name'})
dropIndex()
db.collection.dropIndex('idx_name')
인덱스 확인
db.collection.getIndexes()
2. 인덱스 구축
데이터가 먼저 삽입되고 난 후에 인덱스를 구축하게 되는 경우
- DB를 MongoDB로 Migration할 때: 전송해 온 이후에 인덱스를 생성하면 균형&압축된 인덱스 생성 가능
- 새로운 쿼리에 인덱스를 최적화해야 할 때: 기능 추가 혹은 변경 시
대량의 데이터가 있는 경우, 인덱스를 생성하는데 몇시간 ~ 며칠까지도 걸릴 수 있다
진행 상황은 MongoDB 서버 로그를 확인해 보거나, db.currentOp()
로도 확인 가능함
인덱스 구축 단계
- 인덱스할 필드 값을 정렬함 → B-Tree에 추가
정렬의 진턱도는 도큐먼트의 총 수 대비 정렬된 도큐먼트의 개수로 나타난다 - 정렬된 값들을 인덱스로 삽입한다
인덱스 구축 완료까지 소요된 시간을 system.indexes 컬렉션에 삽입한다
인덱스 구축 시 문제점
인덱스 생성 시, 해당 데이터 베이스가 쓰기 Lock이 걸려 읽기/쓰기 모두가 불가능하다
해결책
- 백그라운드 인덱싱
백그라운드에서 인덱스 구축하면, 다른 읽기/쓰기 요청 시 잠시 구축을 멈춘다db.collection.createIndex({'idx_field': 1}, {background: true})
트래픽이 최소화되는 시간에 인덱스를 구축한다면 좋은 방식 - 오프라인 인덱싱
1) 하나의 복제 노드를 오프라인으로 바꾸고, 인덱스 구축 후, 마스터에서 그 동안의 업데이트를 받아 완료함
2) 새로운 인덱스로 업데이트 된 노드를 프라이머리로 변경하고, 다른 복제 노드들도 오프라인 인덱스 구축
복제 oplog가 충분히 크다는 가정을 전제로 사용하는 방법 - 백업
인덱스 구축은 어려우므로 백업이 필요함, but 모든 백업이 인덱스 데이터까지 다 포함하지는 않음
→ MongoDB 데이터 파일 자체를 백업해야 함 - Defragmentation 비단편화
다량의 데이터 CUD로 인덱스의 단편화가 심해졌을 때: B-Tree가 스스로 재구성할 수준을 넘게 될 때
주로 데이터 크기보다 인덱스의 크기가 훨씬 더 클 때 → 인덱스가 RAM을 필요 이상으로 차지함//해당 컬렉션의 모든 인덱스를 재구축함 db.collection.reIndex()
- 마찬가지로 재구축 되는동안 DB는 Lock이 걸림 → 오프라인 방식이 최적의 활용법
3. 쿼리 최적화
느린 쿼리를 찾아 원인을 발견하고 속도를 개선하려는 조치를 취하는 과정
자세히는 쿼리를 재구성하고 더 유용한 인덱스를 구축하는 과정
1. 느린 쿼리 탐지
느려졌다고 느끼면 쿼리 프로파일이 필요함
1. MongoDB 서버 로그 확인하기
- 일반적으로 쿼리가 100밀리초 내에 실행되면 안전: Mongo 로거는 100밀리초를 넘는 쿼리에 경고 메세지
→ 느려질 땐 가장 먼저 로그를 확인하면 됨
//시간으로만 찾아볼 수 있음 grep -E '[0-9]+ms' mongod.log
- 100밀리초가 임계값으로 너무 높으면 MongoDB 시작 시
--slowms 50
등의 서버 옵션으로 시작 - 절차가 정교하지는 않으므로 로그 확인 절차는 일종의 점검 장치로만 생각함
2. 프로파일러 사용
- 디폴트로 사용 불가능 상태라서
db.setProfilingLeve(2)
으로 바꿀 수 있음
(2: 프로파일러 가 모든 읽기/쓰기 로그에 기록, 1: 100ms 이상 느린 쿼리만 기록, 0: 사용 불가능)db.setProfilingLevel(1,50)
과 같은 식으로 임계치를 지정할 수 있음
2-2. 사용 전략
- 적당히 높은 임계치에서 점점 수치를 낮춰가며 수행한다
e.g. 100보다 더 오래 걸리는 쿼리가 없는지 확인한 후 75로 낮춰서 확인하는 식 - App의 실제 서비스 상황에서도 작동하기 위해, App의 모든 쿼리를 테스트해야 함
- 실제 서비스 환경과 동일한 조건 하에서 테스트를 수행해야 함
3. 프로파일링 결과로 확인
- 프로파일링 결과는 해당 DB의 system.profile 컬렉션에서 확인할 수 있음
system.profile은 캡드capped 컬렉션
→ 고정된(허용된) 크기(128KB)에 도달하면 가장 오래된 도큐먼트부터 Overwrite
//150 밀리초 이상 소요된 쿼리를 확인한다 db.system.profile.find({millis: {$gt: 150}})
2. 느린 쿼리 분석
이유가 무엇일까?EXPLAIN()
의 사용과 이해
- 쿼리 뒤에 .explain()을 추가하기만 하면 됨
cursor
field: 스캔 방법
BasicCursor → 컬렉션 전체 스캔
BtreeCursor → 인덱스를 사용함scanAndOrder
필트값이 true라면 쿼리 프로세서가 스캔하고 정렬까지 다 처리해야 한다는 말n
필드와nscanned
필드가 가능한 비슷한 값을 가져야 이상적이다- MongoDB 2버전에서 쿼리 최적화는 일반적으로
nscanned
를 가능한 줄이는 것을 뜻함 - MongoDB 3버전에서는
nReturned
가 반환된 도큐먼트,totalDocsExamined
값이 낮을 수록 좋음
MongoDB 쿼리 옵티마이저
해당 쿼리를 가장 효율적으로 실행하기 위해 어떤 인덱스를 사용할 지 결정하는 소프트웨어
- 이상적인 인덱스를 선택하기 위한 규칙
- scanAndOrder를 false로: 쿼리가 정렬을 포함하고 있으면 인덱스를 통해 정렬하도록 시도
- 쿼리 셀렉터 (질의 조건)에 지정된 필드는 최대한 인덱스를 생성하여 사용하도록
- 쿼리가 범위나 정렬을 내포하면, 마지막 키에 대헤 범위/정렬에 도움이 되는 인덱스를 선택해야 한다
- 쿼리 옵티마이저의 인덱스 선택과정
- 쿼리가 최초로 실행될 때 각 인덱스에 대한 쿼리 수행 플랜을 생성함
- 병렬로 각 플랜을 실행한다
- 완료 후에
nscanned
가 최솟값을 갖는 플랜을 선택
(드물긴 하지만 전체 컬렉션 스캔을 선택할 때도 있음) - 실행 중인 다른 플랜을 중지시키고, 선택된 플랜은 이후 사용을 위해 저장함
쿼리 플랜과 HINT()
사용하기
.explain(true)
를 사용하면 쿼리 수행 플랜을 모두 확인할 수 있다
→ true: allPlansExecution / false: queryPlanner / +) executionStats도 있음- 사용되지 않은 플랜들은 MongoDB 3버전에서 rejectedPlans라고 함
.hint({'idx_field': 1})
로 nscanned를 확인해서 왜 rejectedPlans인지 확인이 가능함
→ 쿼리 옵티마이저에게 idx_field를 인덱스로 사용하라고 명령함
쿼리 플랜 캐시
옵티마이저가 모든 쿼리마다 이 모든 플랜을 병렬로 실행하는 과정을 거치는 것은 비효율적
- 성공적인 플랜 발견시, 쿼리 패턴, nscanned 값, 인덱스 규칙이 기록됨
{ pattern: { field_1: 'equality'. //일치 조건 field_2: 'bound', //범위 조건 index: { 'idx_field_name': 1 }, nscanned: ### //성공했을 때의 스캔된 도큐먼트 갯수 } }
- 해당 패턴과 일치할 때마다 해당 인덱스를 사용하게 될 것임
- 다른 인덱스가 더 효율적인 것으로 판명나게 되면 즉각 그 플랜을 이용한다
쿼리 플랜 캐시 소멸
- 컬렉션에 대해 100회의 쓰기가 실행 됐을 때
- 컬렉션에 인덱스가 추가되거나 삭제 됐을 때
- 쿼리 플랜 사용 쿼리가, 캐시 플랜에서의 nscanned값의 최소 10배를 능가하는 값을 표시할 때
3. 쿼리 패턴
1. 단일 키 인덱스의 사용
- 정확한 일치 (완전 일치)
- 정렬: 쿼리 셀렉터 없이 정렬할 거라면, 컬렉션 전체가 아니라면 제한을 설정하는 것이 좋음
- 범위 → 해당 범위를 정렬하려면 동일한 인덱스로 정렬 사용
2. 복합 키 인덱스
중요한 점: 쿼리 당 하나의 범위나 정렬을 할 때만 효율적이다
- 정확한 일치
- 범위 일치
1) 정확한 일치를 위한 키가 오고 (필요하다면)
2) 범위/정렬이 뒤따른다
3. 커버링 인덱스 Covering Index
인덱스를 사용하는 방식 중 하나,
질의로 찾으려는 데이터가 모두 인덱스 필드 값 내에 있을 때 = "인덱스가 쿼리를 커버한다"
Covered Index Query는 인덱스 키가 가리키는 도큐먼트를 참고하지 않아도 되므로,
인덱스만의 쿼리index-only query라고도 부르고 쿼리 성능이 향상될 수 있다
사용법:
인덱스에서 반환할 필드를 선택하고 _id 필드를 제외하면 된다 (_id는 인덱스의 일부가 아니기 때문)
db.collection.find({field_to_find: 1}, {field_in_idxfield: 1, _id: 0})
전체 요약
- 쿼리 로그 확인, 쿼리 프로파일링,
explain()
으로 쿼리를 최적화하자 - 인덱스는 검색에는 유용해도, CUD가 느려지는 비용이 있다
- 일반적으로 쿼리에서 하나의 인덱스만 사용한다 → 여러 필드의 쿼리는 복합 인덱스를 활용
- 트래픽이나 데이터가 너무 많아지기 전에 쿼리를 최적하고, 조기에 인덱스를 생성하자
- 최적화는 스캔한 도큐먼트 수를 줄임으로써 가능하다.
explain()
로nscanned
를 확인하자
'개념 공부 > 데이터베이스' 카테고리의 다른 글
'MongoDB in Action'로 정리해보는 플러그형 스토리지 엔진 (0) | 2019.09.30 |
---|---|
'MongoDB in Action'으로 정리해보는 텍스트 검색 인덱스 (0) | 2019.09.30 |
'이것이 MySQL이다'로 정리해보는 인덱스 개념 (0) | 2019.09.28 |
Node.js MongoDB 드라이버 : collection.find() option 사용법 (0) | 2019.07.18 |
MongoDB Node.js 드라이버, 간단하게 시작해보기 (0) | 2019.07.17 |