Joins mit Unterabfragen ausführen

Hintergrund

Pipeline-Vorgänge sind eine neue Abfrageschnittstelle für Cloud Firestore. Diese Schnittstelle bietet erweiterte Abfragefunktionen, einschließlich komplexer Ausdrücke. Die Firestore Enterprise-Edition unterstützt relationale Joins durch korrelierte Unterabfragen. Im Gegensatz zu vielen NoSQL-Datenbanken, bei denen häufig Daten denormalisiert oder mehrere clientseitige Anfragen ausgeführt werden müssen, können Sie mit Unterabfragen Daten aus verknüpften Sammlungen oder Unterabfragen direkt auf dem Server kombinieren und aggregieren.

Unterabfragen sind Ausdrücke, die für jedes Dokument, das von der äußeren Abfrage verarbeitet wird, eine verschachtelte Pipeline ausführen. Dies ermöglicht komplexe Muster zum Abrufen von Daten, z. B. das Abrufen eines Dokuments zusammen mit den zugehörigen Unterabfrageelementen oder das Verknüpfen von logisch verknüpften Daten aus verschiedenen Stammsammlungen.

Konzepte

In diesem Abschnitt werden die wichtigsten Konzepte für die Verwendung von Unterabfragen zum Ausführen von Joins in Pipeline-Vorgängen vorgestellt.

Unterabfragen als Ausdrücke

Eine Unterabfrage ist keine Phase der obersten Ebene, sondern ein Ausdruck, der in jeder Phase verwendet werden kann, die Ausdrücke akzeptiert, z. B. select(...), add_fields(...), where(...) oder sort(...).

Cloud Firestore unterstützt drei Arten von Unterabfragen:

  • Array-Unterabfragen:Das gesamte Ergebnis der Unterabfrage wird als Array von Dokumenten materialisiert.
  • Skalare Unterabfragen:Sie werden zu einem einzelnen Wert ausgewertet, z. B. einer Anzahl, einem Durchschnitt oder einem bestimmten Feld aus einem verknüpften Dokument.
  • subcollection(...) -Unterabfragen : Vereinfachte Joins für eine 1:n-Beziehung zwischen über- und untergeordneten Elementen.

Umfang und Variablen

Beim Schreiben eines Joins muss die verschachtelte Unterabfrage häufig auf Felder aus dem „äußeren“ Dokument (dem übergeordneten Element) verweisen. Um diese Bereiche zu überbrücken, verwenden Sie die let(...) Phase (in einigen SDKs als define(...) bezeichnet), um Variablen im übergeordneten Bereich zu definieren, auf die dann in der Unterabfrage mit der Funktion variable(...) verwiesen werden kann.

Syntax

In den folgenden Abschnitten erhalten Sie einen Überblick über die Syntax zum Ausführen von Joins.

Die Phase let(...)

Die let(...) Phase (in einigen SDKs als define(...) bezeichnet) ist eine nicht filternde Phase, die Daten aus dem übergeordneten Bereich explizit in eine benannte Variable für die Verwendung in nachfolgenden verschachtelten Bereichen überträgt.

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);
    }
  

Array-Unterabfragen

Eine Array-Unterabfrage ist ein Sonderfall der Ausdruck-Unterabfrage, bei der das gesamte Ergebnis der Unterabfrage in einem Array materialisiert wird. Wenn die Unterabfrage keine Zeilen zurückgibt, wird sie zu einem leeren Array ausgewertet. Sie gibt niemals ein null-Array zurück. Solche Abfragen sind nützlich, wenn die vollständigen Ergebnisse im Endergebnis erforderlich sind, z. B. beim Materialisieren einer verschachtelten oder korrelierten Sammlung.

Abfragen können in der Unterabfrage gefiltert, sortiert und aggregiert werden, um die Menge der abzurufenden und zurückzugebenden Daten zu reduzieren und so die Kosten der Abfrage zu senken. Die Reihenfolge der Unterabfrage wird berücksichtigt. Das bedeutet, dass eine sort(...)-Phase in der Unterabfrage die Reihenfolge der Ergebnisse im endgültigen Array steuert.

Verwenden Sie den SDK-Wrapper toArrayExpression(), um eine Abfrage in ein Array zu konvertieren.

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"
      });
    }
  

Antwort

    {
        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"
      });
    }
    

Antwort

    {
      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)
    }
  

Antwort

    {
      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);
    }
  

Antwort

    {
        id: "project_1",
        name: "Alpha Build",
        taskTitles: [
          "System Architecture", "Database Schema Design"
      ]
    }
    

Skalare Unterabfragen

Skalare Unterabfragen werden häufig in einer select(...) oder where(...) Phase verwendet, um das Ergebnis einer Unterabfrage zu filtern oder zu erhalten, ohne die vollständige Abfrage direkt zu materialisieren.

Eine skalare Unterabfrage, die keine Ergebnisse liefert, wird selbst zu null ausgewertet. Eine Unterabfrage, die zu mehreren Elementen ausgewertet wird, führt zu einem Laufzeitfehler.

Wenn eine skalare Unterabfrage nur ein einzelnes Feld pro Ergebnis erzeugt, wird das Feld heraufgestuft und ist das Ergebnis der obersten Ebene für die Unterabfrage. Dies ist am häufigsten der Fall, wenn die Unterabfrage mit einem select(field("user_name")) oder aggregate(countAll().as("total")) endet und das Schema der Unterabfrage nur ein einzelnes Feld enthält. Andernfalls werden mehrere Felder, die von einer Unterabfrage erzeugt werden können, in einer Map zusammengefasst.

Verwenden Sie den SDK-Wrapper toScalarExpression(), um eine Abfrage in einen skalaren Ausdruck zu konvertieren.

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
              });
            }
        

Antwort

        {
            "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
        ])
        

Antwort

        {
            "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)
    }
  

Antwort

        {
            "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);
    }
  

Antwort

        {
            "id": "author_202",
            "name": "Charles Dickens",
            "averageBookRating": 4.65
        }
      

subcollection(...)-Unterabfragen

Die Eingabephase subcollection(...) wird als Phase angeboten und ermöglicht Joins für das hierarchische Datenmodell von Cloud Firestore. In einem hierarchischen Modell müssen Abfragen häufig ein Dokument zusammen mit Daten aus den zugehörigen Unterabfragen abrufen. Sie können dies mit einer collection_group(...)-Eingabephase und einem Filter für die übergeordnete Referenz erreichen, aber subcollection(...) bietet eine viel präzisere Syntax.

Abgesehen von der impliziten Join-Bedingung verhält sich dies ähnlich wie eine Array-Unterabfrage. Es wird ein leeres Ergebnis zurückgegeben, wenn keine Dokumente gefunden werden, auch wenn die verschachtelte Sammlung nicht vorhanden ist.

Es handelt sich im Grunde um syntaktischen Zucker: Der __name__ des Dokuments im äußeren Bereich wird automatisch als Join-Schlüssel verwendet, um die hierarchische Beziehung aufzulösen. Daher ist dies die bevorzugte Methode, um in Sammlungen zu suchen, die in einer Beziehung zwischen über- und untergeordneten Elementen verknüpft sind.

Best Practices

  • Speicher mit toArrayExpression() verwalten:Seien Sie vorsichtig mit toArrayExpression() Unterabfragen, da die Materialisierung einer großen Anzahl von Dokumenten das Speicherlimit für Abfragen (128 MiB) überschreiten kann. Um dies zu vermeiden, verwenden Sie select(...) in der Unterabfrage, um nur die erforderlichen Felder zurückzugeben, und wenden Sie where(...)-Filter an, um die Anzahl der zurückgegebenen Dokumente zu begrenzen. Verwenden Sie gegebenenfalls limit(...), um die Anzahl der von der Unterabfrage zurückgegebenen Dokumente zu begrenzen.
  • Indexierung: Achten Sie darauf, dass die Felder, die in der where(...)-Klausel einer Unterabfrage verwendet werden, indexiert sind. Leistungsstarke Joins beruhen darauf, dass Indexsuchen anstelle von vollständigen Tabellenscans ausgeführt werden können.

Weitere Best Practices für Abfragen finden Sie in unserem Leitfaden zur Abfrageoptimierung.

Beschränkungen

  • subcollection(...)-Bereich: Die subcollection(...) Eingabephase wird nur in Unterabfragen unterstützt, da der Kontext eines übergeordneten Dokuments erforderlich ist, um die hierarchische Beziehung aufzulösen und den Join auszuführen.
  • Verschachtelungstiefe:Unterabfragen können bis zu 20 Ebenen tief verschachtelt werden.
  • Speichernutzung:Das Limit von 128 MiB für materialisierte Daten gilt für die gesamte Abfrage, einschließlich aller verknüpften Dokumente.