خانه/ پرس‌وجوهای پیشرفته/ زیرپرس‌وجوها

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

متوسط ۱۵ دقیقه مطالعه

فصل ۴ به ما یاد داد چطور با JOIN چند جدول را کنار هم بگذاریم. اما گاهی نیازی به ترکیب کل جدول‌ها نداریم — فقط می‌خواهیم نتیجه‌ی یک پرس‌وجو را به‌عنوان ورودی پرس‌وجوی دیگری استفاده کنیم. به این تکنیک زیرپرس‌وجو (Subquery) می‌گویند: یک SELECT کامل، داخل یک SELECT دیگر.

زیرپرس‌وجوی اسکالر (Scalar Subquery)

ساده‌ترین نوع زیرپرس‌وجو، دقیقاً یک مقدار (یک ردیف، یک ستون) برمی‌گرداند و می‌تواند هرجا یک عدد یا متن مجاز باشد، به‌کار برود:

sql
-- محصولاتی که قیمتشان از میانگین قیمت همه‌ی محصولات بیشتر است
SELECT product, price
FROM sales
WHERE price > (SELECT AVG(price) FROM sales);

این‌جا زیرپرس‌وجوی (SELECT AVG(price) FROM sales) اول اجرا و به یک عدد تبدیل می‌شود؛ سپس پرس‌وجوی بیرونی هر ردیف را با همان عدد مقایسه می‌کند. بیا ببینیم:

امتحانش کن — زیرپرس‌وجوی اسکالر

زیرپرس‌وجو با IN

وقتی زیرپرس‌وجو ممکن است چند مقدار برگرداند (نه فقط یکی)، از IN استفاده می‌کنیم — دقیقاً همان‌طور که در درس WHERE با لیست‌های ثابت دیدیم، فقط این‌بار لیست از یک پرس‌وجوی دیگر می‌آید:

sql
-- دانش‌آموزانی که حداقل در یک درس ثبت‌نام کرده‌اند
SELECT full_name
FROM students
WHERE id IN (SELECT student_id FROM enrollments);

نکته‌ی جالب: این پرس‌وجو دقیقاً همان نتیجه‌ای را می‌دهد که در فصل ۴ با INNER JOIN (به‌همراه DISTINCT) هم می‌شد گرفت — یعنی اغلب چند راه مختلف برای رسیدن به یک نتیجه وجود دارد. بیا امتحان کنیم:

امتحانش کن — زیرپرس‌وجو با IN

⚠️ هشدار مهم: NOT IN و مقادیر NULL

این‌جا یکی از رایج‌ترین و خطرناک‌ترین تله‌های SQL قرار دارد. فرض کن می‌خواهی برعکس مثال بالا را بپرسی: «کدام درس‌ها هیچ دانش‌آموزی ندارند؟» و می‌نویسی:

sql
SELECT title
FROM courses
WHERE id NOT IN (SELECT course_id FROM enrollments);

اگر حتی یک ردیف در جدول enrollments مقدار course_idاش NULL باشد (مثلاً ثبت‌نامی که هنوز درسش انتخاب نشده)، این پرس‌وجو به‌طور کامل و بی‌سروصدا هیچ نتیجه‌ای برمی‌گرداند — حتی اگر واقعاً درسی بدون دانش‌آموز وجود داشته باشد! علتش یادت هست؟ در درس NULL دیدیم مقایسه با NULL همیشه UNKNOWN می‌شود، نه TRUE و نه FALSE؛ و کافی است یکی از مقادیر داخل لیست IN نامشخص باشد تا کل شرط NOT IN برای همه‌ی ردیف‌ها غیرقابل‌اعتماد شود. بیا این تله را با چشم خودت ببینی:

امتحانش کن — تله‌ی NOT IN با NULL
نتیجه خالی است — با این‌که «هنر» واقعاً هیچ ثبت‌نامی ندارد! این رفتار در SQL Server، MySQL، PostgreSQL و SQLite (همین Playground) کاملاً یکسان و استاندارد است؛ یعنی یک باگ موتور نیست، بلکه نتیجه‌ی منطقی سه‌مقداری SQL است. راه‌حل امن این مشکل، NOT EXISTS است که موضوع درس بعدی است.

زیرپرس‌وجوی همبسته (Correlated Subquery)

بر خلاف مثال‌های بالا که زیرپرس‌وجو مستقل و فقط یک‌بار اجرا می‌شد، زیرپرس‌وجوی همبسته به ستون‌های پرس‌وجوی بیرونی ارجاع می‌دهد و برای هر ردیف بیرونی، جداگانه دوباره اجرا می‌شود:

sql
-- تعداد درس‌های ثبت‌نامی هر دانش‌آموز
SELECT
  s.full_name,
  (SELECT COUNT(*) FROM enrollments e WHERE e.student_id = s.id) AS CourseCount
FROM students s;

این‌جا e.student_id = s.id باعث می‌شود زیرپرس‌وجو به ازای هر دانش‌آموز، دوباره اجرا شود. توجه کن این دقیقاً همان نتیجه‌ای است که در درس LEFT JOIN با ترکیب LEFT JOIN و GROUP BY گرفتیم — فقط این‌بار بدون نیاز به GROUP BY. بیا ببینیم:

امتحانش کن — زیرپرس‌وجوی همبسته
نکته‌ی کارایی: چون زیرپرس‌وجوی همبسته به ازای هر ردیف بیرونی دوباره اجرا می‌شود، روی جدول‌های بزرگ می‌تواند کند باشد. در بسیاری موارد همان کار را می‌توان با JOIN سریع‌تر انجام داد — موضوعی که در فصل ۱۰ (ایندکس‌گذاری و کارایی) بیشتر بررسی می‌کنیم.

زیرپرس‌وجو در FROM (جدول مشتق‌شده)

زیرپرس‌وجو را می‌توان به‌جای یک جدول واقعی، در FROM هم به‌کار برد — به این حالت جدول مشتق‌شده (Derived Table) می‌گویند. این‌جا دقیقاً همان جایی است که مفید می‌شود که SQL اجازه‌ی تو در تو کردن مستقیم توابع تجمیعی را نمی‌دهد — نمی‌توانی بنویسی AVG(AVG(price)). اما با یک جدول مشتق‌شده، می‌شود:

sql
-- میانگینِ میانگین‌قیمتِ هر دسته (نه میانگین کل محصولات)
SELECT AVG(avg_price) AS AvgOfCategoryAverages
FROM (
  SELECT category, AVG(price) AS avg_price
  FROM sales
  GROUP BY category
) AS category_avgs;

دقت کن: جدول مشتق‌شده حتماً باید نام مستعار (این‌جا category_avgs) داشته باشد، وگرنه SQL Server خطا می‌دهد. بیا امتحان کنیم:

امتحانش کن — جدول مشتق‌شده
در همین جدول sales، هر دو دسته («کتاب» و «نوشت‌افزار») دقیقاً ۳ محصول دارند، پس این عدد تصادفاً با AVG(price)ی ساده روی کل جدول یکسان از آب درمی‌آید. اگر تعداد محصولات هر دسته متفاوت بود (مثلاً دسته‌ای با ۱۰ محصول ارزان در برابر دسته‌ای با ۲ محصول گران)، این دو عدد حتماً با هم فرق می‌کردند — چون میانگینِ میانگین‌ها، به هر دسته وزن یکسان می‌دهد، نه به هر محصول.

جمع‌بندی این درس

  • زیرپرس‌وجوی اسکالر یک مقدار برمی‌گرداند و می‌تواند در WHERE یا SELECT استفاده شود.
  • زیرپرس‌وجو با IN لیستی از مقادیر برمی‌گرداند.
  • هرگز از NOT IN با زیرپرس‌وجویی که ممکن است NULL برگرداند استفاده نکن — نتیجه‌اش می‌تواند به‌طور کامل و بی‌خطا، خالی باشد.
  • زیرپرس‌وجوی همبسته به ازای هر ردیف بیرونی دوباره اجرا می‌شود؛ برای مقادیر محاسبه‌شده‌ی وابسته به هر ردیف مفید است.
  • زیرپرس‌وجو در FROM (جدول مشتق‌شده) امکان تجمیع چندمرحله‌ای را فراهم می‌کند و حتماً به نام مستعار نیاز دارد.

در درس بعدی سراغ EXISTS می‌رویم: راه‌حل امن و اغلب سریع‌تر برای بررسی «آیا رابطه‌ای وجود دارد؟» — بدون تله‌ی NULL که همین حالا دیدیم.