الاستعلامات الفرعية في SQL: استعلام داخل استعلام
«من هم الطلاب الذين درجتهم أعلى من المتوسط؟» سؤال بسيط، لكنه يحتاج خطوتين: احسب المتوسط أولاً، ثم قارن كل طالب به. بدل تشغيل استعلامين ونقل الرقم بيدك، تسمح SQL بوضع استعلام داخل استعلام. هذا هو الاستعلام الفرعي (Subquery)، وهو موضوع هذا الدرس.
سنعمل على الجداول نفسها: students (سارة 92، وعمر 78، ومنى 85، ويوسف 64، وليلى بلا درجة، وأحمد 71)، و courses (Python و HTML و SQL)، و enrollments الذي يربط بينهما.
المقارنة بقيمة محسوبة
Section titled “المقارنة بقيمة محسوبة”SELECT name, score FROM studentsWHERE score > (SELECT AVG(score) FROM students);الناتج:
| name | score |
|---|---|
| سارة | 92 |
| منى | 85 |
الاستعلام بين القوسين يُنفَّذ أولاً ويُرجع قيمة واحدة: المتوسط 78. بعدها يصبح الاستعلام الخارجي كأنك كتبت WHERE score > 78. والفائدة أن النتيجة تبقى صحيحة مهما تغيرت البيانات، فلا رقم مكتوب بيدك.
تخيّل الاستعلام الفرعي مثل سؤال جانبي تسأله لزميلك قبل أن تجيب عن السؤال الأصلي: «كم المتوسط؟»، ثم تكمل عملك بإجابته.
ويمكن وضع الاستعلام الفرعي في قائمة الأعمدة أيضاً. هنا نحسب الفارق بين كل طالب وصاحب أعلى درجة:
SELECT name, score, (SELECT MAX(score) FROM students) - score AS gapFROM studentsWHERE score IS NOT NULLORDER BY gap;الناتج:
| name | score | gap |
|---|---|---|
| سارة | 92 | 0 |
| منى | 85 | 7 |
| عمر | 78 | 14 |
| أحمد | 71 | 21 |
| يوسف | 64 | 28 |
IN مع قائمة من استعلام
Section titled “IN مع قائمة من استعلام”تعرفت على IN مع قائمة مكتوبة باليد. الأقوى أن تأتي القائمة من استعلام آخر. من هم المشتركون في كورس Python (رقمه 1)؟
SELECT name FROM studentsWHERE id IN (SELECT student_id FROM enrollments WHERE course_id = 1);الناتج:
| name |
|---|
| سارة |
| منى |
| أحمد |
الاستعلام الداخلي يُرجع قائمة أرقام الطلاب: 1 و 3 و 6. ثم يختار الخارجي الطلاب الذين أرقامهم ضمن هذه القائمة. هنا الاستعلام الفرعي يُرجع عموداً واحداً بأي عدد من الصفوف.
والعكس بـ NOT IN: من لم يشترك في أي كورس؟
SELECT name FROM studentsWHERE id NOT IN (SELECT student_id FROM enrollments);الناتج:
| name |
|---|
| ليلى |
EXISTS: هل يوجد صف؟
Section titled “EXISTS: هل يوجد صف؟”EXISTS لا تهتم بالقيم، بل تسأل سؤالاً واحداً: هل يُرجع الاستعلام الفرعي صفاً واحداً على الأقل؟
SELECT s.name FROM students sWHERE 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 cWHERE 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 |
الأفضل في مجموعته
Section titled “الأفضل في مجموعته”الاستعلام المترابط يحل سؤالاً مشهوراً: من صاحب أعلى درجة في كل مدينة؟
SELECT s1.name, s1.city, s1.scoreFROM students s1WHERE s1.score = ( SELECT MAX(s2.score) FROM students s2 WHERE s2.city = s1.city);الناتج:
| name | city | score |
|---|---|---|
| سارة | القاهرة | 92 |
| عمر | الإسكندرية | 78 |
| منى | طنطا | 85 |
استخدمنا الجدول نفسه مرتين باسمين مختلفين: s1 للصف الحالي، و s2 لحساب أعلى درجة في مدينته. وليلى لم تظهر لأن درجتها NULL، والمقارنة مع NULL لا تتحقق أبداً.
استعلام فرعي في FROM
Section titled “استعلام فرعي في FROM”نتيجة أي استعلام هي جدول. إذن يمكن أن تستعلم منها كأنها جدول حقيقي. ما المدن التي متوسط طلابها أعلى من 75؟
SELECT city, averageFROM ( SELECT city, AVG(score) AS average FROM students GROUP BY city) AS city_statsWHERE 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_statsWHERE average > 75ORDER BY average DESC;الناتج:
| city | average |
|---|---|
| طنطا | 85.0 |
| القاهرة | 78.0 |
النتيجة نفسها، لكن الكود يُقرأ من الأعلى للأسفل كخطوات مرتبة. هذه الصيغة اسمها تعبير الجدول المشترك (CTE)، وتدعمها كل الأنظمة الحديثة.
استعلام فرعي أم JOIN؟
Section titled “استعلام فرعي أم JOIN؟”كثير من الأسئلة يُحل بالطريقتين. المشتركون في Python بـ JOIN:
SELECT DISTINCT s.nameFROM students sJOIN enrollments e ON e.student_id = s.idWHERE e.course_id = 1;يعطي الأسماء الثلاثة نفسها. فأيهما تختار؟
| استخدم | عندما |
|---|---|
JOIN |
تريد عرض أعمدة من الجدولين معاً |
IN أو EXISTS |
تريد تصفية جدول بناءً على جدول آخر فقط |
| استعلام فرعي بقيمة واحدة | تقارن برقم محسوب مثل المتوسط أو الأعلى |
جرّب بنفسك
Section titled “جرّب بنفسك”- اعرض الطلاب الذين عمرهم أكبر من متوسط الأعمار.
- اعرض أسماء الكورسات التي لم يشترك فيها الطالب رقم 1، باستخدام
NOT IN. - اعرض الطلاب المشتركين في أكثر من كورس. (تلميح: استعلام فرعي فيه
GROUP BYوHAVING.) - تحدٍّ: اعرض اسم كل طالب وبجانبه عدد الكورسات المشترك فيها، باستعلام فرعي مترابط في قائمة الأعمدة.
أخطاء شائعة
Section titled “أخطاء شائعة”- استعلام فرعي يُرجع أكثر من قيمة مع
=: المقارنة بـ=أو>تحتاج قيمة واحدة. إذا كان الناتج قائمة فاستخدمIN. - نسيان الأقواس: الاستعلام الفرعي يُكتب دائماً بين قوسين.
- NOT IN مع عمود فيه NULL: تحصل على نتيجة فارغة دون أي رسالة خطأ. استخدم
NOT EXISTS. - نسيان اسم الجدول المؤقت: بعض الأنظمة مثل MySQL تشترط اسماً بعد الاستعلام الفرعي في
FROM:(...) AS name.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
متى يُنفَّذ الاستعلام الفرعي غير المترابط في WHERE score > (SELECT AVG(score) FROM students)؟
لا يعتمد على الصف الخارجي، فيُحسب مرة واحدة وتُستخدم قيمته في المقارنة مع كل الصفوف.
سؤال
ما الذي تفحصه EXISTS؟
EXISTS تتحقق من وجود صفوف فقط، ولا تهتم بالقيم التي يُرجعها الاستعلام الفرعي.
الخلاصة
Section titled “الخلاصة”- الاستعلام الفرعي استعلام بين قوسين داخل استعلام آخر.
- مع عوامل المقارنة يُرجع قيمة واحدة، ومع
INيُرجع عموداً من القيم. EXISTSتفحص وجود صفوف، والاستعلام المترابط يُعاد تقييمه لكل صف خارجي.- يمكن الاستعلام من نتيجة استعلام في
FROM، وWITHتجعل ذلك أوضح. - احذر
NOT INمعNULL، واخترJOINعندما تحتاج أعمدة من الجدولين.