7장. 데이터 모델링

데이터베이스와 컬렉션

  • 네임스페이스
    • Mongo에서 네임스페이스는 데이터베이스 이름과 컬렉션 이름을 조합한 문자열로, 특정 컬렉션을 고유하게 식별하는 역할을 한다
    • . 으로 네임스페이스로 각 객체의 관리 참조가 된다
  • 데이터베이스
    • RDB에서 데이터베이스 분리/통합은 성능보다는 샤딩이나 서비스 통합과 관련한 고려사항
      • 초기 버전에서는 데이터베이스 단위로 락이 걸렸기 때문에 성능과 관련이 있었지만, 2.2 버전 이후 글로벌 락(인스턴스락)이 개선되었다
      • MMAPv1 스토리지 엔진은 데이터베이스 수준의 락을 세분화하여 컬렉션 수준의 잠금으로 개선
      • WiredTiger 스토리지 엔진에 RDB 같이 도큐먼트(레코드) 수준의 잠금 제공
    • 데이터베이스에 새로운 컬렉션이나 인덱스를 추가/변경할 때에는 데이터베이스 수준의 잠금 필요
    • 맵 리듀스 작업도 마지막 단계에서 데이터베이스 수준의 잠금 필요
  • 컬렉션
    • Mongo는 기본적으로 조인을 지원하지 않기 떄문에 컬렉션에 가능한 많은 데이터를 내장할 것을 권장하고 있다.
    • 모델링 측면에서는 맞을 수 있지만 성능 측면에서는 그렇지 않을 수 있다.
      • 컬렉션 크기가 커지면 그로 인해 많은 디스크 읽기 오퍼레이션 필요하며 캐시 효율이 떨어진다
    • MMAPv1 스토리지 엔진은 컬렉션 수준의 잠금을 제공
      • 쓰기 작업이 많이 발생할 경우 컬렉션을 분리하여 설계하는 것이 좋다
    • WiredTiger 스토리지 엔진에 경우 동시성 처리를 위해 DB, 컬렉션을 물리적으로 분리할 필요는 없다.
    • 컬렉션 설계에서 가장 중요한 것은 샤드 키 선정
  • $ db.createView('view_name', 'collection_name', 'pipeline)
    
    • 복잡한 형태의 데이터 가공 로직을 캡슐화하여 접근 용이성 항상, 테이블의 일부 데이터에 대해서 접근 권한 허용하여 보안 강화의 목적으로 사용
      • 아직 구체화 뷰(Materialized View), 업데이트 가능 뷰(Updatable View)를 지원하지 않기 때문에 뷰를 자주 사용하면 성능 저하 발생
  • BSON 도큐먼트
    • Binary JSON 의 약자로 JSON 형태의 도큐먼트를 바이너리 포맷으로 인코딩한 도큐먼트
      • Lightweight: binary 타입을 사용하여 공간적인 절약과 네트워크 전송 시 빠르다
      • traversable: 복잡한 파싱 과정 없이 필요한 필드만 빠르게 찾아갈 수 있도록 만들어졌다
      • efficient: C언어의 primitive 타입을 사용하기 때문에 매우 빠르게 인코딩/디코딩 가능
    • BSON 기본 타입
      • BYTE, INT32, INT64, DOUBLE
  • 제한 사항
    • Mongo에서는 JSON으로 표시되지만 BSON으로 변환하여 저장
    • BSON 도큐먼트 포맷의 대표적인 특성
      • 하나의 도큐먼트는 “{“, “}”로 감싸지며 모든 원소는 키/쌍 구성, 중첩된 도큐먼트의 깊이는 100lv, 도큐먼트 최대 크기는 16MB 까지 지원
    • 기존 JSON 포맷은 훨씬 다양하기 때문에 그대로 Mongo에 사용할 수 있는지 호환성 테스트 필요
    • 도큐먼트의 크기 제한(16MB) 때문에 대용량 데이터를 저장할 때 크기 고려가 필요하며 크기를 고정한 이유는 성능 이슈 발생 가능성 때문

데이터 타입

  • BSON 데이터 타입
    • MongoDB는 다양한 데이터 타입을 지원하며, 각 타입은 특정한 용도와 특성을 가지고 있다. 주요 BSON 데이터 타입은 다음과 같다
      • ObjectId: 12바이트의 Binary Data 타입을 Primitive 타입으로 사용
      • Integer: 32비트 또는 64비트 정수 값을 나타내는 타입
        • Double: 부동 소수점 숫자 값을 나타내는 타입
      • Decimal128: 고정 소수점 숫자를 저장하는 타입 (금융 애플리케이션에 유용)
      • String: UTF-8 인코딩된 문자열 값을 나타내는 타입
      • Timestamp: 타임스탬프 값을 저장하는 타입 (주로 내부 용도로 사용)
      • Date: 날짜와 시간을 나타내는 타입 (밀리초 단위로 저장)
        • 타임존 확인을 위해서는 자바스크립트로 타입존 Offset을 확인해볼 수 있다
      • Null: 값이 없음을 나타내는 타입
      • Boolean: true 또는 false 값을 나타내는 타입
      • Array: 값의 배열을 나타내는 타입
      • Binary Data: 바이너리 데이터를 저장하는 타입 (예: 이미지, 파일 등)
      • Regular Expression: 정규 표현식을 저장하는 타입
  • 데이터 타입 비교
    $ db.collection.find( { field: { $type: <BSON type> } } ) // 해당 타입의 도큐먼트만 조회 가능
    
    • Mongo에서는 한 필드의 서로 다른 데이터 타입의 값을 가질 수 있다.
    • $type 연산자를 사용하여 도큐먼트의 특정 필드가 어떤 데이터 타입인지 확인할 수 있다
  • 필드 값의 비교 및 정렬
    • MinKey(mongo 내부 타입) < Null < 숫자형 < 문자열형 < Object < Array < BinData < ObjectId < Boolean < Date < Timestamp < 정규표현식 < MaxKey(mongo 내부 타입)
  • 문자셋과 콜레이션
    • 문자셋 (Character Set)
      • 문자셋은 텍스트에 사용되는 문자(글자, 숫자, 기호 등)를 컴퓨터가 저장하고 처리할 수 있도록 숫자 코드(인코딩)에 매핑하는 규칙의 집합
      • MongoDB는 기본적으로 모든 문자열 데이터를 저장할 때 UTF-8 인코딩 사용
      • UTF-8은 유니코드(Unicode) 표준의 한 형태로, 전 세계 대부분의 언어(한국어, 영어, 일본어, 중국어 등)를 표현할 수 있어 국제화(Internationalization)에 가장 적합하다.
      • 사용자가 별도로 설정할 필요가 없으며, 모든 문자열 데이터는 UTF-8로 처리
    • 콜레이션 (Collation)
      • 콜레이션은 문자열 데이터를 비교하고 정렬하는 데 사용되는 언어별, 로케일별 규칙의 집합
      • 문자열의 동등성 판단(검색) 및 정렬 순서는 단순히 문자 코드값(바이너리 비교)으로만 결정되지 않고, 언어의 관습에 따라 달라지기 때문에 중요.
      • MongoDB의 기본 콜레이션은 ‘simple’ 이라는 바이너리 비교(Binary Comparison). 이는 단순히 UTF-8 코드 값 순서대로 비교하므로, 대소문자나 악센트 등을 구분
      • 콜레이션의 주요 역할
        • 대소문자 구분 (Case Sensitivity): ‘a’와 ‘A’를 같은 것으로 볼지 다른 것으로 볼지 결정
        • 악센트 구분 (Accent Sensitivity): ‘e’와 악센트가 붙은 ‘é’나 ‘è’를 같은 것으로 볼지 다른 것으로 볼지 결정
        • 정렬 순서 (Sort Order): 특정 언어(예: 스웨덴어, 독일어)에서 문자를 정렬하는 고유한 순서를 따른다
        • 숫자 문자열 정렬 (Numeric Ordering): ‘10’과 ‘2’를 문자열로 정렬하면 ‘10’이 ‘2’보다 앞에 오지만, 콜레이션을 사용하면 숫자처럼 인식하여 ‘2’가 ‘10’보다 앞에 오도록 정렬 가능
      • 콜레이션 설정 방법
        $ db.createCollection("myCollection", {
              collation: { locale: "ko" }
          });
        
        • 콜레이션은 ICU(International Components for Unicode) 표준을 따르며, 주로 locale 매개변수를 사용하여 설정
        • 컬렉션 생성 시: db.createCollection() 명령어로 컬렉션을 만들 때 기본 콜레이션 지정. 해당 컬렉션에 대한 모든 쿼리는 이 규칙을 따른다.
        • 인덱스 생성 시: db.collection.createIndex() 명령어로 인덱스를 만들 때 콜레이션을 지정. 쿼리의 콜레이션이 인덱스의 콜레이션과 정확히 일치해야만 해당 인덱스를 사용하여 정렬 및 비교 성능을 최적화할 수 있다.
        • 개별 연산 시: CRUD 작업 시 콜레이션을 지정하여 컬렉션의 기본 설정을 재정의할 수 있다.
  • MongoDB 확장 JSON (Extended JSON)
    • 내부적으로 BSON의 일부 타입을 지원하기 때문에 mongo에서 생성된 JSON 도큐먼트를 다른 도구들이 인식하지 못하는 경우 발생
      • mongo의 JSON을 STRICT 모드와 mongo shell 모드로 구분하여 사용할 수 있다
    • STRICT 모드는 mongo 도구뿐만 아니라 외부의 모든 JSON 도구들이 JSON 도큐먼트를 파싱할 수 있다
      • 하지만 Binary, ObjectId와 같은 타입은 인식하지 못한다.
    • mongo shell 모드의 JSON 표기법을 사용하면 예외 발생

모델링 고려 사항

  • 도큐먼트 크기
    • 일반적으로 도큐먼트는 RDB의 레코드의 크기보다 큰 경우가 많다.
      • Document DB 특성 상 하나의 도큐먼트의 여러 데이터를 모아 저장하는 경우가 있는 것으로 보임
    • 최대 크기 제한
      • 단일 document의 최대 크기는 16MB로 고정되어 있다.
      • 16MB를 초과하는 파일, 이미지, 대용량 로그 등의 경우 GridFS를 활용해 여러 청크로 분산 저장해야 한다.
    • 성능 및 리소스
      • 큰 document는 조회, 수정, 저장 시 더 많은 RAM과 디스크 I/O가 필요하므로 성능 저하 문제가 발생할 수 있다.
      • read/write 시 불필요한 large field까지 모두 불러오게 되어 네트워크 대역폭과 전체 시스템의 부담으로 이어진다.
      • frequently accessed document 크기가 크면 working set이 RAM을 쉽게 초과하여, 디스크 접근 빈도가 늘어나 DB 성능에 직접적인 영향을 준다.
    • 스키마 설계 Best Practice
      • 필요한 정보만 document에 포함시키고, 불필요한 필드를 최소화한다.
      • 깊은 중첩(nesting)을 피하며, embedded document(역정규화)와 referenced document(정규화) 전략을 적절하게 섞는다.
      • 대형 배열, unbounded array는 도큐먼트 크기를 빠르게 증가시키므로, 배열 내 개수를 제어하거나 분할 설계를 적용해야 한다.
      • 인덱스 대상 필드를 신중히 선정하며, 모든 필드를 인덱싱하면 오버헤드가 커진다.
      • 성장 가능성이 높은 document 구조는 사전에 분리(분할) 설계를 고려한다.
    • 기타 참고사항
      • Object.bsonsize() 등 shell 함수로 BSON 크기를 직접 체크할 수 있다.
      • 도큐먼트 크기 제한은 디버깅이나 ETL, batch 작업에서도 자주 문제가 되므로, 예상 성장 패턴까지 감안한 모델링이 필요하다.
  • 정규화와 역정규화
    • 정규화 (Document Referencing)
      • 관계형 DB의 “정규화”와 유사하게, 관련 데이터를 별도의 컬렉션(테이블)로 분리하고 필요할 때 참조(reference)하는 방식
        • 학생과 수강과목 데이터를 각각 독립된 컬렉션에 저장하고, 학생 도큐먼트에는 각 과목의 id만 리스트 형태로 저장
      • 장점
        • 데이터 중복 최소화(변경 발생 시 하나만 수정)
        • 데이터 일관성, 무결성 보장에 유리
        • 컬렉션(테이블)의 크기를 작게 유지 가능
      • 단점
        • 데이터 조회 시 여러 컬렉션을 조합해야 하므로 join(lookup) 비용 증가
        • MongoDB는 복잡한 join이나 조인 성능이 RDB에 비해 떨어지는 편이다.
      • 권장 사례
        • 큰 서브 도큐먼트, 자주 갱신되는 데이터, 즉각적 일관성 필요, 증가량 많은 데이터, 빠른 쓰기 필요
        • 쓰기나 수정이 잦고, 일관성·데이터 크기가 큰 경우 referencing이 더 적합합니다.
    • 역정규화 (Embedding)
      • 관련 데이터를 한 도큐먼트 안에 중첩(embedded) 형태로 직접 포함시키는 방식
        • 학생 도큐먼트 내에 수강과목 정보를 배열로 직접 포함(예시: { classes: [{ class: “이산수학” }, …] }).
      • 장점
        • 조회 시 필요한 데이터가 한 번에 로드되어 읽기 성능(쿼리 효율)이 좋음
        • 데이터 구조가 간단해져 복합 쿼리를 줄일 수 있음
        • join이 필요없는 구조이므로 빠른 read에 최적화
      • 단점
        • 데이터 중복 증가(같은 정보가 여러 문서에 있음)
        • 일관성 관리가 어렵고, 데이터 갱신 시 여러 문서를 동시 수정 필요
        • 단일 도큐먼트 최대 크기(16MB) 제한에 주의 필요
      • 권장 사례
        • 작은 서브 도큐먼트, 자주 변하지 않는 데이터, 결과적 일관성 허용, 증가량 적은 데이터, 빠른 읽기 필요
        • 읽기 빈도가 높고, 데이터 변경이 적거나 크기가 작으면 embedding을 추천합니다.
  • 서브 도큐먼트
    • 갱신 패턴과 단편화 문제
      • 도큐먼트 갱신은 항상 전체 도큐먼트 단위(atomic)로 이뤄진다.
      • 서브 도큐먼트가 빈번히 추가/삭제/변경되면, 내부적으로 단편화(fragmentation)가 심화되어 성능 저하 발생
      • 주기적 갱신이 많은 데이터는 Reference(정규화) 방식이 더 적합하다.
    • 데이터 액세스 패턴
      • 서브 도큐먼트가 항상 부모 도큐먼트와 함께 조회된다면 embedding이 이상적이다.
      • 반면, 독립 조회나 별도 join이 빈번하다면 referencing이 효율적일 수 있다.
    • 일관성과 중복
      • embedded 구조는 데이터 일관성 유지가 쉽지 않다. 부모 도큐먼트가 여러 곳에 중복 저장될 수 있으므로, 일괄 갱신이 어렵다.
      • 자주 변하지 않는 데이터, 작은 정적 데이터(코드, 속성 등)에 적합하다.
    • 배열의 크기와 관리
      • 큰 배열이나 크기가 계속 증가하는 embedded array는 도큐먼트 크기 한계에 가까워질 수 있다.
      • 배치가 필요하거나 배열이 무제한적으로 늘어날 수 있다면, 별도 컬렉션에 저장 후 referencing을 권장한다.
    • 인덱싱과 쿼리 성능
      • 서브 도큐먼트 내 필드도 인덱스 생성 가능하지만, 지나치게 깊은 중첩은 인덱스 성능에 영향을 줄 수 있다.
      • 주로 사용하는 쿼리 조건과 인덱스 구성을 미리 설계해야 한다.
      • 정리하면, 서브 도큐먼트 구조는 데이터의 라이프사이클, 크기, 변경 빈도, 접근 패턴, 일관성 요구, 쿼리의 효율성을 모두 종합적으로 고려하여 설계해야 한다.
      • 불필요한 대형 도큐먼트 생성을 피하고 유지보수와 성능 모두를 균형 있게 신경 쓰는 것이 중요
  • 배열
    • RDB에서는 다른 형태의 데이터를 저장할 수 없고 배열같은 타입은 정규화를 통해 해결한다.
    • Mongo는 배열 타입을 가질 수 있으며 멀티키를 통해 인덱스도 가능하다.
    • 또한 단일 도큐먼트 내에서만 원자성을 보장하기 때문에 배열 타입은 트랜잭션이 지원하지 않는 단점을 보완해준다
    • 도큐먼트 크기 증가
      • 만약 하나의 게시물 도큐먼트의 댓글을 배열로 저장하면 도큐먼트의 크기는 계속 증가하게 되며 이는 디스크나 메모리, CPU 자원 낭비 발생
    • 배열 관련 연산자 선택
      • $push, $pop
        • 배열 타입에 저장된 모든 아이템을 비교하지 않아도 되어 빠른 처리 가능
      • $addToSet, $pull($pullAll)
        • 기존 배열에 있는지/없는지 확인해야 하므로 배열의 모든 아이템과 비교 작업 수행
    • 배열과 복제
      • 가용성을 높이기 위해 레플리카셋을 활용하여 동일한 데이터 복제본을 갖게 되며 복제를 위해 oplog 사용
      • 복제 방식
        • 모든 데이터 변경은 Primary 노드에서만 이루어지며, 세부적 오퍼레이션 단위로 OpLog에 기록
        • 배열 내 원소의 추가, 삭제, 수정 같은 배열 연산도 OpLog에 개별 오퍼레이션 형태로 남는다.
        • Secondary 멤버는 OpLog의 각 엔트리를 읽어 원본과 동일하게 배열 연산도 순서대로 적용한다.
        • OpLog에는 “update” 오퍼레이션 발생하면,
          • 업데이트 명령(예: 배열 추가 시 $push, $addToSet, $pull 등)이 실제 쿼리 조건 및 변경된 데이터와 함께 저장
      • 일관성과 최종 상태
        • 모든 도큐먼트 복제는 최종 일관성 모델을 보장
          • 네트워크 이상 없이 충분히 시간이 지나면 모든 레플리카 노드의 배열 필드 내용도 Primary와 정확히 일치
        • 배열 크기나 배열의 경우에도 단일 document 크기(16MB) 제한은 그대로 적용된다.
      • 배열 필드 복제 관련 유의점
        • document-level 복제이므로, 배열 필드가 크거나 변경이 잦을수록 OpLog 사이즈와 Secondary 동기화 부하가 커진다.
        • 대규모 배열 변경(배열 교체, 전체 삭제 등)은 diff가 아닌 전체 값 교체로 기록될 수 있다.
        • 복제 실패나 재동기화 시에는 전체 document가 다시 복제된다.
  • 필드 이름
    • mongo는 스키마를 갖지 않기 때문에 필드명을 정의할 필요 없이 필드명-필드값을 key-value 쌍으로 데이터 저장
    • 즉, 필드명도 데이터의 일부가 되고, 필드의 이름이 차지하는 공간이 크면 데이터의 크기 또한 커진다
  • 프레그멘테이션과 패딩
    • 도큐먼트 단편화(Fragmentation)
      • 도큐먼트가 반복적으로 갱신·확장되어 크기가 커질 경우, 기존 저장 공간에 다 들어가지 못하면 WiredTiger(storage engine)는 새로운 위치에 저장하고 원래 공간은 미사용(unallocated) 처리
      • 이런 미사용 공간이 누적되면, 디스크에 큰 단편화가 발생하며, 읽기 및 저장 효율이 계속 떨어진다.
      • 잦은 배열/서브도큐먼트 추가, 값 확장, remove & insert 등이 심한 데이터에서 단편화 문제가 주로 발생한다.
    • 패딩(Padding) 정책
      • 패딩은 도큐먼트가 늘어날 것을 예상해, 실제 저장 시 추가 공간을 ‘버퍼’로 남겨두는 정책이다.
      • MMAPv1 엔진에서 자동 패딩을 적용했지만, WiredTiger는 기본적으로 패딩 없이 도큐먼트를 효율적으로 압축/저장한다.
      • 패딩이 없으면 도큐먼트 크기가 늘 때마다 공간이 부족해져 빈번한 재배치가 일어나고, 이는 곧 단편화로 연결된다.
    • 실전에서 고려할 점
      • 도큐먼트가 자주 커지는 데이터 구조(배열, 서브도큐먼트의 반복적 추가)는 최대 크기 예측과 더불어, 단편화 영향까지 설계 단계에서 신경 써야 한다.
      • 대형 배열, 주기적 구조 변동이 예상되면 정규화(Referencing)로 설계를 분산하는 것이 디스크 효율과 성능 관리에 유리하다.
      • 저장 공간 reclaim(회수)은 기본적으로 자동화되지 않으므로, 심각한 단편화가 누적된 경우 컬렉션 compact, reIndex, 데이터 마이그레이션 등의 운영 작업이 필요
      • WiredTiger는 단편화를 줄이기 위한 내부 압축과 공간 관리 기능을 제공
        • 그러나 구조적 단편화(모델링에서 오는 문제)는 사전에 설계로 예방하는 것이 가장 효율적이다.
  • 도큐먼트 유효성 체크
    • 스키마 유효성 규칙 정의
      • MongoDB는 JSON Schema 기반 validation을 컬렉션 별로 적용할 수 있다.
      • 필수 필드 존재 확인, 타입 제약, 값 범위, 패턴(정규표현식), 중첩 구조 등 복잡한 조건을 스키마에 명시할 수 있다.
      • 변경 요구가 많은 환경은 너무 세밀한 규칙을 피하는 게 유리하다.
    • 성능 영향
      • 유효성 검사는 document insert/update 시마다 수행되므로, 복잡한 rule이 많으면 쓰기 성능이 저하될 수 있다.
      • 대량 데이터 입력·마이그레이션 시, validation 옵션을 일시적으로 해제할 필요도 있다.
    • 유연성 vs. 무결성의 균형
      • NoSQL의 장점(유연함)을 살리되, 비즈니스 중요 정보는 꼭 validation rule을 적용해야 한다.
      • 버전 관리(스키마 진화) 상황에서는 규칙 변경·이관이 쉬운 구조로 설계하는 것이 편하다.
    • 운영 및 관리
      • 유효성 오류 발생 시 명확한 에러 메시지로 디버깅이 가능해야 하며, 개발/운영 환경별로 rule을 다르게 적용할 수도 있다.
      • 잘못된 데이터가 누적되지 않도록, validation 수준(서버의 strict, moderate, off 옵션), application 레이어에서의 복합 체크도 함께 고려한다.
  • 조인
    • $lookup 사용 시 성능 이슈
      • MongoDB는 document 기반이며 native join은 없다.
      • 하지만 Aggregation Pipeline의 $lookup 스테이지를 사용해 컬렉션 간 조인을 구현할 수 있다.
      • $lookup은 별도의 컬렉션 전체 스캔(특히 인덱스가 없거나 join 키가 분산적일 경우)으로 인해, 대량 데이터/높은 QPS 환경에서 성능 저하가 발생할 수 있다.
      • join 대상이 커질 경우, 작업 시간이 길어지고 서버 리소스 소비가 매우 늘어난다.
    • 데이터 구조와 read/write 효율
      • 조인이 자주 발생하는 구조라면, 한 컬렉션에 embedding(역정규화)으로 데이터를 포함시키는 모델이 성능상 더 유리할 수 있다.
      • referencing(정규화)로 분할한 경우, join·lookup이 불가피하지만 데이터 중복은 최소화된다.
    • 인덱스 설계 필수
      • $lookup의 join 키(외래키 역할)는 반드시 인덱스를 생성해야 컬렉션 스캔을 막고 속도를 최대한 높일 수 있다.
      • join 빈도가 높은 필드는 단일 인덱스 혹은 복합 인덱스로 관리한다.
    • 확장성과 분산 환경
      • Sharding 환경에서는 $lookup 사용 시 제한과 구조적 제약이 있다. sharded collection 간의 lookup은 shard key, 데이터 분포에 따라 불가능하거나 성능 저하가 심할 수 있다.
      • 대형 서비스에서는 lookup을 최소화하고, embedding, application-side join(애플리케이션단에서 두 번 쿼리 후 병합) 등의 구조도 고려해야 한다.
    • 유지보수와 구조 변경
      • 데이터 모델 변경, 구조 진화 시 embedding과 referencing 전략의 전환이 요구될 수 있다.
      • 조인이 많아질수록 스키마 관리, 마이그레이션의 난이도가 높아진다.
      • 복잡한 multi-way join, 집계 join 등은 RDBMS 대비 구현/운영이 어렵다.

8장 쿼리 개발과 튜닝

8-1 기본 쿼리

  • 기본 쿼리
    • insert: db.collection.insert()
    • batched DML(insert, update, delete): db.collection.bulkWrite()
    • update: db.collection.update()
      • replace: db.collection.update({}, {$set: {}}, {upsert: true})
    • delete: db.collection.remove()
    • select: db.collection.find()
    • select .. group by .. : db.collection.aggregate() // MapReduce
insert
$ db.users.insert({name: "matt"}) // {name: "matt"} 도큐먼트를 users 컬렉션에 입력
$ db.users.insert([{name: "matt"}, {name: "lara"}]) // 두개의 도큐먼트를 users 컬렉션에 입력

$ db.users.insert({name: "matt", score: "90}, {writeConcernt: {w:1, j:true}})
$ db.users.insert([{name: "matt"}, {name: "lara"}], {ordered: false))
  • 일반적으로 두개의 인자 사용
    • 첫째는 저장하고자 하는 도큐먼트, 두번째는 처리 옵션을 지정할 수 있다.
  • 처리 옵션
    • writeConcern: 완료 응답을 반환할지 결정할 수 있도록 설정
    • ordered: 첫번째 인자가 배열이어서 여러 도큐먼트를 입력할 때 순서대로 입력할지에 대한 정의
      • ordered 가 true인 경우 단일 스레드로 순차 insert하며 저장 과정에서 에러가발생하면 해당 지점부터 insert가 멈춘다
      • false에 경우 멀티 스레드로 배분하여 동시 insert 진행, 에러가 발생한 경우 무시하고 나머지 도큐먼트 insert
  • insert 도큐먼트의 ObjectId 조회
    $ var newId = new ObjectId()
    $ print(newId)
    $ db.users.insert({_id: newId, name: "matt"}) // 생성한 ObjectId로 도큐먼트 저장
    
    • ObjectId를 통해 AUTO_INCREMENT 아이디 값 제공
    • _id 필드에 ObjectId를 생성하여 할당하는 방식으로 insert될 때 _id 값을 확인할 수 있다.
update
  • 4가지 형태의 update 명령어
    • update() \ updateOne() \ updateMany() \ replaceOne()
      $ db.users.update(
      {name: "matt"}, // 업데이트 대상 도큐먼트 검색 조건
      {$set: {score: 100}}, // 업데이트 내용
      {upsert: true} // 도큐먼트 업데이트 옵션
      )
      $ db.users.update({name: "matt"}, {$set: {score: 100}, $currentDate: {lastModified: true}}}
      // {"_id": ObjectId(...), "name": "matt", "score" 100, "lastModified": ISODate("2025-xxxxxx")}
      
  • 두번째 인자에 다양한 오퍼레이션
    • $set 옵션이 없으면 통째로 덮어써버리기 떄문에 주의해야 한다
    • $currentDate 등의 오퍼레이션을 이용하여 타임스탬프 컬럼처럼 업데이트도 가능하다
    • $inc: 주어진 값만큼 증가시켜 저장, 조회와 저장을 원자적으로 처리 //{$inc: {quantity: -2,”matrics.orders”: 1}}
    • $mul: 값의 배수로 저장하며 조회와 저장을 원자적으로 처리 // {$mul: {price: 1.25}}
    • $rename: 필드 이름 변경 // {$rename: {“name”: “name”}}
    • $setOnInsert: upsert 옵션이 true일 경우 insert를 해야할 때 적용
    • $unset: 도큐먼트의 필드 삭제 // {$unset: {quantity:””, instock:””}}
  • 세번째 인자에는 여러가지 옵션을 설정할 수 있다.
    • upsert: true로 설정하면 update할 내용을 찾지 못할 때 insert한다
    • multi: true로 설정하면 일치하는 모든 도큐먼트 변경
      • 기본적으로 update 명령어는 단일 도큐먼트의 업데이트를 수행하여 2개 일치하더라도 하나만 변경
    • writeConcern: 완료 응답을 반환할 지 결정할 수 있다
    • collation: update 명령이 변경할 대상 도큐먼트를 검색할 때 사용할 문자셋과 콜레이션 명시
  • 배열 필드 업데이트
    $ db.collection.updateOne({ _id: ... }, { $push: { arr: "newValue" } })
    $ db.collection.updateOne({ _id: ... }, { $addToSet: { arr: "uniqValue" } })
    $ db.collection.updateOne({ _id: ... }, { $pull: { arr: "valueToRemove" } })
    $ db.collection.updateOne({ _id: ... }, { $pop: { arr: 1 } }) // 마지막 삭제
    $ db.collection.updateOne({ _id: ... }, { $pop: { arr: -1 } }) // 첫번째 삭제
    $ db.collection.updateOne({ _id: ..., "arr.value": 1 }, { $set: { "arr.$.updated": true } })
    $ db.collection.updateOne(
      { _id: ... },
      { $set: { "arr.$[elem].field": "value" } },
      { arrayFilters: [ { "elem.status": "pending" } ] }
    )
    
    • 배열 필드의 데이터를 변경하기 위해 도큐먼트를 가져와 가공 후 변경하는 형태로 업데이트를 하는 것이 아니다.
    • 특정 위치의 배열 엘리먼트를 수정할 수 있다.
    • 배열에 값 추가
      • $push: 배열 끝에 요소를 추가한다.
        • $each를 결합해 여러 값을 한 번에 추가
        • $position으로 삽입 위치 지정
        • $slice로 최대 길이 유지 등 추가 옵션 활용 가능.​
      • $addToSet: 해당 값이 없을 때만 추가(중복 방지).​
    • 배열에서 값 삭제
      • $pull: 조건에 맞는 값을 배열에서 제거한다.​
      • $pop: 배열의 첫(−1) 또는 마지막(1) 요소 삭제.​
    • 배열 내부 요소 값 수정
      • $ 위치 연산자: 쿼리 조건에 맞는 첫번째 배열 요소만 업데이트.​
      • arrayFilters 옵션: 조건에 부합하는 여러 배열 요소에 동시에 업데이트 적용(MongoDB 3.6+).​
remove(delete)
$ db.collection.remove(
  {name: "matt"}, // 삭제 대상 검색 조건
  {justOne: true} // 도큐먼트 삭제 옵션
)
  • 3가지 형태의 삭제 명령
    • remove // deleteOne // deleteMany
  • 삭제 옵션
    • justOne: 여러 도큐먼트가 삭제되는 것이 디폴트이기 때문에 justOne을 true로 선택하면 첫번째 도큐먼트만 삭제
    • writeConcern: 완료 응답을 반환할 지 결정할 수 있다
    • collation: update 명령이 변경할 대상 도큐먼트를 검색할 때 사용할 문자셋과 콜레이션 명시
격리된($isolated) update와 remove
$ db.users.remove({score: {$lt: 50}, $isolate: 1})
$ db.users.update({score: {$gt: 90}, $isolate: 1}, {$set: {grade: "A"}}, {multi: true})
  • mongo에서는 도큐먼트 단위의 원자성 제공
    • 하나의 명령어로 여러 도큐먼트를 변경/삭제해도 하나의 트랜잭션으로 하나의 도큐먼트를 변경하거나 삭제
    • 그래서 여러 도큐먼트를 변경할 때 오퍼레이션이 완료되기 전에 먼저 변경된 데이터를 조회할 수 있게 된다.
  • 격리된 update, remove
    • 명령이 완료되기 전까지 다른 커넥션에서 변경 내용을 확인하지 못하게 하기 위해 사용
bulkWrite
  • insert, update, remove 등을 모아서 한번에 실행할 수 있는 명령으로 하나의 컬렉션에 대해서만 데이터를 변경할 수 있다
  • bulkWrite 도중 에러가 발생하면 BulkWriteError가 발생하며 예외 발생 시 중단
    • ordered 옵션에 따라 동작이 다르다
    • ordered: true (기본값): 모든 작업이 배열 순서대로 순차 실행된다.
      • 중간에 쓰기 작업 중 하나가 실패(예: 중복 키, validation 등)하면 중단되어 남은 작업을 수행하지 않는다.
      • 이미 성공한 작업은 롤백되지 않고 그대로 반영된다. 즉, RDBMS의 트랜잭션 롤백과는 다르다.
    • ordered: false: 모든 명령이 순서에 구애받지 않고 병렬로 최대한 실행된다.
      • 중간에 일부 작업이 실패해도 나머지 작업들이 계속 실행
      • 성공·실패 여부를 개별적으로 집계해서 BulkWriteError에 담아 반환하여 상세 내역에서 확인 가능
find
$ db.users.find({name: "matt"}, {_id: 0, name: 1, score: 1})
  • find 명령 검색 조건과 프로젝션(반환할 필드)를 인자로 받는다.
    • 프로젝션에서 0(가져오지 않는 경우) 또는 1(가져오는 경우)로 설정
    • 프로젝션 필드를 설정하면 나머지는 그 반대로 설정돼어 필드가 결정된다
  • find 연산자
    • 비교 오퍼레이터
      $ db.users.find({name: {$eq: "matt"}})
      $ db.users.find({score: {$gt: 90}})
      
      • $eq, $gt, $gte, $lt, $lte, $ne, $in, $nin
    • 논리 결합 오퍼레이터
      $ db.users.find($and: [{name: "matt"}, {namr: "lora"}]) // and, or, nor은 배열 형태
      $ db.users.find({score: { $not: {$gte: 90} } })
      
      • $or, $and, $not, $nor
    • 필드 메타 오퍼레이터
      $ db.users.find($and: [{name: {$exists: true}}, {name: {$type: "string"}}])
      
      • $exists, $type
    • 평가 오퍼레이터
      • $mod, $regex, $text(전문 검색 비교 수행), $where(자바스크립트 표현식)
    • 배열 오퍼레이터
      • $all, $elemMatch(모든 조건에 일치하는 엘리먼트를 하나라도 가진 도큐먼트 검색), $size
  • find 조건
    • 하나의 필드에 대한 조건은 반드시 하나의 서브 도큐먼트로 작성해야 한다
      $ db.users.find({name: {$lte: "u"}}, {name:{$gte:"m"}}) // 조건 하나는 버려진다
      $ db.users.find({name: {$lte: "u", $gte:"m"}}}) // 하나의 서브도큐먼트로 작성해야 원하는 값을 찾을 수 있다
      
    • 논리 연산을 포함하지 않으면 AND 연산을 수행
      $ db.users.find({name: "matt", scores: 85})
      $ db.users.find($and: [{name: "matt"}, {score: 85}])
      
    • 서브 도큐먼트 필드 검색 쿼리
      {name: "matt", "contact": {type: "office", phone: "031-000-0000"}}
      {name: "matt", "contact": {type: "office", phone: "031-000-0000", "extension_no": 122}}
      $ db.users.find({contact: {type: "office", phone: "031-000-0000"}})
      // {name: "matt", "contact": {type: "office", phone: "031-000-0000"}} 조회
      $ db.users.find({contact: {phone: "031-000-0000", type: "office"}})
      // Not Found
      $ db.users.find({ "contact.type": "office", "contact.phone": "031-000-0000"})
      // {name: "matt", "contact": {type: "office", phone: "031-000-0000"}}, {name: "matt", "contact": {type: "office", phone: "031-000-0000", "extension_no": 122}} 2건 조회
      
      • 첫번째 쿼리처럼 서브 도큐먼트를 통째로 BSON 문서로 변환하여 일치 여부 판단
        • 도큐먼트의 인자 순서가 달라져도 검색하지 못한다
      • 마지막 쿼리는 필드가 다르더라도 검색 조건에 있는 필드만 비교하여 검색
    • 배열 필드 검색 쿼리
      • 배열 전체 vs 배열 요소 검색 구분
        • 기본적으로 { arrayField: value } 쿼리는 배열 내 어떤 요소라도 value와 일치하는 경우를 찾는다.
        • 따라서 배열 전체가 특정 값과 정확히 일치하는지를 찾으려면 완전한 배열 값을 넣어야 한다.
          • { arrayField: [value1, value2, …] }
      • 중첩 배열 및 다중 조건 검색 시 $elemMatch 사용
        // 예를 들어 배열 내에 { x: 5, y: 10 } 형태의 객체가 있어야 할 때
        $ db.coll.find({ arrayField: { $elemMatch: { x: 5, y: 10 } } })
        $ db.{ "arrayField.x": 5, "arrayField.y": 10 } //각각 다른 배열 요소를 참조할 수 있어 결과가 달라진다
        
        • 배열 요소에 여러 조건을 동시에 검사하려면 $elemMatch를 써야 한다.
      • 배열 크기 및 요소 개수 조건
        • $size 연산자를 활용하여 배열 크기를 정확히 지정할 수 있다.
        • $size는 인덱스 지원이 안 되고, 성능 부담이 있다.
      • 중복 요소 및 정렬 주의
        • 배열 내 중복 요소 존재 시 find 쿼리 결과에 영향이 있을 수 있다.
        • 정렬이 중요한 경우 배열 내 순서에 따라 결과가 달라질 수 있으니 주의한다.
      • 인덱스 설계 및 멀티키 인덱스 주의
        • 배열 필드에 인덱스가 생성되면 멀티키 인덱스가 되는데, 다중 배열 필드를 결합하는 복합 멀티키 인덱스는 제한과 복잡성이 있다.
        • 쿼리 성능을 위해 배열 인덱스 설계 시 데이터 분포와 사용 쿼리를 면밀히 검토해야 한다.
      • null/존재 여부 검사 시 차이
        • 배열 필드가 아예 없거나 빈 배열인 경우 null/존재 여부 검사 결과가 달라질 수 있다.
findAndModify
Aggregation
  • 목적
    • Aggregation 도입 목적은 간단한 분석 쿼리조차 MapReduce를 사용해야 하며 성능적인 제약이 있기 때문이다
    • MapReduce 보다 간단하고 빠른 성능 가능
  • 동작 방식
    • 단순한 find 쿼리나 복잡한 aggregation 쿼리 모두 단일 샤드에서 실행되고 결과가 클라이언트에게 전송
    • 샤딩된 환경에서 Aggregation 파이프라인의 각 스테이지가 어떤 샤드에서 실행되는지가 부하 분산 차원에서 중요하다
      $ db.login_history.aggregate( {
        {$match: {status: 'SUCCEED'}},
        {$group: {_id: "$name", total_sum: {$sum: 1}}},
        {$sort: {total_count: -1}}
      }) // SUCCEED 상태를 조회하여 name으로 그룹핑하여 sum을 합한 후 정렬하는 aggregate query
      
    • mongos 는 aggregate 쿼리를 전달받으면
      • $match 스테이지가 있는지, 있다면 샤딩키를 포함하는 지 비교
      • 데이터가 단일 서버에만 있다면 해당 서버에 전달하고 아닌 경우 대표 서버에 쿼리 전달
    • 대표샤드는 쿼리를 전달받으면 필요한 나머지 샤드로 쿼리 전송
      • 대표 샤드는 쿼리 결과를 병합하고 정렬하는 작업을 수행하여 mongos에게 결과 전달
    • 샤드에서 실행 가능한 단계
      • $match, $project
        • 샤드 내 데이터 필터링 및 필드 변환을 수행한다.
        • 특히 $match에 샤드 키 조건이 포함되어 있다면, 해당 샤드만 쿼리를 받기 때문에 효율적이다.
      • $group, $sort 등 일부 집계 단계도 샤드에서 분산 작업 가능하나, 모든 데이터를 모아야 하는 경우 후처리 필요
      • $limit, $skip는 샤드에서 제한적으로 처리되고, 최종 병합 단계에서 조정될 수 있다.
    • 샤드에서 실행 불가능하거나 제한적 단계
      • $lookup, $out, $merge
        • 여러 샤드에 걸친 조인이나 외부 컬렉션으로 결과 쓰기 작업은 최종 결과를 단일 샤드나 mongos가 처리해야 한다.
      • 복잡한 $group, $sort(특히 대규모 정렬)
        • 이런 경우 모든 샤드의 결과를 한 곳에 모아 처리해야 하므로 단일 샤드에서 실행된다.
        • 파이프라인 전체가 아닌 일부 단계만 샤드에서 실행되고, 나머지 단계는 병합해서 처리된다.
  • 단일 목적의 aggregation
    • 단순하지만 사용빈도가 높아 별도의 명령을 지원하는 명령어
    • count
      $ db.collection.count(query, option)
      // option: limit(조건에 일치하는 최대 개수 지정), skip(건너뛸 도큐먼트 개수), hint(유도할 인덱스 힌트)
      // maxTimeMs(count 실행 최대 시간), readConcern(default local)
      $ db.users.count({name: "matt"}, {limit: 5})
      $ db.users.find({name: "matt"}).limit(5).count({applySkipLimit: true})
      
      • applySkipLimit: true를 해야 skip, limit 이 동작
    • distinct
      $ db.collection.distinct(field, query, options)
      
  • 범용 Aggregation
    $ db.collection.aggregation(pipeline, options)
    
    $ db.users.aggregate([{$group: {_id: "$name", counter: {$sum: 1}}}])
    $ db.users.aggregate([{$match: {score: {$gt: 50}}}, {$group: {_id: "$name", avg: {$avg: "$score"}}}])
    $ db.users.aggregate([{$group: {_id: "name", counter: {$sum: 1}, avg: {$avg: "$score"}}}])
    $ db.users.aggregate([{$project: {year: {$substr: ["$birthday", 0, 4]}}}, {$group: {_id: "$year", number: {$sum: 1}}}])
    $ db.users.aggregate([{$group: {_id: "$name", cnt: {$sum: 1}}}, {$sort: {"cnt": 1}}, {$limit: 20}])
    
    • 데이터를 가공하는 작업은 stage 단위로 작업 구성, stage는 pipeline으로 흘러가면서 원하는 형태의 데이터로 변환
    • option
      • explain: 실행 계획 확인할 수 있다.
      • allowDiskUse: 정렬 시 100MB 메모리를 사용할 수 있고 true일 경우 디스크를 이용하여 정렬 사용
        • _tmp 디렉터리를 만들어 임시 가공용 데이터 파일 저장
      • cursor: aggregate로 반환되는 커서의 배치 사이즈 설정
      • maxTimeMS: Aggregate 명령이 실행될 최대 시간 설정
      • readConcern: aggregate 명령이 도큐먼트 개수를 확인할 때 사용할 readConcern 옵션 설정 (default local)
      • bypassDocumentVadliation: 쿼리 결과를 다른 컬렉션으로 저장하는 경우 저장될 도큐먼트의 유효성 체크 여부 설정
      • collation: 필요한 도큐먼트를 검색할 쿼리에서 사용할 콜레이션 설정 가능
$project
$ db.orders.aggregate([
  { $project: { orderId: 1, total: 1, discountPrice: { $multiply: ["$price", 0.9] } } }
])
  • 역할: 도큐먼트에서 필드를 선택하거나 이름을 변경, 계산된 필드를 생성해 다음 단계로 전달.
  • 용도: 필요한 필드만 골라내어 데이터 양을 줄이고, 연산 최적화.
  • 특징: 필드 숨기기(field: 0), 새 필드 계산($add, $multiply 등 표현식 사용) 가능.
$match
$ db.orders.aggregate([
  { $match: { status: "shipped" } }
])
  • 역할: 조건에 맞는 도큐먼트만 필터링하여 다음 단계로 전달.
  • 용도: SQL의 WHERE 절과 유사, 초기 단계에 배치 시 쿼리 효율 크게 개선.
  • 특징: 필터링 조건은 인덱스 활용 가능하면 매우 빠름. 필터 단계 최대한 앞에 배치하는 게 좋음.
$unwind
db.collection.aggregate([
  { $match: { "myArrayField.myCriteriaField": "myValue" } },
  { $project: {
      myArrayField: {
        $filter: {
          input: "$myArrayField",
          as: "element",
          cond: { $eq: ["$$element.myCriteriaField", "myValue"] }
        }
      }
    }
  },
  { $unwind: "$myArrayField" }
])
  • 역할: 배열 필드를 분해하여 배열의 각 원소마다 별도의 도큐먼트로 확장.
  • 용도: 배열 내부 요소별로 개별 처리를 할 때 필요.
  • 특징: 배열이 없는 도큐먼트는 기본적으로 무시되는데, 옵션으로 처리 가능(preserveNullAndEmptyArrays).
$group
db.collection.aggregate([
  {
    $group: {
      _id: <grouping_expression>,  // 그룹화 기준 필드
      <field1>: { <accumulator1> : <expression1> },
      ...
    }
  }
])
  • 역할: 문서들을 지정된 키(_id)로 그룹화하여 집계연산(합계, 평균, 최대, 최소 등)을 수행.
  • 주요 누적 연산자(Accumultors):
    • $sum: 합계, $avg: 평균, $max: 최대 값, $min: 최소 값, $push: 배열로 값 추가, $addToSet: 중복 없는 배열 생성
  • 특징 그룹화 기준 _id 필드는 필수이며 그룹화된 결과는 이 필드에 저장된다. $group 단계에서는 정렬 지원이 없으므로, 정렬이 필요하면 $sort를 별도로 사용.
$sample
db.collection.aggregate([
  { $sample: { size: <number> } }
])
  • 역할: 컬렉션에서 임의의 문서들을 랜덤으로 지정한 수 만큼 샘플링하여 반환.
  • 특징 정확한 랜덤 샘플링을 수행하며, 대량 데이터에서 랜덤 데이터 추출 시 유용. 단, 데이터 규모가 매우 크면 성능 고려 필요.
$out
db.collection.aggregate([
  ... ,
  { $out: "newCollectionName" }
])
  • 역할: 파이프라인 처리 결과를 새로운 컬렉션에 저장하거나 기존 컬렉션에 결과를 덮어씀.
  • 특징
    • 컬렉션 쓰기 작업이기 때문에, 파이프라인 마지막에 단독으로 사용해야 함
    • 새로운 컬렉션이 없으면 생성하며, 있으면 완전히 대체
    • 인덱스는 복사되지 않으므로 별도 생성해야 함
$addFields
db.scores.aggregate([
  { $addFields: { total: { $add: ["$homework", "$quiz"] } } }
])
// 각 도큐먼트에 total이라는 새 필드가 추가되어 homework와 quiz 점수의 합이 저장됩니다.
  • 역할: 도큐먼트에 새로운 필드를 추가하거나 기존 필드를 수정합니다.
  • 사용법: { $addFields: { : , ... } }
  • 특징
    • 기존 도큐먼트의 모든 필드를 유지하면서 새 필드를 덧붙이거나 변경합니다.
    • 도큐먼트 자체를 변경하는 것이 아니라, 집계 결과에만 반영됩니다.
    • 여러 필드를 동시에 추가/수정할 수 있습니다.
$replaceRoot
db.produce.aggregate([
  { $replaceRoot: { newRoot: "$in_stock" } }
])
// 각 도큐먼트에서 in_stock 필드가 새로운 루트가 되어, 그 안의 필드들만 최상위에 나타납니다.
  • 역할: 도큐먼트의 루트(root) 필드를 새 도큐먼트로 대체합니다.
  • 사용법: { $replaceRoot: { newRoot: } }
  • 특징
    • 도큐먼트 전체를 새 필드 또는 서브도큐먼트로 교체하여 그 필드의 내용만 최상위 도큐먼트로 만듭니다.
    • 주로 중첩된 서브도큐먼트를 루트로 만들어서 더 간단한 형태로 결과를 바꿀 때 사용합니다.
$count
// 조건에 맞는 도큐먼트 개수 세기
db.orders.aggregate([
  { $match: { status: "shipped" } },
  { $count: "shipped_count" }
])
  • 역할: 파이프라인에서 현재까지 처리된 도큐먼트 수를 세고, 그 수를 포함하는 도큐먼트 하나를 결과로 출력합니다.
  • 사용법: { $count: }
  • 특징
    • 파이프라인 내 특정 조건을 만족하는 도큐먼트 개수를 빠르게 집계할 때 사용.
    • 결과 도큐먼트는 { : } 구조입니다.
    • 단독 사용도 가능하며, 다른 스테이지와 함께 조합할 수 있습니다.
  • aggregation 파이프라인 최적화
    • aggregation은 각 스테이지가 순차적으로 처리되며 그 결과를 다음 스테이지로 전달되며 요청을 처리하므로 스테이지 별 건수를 줄이면 성능을 높일 수 있다.
    • $project 스테이지
      • 전체 도큐먼트에서 필요한 필드만 뽑아서 다음 스테이지로 전달하는 역할
      • 꼭 필요한 필드만 뽑아서 스테이지로 데이터를 전달하면 CPU, 메모리 사용량을 낮출 수 있다
      • mongo서버에서는 각 스테이지를 스캔하여 처리해야할 필드만 뽑아서 처리한다.
        • 사용하기 때문에 필드를 조합, 가공 새로운 서브 도큐먼트나 배열 필드가 필요한 경우가 아니라면 명시하지 않아도 된다.
    • 스테이지 순서 최적화($match, $sort, $project, $skip)
      • $sort, $match 스테이지가 순서대로 연결된 경우에는 $match, $sort 스테이지의 순서를 바꿔 실행하도록 최적화한다
        • $match 조건에 일치하는 도큐먼트만 필터링한다음 정렬하도록 최적화
      • $project 스테이지 뒤에 $skip 스테이지가 사용되면 $skip + $project 형태로 앞쪽으로 옮겨서 실행
        • 다음 스테이지에 버려질 도큐먼트를 불필요하게 가공할 필요 없게 한다
    • 스테이지 결합
      • 처리 성능을 높이기 위해 파이프라인에서 2개 이상의 스테이지를 결합하여 처리하기도 한다
      • 동일한 스테이지가 반복해서 사용하고 있는 경우 결합하여 필요한 경우만 사용
    • 인덱스 사용
      • 스테이지에서 가능하면 인덱스를 활용할 수 있는 형태로 최적화한다.
        • 이전 스테이지에서 가공된 결과를 전달받기 때문에 파이프라인의 앞쪽 한두개만 인덱스를 활용하여 최적화 가능
    • 메모리 사용
      • 내부적으로 그룹핑 작업을 처리하기 위해 메모리 사용하며 메모리 사용량은 100MB로 제한되어 있다.
        • 그룹핑해야할 도큐먼트 전체크기가 100MB가 아닌 임시 결과가 100MB를 넘으면 에러 발생
      • allowDiskUse 옵션을 사용하면 메모리 공간 외에 디스크 용량 사용 가능
        • mongo의 데이터 디렉터리 하위에 _tmp 디렉터리를 만들어 임시 가공용 데이터 파일 저장
  • $lookup, $graphLookup
  • $facet
$ db.stores.find({$text: {$search: "java coffee"}}) // 전문 검색 인덱스와 전문 검색을 위한 쿼리 문법 제공
  • 불리언 검색
    db.stores.find({ $text: { $search: "java coffee" } })
    // “java” 또는 “coffee”가 포함된 문서.
    db.stores.find({ $text: { $search: "java coffee -shop" } })
    // “java” 또는 “coffee”를 포함하지만 “shop”은 제외한 문서.
    db.articles.find({$text: { $search: "\"impact crater\" lunar" }})
    // "impact crater" AND "lunar"
    db.articles.find({$text: { $search: "\"impact crater\" lunar meteor"}})
    // "impact crater" AND ("lunar" OR "meteor")
    
    • ”-“ 부호를 이용하여 검색대상에서 제외할 수 있다.
  • 중요도 설정
    $ db.stores.find({$text: {$search: "coffee"}}, {score: {$meta: "textScore"}})
        .sort({score: {$meta: "textScore"}})
    
    • 일치하는 검색어가 어떤 필드에 저장된 값인지에 따라 중요도(weight) 설정 가능
    • getIndexes에서 필드 별 설정된 중요도를 확인할 수 있다
  • 한글과 전문 검색
    • 도큐먼트가 저장될 때 각 필드의 값을 분석해 전문 인덱스를 구성하는 부분을 전문 파서라고 한다.
    • 전문 인덱스는 주요 언어에 대해서 형태소 분석 작업을 거쳐 각 단어의 원형을 인덱스에 저장
    • 한글을 위한 전문 검색 기능은 n-Gram 형태의 전문 파서가 도입해야 한다
  • 한글과 n-Gram 검색
    • 한글에서의 n-gram 토큰화는 공백이 명확히 구분되지 않는 언어적 특성을 보완해준다
    • 텍스트를 연속된 문자 단위로 분할하여 토큰화하는 방식이다
    • 한글 n-gram의 기본 원리
      • n-gram은 원문 문자열을 겹치게 중첩된 문자 조각(sequence) 으로 나누며 각 조각은 n개의 연속된 글자로 구성된다.
        “서울마라톤” → n=2 (bigram)
        text
        서울, 울마, 라마, 마라, 라톤
        
      • 이렇게 분리된 문자 조합(n-gram)은 서로 겹치므로, “라톤”만 입력해도 “서울마라톤”을 검색할 수 있다.
    • 한글 n-gram의 특징
      • 공백 무시: “철학은 어떻게 삶의 무기가 되는가”
        • “철학”, “학은”, “어떻”, “떻게”, “삶의”, “무기”, “기가”, “되는”, “는가” 등으로 분리.
      • 부분 검색 가능: 예를 들어, “삶의” 또는 “무기”로 검색해도 문장 전체가 검색된다.
      • 형태소 분석 대체: 형태소 분석보다 계산은 단순하지만 어절 경계 문제를 회피할 수 있다.
  • 전문 인덱스 성능
    • 전문인덱스(Full-Text Search Index) 성능은 인덱스 생성, 검색 쿼리, 시스템 자원 등 복합적으로 영향을 미친다.
    • 인덱스 구조와 생성
      • 전문인덱스는 텍스트 데이터를 토큰화(단어 분리, 형태소 분석, 불용어 제거 등)해 키로 저장한다.
      • 멀티키 인덱스(multi-key index) 구조를 활용, 대량의 단어 및 문장 패턴을 효과적으로 관리한다.
      • 자동으로 단어의 어근 형태(형태소 분석)를 추출하여 데이터가 많을수록 생성 시간이 증가하고 CPU/메모리 사용량도 커진다.​
    • 쿼리 성능
      • 검색 시 $text 연산자는 멀티키 인덱스를 통해 단어별로 빠르게 일치하는 문서를 찾아낸다.
      • 불용어(stop words)와 어근처리로 검색어와 인덱스의 단어 매칭 정확도를 높인다.
      • 인덱스가 없는 필드나 복잡한 다중 조건에서는 컬렉션 전체 스캔이 발생해 성능 저하가 크다.​
    • 시스템 자원 영향
      • 전문인덱스 유지 관리에 CPU와 메모리, I/O 부하가 상당하다.
      • 대량 데이터, 지속적인 쓰기 작업 환경에서는 성능 저하, 지연 현상이 발생할 수 있어 모니터링과 인덱스 구성 최적화가 중요
      • NVMe 스토리지 등 고성능 하드웨어를 활용하면 인덱스 성능 향상에 도움이 된다.​
    • 최적화 방법
      • 적절한 불용어 사전 관리 및 형태소 분석 도구 설정으로 인덱스의 크기와 질을 조절
      • 쿼리 플래너(explain)를 활용해 실제 인덱스 사용 여부 및 비용을 정기적으로 점검한다.​
      • 필요한 필드만 전문인덱스 대상으로 선택하며, 불필요한 인덱스는 정리해 자원 낭비를 줄인다

8-3 스키마 변경

데이터베이스 관리
  • 데이터베이스 생성 및 삭제
    $ use mysns
    $ db.createCollection("articles")
    $ db.articles.insert({title: "MongoDB", body: "..."}
      
    $ user mysns
    $ db.dropDatabase()
    
    • Mongo에서는 데이터베이스 생성 명령이 제공되지 않고 컬렉션이 생성되거나 저장되는 시점에 데이터베이스가 자동 생성
  • 데이터 베이스 복사
    $ db.copyDatabase(fromDb, toDb)
    $ db.copyDatabase(fromDb, toDb, username, password, mechanism)
    
    • fromhost 외 추가되는 인스턴스가 없으면 같은 mongodb 인스턴스에서 데이터베이스 복제
    • fromhost를 정의하면 리모트 mongo 서버로 접속하여 데이터 복사
  • 샤딩 활성화
    $ db.database.find()
    {_id: "mysns", "primary": "shard01", "partitioned": false}
    
    $ sh.enableSharding("mysns")
    
    • mongo에서 데이터베이스가 최초 생성되면 파티션이 활성화되지 않는다.
    • partitioned 필드가 없거나 false로 설정돼 있다면 샤딩을 적용할 수 없다. enableSharding으로 적용해주어야 한다
  • 데이터베이스 컴팩션
    • 컬렉션에서 삭제·수정·갱신 등으로 인해 데이터 파일이 내부적으로 단편화(fragmentation)되어 불필요한 공간이 늘어난다.
    • MongoDB가 그 빈 공간을 모아 실질적으로 필요한 데이터만 연속적으로 재배치하여 디스크 사용을 최적화하는 과정
    • 디스크 공간이 낭비되고 쿼리 성능에도 불이익이 있을 수 있어 컴팩션을 실행하면 컬렉션 파일 크기를 줄이고 접근 효율을 개선 가능
    • 운영 방식과 제약
      • WiredTiger에서는 compact 명령을 수동으로 실행할 수 있다.​ 컬렉션을 락(lock)해 동작하여 쓰기 작업이 제한
      • 컴팩션은 물리적으로 파일을 재배치하므로, 대용량 컬렉션에서는 상당한 시간과 I/O가 소요될 수 있다.
컬렉션 관리
  • 컬렉션 생성, 삭제, 변경
    • 도큐먼트가 저장되거나 인덱스 생성될 때 컬렉션이 없는 경우 자동으로 컬렉션 생성
    • createCollection 명령으로 컬렉션을 생성할 수 있고 옵션을 주어 사용할 수 있다.
    • 컬렉션 삭제는 drop 명령 사용
  • 컬렉션 복사 및 이름 변경
    $ db.collection.copyTo()
    $ db.cloneCollection()
    $ db.cloneCollectionAsCapped()
    
    • db.collection.copyTo(“newCollection”)
      • 해당 컬렉션의 모든 문서를 지정한 이름의 새 컬렉션으로 복사
      • MongoDB 4.0 이하에서만 사용 가능하고, 그 이후는 지원되지 않는다.
      • 내부적으로 eval을 사용하기 때문에 락이 길게 지속되며, 서비스에 영향이 커서 최신 MongoDB에서는 사용 비추천 또는 불가.​
    • db.cloneCollection()
      • 공식적인 mongosh 명령이 아니라, MongoDB shell·API에서는 지원되지 않는다
      • 공식적으로 컬렉션을 클론(복제)하려면, 집계 파이프라인의 $out 또는, mongodump/mongorestore, mongoexport/mongoimport 등 도구를 사용한다.​
    • db.cloneCollectionAsCapped(“newCollection”, size)
      • 기존 컬렉션을 capped collection(고정 크기)으로 복제한다.
      • 소스 컬렉션의 모든 도큐먼트를 지정한 크기로 capped 컬렉션을 생성하면서 복사한다.
        • 사용 사례는 로그, 큐 등 일정 크기의 순환 저장소가 필요할 때.​
      • 일부 버전만 지원되며, 최신 MongoDB에서는 $out으로 capped 옵션을 조합하거나 애플리케이션단에서 처리해야 할 수 있다.
    • 최신 MongoDB(4.4+)에서 컬렉션 복제
      • 집계 파이프라인의 $out 또는 find().forEach() 루프 활용
      • mongodump/mongorestore 도구를 사용하는 것이 일반적이다.​
      • 인덱스, 설정까지 복사하려면 수동으로 인덱스 생성하거나 관리 도구를 사용 필요
  • 컬렉션 상태 및 메타 정보
    $ db.collection.stats() // 컬렉션 상세 정보 확인
    
    • MMAPv1 스토리지 엔진 출력 정보
      • count: 컬렉션이 가진 도큐먼트 수, size: 도큐먼트가 사용하고 있는 공간의 바이트 수
      • avgObjSize: 평균적으로 하나의 도큐먼트가 사용중인 공간 크기, storageSize: 실제 파일 시스템에 저장된 데이터 파일 크기
      • numExtents: 현재 컬렉션이 사용중인 데이터 파일의 익스텐트 개수, lastExtentSize: 마지막 추가된 익스텐트 크기
        • MMAPv1 스토리지 엔진이 데이터 파일이 확장될 때에는 익스텐트(extent) 단위로 확장
      • paddingFactor: 도큐먼트가 변경될 때, 데이터 파일 내에서 도큐먼트 이동을 최소화하기 위해 붙여두는 빈공간의 크기 비율
      • totalIndexSize: 전체 인덱스가 사용중인 디스크 공간 크기, indexSizes: 인덱스별로 사용중인 디스크 공간 크기
    • WiredTiger 스토리지 엔진 출력 정보
      • ns: 컬렉션의 네임스페이스(database.collection). count: 컬렉션에 저장된 도큐먼트 수
      • size: 컬렉션 내 실제 도큐먼트(압축 해제 기준)의 총 바이트 크기.
      • storageSize: WiredTiger가 물리적으로 디스크에 할당한 컬렉션 데이터 파일의 크기.
      • totalIndexSize: 해당 컬렉션의 모든 인덱스가 디스크에 차지하는 총 용량.
      • nindexes: 인덱스 개수.
      • wiredTiger.uri: 컬렉션의 실제 WiredTiger 테이블 식별자(e.g. statistics:table:collection-12345-6789).
      • wiredTiger.creationString: WiredTiger 내부 파라미터 (압축 알고리즘·페이지 크기 등).
      • LSM / B-Tree 통계: WiredTiger는 기본적으로 B-Tree 구조를 사용한다.
        • btree 하위에는 페이지 수, 압축된 데이터 블록(Node) 수, split 횟수, 페이지 캐시 적중률(lookups) 등이 포함된다.
      • cache: WiredTiger 내부 캐시(memory pool) 사용 현황.
      • bytes currently in cache: 컬렉션 데이터 중 캐시에 적재된 바이트
      • tracked dirty bytes in cache: 아직 디스크에 flush되지 않은 더티 페이지 크기
      • pages read into cache, pages written from cache: 읽기/쓰기 캐시 I/O 횟수
        • 캐시 적중률(Cache hit ratio) 판단에 활용 가능.
      • transaction: MVCC 기반의 트랜잭션 처리 관련 정보.
      • update conflicts: 쓰기 충돌 횟수
      • checkpoint generation: 마지막 체크포인트 주기 (기본 60초마다 새 디스크 스냅샷 생성).​
      • compression: 압축 비율 통계.
        • compressed pages read/written 및 page size reductions로 압축 효율 파악 가능.
      • cursor: find, scan 처리 시 사용된 WiredTiger cursor 열고 닫은 횟수.
        • 성능 및 open cursor 리소스 점검 등에 활용.
      • avgObjSize: 도큐먼트 평균 크기.
      • freeStorageSize: WiredTiger의 내부 단편화(비활용 공간) 추정치.
      • indexDetails: 각 인덱스별 개별 wiredTiger 스토리지 통계가 포함되어 실제 I/O·압축 효율을 별도로 확인할 수 있다.
  • 컬렉션의 프레그먼테이션과 컴팩션
    $ db.runCommand( {compact: '<collection>', force: false} )
    
    • 프레그멘테이션(사용되지 않는 빈 공간)이 많아지면 디스크 공간이 낭비되고 full scan 등에서 성능이 떨어지게 되어 있다.
    • 컴팩션을 통해 프레그멘테이션을 최소화하고 최적화할 수 있다.
      • 데이터 파일의 크기가 커져 여유 공간이 부족할 경우
      • 장착된 메모리보다 데이터 파일이 ㅓ져 쿼리 실행 시 디스크 읽기가 많이 발생할 경우
    • compact 명령은 데이터베이스의 쓰기 잠금을 획득
      • 그래서 기본적으로 프라이머리 멤버에서 실행하지 않는데, 필요하다면 force를 true로 주어 사용할 수 있다
    • 새롭게 secondary 를 추가하여 처음부터 동기화시키면 프레그멘테이션이 최소화된 데이터 파일을 얻을 수 있고, 컴팩션 효과도 얻을 수 있다.
  • 컬렉션 샤딩
    $ db.runCommand({shardCollection: 'database.collecton', key: shardKey, unique: boolean, numInitialChunks: integer, collaction: {locale: "simple"}})
    // unique 는 샤드키를 통해 값이 유니크한 것인지 여부
    // numInitialChunks 는 해시 샤딩인 경우에 사용할 수 있으며 미리 청크 스플릿을 해둘 수 있는데, 미리 생성해 두는 것
    
    • 샤딩 여부에 대해서는 쿼리 빈도나 패턴, 데이터 크기등을 고려하여 결정해야 한다
      • 도큐먼트 건수가 너무 많은 경우 샤드로 분산해야 하며 컬렉션 크기가 커질 수록 쿼리 사용도 많아지게 되어 처리량도 분산
      • 건수가 많진 않아도 데이터 변경이 빈번히 발생하는 경우 샤딩을 통해 쿼리를 분산할 수도 있다
    • 샤드키에 대한 조건을 통해 Targetted Query, Broadcast Query 로 나뉜다
    • 레인지 샤딩보다는 해시 샤딩을 사용하는 것이 좋다
      • 데이터 불균형이 심해질 수 있고 샤드 추가/제거 시 청크 이동이 심해질 수 있다
    • 이미 많은 데이터를 갖고 있는 경우 “청크 스플릿 실행”/”청크 밸런싱” 과정을 거친다
      • 청크 스플릿을 위해 컬렉션의 풀스캔 실행, 데이터 파일이 매우 크면 청크 스플릿이 실패할 수 있다
인덱스 관리
  • 인덱스 생성 및 삭제
    $ db.users.createIndex( {username: 1} ) // 인덱스 포그라운드에서 생성
    $ db.users.createIndex( {username: 1}, {background: true} ) // 인덱스 백그라운드에서 생성
    
    $ db.users.dropIndex( {username: 1} )
    
    • 인덱스 생성
      • 백그라운드에서 인덱스를 생성하면 내부적으로 컬렉션이 소속된 데이터베이스에 잠금을 걸고 컬렉션의 부분 데이터를 읽어 인덱스를 빌드하는 작업 진행
        • 그리고 잠금을 해제하여 다른 커넥션들이 쿼리를 실행할 수 있도록 여유 시간을 반들어준다 (Yield)
        • 즉 인덱스 생성/쿼리가 동시에 진행하는 것이 아닌 처리 시간을 나누어 가지므로 생성하는 데 더 많은 시간 필요
      • 포그라운드 방식
        • 해당 컬렉션에 대해 글로벌 락(exclusive lock)이 걸려, 모든 읽기와 쓰기 작업이 중단됨.
        • 인덱스 빌드가 빠르고 최적화된 인덱스 구조를 생성할 수 있지만 서비스 중단이 불가피하여, 장애나 서비스 지연이 발생할 수 있다
      • 샤딩 환경 주의해야 한다
        • sharded collection에서는 인덱스 구조, shard key, 인덱스 일치(모든 shard에 동일 인덱스)를 반드시 지켜야 한다.
    • 인덱스 삭제
      • 인덱스를 삭제하면 해당 인덱스를 활용하던 쿼리가 collection scan으로 바뀌면서 성능이 악화될 수 있다.
      • 샤딩 환경에서는 shard key 인덱스는 삭제 불가하다
        • shard key로 지정된 인덱스는 삭제할 수 없으며, 관련 인덱스 관리에 주의해야 한다.
    • Primary/Secondary 동기화
      • 인덱스 삭제/생성은 레플리카셋 환경에서 Primary에서만 작업해야 하며, Secondary에는 자동으로 반영된다.
  • 인덱스 목록 조회
    $ db.users.getIndexes() 
    $ db.users.getIndexKeys() // 인덱스를 구성하는 필드만 출력
    $ db.users.stats({"indexDetails": true}) // 인덱스가 저장된 데이터파일 및 인덱스 상세 정보 출력
    

    9장 실행 계획 및 쿼리 최적화

  • 세컨더리 인덱스를 지원하기 때문에 최소 1개 이상의 인덱스를 가질 수 있다
    • 조회 쿼리에서 어떤 인덱스를 사용하여 최적화를 할것인지, 그룹핑, 정렬 등에서도 인덱스에 대한 작업이 필요하다

실행 계획

  • 쿼리의 처리 과정
    • 일반적인 4개의 실행 계획을 트리 구조로 표현
      • fetch -> ixscan: 인덱스 레인지 스캐을 실행한 다음 컬렉션 데이터 파일에서 도큐먼트를 읽는 실행 계획
      • sort -> collscan: 컬렉션 풀 스캔으로 조건에 일치한 도큐먼트를 읽은 후 정렬을 수행하는 실행 계획
      • sort -> fetch -> ixscan: 인덱스 레인지 스캐을 실행하여 컬렉션 데이터 파일에서 도큐먼트를 읽고 정렬하는 실행 계획
      • fetch -> sort_merge -> (ixscan, ixscan)
        • 인덱스 인터섹션으로 레인지 스캔을 실행한 다음 컬렉션 데이터 파일에서 도큐먼트를 읽는 실행 계획
    • 실행 계획 트리는 최상위 스테이지를 루트라 하며, 자식 스테이지를 호출한다.
      • 각 스테이지를 호출하는 API의 이름이 work 라는 단어로 표현
      • work는 3종류(ADVANCE, NEED_TIME, IS_EOF)의 결과를 리턴한다.
        • ADVANCE: 스테이지의 처리 결과 한건의 도큐먼트/ID 반환
        • NEED_TIME: 처리는 완료됐지만, 결과 도큐먼트/ID 가 반환되진 않음(인덱스를 이용하여 읽었는데, 다른 조건에 의해 필터링)
        • IS_EOF: 처리 완료 및 더이상 읽을 데이터가 없는 경우
  • 실행 계획 수립
    • 한번 실행했던 쿼리의 실행 계획은 캐시에 저장하고, 만약 같은 패턴의 쿼리가 다시 요청하면 재사용된다
      • 같은 쿼리인지 판단을 위해 Query Shape(3가지 정보 - 쿼리 조건, 정렬 조건, 조회 필드) 사용
    • 쿼리에서 실행 계획을 수립할 때
      • QueryShape 검색하여 있으면 캐싱된 쿼리 실행계획으로 쿼리 실행
      • QueryShape 검색하여 없으면 후보 실행 계획 수립, 평가, 최종 실행 계획 선택, 캐시 등록 후 쿼리 실행
    • 컬렉션이 삭제되거나 컬렉션의 인덱스 생성/삭제될 때 캐시된 실행계획은 모두 삭제된다
  • 옵티마이저 옵션
    • internalQueryPlanEvaluationCollFraction(0,3), internalQueryPlanEvaluationWorks(10000) 옵션을 활용하여 실행계획 수립 과정에서 사용할 수 있는 최대 works 횟수 결정
    • works 함수 호출 횟수 내에 internalQueryPlanEvaludationMaxResults(101)로 설정된 건수만큼 도큐먼트 반환
    • internalQueryPlannerEnableIndexIntersection과 internalQueryPlannerEnableHashIntersection 옵션은 인덱스 인터섹션 최적화를 사용할 것인지 설정
      • Index Intersection(인덱스 교차)
        • 단일 쿼리에서 여러 필드가 검색 조건에 포함되어 있고, 각 필드에 각각 단일 인덱스가 존재할 때, MongoDB가 각각의 인덱스 결과를 내부적으로 교집합(AND) 연산하여 사용하는 기능이다.​
        • 장점: 복합 인덱스 없이 여러 인덱스의 조합도 활용 가능하므로, 인덱스 설계 유연성이 높아진다.
        • 제약: 복합 인덱스(compound index)에 비해 성능은 떨어질 수 있으며, 특정 정렬이나 복잡한 조건에서 사용 제한
      • Hash Intersection(AND_HASH)
        • 여러 인덱스의 결과를 해시 자료구조로 변환해, 각 인덱스의 결과집합 간 빠른 교집합 연산을 수행하는 내부 방식
        • 관련 유의사항
          • index intersection은 복합 인덱스가 모든 쿼리 조건을 커버하지 못하거나, 인덱스 개수가 너무 많아질 때 MongoDB가 자동 선택하는 절충적 방법이다.
          • 복합 인덱스를 잘 설계하면 더 빠르지만, 모든 조합을 커버하는 복합 인덱스가 어려운 경우 index intersection이 쿼리 효율을 높인다.​
  • 플랜 캐시
    • 쿼리가 느리다면 실제 실행 계획을 수립하는 과정에서 많은 시간이 걸리기도 하며, 이렇게 플랜 캐시에 저장된 실행 계획은 아래 이벤트로 인해 삭제되기도 한다.
      • 인덱스가 생성/삭제되는 경우, reindex() 명령이 실행되는 경우, mongodb 가 재시작되는 경우
    • 2.6 버전 이전에는 쓰기 작업이 1000번 이상이면 실행 계획을 새로 수립하도록 했다(인덱스 분포도가 달라지기 떄문)
    • 3.0부터는 쓰기 횟수 상관없이 실행계획을 유지하고 쿼리가 실행될 때 간단히 평가하는 단계를 구현하여 성능이 나쁜 경우 실행계획을 다시 수집하는 방식으로 해결
    • PlanCache 객체와 관련된 기능 제공
      $ db.users.getPlanCache().listQueryShapes() // 컬렉션의 캐시된 실행 계획 확인
      $ db.users.getPlanCache().getPlansQyQuery({name: "Lara"}) // 해당 실행계획이 선택된 이유 확인
      $ db.users.getPlanCache().clear() // 플랜 캐시에 저장된 실행계획 전체 삭제
      $ db.users.getPlanCache().clearPlansByQuery({name: "Lara"}) // 플랜 캐시에 저장된 실행계획 삭제
      
  • 실행계획 스테이지
    • collscan: 풀스캔 스테이지로 주어진 컬렉션으로부터 데이터를 읽어 출력만 만들어내는 스테이지
    • ixscan: 인덱스 레인지 스캔 접근 방식으로 데이터를 읽는 스테이지
    • queued_data: 컬렉션 없이 자체적으로 임시 데이터를 만드는 스테이지
    • fetch: 인덱스 레인지 스캔으로 읽은 인덱스 키와 record id 를 이용해 컬렉션의 도큐먼트를 읽는 스테이지
      • ixscan으로 부터 입력(record id)를 받아 도큐먼트를 출력으로 반환
    • keep_mutations: 인덱스 인터섹션 실행 계획을 사용. MMAPv1 스토리지 엔진에서 사용, WiredTiger에서는 사용하지 않음
    • and_sorted, and_hash: 인덱스 인터섹션 계획을 사용할 때, 인덱스를 통해 읽은 도큐먼트의 교집합을 찾는다
    • sort_key_generator: 인덱스를 사용하지 못할 때 정렬 기준 필드를 먼저 추출하는 과정
    • count: 자식 스테이지에서 반환한 도큐먼트의 건수를 누적하는 스테이지
    • count_scan: db.collection.count() 명령이 인덱스를 사용할 수 있을 때의 스테이지
    • distinct_scan: 인덱스 레인지 스캐의 변형된 형태로 순차적으로 읽으면서 유니크한 값을 나타낼 때 부모 스테이지로 결과 반환
    • ensure_sorted: 자식 스테이지 결과가 정렬 기준에서 어긋나는 경우 버리는 처리 수행
    • group: aggregation $group 파이프라인을 위새 사용하는 스테이지
    • idhack: _id 필드를 동등 비교로 검색하는 쿼리
    • index_iterator: 인덱스 스캔을 이용하는 커서를 끝까지 인덱스 키를 읽는 스테이지
    • limit: limit 이 사용될 때 N건의 도큐먼트만 반환하는 스테이지
    • skip: skip이 사용되는 경우 M건의 도큐먼트를 버리고 나머지를 반환
    • sort_merge: 두개 이상의 자식 노드(스테이지)에서 반환된 결과 집합을 병합
    • multi_iterator: 병렬 컬랙션 쿼리 나 RepairCursor에서 사용되는 스테이지
    • sharding_filter: 샤드된 데이터에서 청크 범위 외에 고아 도큐먼트를 가질 수 있는데, 이런 고아 도큐먼트를 필터링해서 버림
    • sort: 인덱스를 이용하지 못하는 정렬처리를 위해서 쿼리 실행 시점에 도큐먼트 정렬 수행
    • text, text_match, text_or:: 쿼리의 전문 검색 조건
    • update: update 명령을 처리하는 스테이지
    • delete: delete 명령 처리 스테이지
  • 쿼리 실행 계획 해석
    • queryPlanner: 디폴트 값으로 최적의 실행 계획을 보이며 가장 단순한 형태
      • winningPlans: 선택된 최적 실행 계획, rejectedPlans: 버려진 실행 계획
      • queryPlanner 하위 스테이지는 왜 인덱스를 사용하는 지, 왜 정렬을 사용하는지 등의 내용 표현
      • 실제 트리 형태로 스테이지가 나타나며 부모 스테이지에서 자식 스테이지로 나타난다.
      • 실제 자식 스테이지부터 부모 스테이지 방향으로 실행된다.
    • executionStats: queryPlanner 모드의 모든 내용을 포함하며 최적 실행 계획과 실행 내역을 보여준다
      • executionStages 필드에 표시되는 스테이지를 처리하기 위해 work 함수가 몇번 호출됐고, 반환 등의 상세 정보 표현
      • stage: 현재 스테이지 타입
      • nReturned: 부모 스테이지로 반환된 결과 건수
      • executionTimeMillisEstimate: 자식 스테이지를 포함한 현재 스테이지 처리 시간
      • works: 현재 스테이지에서 호출된 work 수
      • needTime: 현재 스테이지에서 반환하지 못한 건수
      • needYield 현재 스테이지에서 yield 실행 건수
      • isEOF: 현재 스테이지에서 eof가 반환 여부
      • executionTimeMillisEstimated: 현재 스테이지 처리 시간
    • allPlansExecution: 최적 실행 계획과 그 상세 내역, 나머지 후보 실행 계획들의 내용도 포함
      • executionStages 를 배열로 표현되며 각 후보 실행 계획의 상태 정보 출력
  • update와 remove 그리고 aggregation 쿼리의 실행 계획
    $ db.users.explain("queryPlanner").update({name: "6094"}, {$set: {fd: 1}})
    $ db.users.explain("queryPlanner").remove({name: "6094"})
    $ db.collections.aggregate([], {explain: true})
    $ db.collection.explain().aggregate()
    
    • 이런 형태로 실행 계획을 실행할 수 있다.

쿼리 최적화

  • 실행 계획의 쿼리 튜닝 포인트
    • 쿼리가 인덱스를 사용하는 지
      • COLLSCAN 스테이지가 아닌 IXSCAN이 발생하도록 하는 것이 중요
    • 도큐먼트 정렬이 인덱스를 사용하는 지
      • RDB처럼 정렬 순서가 인덱스 순서대로 데이터를 반환하기 때문에 별도 정렬처리를 제외할 수 있다.
      • 만약 그렇지 않으면 SORT 스테이지에서 도큐먼트를 정렬하는 단계를 거친다.
    • 필드 프로젝션이 인덱스르 사용(커버링 인덱스) 하는 지
      • 인덱스를 통해 RecordId를 검색하고, 실제 컬렉션 데이터 파일에서 해당 도큐먼트를 찾는 랜덤 액세스 방식으로 처리
      • 만약 필요한 필드가 인덱스에만 있는 경우 인덱스 스캔으로만 해결 가능(fetch 스테이지가 있는지, 없는지)
    • 인덱스 키 엔트리와 도큐먼트를 얼마나 읽었는 지
      • totalKeyExamined, totalDocsExamined 필드 값은 쿼리가 처리되면서 읽은 인덱스 키와 도큐먼트 개수 의미
      • totalKeyExamined 필드 값이 조금 높은 수치는 괜찮은 성능을 보이지만 totalDocsExamined 값이 높다면 튜닝이 필요할 수 있다.
    • 인덱스의 선택도는 얼마나 좋은 지
      • 실행계획에서 사용되는 인덱스가 조건을 얼마나 커버하는 지 성능상 매우 종요 요소
        $ db.users.find({birth_year: 1990, user_type: "P"})
        
      • 만약 birth_year 만 인덱스가 되어 있다면 user_type 조건은 한건씩 도큐먼트를 읽어서 비교해야 한다
      • birth_year, user_type으로 복합 인덱스를 준비하면 더 빠른 성능을 보인다
      • 만약 대부분의 user_type이 P 라면 user_type + birth_year 복합인덱스는 큰 도움이 되지 않는다.
      • 즉, 인덱스의 각 필드가 쿼리 작업 범위를 줄일 수 있는지 확인 필요
      • find, update, remove 전체를 고려한 인덱스 설계 필요
    • 어떤 스테이지가 가장 많은 시간을 소모하는가
      • 각 스테이지는 자식 스테이지를 포함하여 자신이 실행되는 데 소요된 시간(executionTimeMillis)를 보여준다
      • 각 스테이지가 처리되는 데 걸린 시간을 계산하여 많이 걸린 스테지를 집중적으로 최적화할 수 있다
  • 슬로우 쿼리 로그 분석 및 튜닝
    • mongo에서는 100 밀리초가 넘으면 로그 파일에 모두 로깅되며 100 밀리초는 서버에 설정된 기본값으로 slowMs 옵션 조정 가능
    • 로그 파일에 가장 중요한 값은 planSummary, keyExamined, docsExamined, numYields 값
      • planSummary는 collscan 또는 ixscan 이 발생했는 지 알 수 있다
      • keyExamined, docsExamined는 쿼리를 처리하기 위해 읽은 인덱스 키의 갯수와 도큐먼트 개수를 보여준다.
        • 이 두값의 차이를 비교하면 인덱스 효율성을 알 수 있다
      • numYields 는 쿼리가 장시간 실행되면서 다른 커넥션들이 쿼리를 실행할 수 있도록 잠금을 해제/획득하는 과정을 보여준다.
  • 쿼리 프로파일링
    • 로그 파일에는 매우 많은 정보가 기록되며, 때로는 로그 파일로 슬로우 쿼리 로그 정보를 수집하는 것이 어려울 수 있다.
    • 쿼리 프로파일링 컬렉션(system.profile)에 저장된 슬로우 쿼리 로그의 프로파일링 정보를 샘플링
      $ db.system.profile.find().sort({$natural: -1}).pretty()
      
      • 1MB를 넘어서면 오래된 로그는 삭제하고 새로 발생한 슬로우 쿼리 로그 저장
      • system.profile 컬렉션의 슬로우 쿼리 로그를 확인할 때는, 역순으로 정렬해야 시간 순으로 볼 수 있다
      • system.profile 컬렉션이나 커맨드의 종류별로 필터링해서 슬로우 쿼리 로그를 확인할 수 있는 장점 제공
  • 인덱스 힌트
    • mongo에서도 세컨드리 인덱스를 지원하며 쿼리 처리할 때 최적 인덱스를 선정하는 옵티마이저의 실행 계획 수립 단계를 거친다
    • 옵티마이저가 수립하는 계획이 최적이 아닐 수 있기 때문에 hint를 주어 다른 방향으로 유도할 수 있다.
      $ db.users.find({name: "matt"}).sort({score: 1).hint({score: 1, name: 1}).explain()
      $ db.users.find({name: "matt"}).sort({score: 1).hint("ix_score_name").explain()
      $ db.users.find({name: "matt"}).sort({score: 1).hint({$natural: 1}).explain() // 풀스캔 유도
      
    • 인덱스가 변경되면 응용 프로그램에서 사용되는 쿼리 문장의 인덱스 힌트도 같이 변경되어야 한다