概要
Firestore Enterprise エディションは、相関サブクエリを介したリレーショナル スタイルの結合をサポートしています。データの非正規化や複数のクライアントサイド リクエストの実行が必要になることが多い多くの NoSQL データベースとは異なり、サブクエリを使用すると、関連するコレクションまたはサブコレクションのデータをサーバー上で直接結合して集計できます。
サブクエリは、外部クエリによって処理されるドキュメントごとにネストされたパイプラインを実行する式です。これにより、ドキュメントとその関連サブコレクション アイテムを同時に取得したり、異なるルート コレクション間で論理的にリンクされたデータを結合したりするなど、複雑なデータ取得パターンが可能になります。
コンセプト
このセクションでは、Pipeline オペレーションでサブクエリを使用して結合を行う際の基本コンセプトについて説明します。
式としてのサブクエリ
サブクエリはトップレベルのステージではなく、select(...)、add_fields(...)、where(...)、sort(...) など、式を受け入れる任意のステージで使用できる式です。
Cloud Firestore は、次の 3 種類のサブクエリをサポートしています。
- 配列サブクエリ: サブクエリの結果セット全体をドキュメントの配列として実体化します。
- スカラー サブクエリ: カウント、平均、関連ドキュメントの特定のフィールドなど、単一の値に評価されます。
subcollection(...)サブクエリ: 1 対多の親子関係の結合を簡素化します。
スコープと変数
結合を記述するときに、ネストされたサブクエリが「外部」ドキュメント(親)のフィールドを参照する必要があることがよくあります。これらのスコープをブリッジするには、let(...) ステージ(一部の SDK では define(...) と呼ばれます)を使用して、親スコープで変数を定義します。これらの変数は、variable(...) 関数を使用してサブクエリで参照できます。
構文
以降のセクションでは、結合を実行するための構文の概要について説明します。
let(...) ステージ
let(...) ステージ(一部の SDK では define(...) と呼ばれます)は、親スコープから名前付き変数にデータを明示的に取り込み、後続のネストされたスコープで使用する非フィルタリング ステージです。
配列サブクエリ
配列サブクエリは、サブクエリの結果セット全体を配列にマテリアライズする式サブクエリの特殊なケースです。サブクエリがゼロ行を返す場合、空の配列に評価されます。null 配列が返されることはありません。このようなクエリは、ネストされたコレクションや相関コレクションをマテリアライズする場合など、最終結果に完全な結果が必要な場合に役立ちます。
クエリは、サブクエリでフィルタ、並べ替え、集計を行うことで、取得して返す必要があるデータ量を減らし、クエリの費用を削減できます。サブクエリの順序が尊重されます。つまり、サブクエリの sort(...) ステージによって、最終的な配列の結果の順序が制御されます。
toArrayExpression() SDK ラッパーを使用して、クエリを配列に変換します。
スカラー サブクエリ
スカラー サブクエリは、select(...) ステージまたは where(...) ステージでよく使用されます。これにより、クエリ全体を直接実体化することなく、サブクエリの結果をフィルタリングまたは生成できます。
結果がゼロのスカラー サブクエリは null 自体として評価されますが、複数の要素として評価されるサブクエリはランタイム エラーになります。
スカラー サブクエリが結果ごとに 1 つのフィールドのみを生成する場合、そのフィールドはサブクエリの最上位の結果に昇格されます。これは、サブクエリのスキーマが単一のフィールドである select(field("user_name")) または aggregate(countAll().as("total")) でサブクエリが終了する場合によく見られます。それ以外の場合、サブクエリが複数のフィールドを生成できるときは、それらのフィールドはマップにラップされます。
toScalarExpression() SDK ラッパーを使用して、クエリをスカラー式に変換します。
subcollection(...) サブクエリ
subcollection(...) 入力ステージはステージとして提供されますが、Cloud Firestore の階層データモデルで結合を実行できます。階層モデルでは、クエリは多くの場合、独自のサブコレクションのデータとともにドキュメントを取得する必要があります。これは、collection_group(...) 入力ステージの後に親参照のフィルタを適用することで実現できますが、subcollection(...) を使用すると、より簡潔な構文になります。
暗黙的な結合条件を除き、これは配列サブクエリと同様に動作し、ネストされたコレクションが存在しない場合でも、一致するドキュメントがない場合は空の結果を返します。
これは基本的に構文糖衣です。外部スコープのドキュメントの __name__ を結合キーとして自動的に使用し、階層関係を解決します。このため、親子関係でリンクされたコレクション間でルックアップを実行する場合に推奨される方法です。
例
データの例
次の例では、以降のすべての例で使用するテストデータのセットを読み込みます。
Node.js
// Load set of cities.
const cities = db.collection("cities");
await cities.doc("SF").set({
name: "San Francisco",
state: "CA",
country: "USA",
});
await cities.doc("LA").set({
name: "Los Angeles",
state: "CA",
country: "USA"
});
await cities.doc("DC").set({
name: "Washington, D.C.",
state: null,
country: "USA"
});
await cities.doc("TOK").set({
name: "Tokyo",
state: null,
country: "Japan"
});
// Load restaurants in various cities.
const sfRestaurants = db.collection("cities/SF/restaurants");
const laRestaurants = db.collection("cities/LA/restaurants");
const dcRestaurants = db.collection("cities/DC/restaurants");
const rest1 = await sfRestaurants.add({
name: "Golden Gate Pizza",
type: "pizza",
owner_id: "Mario Rossi"
});
const rest2 = await sfRestaurants.add({
name: "Bay Area Burger",
type: "burger",
owner_id: "Sarah Jenkins"
});
const rest3 = await sfRestaurants.add({
name: "Sunset Taco",
type: "mexican",
owner_id: "Edward"
});
const rest4 = await laRestaurants.add({
name: "Hollywood Sushi",
type: "sushi",
owner_id: "Ken Kenji"
});
const rest5 = await laRestaurants.add({
name: "Venice Pizza",
type: "pizza",
owner_id: "Luigi Romano"
});
const rest6 = await dcRestaurants.add({
name: "Capitol Tacos",
type: "mexican",
owner_id: "Maria Garcia"
});
const rest7 = await dcRestaurants.add({
name: "Georgetown Coffee",
type: "cafe",
owner_id: "David Kim"
});
// Load collection of reviews.
const reviews = db.collection("reviews");
await reviews.add({ restaurant: rest1, rating: 5, reviewer_id: "Alice" });
await reviews.add({ restaurant: rest1, rating: 4, reviewer_id: "Bob" });
await reviews.add({ restaurant: rest2, rating: 4, reviewer_id: "Charlie" });
await reviews.add({ restaurant: rest3, rating: 5, reviewer_id: "Diana" });
await reviews.add({ restaurant: rest3, rating: 4, reviewer_id: "Edward" });
await reviews.add({ restaurant: rest3, rating: 4, reviewer_id: "Fiona" });
// rest4 has 0 reviews
await reviews.add({ restaurant: rest5, rating: 3, reviewer_id: "George" });
await reviews.add({ restaurant: rest6, rating: 5, reviewer_id: "Hannah" });
await reviews.add({ restaurant: rest6, rating: 4, reviewer_id: "Ian" });
await reviews.add({ restaurant: rest7, rating: 5, reviewer_id: "Julia" });
別のコレクションでドキュメントを検索する
reviews コレクション グループに対する次のクエリは、主キー参照を使用して restaurant コレクション グループを検索します。
Node.js
let results = await db.pipeline()
.collectionGroup("reviews")
.define(field("restaurant").as("restaurant_name"))
.addFields(db.pipeline()
.collectionGroup("restaurant")
.where(field("__name__").equal(variable("restaurant_name")))
.select("name", "type")
.toScalarExpression()
.as("restaurant"))
.execute();
レスポンス
{
rating: 5,
reviewer_id "Alice",
restaurant: { name: "Golden Gate Pizza", type: "pizza" }
},
{
rating: 4,
reviewer_id "Bob",
restaurant: { name: "Golden Gate Pizza", type: "pizza" }
},
{
rating: 4,
reviewer_id "Charlie",
restaurant: { name: "Bay Area Burger", type: "burger" }
},
{
rating: 5,
reviewer_id "Diana",
restaurant: { name: "Sunset Taco", type: "mexican" }
},
{
rating: 4,
reviewer_id "Edward",
restaurant: { name: "Sunset Taco", type: "mexican" }
},
{
rating: 4,
reviewer_id "Fiona",
restaurant: { name: "Sunset Taco", type: "mexican" }
},
{
rating: 3,
reviewer_id "George",
restaurant: { name: "Venice Pizza", type: "pizza" }
},
{
rating: 5,
reviewer_id "Hannah",
restaurant: { name: "Capitol Tacos", type: "mexican" }
},
{
rating: 4,
reviewer_id "Ian",
restaurant: { name: "Capitol Tacos", type: "mexican" }
},
{
rating: 5,
reviewer_id "Julia",
restaurant: { name: "Georgetown Coffee", type: "cafe" }
}
複数のコレクションを結合する
次のクエリは、restaurants コレクション グループからすべてのピザ店を取得し、配列サブクエリを使用して、関連するレビューを取得してレスポンスに直接埋め込みます。
Node.js
let results = await db.pipeline()
.collectionGroup("restaurants")
.where(field("type").equal("pizza"))
.define(field("__name__").as("restaurant_name"))
.select(
field("name"),
db.pipeline()
.collectionGroup("reviews")
.where(field("restaurant").equal(variable("restaurant_name")))
.select("rating", "reviewer_id")
.toArrayExpression()
.as("reviews"))
.execute();
レスポンス
{
name: "Golden Gate Pizza",
reviews: [
{ rating: 5, reviewer_id "Alice" },
{ rating: 4, reviewer_id "Bob" }
]
},
{
name: "Venice Pizza",
type: "pizza",
owner_id: "Luigi Romano",
reviews: [
{ rating: 3, reviewer_id "George" }
]
}
複数のコレクションにわたる集計
restaurants コレクション グループに対する次のクエリは、相関サブクエリを使用して、reviews コレクション グループから各レストランの平均評価を取得します。
Node.js
let results = await db.pipeline()
.collectionGroup("restaurants")
.where(field("type").equal("pizza"))
.define(field("__name__").as("restaurant_name"))
.select(
field("name"),
db.pipeline()
.collectionGroup("reviews")
.where(field("restaurant").equal(variable("restaurant_name")))
.aggregate(average("rating").as("avg_rating"))
.toScalarExpression()
.as("avg_rating"))
.execute();
レスポンス
{
name: "Golden Gate Pizza",
avg_rating: 4.5
},
{
name: "Venice Pizza",
avg_rating: 3.0
}
グループごとの上位 N 件(LIMIT を指定したサブクエリ)
次のクエリは、restaurants コレクション グループからすべてのドキュメントを取得し、相関サブクエリを使用して、各レストランの評価の高いレビューの上位 2 件を取得します。
これにより、レビューの配列が大きくなりすぎてクエリのメモリ上限に達することがなくなります。
Node.js
let results = await db.pipeline()
.collectionGroup("restaurants")
.define(field("__name__").as("restaurant_name"))
.select(
field("name"),
db.pipeline()
.collectionGroup("reviews")
.where(field("restaurant").equal(variable("restaurant_name")))
.sort(field("rating").descending())
.limit(2)
.select("rating", "reviewer_id")
.toArrayExpression()
.as("top_reviews"))
.execute();
レスポンス
{
name: "Golden Gate Pizza",
top_reviews: [
{ rating: 5, reviewer_id "Alice" },
{ rating: 4, reviewer_id "Bob" }
]
},
{
name: "Bay Area Burger",
top_reviews: [
{ rating: 4, reviewer_id "Charlie" }
]
},
{
name: "Sunset Taco",
top_reviews: [
{ rating: 5, reviewer_id "Diana" },
{ rating: 4, reviewer_id "Edward" }
]
},
{
name: "Hollywood Sushi",
top_reviews: []
},
{
name: "Venice Pizza",
top_reviews: [
{ rating: 3, reviewer_id "George" }
]
},
{
name: "Capitol Tacos",
top_reviews: [
{ rating: 5, reviewer_id "Hannah" },
{ rating: 4, reviewer_id "Ian" }
]
},
{
name: "Georgetown Coffee",
top_reviews: [
{ rating: 5, reviewer_id "Julia" }
]
}
サブコレクションを結合する
次のクエリは、cities コレクションをスキャンし、subcollection(...) ステージを使用して、ネストされたコレクションのドキュメントを暗黙的に結合し、都市ごとのレストランの数を検索します。
Node.js
let results = await db.pipeline()
.collection("cities")
.addFields(subcollection("restaurants")
.toArrayExpression()
.length()
.as("restaurant_count"))
.execute();
レスポンス
{
__name__: cities/SF,
name: "San Francisco",
state: "CA",
country: "USA",
restaurant_count: 3
},
{
__name__: cities/LA,
name: "Los Angeles",
state: "CA",
country: "USA",
restaurant_count: 2
},
{
__name__: cities/DC,
name: "Washington, D.C.",
state: null,
country: "USA",
restaurant_count: 2
},
{
__name__: cities/TOK,
name: "Tokyo",
state: null,
country: "Japan",
restaurant_count: 0
}
複数の結合条件を表現する
次のクエリは、restaurants コレクション グループをスキャンし、reviews コレクション グループとのマルチフィールド結合を実行して、自分のレストランをレビューしているオーナーを見つけます。
Node.js
let results = await db.pipeline()
.collectionGroup("restaurants")
.define(field("owner_id"), field("__name__"))
.where(db.pipeline()
.collectionGroup("reviews")
.where(field("restaurant").equal(variable("__name__")))
.where(field("author").equal(variable("owner_id")))
.aggregate(count().as("c"))
.toScalarExpression()
.greaterThan(0))
.execute();
レスポンス
{
__name__: cities/SF/restaurants/X9An0HIlx29A9GPuRthS,
name: "Sunset Taco",
type: "mexican",
owner_id: "Edward"
}
アンチ結合(NOT EXISTS)
次のクエリは、restaurants コレクション グループをスキャンし、まだレビューがないすべてのレストランを検索します。
Node.js
let results = await db.pipeline()
.collectionGroup("restaurants")
.define(field("__name__").as("restaurant_name"))
.where(db.pipeline()
.collectionGroup("reviews")
.where(field("restaurant").equal(variable("restaurant_name")))
.aggregate(count().as("review_count"))
.toScalarExpression()
.equal(0))
.execute();
レスポンス
{
__name__: "cities/LA/restaurants/X9An0HIlx29A9GPuRthS",
name: "Hollywood Sushi",
type: "sushi",
owner_id: "Ken Kenji"
}
結合としてのサブクエリ
次のクエリは、各ピザ店とそのレビューの関係をフラット化します。サブクエリを unnest(...) ステージ内に配置すると、サーバーは一致するレビューごとに外側のレストラン ドキュメントを複製し、フラットな結合ドキュメントを生成します(SQL INNER JOIN と同様)。
Node.js
let results = await db.pipeline()
.collectionGroup("restaurants")
.where(field("type").equal("pizza"))
.define(field("__name__").as("restaurant_name"))
.unnest(
db.pipeline()
.collectionGroup("reviews")
.where(field("restaurant").equal(variable("restaurant_name")))
.select("rating", "reviewer_id")
.toArrayExpression()
.as("review"))
.execute();
レスポンス
{
__name__: "cities/SF/restaurants/xU4pu8nFpnJDPZOwcSPP",
name: "Golden Gate Pizza",
type: "pizza",
owner_id: "Mario Rossi"
review: { rating: 5, reviewer_id "Alice" }
},
{
__name__: "cities/SF/restaurants/xU4pu8nFpnJDPZOwcSPP",
name: "Golden Gate Pizza",
type: "pizza",
owner_id: "Mario Rossi",
review: { rating: 4, reviewer_id "Bob" }
},
{
__name__: "cities/LA/restaurants/6CYntvNgbYzgaW652Gq1",
name: "Venice Pizza",
type: "pizza",
owner_id: "Luigi Romano",
review: { rating: 3, reviewer_id "George" }
}
フィルタとしての非相関サブクエリ
reviews コレクションに対する次のクエリは、それ自体に対して相関のないサブクエリを使用してフィルタを実行し、平均評価よりも高いレビューを検索します。
Node.js
let results = await db.pipeline()
.collection("reviews")
// Average review rating is 4.3
.where(field("rating").greaterThan(db.pipeline()
.collection("reviews")
.aggregate(average("rating").as("avg"))
.toScalarExpression()))
.select("rating", "reviewer_id")
.execute();
レスポンス
{
rating: 5,
reviewer_id "Alice"
},
{
rating: 5,
reviewer_id "Diana"
},
{
rating: 5,
reviewer_id "Hannah"
},
{
rating: 5,
reviewer_id "Julia"
}
ベスト プラクティス
toArrayExpression()でメモリを管理する:toArrayExpression()サブクエリは慎重に使用してください。多数のドキュメントを実体化すると、クエリのメモリ上限(128 MiB)を使い果たす可能性があります。これを軽減するには、サブクエリ内でselect(...)を使用して必要なフィールドのみを返し、where(...)フィルタを適用して返されるドキュメントの数を制限します。必要に応じてlimit(...)を使用して、サブクエリから返されるドキュメントの数を制限することを検討してください。- インデックス登録: サブクエリの
where(...)句で使用されるフィールドがインデックス登録されていることを確認します。パフォーマンスの高い結合は、テーブルのフルスキャンではなく、インデックス シークを実行する機能に依存します。
クエリのベスト プラクティスについては、クエリの最適化に関するガイドをご覧ください。
制限事項
subcollection(...)スコープ:subcollection(...)入力ステージは、サブクエリ内でのみサポートされます。これは、階層関係を解決して結合を実行するために、親ドキュメントのコンテキストが必要になるためです。- ネストの深さ: サブクエリは最大 20 レイヤーまでネストできます。
- メモリ使用量: 実体化されたデータに対する 128 MiB の上限は、結合されたすべてのドキュメントを含むクエリ全体に適用されます。