使用子查询执行联接

背景

流水线操作是 Cloud Firestore的一个新查询接口。此接口提供高级查询功能,包括复杂的表达式。Firestore 企业版通过相关子查询 支持关系型联接。与许多通常需要对数据进行反规范化或执行多个客户端请求的 NoSQL 数据库不同,子查询允许您直接在服务器上合并和汇总相关集合或子集合中的数据。

子查询是为外部查询处理的每个文档执行嵌套流水线的表达式。这支持复杂的数据检索模式,例如提取文档及其相关子集合项,或联接不同根集合中逻辑关联的数据。

概念

本部分介绍了在流水线操作中使用子查询执行联接的核心概念。

子查询作为表达式

子查询不是顶级阶段,而是一个表达式,可用于接受表达式的任何阶段,例如 select(...)add_fields(...)where(...)sort(...)

Cloud Firestore 支持三种类型的子查询:

  • 数组子查询: 将子查询的整个结果集具体化为文档数组。
  • 标量子查询: 求值为单个值,例如计数、平均值或相关文档中的特定字段。
  • subcollection(...) 子查询: 针对一对多父子关系的简化联接。

范围和变量

编写联接时,嵌套子查询通常需要引用“外部”文档(父级)中的字段。如需弥合这些范围,您可以使用 let(...)阶段(在某些 SDK 中称为 define(...))在父级范围内定义变量,然后可以使用 variable(...) 函数在 子查询中引用这些变量。

语法

以下部分简要介绍了执行联接的语法。

let(...) 阶段

The 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__外部范围中文档的 `__name__` 作为联接键来解析分层 关系。这使其成为在父子关系中关联的集合之间执行查找的首选方式。

最佳实践

  • 使用 toArrayExpression() 管理内存: 请谨慎使用 toArrayExpression() 子查询,因为具体化大量 文档可能会超出查询内存限制 (128 MiB)。为缓解此问题, 请在子查询中使用 select(...) 仅返回必要的字段,并 应用 where(...) 过滤条件来限制返回的文档数量。 如果合适,请考虑使用 limit(...) 来限制子查询返回的文档数量 。
  • 索引: 确保子查询的 where(...) 子句中使用的字段已编入索引。高性能联接依赖于执行索引查找而不是全表扫描的能力。

如需了解更多查询最佳实践,请参阅我们的 查询优化指南

限制

  • subcollection(...) 范围subcollection(...) 输入阶段仅在子查询中受支持,因为它需要父 文档的上下文来解析分层关系并执行联接。
  • 嵌套深度: 子查询最多可以嵌套 20 层。
  • 内存使用量: 具体化数据的 128 MiB 限制适用于整个查询,包括所有联接的文档。