使用子查詢執行聯結

背景

管道作業是 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);
    }
  

陣列子查詢

陣列子查詢是運算式子查詢的特例,可將子查詢的整個結果集具體化為陣列。如果子查詢傳回零個資料列,就會評估為空陣列。一律不會傳回 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(...) 階段,可允許篩選或產生子查詢結果,而不直接實現完整查詢。

如果純量子查詢產生零個結果,則會評估為 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() 管理記憶體:請謹慎使用 toArrayExpression() 子查詢,因為實現大量文件可能會耗盡查詢記憶體限制 (128 MiB)。為減輕這個問題,請在子查詢中使用 select(...),只傳回必要的欄位,並套用 where(...) 篩選器,限制傳回的文件數量。如要限制子查詢傳回的文件數量,請考慮使用 limit(...)
  • 建立索引:請確保子查詢 where(...) 子句中使用的欄位已建立索引。如要執行高效能的聯結,必須能夠執行索引搜尋,而非完整資料表掃描。

如需更多查詢最佳做法,請參閱查詢最佳化指南

限制

  • subcollection(...) 範圍:subcollection(...) 輸入階段僅支援子查詢,因為需要父項文件的內容來解析階層式關係並執行聯結。
  • 巢狀結構深度:子查詢最多可巢狀結構化 20 層。
  • 記憶體用量:實體化資料的 128 MiB 限制適用於整個查詢,包括所有已聯結的文件。