백그라운드
파이프라인 작업은 Cloud Firestore의 새로운 쿼리 인터페이스입니다. 이 인터페이스는 복잡한 표현식을 비롯한 고급 쿼리 기능을 제공합니다. Firestore Enterprise 버전은 상관 하위 쿼리를 통해 관계형 스타일 조인을 지원합니다. 데이터를 비정규화하거나 여러 클라이언트 측 요청을 실행해야 하는 경우가 많은 NoSQL 데이터베이스와 달리 하위 쿼리를 사용하면 서버에서 직접 관련 컬렉션 또는 하위 컬렉션의 데이터를 결합하고 집계할 수 있습니다.
서브 쿼리는 외부 쿼리에서 처리한 모든 문서에 대해 중첩된 파이프라인을 실행하는 표현식입니다. 이를 통해 관련 하위 컬렉션 항목과 함께 문서를 가져오거나 서로 다른 루트 컬렉션에서 논리적으로 연결된 데이터를 조인하는 등 복잡한 데이터 검색 패턴을 사용할 수 있습니다.
개념
이 섹션에서는 파이프라인 작업에서 조인을 실행하기 위해 하위 쿼리를 사용하는 데 필요한 핵심 개념을 소개합니다.
표현식으로서의 서브 쿼리
서브 쿼리는 최상위 단계가 아닙니다. 대신 select(...), add_fields(...), where(...) 또는 sort(...)과 같이 표현식을 허용하는 모든 단계에서 사용할 수 있는 표현식입니다.
Cloud Firestore는 세 가지 유형의 하위 쿼리를 지원합니다.
- 배열 하위 쿼리: 하위 쿼리의 전체 결과 집합을 문서 배열로 구체화합니다.
- 스칼라 하위 쿼리: 개수, 평균 또는 관련 문서의 특정 필드와 같은 단일 값으로 평가됩니다.
subcollection(...)하위 쿼리: 일대다 상위-하위 관계의 조인을 간소화합니다.
범위 및 변수
조인을 작성할 때 중첩된 하위 쿼리는 '외부' 문서 (상위)의 필드를 참조해야 하는 경우가 많습니다. 이러한 범위를 연결하려면 let(...) 단계 (일부 SDK에서는 define(...)로 지칭)를 사용하여 상위 범위에서 변수를 정의합니다. 그러면 variable(...) 함수를 사용하여 하위 쿼리에서 변수를 참조할 수 있습니다.
문법
다음 섹션에서는 조인을 실행하는 구문을 간략하게 설명합니다.
let(...) 단계
let(...) 단계 (일부 SDK에서는 define(...)로 지칭)는 필터링되지 않는 단계로, 후속 중첩 범위에서 사용할 수 있도록 상위 범위의 데이터를 명명된 변수로 명시적으로 가져옵니다.
Web
async function defineStageData() {
await setDoc(doc(collection(db, "Authors"), "author_123"), {
"id": "author_123",
"name": "Jane Austen"
});
}
Swift
func defineStageData() async throws { try await db.collection("authors").document("author_123").setData([ "id": "author_123", "name": "Jane Austen" ]) }
Kotlin
fun defineStageData() { val author = hashMapOf( "id" to "author_123", "name" to "Jane Austen", ) db.collection("Authors").document("author_123").set(author) }
Java
public void defineStageData() { Map<String, Object> author = new HashMap<>(); author.put("id", "author_123"); author.put("name", "Jane Austen"); db.collection("Authors").document("author_123").set(author); }
배열 서브 쿼리
배열 서브 쿼리는 서브 쿼리의 전체 결과 집합을 배열로 구체화하는 특수한 표현식 서브 쿼리입니다. 서브 쿼리가 0개의 행을 반환하면 빈 배열로 평가됩니다. 절대 null 배열을 반환하지 않습니다. 이러한 쿼리는 중첩되거나 상관관계가 있는 컬렉션을 구체화하는 등 최종 결과에 전체 결과가 필요한 경우에 유용합니다.
쿼리는 하위 쿼리에서 필터링, 정렬, 집계를 실행하여 가져와 반환해야 하는 데이터의 양을 줄여 쿼리 비용을 절감할 수도 있습니다. 하위 쿼리의 순서가 적용됩니다. 즉, 하위 쿼리의 sort(...) 단계가 최종 배열의 결과 순서를 제어합니다.
toArrayExpression() SDK 래퍼를 사용하여 쿼리를 배열로 변환합니다.
Web
async function toArrayExpressionStageData() {
await setDoc(doc(collection(db, "Projects"), "project_1"), {
"id": "project_1",
"name": "Alpha Build"
});
await addDoc(collection(db, "Tasks"), {
"project_id": "project_1",
"title": "System Architecture"
});
await addDoc(collection(db, "Tasks"), {
"project_id": "project_1",
"title": "Database Schema Design"
});
}
응답
{
id: "project_1",
name: "Alpha Build",
taskTitles: [
"System Architecture", "Database Schema Design"
]
}
Swift
async function toArrayExpressionStageData() { await setDoc(doc(collection(db, "Projects"), "project_1"), { "id": "project_1", "name": "Alpha Build" }); await addDoc(collection(db, "Tasks"), { "project_id": "project_1", "title": "System Architecture" }); await addDoc(collection(db, "Tasks"), { "project_id": "project_1", "title": "Database Schema Design" }); }
응답
{ id: "project_1", name: "Alpha Build", taskTitles: [ "System Architecture", "Database Schema Design" ] }
Kotlin
fun toArrayExpressionData() { val project = hashMapOf( "id" to "project_1", "name" to "Alpha Build", ) db.collection("Projects").document("project_1").set(project) val task1 = hashMapOf( "project_id" to "project_1", "title" to "System Architecture", ) db.collection("Tasks").add(task1) val task2 = hashMapOf( "project_id" to "project_1", "title" to "Database Schema Design", ) db.collection("Tasks").add(task2) }
응답
{ id: "project_1", name: "Alpha Build", taskTitles: [ "System Architecture", "Database Schema Design" ] }
Java
public void toArrayExpressionData() { Map<String, Object> project = new HashMap<>(); project.put("id", "project_1"); project.put("name", "Alpha Build"); db.collection("Projects").document("project_1").set(project); Map<String, Object> task1 = new HashMap<>(); task1.put("project_id", "project_1"); task1.put("title", "System Architecture"); db.collection("Tasks").add(task1); Map<String, Object> task2 = new HashMap<>(); task2.put("project_id", "project_1"); task2.put("title", "Database Schema Design"); db.collection("Tasks").add(task2); }
응답
{ id: "project_1", name: "Alpha Build", taskTitles: [ "System Architecture", "Database Schema Design" ] }
스칼라 서브 쿼리
스칼라 서브 쿼리는 필터링을 허용하거나 전체 쿼리를 직접 구체화하지 않고 서브 쿼리의 결과를 생성하는 select(...) 또는 where(...) 단계에서 자주 사용됩니다.
결과가 0인 스칼라 하위 쿼리는 null 자체로 평가되는 반면, 여러 요소로 평가되는 하위 쿼리는 런타임 오류가 발생합니다.
스칼라 서브 쿼리가 결과당 단일 필드만 생성하는 경우 필드는 서브 쿼리의 최상위 결과로 승격됩니다. 이는 하위 쿼리가 select(field("user_name")) 또는 aggregate(countAll().as("total"))로 끝나고 하위 쿼리의 스키마가 단일 필드인 경우에 가장 흔하게 발생합니다. 그렇지 않고 하위 쿼리에서 여러 필드를 생성할 수 있는 경우 맵으로 래핑됩니다.
toScalarExpression() SDK 래퍼를 사용하여 쿼리를 스칼라 표현식으로 변환합니다.
Web
async function toScalarExpressionStageData() {
await setDoc(doc(collection(db, "Authors"), "author_202"), {
"id": "author_202",
"name": "Charles Dickens"
});
await addDoc(collection(db, "Books"), {
"author_id": "author_202",
"title": "Great Expectations",
"rating": 4.8
});
await addDoc(collection(db, "Books"), {
"author_id": "author_202",
"title": "Oliver Twist",
"rating": 4.5
});
}
응답
{
"id": "author_202",
"name": "Charles Dickens",
"averageBookRating": 4.65
}
Swift
try await db.collection("authors").document("author_202").setData([ "id": "author_202", "name": "Charles Dickens" ]) try await db.collection("books").document().setData([ "author_id": "author_202", "title": "Great Expectations", "rating": 4.8 ]) try await db.collection("books").document().setData([ "author_id": "author_202", "title": "Oliver Twist", "rating": 4.5 ])
응답
{ "id": "author_202", "name": "Charles Dickens", "averageBookRating": 4.65 }
Kotlin
fun toScalarExpressionData() { val author = hashMapOf( "id" to "author_202", "name" to "Charles Dickens", ) db.collection("Authors").document("author_202").set(author) val book1 = hashMapOf( "author_id" to "author_202", "title" to "Great Expectations", "rating" to 4.8, ) db.collection("Books").add(book1) val book2 = hashMapOf( "author_id" to "author_202", "title" to "Oliver Twist", "rating" to 4.5, ) db.collection("Books").add(book2) }
응답
{ "id": "author_202", "name": "Charles Dickens", "averageBookRating": 4.65 }
Java
public void toScalarExpressionData() { Map<String, Object> author = new HashMap<>(); author.put("id", "author_202"); author.put("name", "Charles Dickens"); db.collection("Authors").document("author_202").set(author); Map<String, Object> book1 = new HashMap<>(); book1.put("author_id", "author_202"); book1.put("title", "Great Expectations"); book1.put("rating", 4.8); db.collection("Books").add(book1); Map<String, Object> book2 = new HashMap<>(); book2.put("author_id", "author_202"); book2.put("title", "Oliver Twist"); book2.put("rating", 4.5); db.collection("Books").add(book2); }
응답
{ "id": "author_202", "name": "Charles Dickens", "averageBookRating": 4.65 }
subcollection(...) 서브 쿼리
단계로 제공되지만 subcollection(...) 입력 단계를 사용하면 Cloud Firestore의 계층적 데이터 모델에 대해 조인을 실행할 수 있습니다. 계층적 모델에서 쿼리는 자체 하위 컬렉션의 데이터와 함께 문서를 검색해야 하는 경우가 많습니다. collection_group(...) 입력 단계와 상위 참조 필터를 사용하여 이를 달성할 수 있지만 subcollection(...)는 훨씬 간결한 구문을 제공합니다.
암시적 조인 조건을 제외하고 이는 배열 하위 쿼리와 유사하게 작동하며, 중첩된 컬렉션이 존재하지 않더라도 일치하는 문서가 없으면 빈 결과를 반환합니다.
기본적으로 구문 설탕입니다. 계층적 관계를 해결하기 위해 외부 범위의 문서 __name__를 조인 키로 자동으로 사용합니다. 따라서 상하위 관계로 연결된 컬렉션에서 조회를 실행하는 데 선호되는 방법입니다.
권장사항
toArrayExpression()로 메모리 관리: 많은 수의 문서를 구체화하면 쿼리 메모리 한도 (128MiB)가 소진될 수 있으므로toArrayExpression()하위 쿼리를 사용할 때 주의해야 합니다. 이 문제를 완화하려면 하위 쿼리 내에서select(...)를 사용하여 필요한 필드만 반환하고where(...)필터를 적용하여 반환되는 문서 수를 제한하세요. 하위 쿼리에서 반환되는 문서 수를 제한하는 것이 적절한 경우limit(...)를 사용하는 것이 좋습니다.- 색인 생성: 하위 쿼리의
where(...)절에 사용된 필드가 색인화되어 있는지 확인합니다. 성능이 우수한 조인은 전체 테이블 스캔이 아닌 색인 검색을 실행하는 기능에 의존합니다.
쿼리 권장사항에 대한 자세한 내용은 쿼리 최적화 가이드를 참고하세요.
제한사항
subcollection(...)범위:subcollection(...)입력 단계는 계층적 관계를 해결하고 조인을 실행하기 위해 상위 문서의 컨텍스트가 필요하므로 하위 쿼리 내에서만 지원됩니다.- 중첩 깊이: 하위 쿼리는 최대 20개 레이어까지 중첩할 수 있습니다.
- 메모리 사용량: 구체화된 데이터에 대한 128MiB 제한은 모든 조인된 문서를 포함한 전체 쿼리에 적용됩니다.