Melakukan penggabungan dengan subkueri

Ringkasan

Edisi Firestore Enterprise mendukung gabungan gaya relasional melalui subkueri yang 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 penggabungan dalam operasi Pipeline.

Subkueri sebagai ekspresi

Subkueri bukanlah tahap tingkat teratas; 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: Dievaluasi ke satu nilai, seperti jumlah, rata-rata, atau kolom tertentu dari dokumen terkait.
  • subcollection(...) Subkueri: gabungan yang disederhanakan untuk hubungan induk-turunan one-to-many.

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 penggabungan.

Tahap let(...)

Tahap let(...) (disebut sebagai define(...) di beberapa SDK) adalah tahap non-pemfilteran yang secara eksplisit membawa 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 hasil subkueri ke dalam array. Jika subkueri menampilkan nol baris, subkueri akan dievaluasi ke array kosong. Metode ini tidak pernah menampilkan array null. Kueri ini berguna saat hasil lengkap diperlukan dalam hasil akhir, seperti saat mematerialisasi koleksi bertingkat atau berkorelasi.

Kueri dapat memfilter, mengurutkan, & menggabungkan dalam subkueri untuk juga mengurangi jumlah data yang perlu diambil dan ditampilkan guna membantu mengurangi biaya kueri. Urutan subkueri dipertahankan, yang berarti bahwa 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(...) karena memungkinkan pemfilteran atau menghasilkan hasil subkueri tanpa mematerialisasi kueri lengkap secara langsung.

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

Jika subkueri skalar hanya menghasilkan satu kolom per hasil, kolom tersebut akan diangkat menjadi hasil tingkat teratas untuk subkueri. Hal ini paling sering terjadi saat subkueri diakhiri dengan select(field("user_name")) atau aggregate(countAll().as("total")) yang skema subkuerinya hanya berupa satu kolom. Jika tidak, saat subkueri dapat menghasilkan beberapa kolom, kolom tersebut akan disertakan dalam peta.

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

subcollection(...) Subkueri

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

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

Pada dasarnya, ini adalah gula sintaksis: secara otomatis menggunakan __name__ 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 sekumpulan data pengujian untuk digunakan dalam semua contoh berikutnya.

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 dalam Koleksi Lain

Kueri berikut pada kelompok koleksi reviews melakukan pencarian ke dalam kelompok 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 terkaitnya 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" }
  ]
}

Menggabungkan Beberapa Kumpulan Data

Kueri berikut pada kelompok koleksi restaurants menggunakan subkueri yang berkorelasi untuk mendapatkan rating rata-rata setiap restoran dari kelompok 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 yang berkorelasi untuk mengambil 2 ulasan dengan rating tertinggi untuk setiap restoran.

Hal ini memastikan bahwa array ulasan tidak tumbuh 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 secara implisit dokumen 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
}

Mengekspresikan Beberapa Kondisi Penggabungan

Kueri berikut memindai kelompok koleksi restaurants dan melakukan gabungan multi-kolom dengan kelompok 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-Gabung (NOT EXISTS)

Kueri berikut memindai kelompok 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 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 yang 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 subkueri toArrayExpression(), karena mewujudkan sejumlah besar dokumen dapat menghabiskan batas memori kueri (128 MiB). Untuk mengatasi hal ini, gunakan select(...) dalam subkueri untuk menampilkan hanya kolom yang diperlukan dan terapkan filter where(...) 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(...) dari subkueri diindeks. Penggabungan berperforma tinggi mengandalkan kemampuan untuk melakukan pencarian indeks, bukan pemindaian tabel penuh.

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

Keterbatasan

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