NoSQL 데이터베이스가 발전함에 따라 프로그래머가 복잡한 작업을 쉽게 할 수 있도록 상위 수준의 API 또는 언어를 추가했습니다. SQL은 관계형 데이터에서 그 방법을 보여주었습니다. SQL에서는 개발자가 무엇을 해야 하는지 말하면 데이터베이스 엔진이 어떻게 해야 하는지 알아냅니다. HOW는 그 문을 실행하는 효율적인 절차/알고리즘입니다. 선택, 조인 및 프로젝트는 SQL 처리의 기본 작업입니다. 정규화 없이 데이터를 모델링하는 NoSQL 시스템에서도 여전히 개체 컬렉션을 조인해야 합니다. 주문이 있는 고객, 주문이 있는 재고, 재고가 있는 공급업체, 공급업체가 있는 크레딧 등이 그 예입니다. 따라서 Couchbase N1QL은 첫 번째 릴리스부터 조인 작업을 지원했습니다. 그 후, 버전 3.2에서 MongoDB는 조인 작업을 수행하기 위해 집계 프레임워크에 $lookup 연산자를 추가했습니다.
표현력이 뛰어난 고성능 쿼리 기능이 없으면 애플리케이션 개발자는 애플리케이션 내에서 쿼리를 수행하거나 쿼리를 수행하는 시스템으로 데이터를 내보내야 합니다. 둘 다 비용이 많이 드는 작업입니다.
이 글에서는 Couchbase와 MongoDB를 비교하고 JSON 문서 조인을 위한 서로 다른 접근 방식을 비교하겠습니다. 특히, MongoDB 컬렉션에 대한 조인 사용과 Couchbase에서 조인을 실행하는 방법을 비교 연구합니다. 조인은 기본적으로 Cassandra CQL 및 DynamoDB에서 지원되지 않습니다. 동일한 결과를 얻으려면 개발자가 직접 작업을 수행하거나 Spark 또는 Amazon EMR과 같은 다른 계층을 사용해야 합니다. 따라서 이 문서에서는 다루지 않겠습니다..
카우치베이스에 가입
카우치베이스는 카우치베이스 4.0(2015)부터 INNER 및 LEFT OUTER 조인을 도입했습니다. 이는 자식-부모 관계에서 조인을 지원했습니다. 하위 문서(예: 주문)를 상위 문서(예: 고객)와 조인할 수 있습니다. 4.5(2016)에서 Couchbase는 다음을 도입했습니다. 인덱스 조인 를 사용하여 상위 조인에서 하위 조인으로 쿼리할 수 있습니다. 두 경우 모두 암시된 속성 값과 문서 키 동일성 조건이 있으며, 이는 ON KEY 절에 의해 지정됩니다.
Couchbase 5.5에는 JSON용으로 확장된 ANSI 표준 SQL이 있습니다. INNER JOIN, LEFT OUTER JOIN 및 제한된 RIGHT OUTER JOIN을 지원합니다. 여기서는 Couchbase 5.5를 기반으로 하는 예제를 사용하겠습니다.
카우치베이스는 문서에 합류합니다: https://developer.couchbase.com/documentation/server/5.5/n1ql/n1ql-language-reference/from.html
MongoDB 조인 및 컬렉션:
조인은 집계 프레임워크 내에서 $lookup 연산자를 통해 지원됩니다.
다음은 몽고DB 문서에서 발췌한 내용입니다.
버전 3.2의 새로운 기능.
에서 샤딩되지 않은 컬렉션에 대한 왼쪽 외부 조인을 수행합니다. 동일 데이터베이스를 사용하여 '조인된' 컬렉션의 문서를 필터링하여 처리합니다. 각 입력 문서에 대해 $조회 단계는 "조인된" 컬렉션에서 일치하는 문서를 요소로 하는 새 배열 필드를 추가합니다. 그리고 $조회 단계는 이렇게 재구성된 문서를 다음 단계로 전달합니다.
엘리엇 호로위츠, MongoDB CTO, 말했다: "몽고DB 집계는 유닉스 파이프라인과 유사합니다. 한 단계의 결과물이 다른 단계로 이동합니다....[매우 절차적입니다]. 매우 절차적인 방식으로 생각할 수 있습니다."
MongoDB $lookup : https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/
JOIN에 대해 더 자세히 알고 싶으신가요? 루카스 에더의 글을 읽어보세요. https://dzone.com/articles/a-probably-incomplete-comprehensive-guide-to-the-many-different-ways-to-join-tables-in-sql
Couchbase N1Ql과 MongoDB의 상위 수준 비교.
카우치베이스 N1QL: INNER JOIN, LEFT OUTER JOIN 및 제한된 RIGHT OUTER JOIN을 지원합니다. SQL과 같은 쿼리 언어는 선언적입니다. 개발자가 쿼리를 작성하면 도구가 N1QL 구문으로 쿼리를 생성합니다. 엔진은 계획을 파악하고 쿼리를 실행합니다.
MongoDB: 스칼라 값에 대해서만 좌측 외부 조인을 지원합니다. MongoDB 쿼리 언어로 조인을 설계하면 쿼리를 작성하고 데이터를 절차적 방식으로 처리하는 데 도움이 됩니다.
시사점:
- 왼쪽 외부 조인 결과 집합은 내부 조인 결과 집합의 상위 집합입니다. 왼쪽 외부 조인이 수행된 후 추가 술어를 추가하여 일치하지 않는(조인에서 null이 투영되거나 종속된 쪽이 누락된) 문서를 제거할 수 있습니다. 이는 샌프란시스코에서 런던을 경유하여 시카고로 이동하는 것과 같습니다. 할 수는 있지만 비용이 많이 듭니다. 쿼리 실행에 시간, 메모리, CPU 리소스가 소요되어 시스템의 전체 성능에 영향을 미칩니다.
- 조인에 대한 N1QL 지원은 선언적입니다. MongoDB 언어는 다소 절차적입니다. 술어를 분리하고, 컬렉션 간의 조인 순서를 고려하고, 언제 그룹화, 정렬할지 등을 생각해야 합니다. MongoDB 집계로 쿼리를 작성하는 것은 단계별로 쿼리 계획을 작성하는 것과 같습니다.
예시:
간단한 여행 샘플 모델과 데이터를 사용합니다. 모델 데이터의 세부 사항은 다음과 같습니다. https://developer.couchbase.com/documentation/server/4.5/travel-app/travel-app-data-model.html
Couchbase에서 데이터를 내보낸 후 travel-sample이라는 몽고 데이터베이스로 가져왔습니다. 몽고DB에는 5가지 유형의 문서(랜드마크, 노선, 항공사, 공항, 호텔)가 각각의 이름을 가진 5개의 컬렉션에 저장되어 있습니다.
예제 1: 스칼라 값에 ON 절을 사용하여 왼쪽 외부 조인.
카우치베이스 N1QL
1 2 3 4 5 6 7 |
셀렉트 카운트(*) FROM `여행-sample` 경로 왼쪽 외부 조인 `여행-sample` 항공사 켜기 (route.airlineid = META(airline).id) 어디 경로.유형 = 'route'; |
MongoDB 쿼리
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
db.경로.집계([ { $조회: { 에서:"항공사", localField: "airlineid", 외국 필드: "_id", as: "airline_docs" } }, { $그룹: { _id: null, myCount: { $합계: 1 } } }, { $프로젝트: { _id: 0 } } ]); |
관찰:
이것은 두 컬렉션을 조인한 다음 단순히 생성된 총 문서 수를 계산하는 매우 간단한 왼쪽 외부 조인 쿼리입니다. N1QL(및 SQL)과 달리, MongoDB에서는 단일 그룹이 있더라도 결과 집합을 그룹화해야만 개수를 구할 수 있다는 점에 유의하세요.
예 2: 같은 도시에 있는 공항과 랜드마크를 공항 순으로 나열합니다.
카우치베이스 N1QL:
1 2 3 4 5 6 7 8 9 10 11 12 |
선택 landmark.name AS 랜드마크_이름, MIN(공항.공항명) AS 공항_이름, MIN(airport.tz) AS 랜드마크_시간 FROM `여행-sample` 공항 내부 가입 `여행-sample` 랜드마크 켜기 airport.city = landmark.city 어디 landmark.country = "미국" AND 공항.유형 = "공항" AND 랜드마크.유형 = "랜드마크" 그룹 BY landmark.name 주문 기준 공항_이름 |
MongoDB:
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 |
db.공항.집계([ { $조회: { 에서:"랜드마크", localField: "city", 외국 필드: "city", as: "aplm_docs" } }, { $일치: {"airline_docs": {$ne: []}} }, { $긴장을 풀다: { 경로: "$aplm_docs", 보존Null 및 빈 배열: true }}, { $그룹: { _id: "$aplm_docs.name", 공항_이름: { $분: "1TP4공항명" } , 랜드마크_시간: { $분: "$tz"} } }, { $정렬 : { 공항_이름: 1 } }, { $프로젝트: { _id: 1, 공항_이름:1, 랜드마크_시간:1 } } ]); |
관찰:
- 이 쿼리는 MongoDB에는 없는 INNER JOIN을 사용합니다. 따라서 MongoDB에서는 먼저 조회 조인을 수행하여 왼쪽 외부 조인을 얻은 다음 일치 단계를 사용하여 (왼쪽 외부로 인해) 일치하지 않지만 투영된 문서를 제거합니다(코드: $match: {"airline_docs": {$ne: []}}).
- 그런 다음 일치하는 문서가 배열 데이터 구조에 있다는 것을 기억하고 이를 풀고 landmark.name을 기준으로 그룹화해야 합니다. 그런 다음 정렬 및 최종 투영을 수행합니다.
예상대로 MongoDB 조인 쿼리는 절차적이며 실행 계획을 이해하고 각 단계에 대한 코드를 작성해야 합니다.
예 3: 샌프란시스코에서 출발하는 모든 목적지 공항(SFO에서 출발하는 노선이 있는 공항)을 찾습니다.
카우치베이스 N1QL
1 2 3 4 5 6 7 8 9 |
선택 DISTINCT route.destinationairport FROM `여행-sample` 공항 JOIN `여행-sample` 경로 켜기 (airport.faa = route.sourceairport AND 경로.유형 = "경로") 어디 공항.유형 = "공항" AND airport.city = "샌프란시스코" AND airport.country = "미국" 주문 기준 route.destinationairport |
MongoDB:
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 31 32 33 34 35 36 37 38 39 40 41 42 |
db.공항.집계([ { $일치: { $그리고: [ {"type": "공항"}, { 도시: "샌프란시스코"}, { "country": "미국"} ] } }, { $조회: { 에서:"경로", let: { rfaa : "$faa"}, 파이프라인: [ { $일치: { $expr: { $그리고: [ { $eq: ["$sourceairport", "$$rfaa"]} , { $eq: ["$type", "경로"] } ] } } } ], as: "airline_docs" } }, { $일치: {"airline_docs": {$ne: []}} }, { $긴장을 풀다: { 경로: "$airline_docs", 보존Null 및 빈 배열: true }}, { $프로젝트: { _id:0, "airline_docs.destinationairport" : 1 }}, { $그룹: { _id : "$airline_docs.destinationairport" } }, { $정렬: { _id : 1 }}, ]); |
관찰:
- 이 쿼리의 조인 절은 두 개의 술어가 있어 조금 더 복잡합니다(airport.faa = (route.sourceairport AND route.type = "route"). 이를 위해서는 MongoDB 쿼리에서 번거로운 파이프라인 구문이 필요합니다.
- 그리고 두 컬렉션을 구분해야 하므로 공항 속성에 대한 로컬 변수를 만들기 위해 또 다른 let 단계가 필요합니다.
- 이전과 마찬가지로 일치하지 않는(비어 있는) 항공사 문서를 제거하기 위해 추가 일치 절이 필요하며, 그룹화 및 정렬이 이어집니다.
- 시각적으로 볼 수 있듯이, MongoDB 쿼리는 점점 더 커져서 Couchbase N1QL과 동일한 작업을 수행합니다.
예 4: 요세미티의 모든 호텔과 랜드마크 찾기. 호텔에는 다음이 있어야 합니다. altleast 좋아요 5개.
카우치베이스 N1QL
1 2 3 4 5 6 7 8 9 10 |
선택 hotel.name 호텔_이름, landmark.name 랜드마크_이름, landmark.activity FROM `여행-sample` 호텔 내부 가입 `여행-sample` 랜드마크 켜기 (hotel.city = landmark.city AND hotel.country = landmark.country AND 랜드마크.유형 = "랜드마크") 어디 호텔.유형 = "호텔" AND hotel.title 같은 "요세미티%" AND array_length(hotel.public_likes) > 5; |
MongoDB:
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 31 |
db.호텔.집계([ { $일치: { title: { $정규식: /^요세미티/ } }, }, { $조회: { 에서:"랜드마크", let: { hcity : "$city", hcountry : "$country"}, 파이프라인: [ { $일치: { $expr: { $그리고: [ { $eq: ["$city", "$$hcity"]} , { $eq: ["$country", "$$hcountry"] } ] } } } ], as: "hotel_lm_docs" } }, { $일치 : {"hotel_lm_docs": { $ne: [] }}}, { $프로젝트: {_id:0, hname: "$name", 공개_좋아요: 1, hotel_lm_docs:1}}, { $긴장을 풀다: { 경로: "$hotel_lm_docs", 보존Null 및 빈 배열: true }}, { $프로젝트: { _id: 1, hname : 1 , "hotel_lm_docs.name" : 1, "hotel_lm_docs.name" : 1, "hotel_lm_docs.activity" : 1, mt5 : {$gt: [ {$크기: "$public_likes"}, 5]}}}, { $일치: { mt5 : true } }, { $프로젝트: {_$id:0}} ]); |
관찰:
- LIKE 술어를 정규식으로 변환하는 것은 간단했지만, 공개_좋아요가 5개 이상 있는지 확인하는 것은 쉽지 않았습니다. 마지막에 공개_좋아요의 크기를 계산하기 위해 추가적인 투영과 매칭 단계가 필요했습니다.
- 일치시키고, 조작하고, 투영할 속성이 많은 경우 적절한 단계에서 이름을 적절히 바꿔야 하며, 그렇지 않으면 쿼리에서 참조할 수 없습니다. 예를 들어, hotel.name은 긴장을 풀기 전에 hname으로 이름을 바꿔야 했습니다. 이 단계를 작성하는 더 좋은 방법이 있을 수도 있습니다!
- N1QL은 쿼리를 370자로 표현했습니다. 몽고DB는 956자가 필요했습니다. 이 모든 것이 두 개의 테이블 조인을 위한 것입니다. 복잡성이 증가함에 따라 MongoDB 쿼리가 절차적 방식으로 작성되기 때문에 비율도 증가합니다.
예 5: 요세미티의 모든 호텔과 랜드마크 찾기. 호텔은 좋아요가 5개 이상 있어야 합니다.
예제 4와 같지만 더 빠르게 수행하세요!
카우치베이스 N1QL
1 2 3 4 5 6 7 8 9 10 |
선택 hotel.name 호텔_이름, landmark.name 랜드마크_이름, landmark.activity FROM `여행-sample` 호텔 내부 가입 `여행-sample` 랜드마크 사용 HASH(빌드) 켜기 (hotel.city = landmark.city AND hotel.country = landmark.country AND 랜드마크.유형 = "랜드마크") 어디 호텔.유형 = "호텔" AND hotel.title 같은 "요세미티%" AND array_length(hotel.public_likes) > 5; |
관찰:
Couchbase N1QL의 기본 조인 방법은 중첩 루프 조인입니다. 이 방법은 조인의 각 측면에 관련된 문서 수가 적을 때 잘 작동합니다. 일반적으로 보고 쿼리에서와 같이 데이터 집합이 큰 경우 중첩 루프 조인은 속도가 느려집니다. Couchbase N1QL에는 해시 조인이 있어 조인 속도가 상당히 빨라집니다. 조인의 각 측면에 수천 개의 문서에서 수백만 개의 문서가 있는 경우 속도가 2배에서 20배 이상 향상될 수 있습니다. 더 자세한 내용은 자세한 카우치베이스 블로그 에서 자세한 내용을 확인하세요.
문서와 설명서를 보면, 몽고DB가 어떤 조인 방법을 사용하는지 불분명합니다. 일부 블로그에서는 중첩 루프 조인을 사용하여 $lookup 연산자를 구현한 것으로 나와 있습니다.
요약:
카우치베이스 N1QL | MongoDB | |
조인 접근 방식 | SQL과 같은 선언적입니다.
모든 크기의 분산된 데이터 집합 간의 조인을 허용합니다. |
일부 선언적 측면(예: 인덱스 선택)이 있는 절차적.
샤드된 컬렉션에 참여할 수 있습니다. 비샤드 컬렉션. 두 개의 샤드된 컬렉션을 조인하려면 애플리케이션이 조인 알고리즘을 작성해야 합니다. |
지원되는 조인 | 왼쪽 외부 조인
내부 가입 오른쪽 외부 조인 |
$lookup은 스칼라 값에 대해 왼쪽 외부 조인을 구현합니다. |
온-클레임 지원 | 전체 표현식.
스칼라 배열 |
암묵적 평등
파이프라인 표현식 배열은 $lookup 이전에 $unwind여야 합니다. |
JOIN 구현 | 중첩 루프 차단
사용자 정의 빌드 및 프로브와 해시 조인. |
중첩 루프 |
ON 절 | ON 절을 표현식과 함께 사용합니다. | $파이프라인 표현식 |
ON 절의 배열 표현식 | ANY, IN 표현식을 사용합니다.
UNNEST 지원 |
$match 이전 $unwind를 사용한 파이프라인 |
설명 | 시각적 설명 및
JSON 설명 |
시각적 설명 및
JSON 설명 |
조인 주문 | 사용자가 지정한 대로 왼쪽에서 오른쪽으로 이동합니다. 최적화 도구는 규칙 기반입니다. | 파이프라인에 지정된 대로. |
중첩된 조인 | 파생 테이블을 통해 지원됩니다.
FROM 절은 조인 또는 하위 선택을 차례로 가질 수 있는 하위 선택을 가질 수 있습니다. |
아니요 |
JOIN 술어 처리 | 옵티마이저는 조인 술어, 상수 술어를 처리하고 술어를 인덱스에 자동으로 푸시합니다. | 각 컬렉션의 술어를 수동으로 설계하고, 옵티마이저의 완전한 도움 없이도 파이프라인 단계를 신중하게 주문할 수 있습니다. |
성능은 어떻습니까? 좋은 질문입니다. 그건 다음 블로그에서 다뤄보겠습니다!
이제 인용문입니다:
"그림에 불필요한 선이 없어야 하고 기계에 불필요한 부품이 없어야 하는 것과 같은 이유로 문장에는 불필요한 단어가 없어야 하고 단락에는 불필요한 문장이 없어야 합니다."
- 윌리엄 스트렁크 주니어 스타일의 요소.
참조:
- 카우치베이스 문서: https://docs.couchbase.com
- 몽고DB 문서: https://docs.mongodb.com/
- 카우치베이스 N1QL에 ANSI가 합류합니다: https://www.couchbase.com/blog/ansi-join-support-n1ql/
- N1QL 튜토리얼: https://query-tutorial.couchbase.com/tutorial/#1