تخطَّ إلى المحتوى

الاستعلامات الفرعية في SQL: استعلام داخل استعلام

الدرس 9 من 12 · SQL

«من هم الطلاب الذين درجتهم أعلى من المتوسط؟» سؤال بسيط، لكنه يحتاج خطوتين: احسب المتوسط أولاً، ثم قارن كل طالب به. بدل تشغيل استعلامين ونقل الرقم بيدك، تسمح SQL بوضع استعلام داخل استعلام. هذا هو الاستعلام الفرعي (Subquery)، وهو موضوع هذا الدرس.

سنعمل على الجداول نفسها: students (سارة 92، وعمر 78، ومنى 85، ويوسف 64، وليلى بلا درجة، وأحمد 71)، و courses (Python و HTML و SQL)، و enrollments الذي يربط بينهما.

SELECT name, score FROM students
WHERE score > (SELECT AVG(score) FROM students);

الناتج:

name score
سارة 92
منى 85

الاستعلام بين القوسين يُنفَّذ أولاً ويُرجع قيمة واحدة: المتوسط 78. بعدها يصبح الاستعلام الخارجي كأنك كتبت WHERE score > 78. والفائدة أن النتيجة تبقى صحيحة مهما تغيرت البيانات، فلا رقم مكتوب بيدك.

تخيّل الاستعلام الفرعي مثل سؤال جانبي تسأله لزميلك قبل أن تجيب عن السؤال الأصلي: «كم المتوسط؟»، ثم تكمل عملك بإجابته.

ويمكن وضع الاستعلام الفرعي في قائمة الأعمدة أيضاً. هنا نحسب الفارق بين كل طالب وصاحب أعلى درجة:

SELECT name, score, (SELECT MAX(score) FROM students) - score AS gap
FROM students
WHERE score IS NOT NULL
ORDER BY gap;

الناتج:

name score gap
سارة 92 0
منى 85 7
عمر 78 14
أحمد 71 21
يوسف 64 28

تعرفت على IN مع قائمة مكتوبة باليد. الأقوى أن تأتي القائمة من استعلام آخر. من هم المشتركون في كورس Python (رقمه 1)؟

SELECT name FROM students
WHERE id IN (SELECT student_id FROM enrollments WHERE course_id = 1);

الناتج:

name
سارة
منى
أحمد

الاستعلام الداخلي يُرجع قائمة أرقام الطلاب: 1 و 3 و 6. ثم يختار الخارجي الطلاب الذين أرقامهم ضمن هذه القائمة. هنا الاستعلام الفرعي يُرجع عموداً واحداً بأي عدد من الصفوف.

والعكس بـ NOT IN: من لم يشترك في أي كورس؟

SELECT name FROM students
WHERE id NOT IN (SELECT student_id FROM enrollments);

الناتج:

name
ليلى

EXISTS لا تهتم بالقيم، بل تسأل سؤالاً واحداً: هل يُرجع الاستعلام الفرعي صفاً واحداً على الأقل؟

SELECT s.name FROM students s
WHERE NOT EXISTS (
SELECT 1 FROM enrollments e WHERE e.student_id = s.id
);

الناتج:

name
ليلى

لاحظ الجديد: الاستعلام الداخلي يستخدم s.id القادم من الاستعلام الخارجي. هذا اسمه استعلام فرعي مترابط (Correlated Subquery): لا يُنفَّذ مرة واحدة، بل يُعاد تقييمه لكل صف من الخارج. لكل طالب نسأل: هل له اشتراك؟ وكتابة SELECT 1 عُرف شائع، لأن ما يُرجعه الاستعلام لا يهم، المهم وجود صف.

مثال آخر: الكورسات التي فيها طالب واحد على الأقل درجته 90 فأكثر:

SELECT c.title FROM courses c
WHERE EXISTS (
SELECT 1 FROM enrollments e
JOIN students s ON s.id = e.student_id
WHERE e.course_id = c.id AND s.score >= 90
);

الناتج:

title
Python
SQL

الاستعلام المترابط يحل سؤالاً مشهوراً: من صاحب أعلى درجة في كل مدينة؟

SELECT s1.name, s1.city, s1.score
FROM students s1
WHERE s1.score = (
SELECT MAX(s2.score) FROM students s2 WHERE s2.city = s1.city
);

الناتج:

name city score
سارة القاهرة 92
عمر الإسكندرية 78
منى طنطا 85

استخدمنا الجدول نفسه مرتين باسمين مختلفين: s1 للصف الحالي، و s2 لحساب أعلى درجة في مدينته. وليلى لم تظهر لأن درجتها NULL، والمقارنة مع NULL لا تتحقق أبداً.

نتيجة أي استعلام هي جدول. إذن يمكن أن تستعلم منها كأنها جدول حقيقي. ما المدن التي متوسط طلابها أعلى من 75؟

SELECT city, average
FROM (
SELECT city, AVG(score) AS average
FROM students
GROUP BY city
) AS city_stats
WHERE average > 75;

الناتج:

city average
القاهرة 78.0
طنطا 85.0

الاستعلام الداخلي بنى جدولاً مؤقتاً أسميناه city_stats، والخارجي صفّاه. (كان يمكن حل هذا المثال بـ HAVING أيضاً، لكن الأسلوب نفسه يفيد في حالات أعقد.)

WITH: اسم للاستعلام الفرعي

Section titled “WITH: اسم للاستعلام الفرعي”

الاستعلامات المتداخلة تصعب قراءتها بسرعة. WITH تعرّف الجدول المؤقت أولاً وتعطيه اسماً، ثم تستخدمه:

WITH city_stats AS (
SELECT city, AVG(score) AS average
FROM students
GROUP BY city
)
SELECT city, average FROM city_stats
WHERE average > 75
ORDER BY average DESC;

الناتج:

city average
طنطا 85.0
القاهرة 78.0

النتيجة نفسها، لكن الكود يُقرأ من الأعلى للأسفل كخطوات مرتبة. هذه الصيغة اسمها تعبير الجدول المشترك (CTE)، وتدعمها كل الأنظمة الحديثة.

كثير من الأسئلة يُحل بالطريقتين. المشتركون في Python بـ JOIN:

SELECT DISTINCT s.name
FROM students s
JOIN enrollments e ON e.student_id = s.id
WHERE e.course_id = 1;

يعطي الأسماء الثلاثة نفسها. فأيهما تختار؟

استخدم عندما
JOIN تريد عرض أعمدة من الجدولين معاً
IN أو EXISTS تريد تصفية جدول بناءً على جدول آخر فقط
استعلام فرعي بقيمة واحدة تقارن برقم محسوب مثل المتوسط أو الأعلى
  1. اعرض الطلاب الذين عمرهم أكبر من متوسط الأعمار.
  2. اعرض أسماء الكورسات التي لم يشترك فيها الطالب رقم 1، باستخدام NOT IN.
  3. اعرض الطلاب المشتركين في أكثر من كورس. (تلميح: استعلام فرعي فيه GROUP BY و HAVING.)
  4. تحدٍّ: اعرض اسم كل طالب وبجانبه عدد الكورسات المشترك فيها، باستعلام فرعي مترابط في قائمة الأعمدة.
  • استعلام فرعي يُرجع أكثر من قيمة مع =: المقارنة بـ = أو > تحتاج قيمة واحدة. إذا كان الناتج قائمة فاستخدم IN.
  • نسيان الأقواس: الاستعلام الفرعي يُكتب دائماً بين قوسين.
  • NOT IN مع عمود فيه NULL: تحصل على نتيجة فارغة دون أي رسالة خطأ. استخدم NOT EXISTS.
  • نسيان اسم الجدول المؤقت: بعض الأنظمة مثل MySQL تشترط اسماً بعد الاستعلام الفرعي في FROM: (...) AS name.

سؤال

متى يُنفَّذ الاستعلام الفرعي غير المترابط في WHERE score > (SELECT AVG(score) FROM students)؟

سؤال

ما الذي تفحصه EXISTS؟

  • الاستعلام الفرعي استعلام بين قوسين داخل استعلام آخر.
  • مع عوامل المقارنة يُرجع قيمة واحدة، ومع IN يُرجع عموداً من القيم.
  • EXISTS تفحص وجود صفوف، والاستعلام المترابط يُعاد تقييمه لكل صف خارجي.
  • يمكن الاستعلام من نتيجة استعلام في FROM، و WITH تجعل ذلك أوضح.
  • احذر NOT IN مع NULL، واختر JOIN عندما تحتاج أعمدة من الجدولين.