サブクエリを使用して結合を実行する

背景

パイプライン オペレーションは、Cloud Firestore の新しいクエリ インターフェースです。このインターフェースは、複雑な式を含む高度なクエリ機能を提供します。Firestore Enterprise エディションは、相関サブクエリを使用してリレーショナル スタイルの結合をサポートしています。多くの NoSQL データベースでは、データの非正規化や複数のクライアントサイド リクエストの実行が必要になることが多いですが、サブクエリを使用すると、関連するコレクションまたはサブコレクションのデータをサーバー上で直接結合して集計できます。

サブクエリは、外部クエリで処理されるドキュメントごとにネストされたパイプラインを実行する式です。これにより、ドキュメントとその関連サブコレクション アイテムの取得や、異なるルート コレクション間で論理的にリンクされたデータの結合など、複雑なデータ取得パターンが可能になります。

コンセプト

このセクションでは、Pipeline オペレーションで結合を実行するためにサブクエリを使用する際の基本的なコンセプトについて説明します。

式としてのサブクエリ

サブクエリはトップレベルのステージではなく、select(...)add_fields(...)where(...)sort(...) など、式を受け入れる任意のステージで使用できるです。

Cloud Firestore は、次の 3 種類のサブクエリをサポートしています。

  • 配列サブクエリ: サブクエリの結果セット全体をドキュメントの配列として実体化します。
  • スカラー サブクエリ: 関連ドキュメントのカウント、平均、特定のフィールドなどの単一の値に評価されます。
  • subcollection(...) サブクエリ: 1 対多の親子関係の結合を簡素化します。

スコープと変数

結合を記述する場合、ネストされたサブクエリは多くの場合、「外部」ドキュメント(親)のフィールドを参照する必要があります。これらのスコープをブリッジするには、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 自体に評価されますが、複数の要素に評価されるサブクエリはランタイム エラーになります。

スカラー サブクエリで結果ごとに 1 つのフィールドのみが生成される場合、そのフィールドはサブクエリの最上位の結果に昇格します。これは、サブクエリのスキーマが単一のフィールドである場合に、サブクエリが 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() でメモリを管理する: 多数のドキュメントを実体化すると、クエリのメモリ上限(128 MiB)を超える可能性があるため、toArrayExpression() サブクエリには注意してください。この問題を軽減するには、サブクエリ内で select(...) を使用して必要なフィールドのみを返し、where(...) フィルタを適用して返されるドキュメントの数を制限します。サブクエリから返されるドキュメントの数を制限する場合は、必要に応じて limit(...) の使用を検討してください。
  • インデックス登録: サブクエリの where(...) 句で使用されるフィールドがインデックス登録されていることを確認します。パフォーマンスの高い結合は、テーブル全体のスキャンではなくインデックス シークを実行できるかどうかに依存します。

クエリのベスト プラクティスについては、クエリの最適化に関するガイドをご覧ください。

制限事項

  • subcollection(...) スコープ: subcollection(...) 入力ステージは、階層関係を解決して結合を実行するために親ドキュメントのコンテキストが必要であるため、サブクエリ内でのみサポートされます。
  • ネストの深さ: サブクエリは最大 20 階層までネストできます。
  • メモリ使用量: 実体化されたデータの上限 128 MiB は、すべての結合されたドキュメントを含むクエリ全体に適用されます。