DB/몽고DB 완벽 가이드 3판

[4장] 쿼리

꾸준함. 2025. 3. 29. 21:50

1. find 소개

  • 몽고DB에서 find 함수는 쿼리에 사용
    • 쿼리는 컬렉션에서 도큐먼트의 subset을 반환
    • find의 첫 매개변수에 따라 어떤 도큐먼트를 가져올지 결정
    • 빈 쿼리 도큐먼트({})는 컬렉션 내 모든 것과 일치하며 매개변수에 쿼리 도큐먼트가 없으면 find 함수는 빈 쿼리 도큐먼트 {}로 인식

 

  • 쿼리 도큐먼트에 여러 key-value 쌍을 추가해 검색을 제한할 수 있음
    • 대부분의 데이터형에서 간단히 작동하며 정수형은 정수형에, 불리언형은 불리언형에, 문자열형은 문자열형에 일치함
    • 간단한 데이터형은 찾으려는 값만 지정하면 쉽게 쿼리 할 수 있음

 

 

1.1 반환 받을 키 지정

  • find 또는 findOne의 두 번째 매개변수에 원하는 키를 지정하면 원하는 key-value 정보만 조회 가능
    • 이는 네트워크상의 데이터 전송량과 클라이언트 측에서 도큐먼트를 디코딩하는 데 드는 시간과 메모리를 줄여줌
    • i.g. 사용자 정보 컬렉션에서 "username"과 "email" 키의 값만 원할 때는 아래와 같이 쿼리

 

 

  • 또한 두 번째 매개변수를 사용해서 특정 key-value 쌍을 제외한 결과를 얻을 수도 있음
    • i.g. 다양한 키가 있는 도큐먼트에서 "fatal_weakness" 키 값을 쓸 일이 전혀 없다면 아래와 같이 제외시킴
    • _id 값 또한 반환 제외시킬 수 있음

 

 

1.2 제약 사항

  • 쿼리에는 몇 가지 제약이 존재함
    • 데이터베이스에서 쿼리 도큐먼트 값은 반드시 상수여야 하는데 이는 도큐먼트 내 다른 키의 값을 참조할 수 없음을 의미
    • $where 쿼리와 같은 방법이 있지만 일반 쿼리로 처리할 수 있게 도큐먼트 구조를 약간 재구성하면 더 나은 성능을 얻을 수 있음

 

 

2. 쿼리 조건

  • 쿼리는 완전 일치 외에도 범위, OR 절, 부정 조건 등 복잡한 조건으로 검색 가능

 

2.1 쿼리 조건절

  • <, <=, >, >=에 해당하는 비교 연산자는 각각 "$lt", "$lte", "$gt", "$gte"
    • 조합해 사용하면 특정 범위 내 값을 쿼리 가능

 

 

2.2 OR 쿼리

  • 몽고DB에서 OR 쿼리에는 두 가지 방법이 있음
    • "$in"은 하나의 키를 다양한 값과 비교하는 쿼리에 사용
    • "$or"은 더 일반적이며, 여러 키를 주어진 값과 비교하는 쿼리에 사용

 

  • 하나의 키에 일치시킬 값이 여러 개 있다면 "$in"에 조건 배열을 사용
  • "$in"은 매우 유연해 여러 개의 값을 쓸 수 있을 뿐 아니라 서로 다른 데이터형도 사용 가능
  • "$in"의 조건 배열에 값이 하나만 주어지면 바로 일치하는 것을 찾음
  • "$nin"은 "$in"과 반대로 배열 내 조건과 일치하지 않는 도큐먼트를 반환

 

 

  • "$or"은 가능한 조건들의 배열을 취함
  • "$or"은 다른 조건절도 포함 가능
    • 일반적인 AND 쿼리에서는 최소한의 인수로 최적의 결과를 추려내야 하는 반면 OR 쿼리는 반대로 첫 번째 인수가 일치하는 도큐먼트가 많을수록 효율적

 

 

  • "$or" 연산자가 항상 작동하는 동안에는 가능한 한 "$in"을 사용하는 것을 권장
    • 쿼리 옵티마이저는 "$in"을 더 효율적으로 다룸

 

2.3 $not

  • "$not"은 메타 조건절이며 어떤 조건에도 적용할 수 있음
  • "$not"은 정규 표현식과 함께 사용해 주어진 패턴과 일치하지 않는 도큐먼트를 조회할 때 특히 유용

 

 

3. 형 특정 쿼리

  • 몽고DB에서는 도큐먼트 내 다양한 데이터형을 사용할 수 있으며 일부 데이터형은 쿼리 시 형에 특정하게 작동함

 

3.1 null

  • null은 스스로 일치하는 것을 찾음
    • null은 `존재하지 않음`이라는 뜻이므로 키가 null인 값을 쿼리 하면 해당 키를 갖지 않는 도큐먼트도 반환함 i.g. 아래 사진에서 db.c.find({"z": null}) 참고
    • 값이 null인 키만 찾고 싶다면 키가 null인 값을 쿼리하고, "$exists" 조건절을 사용해 null 존재 여부를 확인하면 됨

 

 

3.2 정규 표현식

  • "$regex"는 쿼리에서 패턴 일치 문자열을 위한 정규식 기능 제공
  • 정규 표현식은 문자열 일치를 유연하게 하는 데 유용
    • i.g. 이름이 Joe나 joe인 사용자를 모두 조회하기 위해 정규 표현식을 이용해 대소문자 구별 없이 조회

 

db.users.find({"name": {"$regex": /joe/i}})

 

  • 몽고DB는 정규 표현식 일치에 펄 호환 정규 표현식 (PCRE) 라이브러리를 사용하며, PCRE에 쓸 수 있는 모든 문법은 몽고DB에서도 사용 가능
    • 쿼리 하기쿼리 하기 전에 먼저 자바스크립트 셸로 해당 정규 표현식이 의도한 대로 동작하는지 확인해보는 것을 권장

 

3.3 배열에 쿼리하기

  • 배열 요소 쿼리는 스칼라 쿼리와 같은 방식으로 동작하도록 설계됨
    • i.g. 아래처럼 쿼리를 이용해서 일치하는 도큐먼트를 성공적으로 찾을 수 있음

 

 

부연 설명

  • 이때 배열을 {"fruit": "apple", "fruit": "banana", "fruit": "peach"}와 같은 도큐먼트로 가정

 

$all 연산자

  • 두 개 이상의 배열 요소가 일치하는 배열을 찾으려면 "$all"을 사용하며 이는 배열 내 여러 요소와 일치하는지 확인하게 해 줌
    • i.g. "apple"과 "banana" 요소를 "$all" 연산자와 함께 써서 해당 도큐먼트를 조회할 수 있음
    • 순서는 중요하지 않으며 배열에 요소가 하나뿐일 때는 "$all" 연산자 사용 여부와 관계없이 결과가 같음

 

 

  • 배열 내 특정 요소를 쿼리 하려면 key.index 구문을 이용해 순서를 지정
    • 배열 인덱스는 0-index

 

 

$size 연산자

  • "$size"는 특정 크기의 배열을 쿼리 하는 유용한 조건절

 

db.food.find({"fruit": {"$size": 3}})

 

  • 자주 쓰이는 쿼리로, 크기의 범위로 쿼리 할 수 있음
    • "$size"는 다른 $ 조건절과 결합해 사용할 수 없지만 도큐먼트에 "size" 키를 추가하면 여러 쿼리를 처리 가능
    • 값의 증가는 매우 빠르게 이뤄지므로 성능은 크게 걱정할 필요 없고 도큐먼트를 저장하고 나면 아래와 같은 쿼리가 가능함

 

 

$slice 연산자

  • find의 두 번째 매개변수에는 반환받을 특정 키를 지정할 수 있음 (optional)
    • "$slice" 연산자를 사용해서 배열 요소의 부분집합을 반환받을 수 있음

 

 

  • 명시하지 않는 키는 반환하지 않는 다른 키 명시자들과는 달리 "$slice" 연산자는 특별히 명시하지 않는 한 도큐먼트 내 모든 키를 반환

 

 

일치하는 배열 요소의 반환

  • 배열 요소의 인덱스를 알고 있다면 "$slice" 연산자가 유용하게 쓰이지만, 때로는 특정 기준과 일치하는 배열 요소를 원할 수도 있음
    • $ 연산자를 사용하면 일치하는 요소를 반환받을 수 있음
    • i.g. 블로그 예제에서 Bob이 쓴 댓글을 얻으려면 아래와 같이 쿼리 작성

 

 

부연 설명

  • 각 도큐먼트에서 첫 번째로 일치하는 댓글만 반환
  • Bob이 해당 게시물에 댓글을 여러 개 남기면 "comments" 배열에 존재하는 첫 번째 댓글만 반환

 

배열 및 범위 쿼리의 상호작용

  • 도큐먼트 내 스칼라 (비배열 요소)는 쿼리 기준의 각 절과 일치해야 함
    • i.g. {"x": {"$gt": 10, "$lt": 20}}과 같이 쿼리 했다면 "x"는 10보다 크고 20보다 작아야 함

 

  • 하지만 도큐먼트와 "x" 필드가 배열이라면 각 절의 조건을 충족하는 도큐먼트가 일치되며 각 쿼리 절을 서로 다른 배열 요소와 일치할 수 있음
    • i.g. "x"의 값이 10과 20 사이인 도큐먼트를 모두 찾으려 할 때, 쿼리를 단순히 db.test.find({"x": {"$gt": 10, "$lt": 20}})과 같이 구성하면 {"x": 15}와 같이 전체 일치하는 도큐먼트뿐만 아니라 {"x": [5, 25]}와 같이 첫 번째 절만 일치하는 도큐먼트도 반환
    • 정리하면 범위가 모든 다중 요소 배열과 일치하기 때문에 해당 방법을 사용하면 배열에 대한 범위 쿼리가 본질적으로 쓸모 없어짐

 

  • 원하는 결과를 얻기 위해서는 다음 두 가지 방법 중 하나를 사용해야 함
    • "$elemMatch" 연산자를 사용하면 몽고DB는 두 절을 하나의 배열요소와 비교하지만 "$elmMatch" 연산자는 비배열 요소를 일치시키지 않는다는 함정이 있음 ("$elemMatch"는 배열 요소에 대한 범위 쿼리에 유용)
    • 쿼리 하는 필드에 인덱스가 있다면 min 함수와 max 함수를 사용해 "$gt"와 "$lt" 값 사이로 인덱스 범위를 제한해 쿼리 할 수 있음

 

 

  • 일반적으로 배열을 포함하는 도큐먼트에 범위 쿼리를 할 때 min 함수와 max 함수를 사용하면 좋음
    • 배열에 대한 "$gt"/"$lt" 쿼리는 어떤 값이든 허용하므로 범위 내 값뿐만 아니라 모든 인덱스 항목을 검색하기 때문에 비효율적

 

3.4 내장 도큐먼트에 쿼리 하기

  • 내장 도큐먼트 쿼리는 다음과 같이 두 가지 방식으로 나뉨
    • 도큐먼트 전체를 대상으로 하는 방식
    • 도큐먼트 내 key-value 쌍 각각을 대상으로 하는 방식

 

  • 전체 도큐먼트를 대상으로 하는 쿼리는 일반적인 쿼리와 동일하게 작동하지만 서브 도큐먼트 전체에 쿼리 하려면 서브 도큐먼트와 정확히 일치해야 함 (순서 또한 일치해야 함)
  • 내장 도큐먼트에 쿼리 할 때는 가능하다면 특정 키로 쿼리하는 방법이 좋음
    • 도큐먼트 전체를 대상으로 정확히 일치시키는 방법이 아니므로 스키마가 변경되더라도 모든 쿼리가 정상적으로 작동함
    • 내장 도큐먼트의 키를 쿼리할 때는 점 표기법을 사용
    • i.g. 아래 쿼리는 name.middle 키가 추가되더라도 점 표기법을 사용했기 때문에 name.first와 name.last가 일치할 경우 도큐먼트를 반환함

 

db.people.find({"name.first": "Joe", "name.last": "Scmoe"})

 

 

  • 쿼리 도큐먼트와 다른 도큐먼트의 타입의 큰 차이점은 점 표기법
    • 쿼리 도큐먼트는 점을 포함할 수 있고, 이는 내장 도큐먼트 내 항목에 접근할 수 있다는 의미
    • 또한 점 표기법은 입력하는 도큐먼트에. 문자를 사용할 수 없는 이유이기도 함
      • 이러한 제약 조건은 URL을 키로 저장할 때 자주 문제를 일으키지만
      • 도큐먼트를 입력하기 전이나 꺼낸 후에 . 문자를 URL에 쓰이지 않는 문자로 치환하는 방법으로 해결 가능

 

  • 내장 도큐먼트 구조가 복잡해질수록 일치하는 내장 도큐먼트를 찾기가 어려워짐
    • i.g. 저장된 블로그 게시물에서 5점 이상을 받은 Joe의 댓글을 찾는다고 가정했을 때 다음과 같이 쿼리를 작성

 

 

부연 설명

  • "$elemMatch"를 사용해 조건을 `그룹화` 가능하고 해당 기능은 내장 도큐먼트에서 두 개 이상의 키의 조건 일치 여부를 확인할 때만 필요

 

4. $where 쿼리

  • key-value 쌍만으로 꽤 다양한 쿼리를 할 수 있지만 정확하게 표현할 수 없는 쿼리도 있음
  • 이때 "$where" 절을 사용해 임의의 자바스크립트를 쿼리의 일부분으로 실행하면 모든 거의 쿼리를 표현 가능
    • 따라서 보안상의 이유로 "$where" 절 사용을 제한해야 함

 

  • "$where" 절은 도큐먼트 내 두 키의 값을 비교하는 쿼리에 가장 자주 쓰임

 

 

  • "$where" 쿼리는 일반 쿼리보다 훨씬 느리니 반드시 필요한 경우가 아니면 사용 안 하는 것을 권장
    • "$where" 절 실행 시 각 도큐먼트는 ABSON에서 자바스크립트 객체로 변환되기 때문에 오래 걸림
    • 또한 "$where" 절에는 인덱스를 쓸 수 없음
    • 따라서 "$where" 절은 달리 쿼리 할 방법이 전혀 없을 때만 사용해야 함

 

  • "$where" 절을 다른 쿼리 필터와 함께 사용하면 성능 저하를 줄일 수 있음
    • 가능한 한 "$where" 절이 아닌 조건은 인덱스로 거르고, "$where" 절은 결과를 세부적으로 조정할 때 사용하는 것을 권장

 

  • $expr을 사용하면 자바스크립트를 실행하지 않아 더 빨리 쿼리할 수 있으므로 가능한 한 $where 대신 $expr을 사용하는 것을 권장

 

5. 커서

  • 데이터베이스는 커서를 사용해 find의 결과를 반환
    • 일반적으로 클라이언트 측의 커서 구현체는 쿼리의 최종 결과를 강력히 제어하게 해 줌
    • 결과 개수를 제한하거나, 결과 중 몇 개를 건너뛰거나, 여러 키를 조합한 결과를 어떤 방향으로든 정렬하는 등 다양하게 조작할 수 있음

 

  • 셸에서 커서를 생성하려면 컬렉션에 도큐먼트를 집어넣고 쿼리 한 후 결과를 지역 변수에 할당
    • 이렇게 하면 결과를 한 번에 하나씩 볼 수 있다는 장점이 있음

 

 

  • 결과를 얻으려면 커서의 next 메서드를 사용하고, 다른 결과가 있는지 확인하려면 hasNext를 사용하며 결과를 확인하는 반복문은 일반적으로 다음과 같음
    • cursor.hasNext()는 다음 결과가 존재하는지 확인하고 cursor.next()는 그 결과를 가져옴

 

 

  • 또한 cursor 클래스는 자바스크립트의 반복자 인터페이스를 구현했으므로 forEach 반복문에 사용할 수 있음

 

 

  • find()를 호출할 때 셸이 데이터베이스를 즉시 쿼리하지는 않으며 결과를 요청하는 쿼리를 보낼 때까지 대기함
    • 따라서 쿼리 하기 전에 옵션을 추가 가능
    • 또한 cursor 객체상의 거의 모든 메서드가 커서 자체를 반환하므로 옵션을 어떤 순서로든 이어 쓸 수 있음

 

 

부연 설명

  • 셸은 next나 hasNext 메서드 호출 시 서버 왕복 횟수를 줄이기 위해, 한 번에 처음 100개 또는 4MB 크기의 결과를 가져옴
  • 클라이언트가 첫 번째 결과 셋을 살펴본 후에, 셀이 데이터베이스에 다시 접근해 더 많은 결과를 요청
  • getMore 요청은 기본적으로 커서에 대한 식별자를 가지며, 데이터베이스가 다음 배치를 반환하도록 요구하며 프로세스는 모든 결과를 반환해 커서가 소진될 때까지 계속됨

 

5.1 제한, 건너뛰기, 정렬

  • 가장 일반적인 쿼리 옵션으로는 반환받는 결과 개수를 제한하거나, 몇 개의 결과를 건너뛰거나, 결과를 정렬하는 옵션이 있음
  • 옵션은 데이터베이스에 전송되기 전에 추가해야 함

 

 

비교 순서

  • 몽고DB에는 데이터형을 비교하는 위계 구조가 있음
    • 정수형과 불리언형, 문자열형과 null형처럼 때로는 하나의 키에 여러 데이터형 값을 저장할 수 있음
    • 데이터형이 섞여 있는 키는 미리 정의된 순서에 따라 정렬됨

 

  • 데이터형 정렬 순서를 최솟값에서 최댓값 순으로 나타내면 다음과 같음
    • 최솟값
    • null
    • 숫자
    • 문자열
    • 객체/도큐먼트
    • 배열
    • 이진 데이터
    • 객체 ID
    • 불리언
    • 날짜
    • 타임스탬프
    • 정규 표현식
    • 최댓값

 

5.2 많은 수의 건너뛰기 피하기

  • 도큐먼트 수가 적을 때는 skip을 사용해도 무리가 없지만 skip은 생략된 결과물을 모두 찾아 폐기하므로 결과가 많이 느려짐
    • 대부분의 데이터베이스는 skip을 위해 인덱스 안에 메타데이터를 저장하지만 몽고DB (4.2.1 버전)는 아직 해당 기능을 지원하지 않음
    • 따라서 많은 수의 건너뛰기는 피해야 함

 

skip을 사용하지 않고 페이지 나누기

  • limit을 사용해 첫 번째 페이지를 반환하고, 다음 페이지들은 첫 페이지부터 오프셋을 주어 반환하면 가장 쉽게 페이지를 나눌 수 있지만 쿼리에 따라 skip을 사용하지 않는 방법을 찾을 수 있음
    • i.g. "date"를 내림차순으로 정렬해 도큐먼트를 표시한다고 가정

 

 

랜덤으로 도큐먼트 찾기

  • 컬렉션에서 랜덤으로 도큐먼트를 가져오는 방법은 자주 문제가 발생함
    • 단순하고 느린 방법으로는 도큐먼트의 개수를 세고 find를 실행한 뒤 0과 컬렉션 크기 사이의 수를 랜덤으로 뽑아 그 개수만큼 건너뛰는 방법 (매우 비효율적)
    • 훨씬 효율적인 방법으로는 도큐먼트를 입력할 때 랜덤 키를 별도로 추가하는 방법 i.g. 셸을 사용한다면 Math.random() 함수를 사용

 

 

5.3 종료되지 않는 커서

  • 커서에는 두 가지 측면이 있음
    • 클라이언트가 보는 커서
    • 클라이언트 커서가 나타내는 데이터베이스 커서

 

  • 서버 측에서 보면 커서는 메모리와 리소스를 점유함
    • 커서가 더는 가져올 결과가 없거나 클라이언트로부터 종료 요청을 받으면 데이터베이스는 점유하고 있던 리소스를 해제함
    • 그러면 데이터베이스가 리소스를 다른 작업에 사용할 수 있으므로 커서도 신속하게 해제해야 함

 

  • 서버 커서를 종료하는 몇 가지 조건이 있음
    • 커서는 조건에 일치하는 결과를 모두 살펴본 후에 스스로 정리함
    • 커서가 클라이언트 측에서 유효 영역을 벗어나면 드라이버는 데이터베이스에 메시지를 보내 커서를 종료해도 된다고 알림
    • 사용자가 아직 결과를 다 살펴보지 않았고, 커서가 여전히 유효 영역 내에 있더라도 10분 동안 활동이 없으면 데이터베이스 커서는 자동으로 죽음

 

  • 사용자가 몇 분 동안 결과를 기다리게 하는 애플리케이션은 거의 없으므로 `타임아웃에 의한 종료`는 바람직한 동작
  • 하지만 종종 커서를 오래 남겨두고 싶을 때가 있으므로 많은 드라이버는 데이터베이스가 커서를 타임아웃시키지 못하게 하는 immortal이라는 함수를 제공
    • 커서의 타임아웃을 비활성화했다면 반드시 결과를 모두 살펴보거나 커서를 명확히 종료해야 함
    • 그렇지 않으면 커서는 데이터베이스에 남아 서버가 재시작할 때까지 리소를 차지함

 

참고

몽고DB 완벽 가이드 3판 - 한빛미디어

반응형

'DB > 몽고DB 완벽 가이드 3판' 카테고리의 다른 글

[3장] 도큐먼트 생성, 갱신, 삭제  (1) 2025.03.28
[2장] 몽고DB 기본  (0) 2025.03.27
[1장] 몽고DB 소개  (1) 2025.03.26