انجام عملیات join با استفاده از subquery ها

نمای کلی

نسخه سازمانی فایراستور از طریق زیرکوئری‌های هم‌بسته، از اتصال‌های رابطه‌ای پشتیبانی می‌کند. برخلاف بسیاری از پایگاه‌های داده NoSQL که اغلب نیاز به غیرنرمال‌سازی داده‌ها یا انجام چندین درخواست سمت کلاینت دارند، زیرکوئری‌ها به شما امکان می‌دهند داده‌ها را از مجموعه‌ها یا زیرمجموعه‌های مرتبط مستقیماً روی سرور ترکیب و تجمیع کنید.

زیرپرس‌وجوها عباراتی هستند که یک خط لوله تودرتو را برای هر سندی که توسط پرس‌وجوی بیرونی پردازش می‌شود، اجرا می‌کنند. این امر الگوهای بازیابی داده‌های پیچیده، مانند واکشی یک سند در کنار اقلام زیرمجموعه مرتبط با آن یا اتصال داده‌های منطقی مرتبط در مجموعه‌های ریشه‌ای متفاوت را امکان‌پذیر می‌سازد.

مفاهیم

این بخش مفاهیم اصلی پشت استفاده از subqueryها برای انجام joinها در عملیات Pipeline را معرفی می‌کند.

زیرپرس‌وجوها به عنوان عبارات

یک subquery یک مرحله سطح بالا نیست؛ در عوض، عبارتی است که می‌تواند در هر مرحله‌ای که عباراتی مانند select(...) ، add_fields(...) ، where(...) یا sort(...) را می‌پذیرد، استفاده شود.

Cloud Firestore از سه نوع زیرپرس‌وجو پشتیبانی می‌کند:

  • زیرپرس‌وجوهای آرایه‌ای: کل مجموعه نتایج زیرپرس‌وجو را به صورت آرایه‌ای از اسناد ارائه می‌دهد.
  • زیرپرس‌وجوهای اسکالر: ارزیابی به یک مقدار واحد، مانند تعداد، میانگین یا یک فیلد خاص از یک سند مرتبط.
  • subcollection(...) زیرپرس‌وجوها: پیوندهای ساده‌شده برای رابطه‌ی والد-فرزندی یک به چند.

دامنه و متغیرها

هنگام نوشتن یک پیوند، زیرپرس‌وجوی تودرتو اغلب نیاز دارد که به فیلدهایی از سند "بیرونی" (والد) ارجاع دهد. برای ایجاد پل بین این حوزه‌ها، از مرحله let(...) (که در برخی SDKها به عنوان define(...) شناخته می‌شود) برای تعریف متغیرهایی در حوزه والد استفاده می‌کنید که سپس می‌توانند با استفاده از تابع variable(...) در زیرپرس‌وجوی به آنها ارجاع داده شوند.

نحو

بخش‌های زیر مروری بر سینتکس اجرای joinها ارائه می‌دهند.

مرحله let(...)

مرحله let(...) (که در برخی SDKها به آن define(...) گفته می‌شود) یک مرحله بدون فیلتر است که صریحاً داده‌ها را از محدوده والد به یک متغیر نامگذاری شده برای استفاده در محدوده‌های تو در تو بعدی می‌آورد.

زیرپرس‌وجوهای آرایه

یک زیرپرس‌وجوی آرایه‌ای (Array subquery) حالت خاصی از زیرپرس‌وجوی عبارت (expression subquery) است که کل مجموعه نتایج زیرپرس‌وجو را در یک آرایه پیاده‌سازی می‌کند. اگر زیرپرس‌وجو هیچ ردیفی را برنگرداند، به یک آرایه خالی ارزیابی می‌شود. هرگز آرایه null array) برنمی‌گرداند. چنین پرس‌وجوهایی زمانی مفید هستند که نتایج کامل در نتیجه نهایی مورد نیاز باشند، مانند زمانی که یک مجموعه تودرتو یا همبسته پیاده‌سازی می‌شود.

پرس‌وجوها می‌توانند در زیرپرس‌وجو فیلتر، مرتب‌سازی و تجمیع شوند تا میزان داده‌هایی که باید واکشی و بازگردانده شوند نیز کاهش یابد و به کاهش هزینه پرس‌وجو کمک کند. ترتیب زیرپرس‌وجو رعایت می‌شود، به این معنی که مرحله sort(...) در زیرپرس‌وجو، ترتیب نتایج را در آرایه نهایی کنترل می‌کند.

از بسته‌بندی SDK با toArrayExpression() برای تبدیل یک کوئری به آرایه استفاده کنید.

زیرپرس‌وجوهای اسکالر

زیرپرس‌وجوهای اسکالر اغلب در مرحله select(...) یا where(...) استفاده می‌شوند، زیرا امکان فیلتر کردن یا استخراج نتیجه یک زیرپرس‌وجو را بدون ارائه مستقیم کل پرس‌وجو فراهم می‌کنند.

یک زیرپرس‌وجوی اسکالر که نتیجه‌ی صفر تولید کند، خودش را null ارزیابی می‌کند، در حالی که یک زیرپرس‌وجوی که چندین عنصر را ارزیابی کند، منجر به خطای زمان اجرا خواهد شد.

وقتی یک زیرپرس‌وجوی اسکالر فقط یک فیلد به ازای هر نتیجه تولید می‌کند، آن فیلد به بالاترین سطح نتیجه برای زیرپرس‌وجو ارتقا می‌یابد. این مورد معمولاً زمانی دیده می‌شود که زیرپرس‌وجو با یک select(field("user_name")) یا aggregate(countAll().as("total")) به پایان می‌رسد که در آن طرحواره زیرپرس‌وجو فقط یک فیلد است. در غیر این صورت، وقتی یک زیرپرس‌وجو می‌تواند چندین فیلد تولید کند، آنها در یک map قرار می‌گیرند.

از بسته‌بندی SDK toScalarExpression() برای تبدیل یک کوئری به یک عبارت اسکالر استفاده کنید.

subcollection(...) زیرپرس‌وجوها

اگرچه به عنوان یک مرحله ارائه می‌شود، اما مرحله ورودی subcollection(...) امکان انجام joinها را بر روی مدل داده سلسله مراتبی Cloud Firestore فراهم می‌کند. در یک مدل سلسله مراتبی، کوئری‌ها اغلب نیاز به بازیابی یک سند در کنار داده‌های زیرمجموع‌های خود دارند. در حالی که می‌توانید با استفاده از مرحله ورودی collection_group(...) و به دنبال آن یک فیلتر روی مرجع والد، به این هدف دست یابید، subcollection(...) سینتکس بسیار مختصرتری ارائه می‌دهد.

به غیر از شرط اتصال ضمنی، این تابع مشابه یک زیرپرس و جوی آرایه عمل می‌کند و در صورت عدم تطابق اسناد، حتی اگر مجموعه تو در تو وجود نداشته باشد، نتیجه خالی را برمی‌گرداند.

اساساً یک دستور زبان شیرین است: به طور خودکار از __name__ سند در محدوده بیرونی به عنوان کلید اتصال برای حل رابطه سلسله مراتبی استفاده می‌کند. این امر آن را به روش ترجیحی برای انجام جستجو در مجموعه‌های مرتبط در یک رابطه والد-فرزندی تبدیل می‌کند.

مثال‌ها

داده‌های نمونه

کد زیر مجموعه‌ای از داده‌های آزمایشی را برای استفاده در تمام مثال‌های بعدی بارگذاری می‌کند.

نود جی اس

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

جستجوی یک سند در مجموعه دیگر

پرس‌وجوی زیر روی گروه مجموعه reviews ، با استفاده از یک مرجع کلید اصلی، جستجویی را در گروه مجموعه restaurant انجام می‌دهد.

نود جی اس

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

پاسخ

{
  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 دریافت می‌کند و از یک زیرکوئری آرایه‌ای برای دریافت و جاسازی مستقیم نظرات مرتبط با آنها در پاسخ استفاده می‌کند.

نود جی اس

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

پاسخ

{
  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 استفاده می‌کند.

نود جی اس

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

پاسخ

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

تعداد بالای N در هر گروه (زیرپرس‌وجو با محدودیت)

کوئری زیر تمام اسناد را از گروه مجموعه restaurants واکشی می‌کند و از یک زیرکوئری همبسته برای واکشی ۲ نقد برتر با بالاترین امتیاز برای هر رستوران استفاده می‌کند.

این تضمین می‌کند که آرایه‌ی نظرات بیش از حد بزرگ نشود و به محدودیت حافظه‌ی کوئری نرسد.

نود جی اس

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

پاسخ

{
  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(...) برای پیوند ضمنی اسناد از یک مجموعه تودرتو استفاده می‌کند تا تعداد رستوران‌ها در هر شهر را پیدا کند.

نود جی اس

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

پاسخ

{
  __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 انجام می‌دهد تا صاحبان نقد رستوران‌های خود را پیدا کند.

نود جی اس

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

پاسخ

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

ضد پیوستن ( NOT EXISTS )

کوئری زیر گروه مجموعه restaurants را اسکن می‌کند و تمام رستوران‌هایی را که هنوز نظری ندارند، پیدا می‌کند.

نود جی اس

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

پاسخ

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

زیرپرس و جو به عنوان اتصال

پرس‌وجوی زیر رابطه بین هر پیتزافروشی و نظرات آن را مسطح می‌کند. با قرار دادن زیرپرس‌وجو در مرحله unnest(...) ، سرور سند رستوران بیرونی را برای هر نظر منطبق کپی می‌کند و اسناد مسطح و پیوسته (مشابه INNER JOIN در SQL) تولید می‌کند.

نود جی اس

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

پاسخ

{
  __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 ، فیلترهایی را با استفاده از یک زیرپرس‌وجوی غیرهمبسته روی خودش انجام می‌دهد تا نظراتی را پیدا کند که از میانگین امتیاز بیشتر باشند.

نود جی اس

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

پاسخ

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

بهترین شیوه‌ها

  • مدیریت حافظه با toArrayExpression() : در مورد زیرپرس‌وجوهای toArrayExpression() محتاط باشید، زیرا ارائه تعداد زیادی سند می‌تواند محدودیت حافظه پرس‌وجو (128 مگابایت) را به پایان برساند. برای کاهش این مشکل، از select(...) در زیرپرس‌وجو استفاده کنید تا فقط فیلدهای ضروری را برگردانید و فیلترهای where(...) را برای محدود کردن تعداد اسناد برگردانده شده اعمال کنید. در صورت لزوم، استفاده از limit(...) را برای محدود کردن تعداد اسناد برگردانده شده توسط زیرپرس‌وجو در نظر بگیرید.
  • فهرست‌بندی: اطمینان حاصل کنید که فیلدهای استفاده شده در عبارت where(...) از یک subquery فهرست‌بندی شده‌اند. پیوندهای Performant به توانایی انجام جستجوی فهرست به جای اسکن کامل جدول متکی هستند.

برای بهترین شیوه‌های پرس‌وجوی بیشتر، به راهنمای ما که شامل بهینه‌سازی پرس‌وجو می‌شود ، مراجعه کنید.

محدودیت‌ها

  • دامنه subcollection(...) : مرحله ورودی subcollection(...) فقط در subqueryها پشتیبانی می‌شود، زیرا برای حل رابطه سلسله مراتبی و انجام اتصال، به چارچوب یک سند والد نیاز دارد.
  • عمق تودرتو: زیرپرس‌وجوها می‌توانند تا عمق ۲۰ لایه تودرتو شوند.
  • میزان استفاده از حافظه: محدودیت ۱۲۸ مگابایت برای داده‌های مادی در کل پرس‌وجو، شامل تمام اسناد پیوست‌شده، اعمال می‌شود.