본문 바로가기

DB/MongoDB

[MongoDB] 18. 데이터 모델링과 인덱스 - 인덱스

728x90
반응형

인덱싱

  • 신문 기사를 노트에 스크랩할 때, 기사가 많아지면 원하는 기사를 쉽게 찾는 방법이 필요
  • 노트의 맨 앞에서, 기사 제목과 페이지(가나다순)을 적은 "색인"을 만듬
  • 제목을 모르고 기사 종류만 알고 있다면 색인을 사용하기 어려움
  • 분야와 제목을 같이 색인에 활용하고 싶을 때는 "분야-기사제목-페이지 번호"로 색인을 만들면 됨.
  • 많은 데이터를 분류하기 위해 색인을 만들 때는 어떤 순서로 만드느냐가 중요
  • 아래 단계로 내려갈수록 범위가 좁아지는 것이 유용

인덱스의 특징

  • 인덱스가 없다면 모든 도큐먼트를 조회해야함
    • 인덱스는 쿼리를 효율적으로 수행하게 함
  • 새로운 도큐먼트가 생성/제거가 되면 인덱스도 함께 수정되어야 함.
    • 속도의 저하 발생 가능성
  • 단순 인덱스를 만들면 해당 필드를 조회할 때만 사용 가능
  • 다수의 필드를 대상으로 조회 시 복합 인덱스가 필요함
    • 순서가 중요

몽고디비 인덱스의 구조 (B-tree)

MongoDB의 인덱스 구조(B-tree) 출처 : https://rastalion.me/mongodb-index-1-architecture/

B-tree 인덱스의 특징

  • 각 리프노드가 동일한 깊이에 있기 때문에 균등한 응답 속도를 보장
    • 하나의 컬렉션 내에서 모든 도큐먼트가 3~4개의 I/O 이상으로 떨어져 있지 않음
  • B-Tree는 깊이가 최대 4(헤더 블록 1개, 브랜치 블록 2개, 리프 블록 1개)이므로 대규모 컬렉션에 대해 우수한 성능을 제공
    • 일반적으로 문서를 찾는데 4번 이상의 I/O가 필요하지 않음
  • B-Tree 인덱스는 정확하고 빠른 조회 뿐만 아니라 range(범위) 스캔 쿼리를 지원
    • 이전 리프 블록과 다음 리프 블록에 대한 링크 덕분에 가능

B-Tree

  • 모든 리프노드들이 같은 레벨을 가질 수 있도록 자동으로 벨런스를 맞추는 트리
  • 정렬된 순서 보장
  • 멀티 레벨 인덱싱을 통한 빠른 검색 기능

검색 과정 - 다음의 B-tree에서 18 찾기

  • 루트 노드에서 시작하여 하향식으로 검색
  • 검색하고자 하는 key를 k라고 할 때 과정18 검색 시작, 루트노드의 key를 순회하면서 검색 시작

  • 1. 18 검색 시작, 루트노드의 key를 순회하면서 검색 시작

  • 2. 18은 10보다 크기 때문에 다음 key를 검색

  • 3. 18은 10보다 크고, 20보다 작기 때문에 10과 20사이의 자식 노드로 이동

  • 4. 자식노드에서 다시 검색 시작, 18은 14보다 크기 때문에 다음 key를 검색

  • 5. 18은 노드의 가장 마지막 key인 17보다 크기 때문에 노드의 가장 마지막 자식 노드로 이동

  • 6. 18 검색 완료

 

삽입 과정 - Case1. 분할이 일어나지 않는 경우

  • 리프노드가 가득 차지 않았다면, 오름차순으로 k를 삽입
  • 9 삽입하기

  • 1. 9삽입 시작, 루트노드에서 key 10이 9보다 크기 때문에 가장 왼쪽 자식 노드로 이동

  • 2. 9는 5보다 크기 때문에 가장 오른쪽 자식노드로 이동

  • 3. 리프노드에 도달, 9는 7보다 크기 때문에 7 오른쪽에 9 삽입 완료

 

삽입 과정 - Case2. 분할이 일어나는 경우

  • 만일 리프노드에 key 노드가 가득 찬 경우, 노드를 분할 해야함
  • 1. 오름차순으로 요소를 삽입, 노드가 담을 수 있는 최대 key 개수를 초과
  • 2. 중앙값에서 분할을 수행
    • 중앙값은 부모 노드로 병합하거나 새로 생성
    • 왼쪽 key들은 왼쪽 자식으로, 오른쪽 키들은 오른쪽 자식으로 분할
  • 3. 부모 노드를 검사해서 또 다시 가득 찼다면, 다시 부모 노드에서 위의 과정을 반복

13 삽입하기

  • 1. 13 삽입 시작, 루트노드의 key 10보다 크고 20보다 작기 때문에 중간 자식 노드로 이동

  • 2. 13은 14보다 작기 때문에, 가장 왼쪽 자식 노드로 이동

  • 3. 13은 12보다 크기 때문에 가장 마지막에 삽입
    • 해당 노드가 최대로 가질 수 있는 key의 개수를 초과 했기 때문에 분할 수행

  • 4. 중앙값 12를 기준으로 분할 수행

  • 5. 12는 부모노드에 오름차순 삽입, 11과 13은 12의 왼쪽 자식, 오른쪽 자식으로 설정

  • 6. 12가 병합된 노드가 최대로 가질 수 있는 key의 개수를 초과, 중앙값 14를 기준으로 분할 수행

  • 7. 14는 부모노드에 오름차순 삽입, 12와 17은 14의 왼쪽 오른쪽 자식으로 설정

  • 8. 14가 병합된 노드가 최대로 가질 수 있는 key의 개수를 초과, 중앙값 14를 기준으로 분할 수행

  • 9. 14가 새로운 루트노드가 되고, 10과 20은 14의 왼쪽, 오른쪽 자식으로 설정, 삽입 과정 완료

삭제 과정 - Case1. 삭제할 key k가 리프에 있는 경우

다른 노드에 영향 없이 해당 k를 삭제

 

Case1-1 : 현재 노드의 key 개수가 최소 key 개수보다 큰 경우

12 삭제

  • 12 삭제, 해당 노드의 key 개수는 최소 key 개수보다 크므로 바로 삭제

Case1-2 : 왼쪽 또는 오른쪽 형제 노드의 key가 최소 key 개수 이상일 경우

  • 부모 key 값으로 k를 대체
  • 최소키 개수 이상의 키를 가진 형제 노드가 
    • 왼쪽 : 가장 큰값을 부모 key로 대체
    • 오른쪽 : 가장 작은값을 부모 key로 대체

15 삭제

  • 15 삭제, 해당 노드를 부모 key로 대체, 왼쪽 형제 노드에서 가장 큰 값을 부모 key로 대체

Case1-3 : 왼쪽, 오른쪽 형제 노드의 key가 최소 key 개수, 부모 노드의 key가 최소개수 이상일 경우

  • k를 삭제한 후, 부모 key를 형제 노드와 병합
  • 부모노드의 key 개수를 하나 줄이고, 자식 수 역시 하나를 줄여 B-Tree를 유지

18삭제

 

  • 부모 key를 내려 형제 노드에 병합

Case1-4 : 자신과 형제, 부모의 노드의 key 개수가 모두 최소 key 개수라면

  • 부모노드를 루트노드로 한 부분 트리의 높이가 줄어들 경우
    • 재구조화의 과정이 일어남 Case3 2번 과정으로 이동

 

삭제 과정 - Case2. 삭제할 key k가 내부 노드에 있고, 노드나 자식에 키가 최소 키수 보다 많을 경우

  • 현재 노드의 "노드의 왼쪽 자손에서 가장 큰 key" 또는 "노드의 오른쪽 자손에서 가장 작은 key"와 k의 자리를 바꿈
  • 리프노드의 k를 삭제하게 되면, 리프노드가 삭제 되었을 때의 조건으로 변함
  • 삭제한 리프노드에 대해서 case 1 조건으로 이동

10 삭제

리프노드가 삭제되었을 때의 조건으로 변환

  • 10 삭제, "노드의 왼쪽 자손에서 가장 큰 key"인 18을 찾아 서로의 key 교환

 

삭제 과정 - Case3. 삭제할 key k가 내부 노드에 있고, 노드에 key 개수가 최소 key 개수 만큼, 노드의 자식 key 개수도 모두 최소 key 개수 인경우

삭제할 key k가 있는 노드도 최소, 자식 노드들도 최소의 key 개수를 가지므로 k를 삭제하면 트리의 높이가 줄어 재구조화 가 일어남

재구조화 과정

  • 1. k를 삭제하고, k의 양쪽 자식을 병합하여 하나의 노드로 만듬
  • 2. k의 부모 key를 인접한 형제 노드에 붙임, 이후 이전에 병합했던 노드를 자식 노드로 설정
  • 3. 해당 과정을 수행하였을 때 부모노드의 개수에 따라 이후 수행 과정이 달라짐
    • 3-1. 만일 새로 구성된 인접 형제노드의 key가 최대 key 개수를 넘어갔다면, 삽입 연산의 노드 분할 과정을 수행
    • 3-2. 만일 인접 형제 노드가 새로 구성되더라도 원래 k의 부모 노드가 최소 key의 개수보다 작아지면, 부모 노드에 대하여 2번 과정부터 다시 수행

Case 3-1) 14 삭제, 양쪽 자식 노드를 병합하고, 14는 삭제

Case 3-2) 부모 key인 10을 형제 노드에 붙이고, 이전에 병합한 자식 노드를 왼쪽 자식으로 가져옴

Case 3-3-1) 최대 key 수를 넘어간 노드가 생겼기 때문에 노드분할 수행

Case 3-3-2) 새로운 트리에서 예시, 17 삭제 시, 연산을 수행하다 보면 17의 부모노드의 key가 최소 key 개수 이하로 내려감

17의 부모노드를 기준으로 하여 Case 3과정을 다시한번 수행

 

WiredTiger의 인덱스 정책

  • 인덱스 키 엔트리에 도큐먼트마다 고유의 식별자를 할당하며 Record-Id로 부여
  • 이 때 도큐먼트의 고유 식별자는 Auto-Increment 방식을 사용
  • Record-Id는 64bit의 Int(정수 타입)을 사용하며 컬렉션 단위로 별도의 자동 증가값을 사용
  • WiredTiger 스토리지 엔진의 Record-Id는 나중에 도큐먼트의 크기가 증가하여도 크게 부하가 없고, 데이터 파일 내에서 위치가 이동하더라도 처음 할당된 논리적인 주소 값은 변하지 않고 계속 유지
  • 내부적인 Record-Id 값을 인덱스 키로 가지는 내부 인덱스가 존재, 이 인덱스는 사용자가 임의적으로 변경하거나 사용할 수 있는 인덱스는 아님

인덱스에 사용된 필드 수에 따른 인덱스 종류

단일키 인덱스

  • 한 가지 종류의 인덱스 키 값을 갖는 방식
  • _id 필드는 대표적인 단일키 인덱스

복합키 인덱스

  • 여러 개의 필드로 정렬이 필요할 때, 여러 개의 필드 값들을 인덱스로 사용
  • (점수, 제목)을 인덱스로 사용한다면 점수에 대한 정렬 방법(오름차순)과 제목(내림차순)에 대한 정렬 방법을 설

값의 형태에 따른 인덱스 종류

다중키 인덱스

  • 키로 사용된 하나의 필드가 배열일 수 있음
  • MongoDB는 배열에 대한 인덱스를 지원, 다중키 인덱스는 배열 속의 각각의 값을 인덱스 키로 저장

텍스트 인덱스

  • 값과 범위에 대한 최적화된 검색은 문자열 검색에 있어 한계가 있음
  • 단어 단위, 단어의 원형으로 검색하게 도와주는 인덱스가 텍스트 인덱스
  • 하나의 컬렉션에 하나의 텍스트 인덱스만 만들 수 있음

지리공간적(GIS) 인덱스

 

문자열 연산자 : $text

  • 제목이나 본문 등 텍스트가 많은 경우에 원하는 텍스트를 쉽게 검색하는 방법을 제공.
  • $text 연산자를 사용하기 위해서는 우선 문자열 "인덱스"가 필요함.
db.inventory.insertMany([
 { item: "journal", qty: 25, tags: ["blank", "red"] },
 { item: "notebook", qty: 50, tags: ["red", "blank"] },
 { item: "paper", qty: 100, tags:[] },
 { item: "planner", qty: 75, tags: ["blank", "red"] },
 { item: "postcard", qty: 45, tags: ["blue"] }
]);

// 인덱스 만들기
db.inventory.createIndex({item:"text",tags:"text"})
// 인덱스 조회
db.inventory.getIndexes()

// 검색
db.inventory.find({$text:{$search:"journal"}})

 

해시 인덱스

  • B-Tree 구조와 해시 함수를 사용한 인덱스
  • 기존의 인덱스보다 크기가 작고, 검색 속도도 향상
  • 샤딩에서 많이 사용

MongoDB 인덱스 명령어

인덱스 명령어 기능
createIndex 인덱스 생성
getIndexes 인덱스 조
dropIndex / dropIndexes 인덱스 삭제

 

createIndex

  • createIndex({인덱스필드:1 or -1}, {옵션})
  • 인덱스 필드 설정시 1은 오름차순, -1은 내림차순 의미
  • 옵션
    • unique : true | false
    • name : <string>
    • partialFilterExpression : 부분 인덱스 설정
    • sparse : 희소 인덱스 설정
    • expireAfterSeconds : TTL 인덱스 설정
db.inventory.insertMany([
 { item: "journal", qty: 25, tags: {colors: ["blank", "red"]} },
 { item: "notebook", qty: 50, tags: {colors: ["red", "blank"] }},
 { item: "paper", qty: 100, tags:{colors: []} },
 { item: "planner", qty: 75, tags:{colors: ["blank", "red"] }},
 { item: "postcard", qty: 45, tags: {colors: ["blue"] }}
]);

  • 단일키 인덱스
db.inventory.createIndex({item : 1})

 

  • 복합키 인덱스
db.inventory.createIndex({item : 1,qty:-1})

 

  • 다중키 인덱스
db.inventory.createIndex({"tags.colors":1})

 

  • 텍스트 인덱스
db.inventory.createIndex({item:"text"})

 

  • 해시 인덱스
db.inventory.createIndex({item:"hashed"})

 

속성에 다른 인덱스 생성

  • 인덱스 이름 정하기
    • 별도로 설정하지 않아도 몽고디비에서 알아서 이름을 지어줌
db.inventory.createIndex({item : 1},{name : "인덱스 이름"})
// name 설정하지 않으면 몽고디비가 알아서 이름을 지어줌

  • TTL 인덱스
    • 일정 시간이 지나면 자동으로 도큐먼트를 삭제하도록 만들어 줌
    • 50초 후 도큐먼트를 삭제함, 삭제 처리는 60초에 한번씩 만료된 도큐먼트들을 찾아서 삭제
db.inventory.createIndex({item : 1},{expireAfterSeconds:50})

  • 고유 인덱스
    • 같은 값이 저장되는 것을 방지
db.inventory.createIndex({item:1},{unique:true})

  • 희소 인덱스
    • 일반 인덱스는 필드값이 널인 경우에도 인덱스가 생성됨
    • 값이 널인 도큐먼트에 인덱스를 생성하고 싶지 않다면 희소 인덱스를 사용
db.inventory.createIndex({item:1},{sparse:true,unique:true})

  • 부분 인덱스
    • 일부 도큐먼트에 대해서만 인덱스를 생성
    • 어떤 경우에 인덱스를 설정하지 않을지 설정할 수 있음
db.inventory.createIndex({item:1},{partialFilterExpression:{item:"postcard"},name:"부분 인덱스"})

 

getIndexes

db.collection.getIndexes()

dropIndex

  • 특정 인덱스 삭제
db.inventory.dropIndex({item:1})

db.inventory.dropIndex("아이템 인덱스")

dropIndexes

  • 모든 인덱스 삭제
db.inventory.dropIndexes()

 

느린 쿼리 탐지

  • 인덱스는 보다 효율적인 쿼리를 수행하기 위하여 생성함.
  • 인덱스는 애플리케이션을 만든 후 쿼리의 성능을 고려함
  • 인덱스가 애플리케이션에서 어떻게 활용되는지 확인할 방법 필요 => explain() 명령어

 

explain()

  • 쿼리가 어떻게 실행되었는지에 대한 단계 표시
  • 출력되는 결과중 "winningPlan"은 쿼리를 실행하는 가장 효율적인 계획을 말하며, 인덱스를 사용했다면 이 부분을 참고하여 인덱스를 사용했는지, 아닌지 참고할 수 있음
  • MongoDB는 내부적으로 winningPlan을 만들고 관리
  • 자주 사용되는 쿼리는 이 계회을 캐시에 기록하여 빠르고 효율적인 처리를 할 수 있도록 관리
  • 각각의 계획은 여러 단계의 "스테이지"를 거쳐서 쿼리를 실행
  • 주요 스테이지
    • COLLSCAN - 컬렉션 전체 스캔(읽기)
    • IXSCAN - 인덱스 키 스캔
    • FETCH - 도큐먼트 불러오기
    • UPDATE - 도큐먼트 수정하기
    • SORT - 도큐먼트 정렬하기
    • PROJECTION - 출력할 필드 선정하기
    • SHARD_MERGE - 샤딩(분산)되어 흩어진 정보 모으기

 

쿼리 프로파일러(profilier)

  • 쿼리를 실행할 때마다 explain()을 확인하기는 어려움
  • 쿼리의 성능이 느려짐을 감지할 때 어느 곳에서 문제가 있는지 확인하기 위해서 MongoDB에 내장된 "프로파일러"를 가동할 수 있음
  • setProfilingLevel() 명령어
    • DB 단위로 읽기와 쓰기 작업의 진행 상황을 로그에 기록
    • 레벨 0 : 프로파일러 가동 중단 
    • 레벨 1 : 특정 레벨값(100ms)을 넘어가는 느린 쿼리를 탐지
    • 레벨 2 : 모든 읽기와 쓰기를 기록
db.setProfilingLevel(2)

  • was : 이전에 사용된 프로파일링 레벨
  • slowms : 느리다고 판단할 실행 시간(milli seconds)
  • sampleRate : 느린 쿼리의 비율
  • 위의 값들은 옵션으로 변결할 수 있음

프로파일러의 기록은 system.profile 컬렉션에 저장됨

이 컬렉션은 capped 컬렉션이기 때문에 일정 크기 이상이 되면 오래 된 정보는 지워짐

db.system.profile.find().sort({ts:-1}).limit(1)

 

프로파일러 필드 해석

필드명 내용
op 작업 유형
ns 작업이 대상으로 하는 네임스페이
command 이 작업과 관련된 전체 명령 개체를 포함하는 문서
keyExamined system.profile.nscanned 작업 수행을 위해 MongoDB가 스캔한 인덱스 키의 수
docsExamined 작업을 수행하기 위해 MongoDB가 스캔한 컬렉션 문서 수
fromMultiplanner 쿼리 계획자가 쿼리에 대해 가장 적합한 실행 계획을 선택하기 전에 여러 계획을 평가했는지 여부를 나타내는 부울임, true 인 작업이 더 느리다면 인덱스가 너무 많지 않은지 의심해볼 수 있
nreturned 최종적으로 반환되는 도큐먼트의 수
writeConflicts 쓰기 작업 중에 발생한 충돌 수
responseLength 작업 결과 문서의 길이(바이트), responseLength 성능에 영향을 줄 수 있음
millis mongod 작업 시작부터 작업이 끝날 때 까지의 시간(밀리 )
planSummary 실행 계획 요
stage 쿼리 실행의 일부로 수행되는 작업을 설명하는 이름
COLLSCAN : 컬렉션 스캔 위해
IXSCAN : 인덱스 키 스캔용
FETCH : 문서 검색을 위
nReturned 상위 스테이지로 반환되는 도큐먼트 수
executionTimeMilliesEstimate 현재 스테이지를 실행하는데 걸린 시간
works work() 함수가 호출된 횟수, work() 함수는 몽고 디비가 일을 하는 작업 단위
advanced 부모 스테이지로 중간에 반환한 도큐먼트 
saveState 명령 수행 단계에서 현재 상태를 일시 중단하고 현재 상태를 저장한 횟수
resoreState 저장한 상태를 불러온 횟수
needTime 아직 작업할 내용이 남았지만 부모 스테이지로 보내지 못한 횟수
needYield 작업을 수행하기 위해 걸려있는 잠금을 풀어 주길 요청했던 횟수
isEOF 스테이지가 끝났으면 1, 아니면 0
728x90
반응형