Melakukan penggabungan dengan subkueri

Ringkasan

Edisi Firestore Enterprise mendukung gabungan gaya relasional melalui subkueri berkorelasi. Tidak seperti banyak database NoSQL yang sering kali memerlukan denormalisasi data atau melakukan beberapa permintaan sisi klien, subkueri memungkinkan Anda menggabungkan dan mengagregasi data dari koleksi atau subkoleksi terkait langsung di server.

Subkueri adalah ekspresi yang menjalankan pipeline bertingkat untuk setiap dokumen yang diproses oleh kueri luar. Hal ini memungkinkan pola pengambilan data yang kompleks, seperti mengambil dokumen bersama dengan item subkoleksi terkait atau menggabungkan data yang ditautkan secara logis di seluruh koleksi root yang berbeda.

Konsep

Bagian ini memperkenalkan konsep inti di balik penggunaan subkueri untuk melakukan gabungan dalam operasi Pipeline.

Subkueri sebagai ekspresi

Subkueri bukan tahap tingkat atas; melainkan ekspresi yang dapat digunakan di tahap mana pun yang menerima ekspresi, seperti select(...), add_fields(...), where(...), atau sort(...).

Cloud Firestore mendukung tiga jenis subkueri:

  • Subkueri Array: Mewujudkan seluruh kumpulan hasil subkueri sebagai array dokumen.
  • Subkueri Skalar: Mengevaluasi ke satu nilai, seperti jumlah, rata-rata, atau kolom tertentu dari dokumen terkait.
  • Subkueri subcollection(...): gabungan yang disederhanakan untuk hubungan induk-turunan satu-ke-banyak.

Cakupan dan variabel

Saat menulis gabungan, subkueri bertingkat sering kali perlu mereferensikan kolom dari dokumen "luar" (induk). Untuk menjembatani cakupan ini, Anda menggunakan tahap let(...) (disebut sebagai define(...) di beberapa SDK) untuk menentukan variabel dalam cakupan induk yang kemudian dapat direferensikan dalam subkueri menggunakan fungsi variable(...).

Sintaksis

Bagian berikut memberikan ringkasan sintaksis untuk melakukan gabungan.

Tahap let(...)

Tahap let(...) (disebut sebagai define(...) di beberapa SDK) adalah tahap non-pemfilteran yang secara eksplisit mengambil data dari cakupan induk ke dalam variabel bernama untuk digunakan dalam cakupan bertingkat berikutnya.

Subkueri Array

Subkueri Array adalah kasus khusus subkueri ekspresi yang mewujudkan seluruh kumpulan hasil subkueri ke dalam array. Jika subkueri menampilkan nol baris, subkueri akan dievaluasi ke array kosong. Subkueri tidak pernah menampilkan array null. Kueri tersebut berguna jika hasil lengkap diperlukan dalam hasil akhir, seperti saat mewujudkan koleksi bertingkat atau berkorelasi.

Kueri dapat memfilter, mengurutkan, &mengagregasi dalam subkueri untuk juga mengurangi jumlah data yang perlu diambil dan ditampilkan guna membantu mengurangi biaya kueri. Urutan subkueri dipertahankan, yang berarti tahap sort(...) dalam subkueri mengontrol urutan hasil dalam array akhir.

Gunakan wrapper SDK toArrayExpression() untuk mengonversi kueri menjadi array.

Subkueri Skalar

Subkueri skalar sering digunakan dalam tahap select(...) atau where(...) sebagai izin pemfilteran atau menghasilkan hasil subkueri tanpa mewujudkan kueri lengkap secara langsung.

Subkueri skalar yang menghasilkan nol hasil akan dievaluasi ke null itu sendiri, sedangkan subkueri yang dievaluasi ke beberapa elemen akan menghasilkan error runtime.

Jika subkueri skalar hanya menghasilkan satu kolom per hasil, kolom tersebut akan ditingkatkan menjadi hasil tingkat atas untuk subkueri. Hal ini paling sering terlihat saat subkueri berakhir dengan select(field("user_name")) atau aggregate(countAll().as("total")) dengan skema subkueri hanya satu kolom. Jika tidak, saat subkueri dapat menghasilkan beberapa kolom, kolom tersebut akan digabungkan dalam peta.

Gunakan wrapper SDK toScalarExpression() untuk mengonversi kueri menjadi ekspresi skalar.

Subkueri subcollection(...)

Meskipun ditawarkan sebagai tahap, tahap input subcollection(...) memungkinkan melakukan gabungan melalui Cloud Firestore's model data hierarkis. Dalam model hierarkis, kueri sering kali perlu mengambil dokumen bersama dengan data dari subkoleksinya sendiri. Meskipun Anda dapat mencapainya menggunakan tahap input collection_group(...) yang diikuti dengan filter pada referensi induk, subcollection(...) memberikan sintaksis yang jauh lebih ringkas.

Selain kondisi gabungan implisit, hal ini bertindak serupa dengan subkueri array, yang menampilkan hasil kosong jika tidak ada dokumen yang cocok, meskipun koleksi bertingkat tidak ada.

Pada dasarnya, sintaksis gula: sintaksis ini secara otomatis menggunakan __name__ dari dokumen dalam cakupan luar sebagai kunci gabungan untuk menyelesaikan hubungan hierarkis. Hal ini menjadikannya cara yang lebih disukai untuk melakukan pencarian di seluruh koleksi yang ditautkan dalam hubungan induk-turunan.

Contoh

Contoh data

Berikut ini memuat kumpulan data pengujian untuk digunakan dalam semua contoh berikut.

Node.js

// Load set of cities.
const cities = collection(db, "cities");

await setDoc(doc(cities, "SF"), {
  name: "San Francisco",
  state: "CA",
  country: "USA",
});
await setDoc(doc(cities, "LA"), {
  name: "Los Angeles",
  state: "CA",
  country: "USA"
});
await setDoc(doc(cities, "DC"), {
  name: "Washington, D.C.",
  state: null,
  country: "USA"
});
await setDoc(doc(cities, "TOK"), {
  name: "Tokyo",
  state: null,
  country: "Japan"
});

// Load restaurants in various cities.
const sfRestaurants = collection(db, "cities", "SF", "restaurants");
const laRestaurants = collection(db, "cities", "LA", "restaurants");
const dcRestaurants = collection(db, "cities", "DC", "restaurants");

const rest1 = await addDoc(sfRestaurants, {
  name: "Golden Gate Pizza",
  type: "pizza",
  owner_id: "Mario Rossi"
});
const rest2 = await addDoc(sfRestaurants, {
  name: "Bay Area Burger",
  type: "burger",
  owner_id: "Sarah Jenkins"
});
const rest3 = await addDoc(sfRestaurants, {
  name: "Sunset Taco",
  type: "mexican",
  owner_id: "Edward"
});

const rest4 = await addDoc(laRestaurants, {
  name: "Hollywood Sushi",
  type: "sushi",
  owner_id: "Ken Kenji"
});
const rest5 = await addDoc(laRestaurants, {
  name: "Venice Pizza",
  type: "pizza",
  owner_id: "Luigi Romano"
});

const rest6 = await addDoc(dcRestaurants, {
  name: "Capitol Tacos",
  type: "mexican",
  owner_id: "Maria Garcia"
});
const rest7 = await addDoc(dcRestaurants, {
  name: "Georgetown Coffee",
  type: "cafe",
  owner_id: "David Kim"
});

// Load collection of reviews.
const reviews = collection(db, "reviews");

await addDoc(reviews, { restaurant: rest1, rating: 5, reviewer_id "Alice" });
await addDoc(reviews, { restaurant: rest1, rating: 4, reviewer_id "Bob" });
await addDoc(reviews, { restaurant: rest2, rating: 4, reviewer_id "Charlie" });
await addDoc(reviews, { restaurant: rest3, rating: 5, reviewer_id "Diana" });
await addDoc(reviews, { restaurant: rest3, rating: 4, reviewer_id "Edward" });
await addDoc(reviews, { restaurant: rest3, rating: 4, reviewer_id "Fiona" });
// rest4 has 0 reviews
await addDoc(reviews, { restaurant: rest5, rating: 3, reviewer_id "George" });
await addDoc(reviews, { restaurant: rest6, rating: 5, reviewer_id "Hannah" });
await addDoc(reviews, { restaurant: rest6, rating: 4, reviewer_id "Ian" });
await addDoc(reviews, { restaurant: rest7, rating: 5, reviewer_id "Julia" });

Mencari Dokumen di Koleksi Lain

Kueri berikut pada grup koleksi reviews melakukan pencarian ke dalam grup koleksi restaurant menggunakan referensi kunci utama.

Node.js

let results = await execute(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")));

Respons

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

Menggabungkan Beberapa Koleksi

Kueri berikut mengambil semua tempat pizza dari grup koleksi restaurants, dan menggunakan subkueri array untuk mengambil dan menyematkan ulasan terkait langsung ke dalam respons.

Node.js

let results = await execute(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")));

Respons

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

Mengagregasi di Beberapa Koleksi

Kueri berikut pada grup koleksi restaurants menggunakan subkueri berkorelasi untuk mendapatkan rating rata-rata untuk setiap restoran dari grup koleksi reviews.

Node.js

let results = await execute(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")));

Respons

{
  name: "Golden Gate Pizza",
  avg_rating: 4.5
},
{
  name: "Venice Pizza",
  avg_rating: 3.0
}

N Teratas Per Grup (Subkueri dengan Batas)

Kueri berikut mengambil semua dokumen dari grup koleksi restaurants, dan menggunakan subkueri berkorelasi untuk mengambil 2 ulasan dengan rating tertinggi untuk setiap restoran.

Hal ini memastikan bahwa array ulasan tidak bertambah terlalu besar dan mencapai batas memori kueri.

Node.js

let results = await execute(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")));

Respons

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

Menggabungkan Subkoleksi

Kueri berikut memindai koleksi cities dan menggunakan tahap subcollection(...) untuk menggabungkan dokumen secara implisit dari koleksi bertingkat guna menemukan jumlah restoran per kota.

Node.js

let results = await execute(db.pipeline()
  .collection("cities")
  .addFields(subcollection("restaurants")
    .toArrayExpression()
    .length()
    .as("restaurant_count")));

Respons

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

Menyatakan Beberapa Kondisi Gabungan

Kueri berikut memindai grup koleksi restaurants dan melakukan gabungan multi-kolom dengan grup koleksi reviews untuk menemukan pemilik yang meninjau restoran mereka sendiri.

Node.js

let results = await execute(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)));

Respons

{
  __name__: cities/SF/restaurants/X9An0HIlx29A9GPuRthS,
  name: "Sunset Taco",
  type: "mexican",
  owner_id: "Edward"
}

Anti-Gabungan (NOT EXISTS)

Kueri berikut memindai grup koleksi restaurants dan menemukan semua restoran yang belum memiliki ulasan.

Node.js

let results = await execute(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)));

Respons

{
  __name__: "cities/LA/restaurants/X9An0HIlx29A9GPuRthS",
  name: "Hollywood Sushi",
  type: "sushi",
  owner_id: "Ken Kenji"
}

Subkueri sebagai Gabungan

Kueri berikut meratakan hubungan antara setiap tempat pizza dan ulasannya. Dengan menempatkan subkueri di dalam tahap unnest(...), server menduplikasi dokumen restoran luar untuk setiap ulasan yang cocok, sehingga menghasilkan dokumen gabungan yang datar (mirip dengan INNER JOIN SQL).

Node.js

let results = await execute(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")));

Respons

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

Subkueri Tidak Berkorelasi sebagai Filter

Kueri berikut pada koleksi reviews melakukan pemfilteran menggunakan subkueri yang tidak berkorelasi pada dirinya sendiri untuk menemukan ulasan yang lebih besar dari rating rata-rata.

Node.js

let results = await execute(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");

Respons

{
  rating: 5,
  reviewer_id "Alice"
},
{
  rating: 5,
  reviewer_id "Diana"
},
{
  rating: 5,
  reviewer_id "Hannah"
},
{
  rating: 5,
  reviewer_id "Julia"
}

Praktik terbaik

  • Mengelola memori dengan toArrayExpression(): Berhati-hatilah dengan toArrayExpression() subkueri, karena mewujudkan sejumlah besar dokumen dapat menghabiskan batas memori kueri (128 MiB). Untuk mengatasinya, gunakan select(...) dalam subkueri untuk hanya menampilkan kolom yang diperlukan dan menerapkan where(...) filter untuk membatasi jumlah dokumen yang ditampilkan. Pertimbangkan untuk menggunakan limit(...) jika sesuai untuk membatasi jumlah dokumen yang ditampilkan oleh subkueri.
  • Pengindeksan: Pastikan kolom yang digunakan dalam klausa where(...) subkueri diindeks. Gabungan berperforma tinggi bergantung pada kemampuan untuk melakukan pencarian indeks, bukan pemindaian tabel penuh.

Untuk mengetahui praktik terbaik kueri lainnya, lihat panduan kami yang membahas pengoptimalan kueri.

Batasan

  • subcollection(...) cakupan: Tahap input subcollection(...) hanya didukung dalam subkueri, karena memerlukan konteks dokumen induk untuk menyelesaikan hubungan hierarkis dan melakukan gabungan.
  • Kedalaman Bertingkat: Subkueri dapat dibuat bertingkat hingga kedalaman 20 lapisan.
  • Penggunaan Memori: Batas 128 MiB pada data yang diwujudkan berlaku di seluruh kueri, termasuk semua dokumen yang digabungkan.