الاستعلامات الفرعية

الاستعلامات الفرعية

تخيّل أنك في سوبر ماركت وتسأل الموظف: "ما أغلى منتج لديكم؟" يتحقق الموظف ويقول: "MacBook Pro." ثم تتابع: "من هم العملاء الذين اشتروا هذا المنتج؟" — ما فعلته للتو هو استعلام فرعي: استخدام إجابة سؤال لطرح السؤال التالي. تعمل الاستعلامات الفرعية في SQL بنفس الطريقة: استخدام نتيجة استعلام كشرط أو مصدر بيانات لاستعلام آخر.


1. المفاهيم الأساسية

ما هو الاستعلام الفرعي؟

الاستعلام الفرعي (يُسمى أيضًا الاستعلام الداخلي أو المتداخل) هو استعلام مُضمَّن داخل بيان SQL آخر. الاستعلام الخارجي يُسمى الاستعلام الرئيسي، والداخلي يُسمى الاستعلام الفرعي. يتم تنفيذ الاستعلام الفرعي أولاً، وتُستخدم نتيجته بواسطة الاستعلام الرئيسي.

SQL
-- استعلام فرعي: العثور على الموظف בעל أعلى راتب
SELECT name, salary
FROM employees
WHERE salary = (SELECT MAX(salary) FROM employees);

ترتيب التنفيذ: أولاً يتم تنفيذ SELECT MAX(salary) FROM employees للحصول على 20000، ثم يتم تنفيذ الاستعلام الخارجي للعثور على الموظف(ين) الذين رواتبهم = 20000.

تصنيف الاستعلامات الفرعية

بناءً على مكان ظهور الاستعلام الفرعي، هناك ثلاثة أنواع رئيسية:

النوع الموقع الغرض ما يُرجعه
استعلام WHERE فرعي في شرط WHERE تصفية البيانات قيمة واحدة أو قائمة
استعلام FROM فرعي (جدول مُشتق) في شرط FROM يعمل كجدول مؤقت مجموعة نتائج
استعلام SELECT فرعي (استعلام مُدرج) في قائمة أعمدة SELECT يعمل كعمود محسوب قيمة واحدة

استعلام WHERE فرعي

الاستخدام الأكثر شيوعًا. يظهر الاستعلام الفرعي في شرط WHERE، ويُحدد ديناميكيًا شرط التصفية.

SQL
-- العثور على الموظفين الذين رواتبهم فوق المتوسط
SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);

يمكن للاستعلامات الفرعية أن تُرجع:

SQL
-- إرجاع عدة قيم: مطابقة مع IN
SELECT name, salary
FROM employees
WHERE department_id IN (
    SELECT id FROM departments WHERE location = 'Beijing'
);

استعلام FROM فرعي (جدول مُشتق)

يظهر الاستعلام الفرعي في شرط FROM، مما يُنشئ فعليًا جدول مؤقت (يُسمى أيضًا جدول مُشتق). يجب إعطاؤه اسمًا مستعارًا.

SQL
-- أولاً احسب متوسط راتب كل قسم، ثم قم بالتصفية منه
SELECT dept_name, avg_salary
FROM (
    SELECT d.name AS dept_name, AVG(e.salary) AS avg_salary
    FROM employees e
    JOIN departments d ON e.department_id = d.id
    GROUP BY d.name
) AS dept_avg
WHERE avg_salary > 12000;
💡 نصيحة: يجب أن يكون لاستعلامات FROM الفرعية اسم مستعار (مثل AS dept_avg أعلاه)، وإلا سيرمي SQL خطأ.

استعلام SELECT فرعي (استعلام مُدرج)

يظهر الاستعلام الفرعي في قائمة أعمدة SELECT كـ عمود محسوب. في كل مرة يعالج فيها الاستعلام الرئيسي صفًا، يتم تنفيذ الاستعلام الفرعي مرة واحدة.

SQL
-- عرض راتب كل موظف ومتوسط راتب الشركة
SELECT name, salary,
       (SELECT AVG(salary) FROM employees) AS avg_salary
FROM employees;

الإخراج:

TEXT
name    salary    avg_salary
------  --------  ----------
Zhang San 15000.00  14000.00
Li Si   18000.00  14000.00
Wang Wu 12000.00  14000.00
Zhao Liu 13000.00  14000.00
Qian Qi 20000.00  14000.00
Sun Ba  11000.00  14000.00
Zhou Jiu 9000.00   14000.00
Wu Shi  14000.00  14000.00
💡 نصيحة: يجب أن يُرجع الاستعلام المُدرج صفًا واحدًا وعمودًا واحدًا بالضبط، وإلا سيرمي خطأ. مناسب لإضافة أعمدة "معلومات ملخصة".

EXISTS و NOT EXISTS

EXISTS يتحقق مما إذا كان الاستعلام الفرعي يُرجع على الأقل صفًا واحدًا. لا يهتم بالقيم المُرجعة — فقط "هل توجد نتائج".

SQL
-- العثور على الأقسام التي لديها موظفين
SELECT d.name
FROM departments d
WHERE EXISTS (
    SELECT 1 FROM employees e WHERE e.department_id = d.id
);
SQL
-- العثور على الأقسام بدون موظفين
SELECT d.name
FROM departments d
WHERE NOT EXISTS (
    SELECT 1 FROM employees e WHERE e.department_id = d.id
);
💡 نصيحة: SELECT 1 في EXISTS هو ممارسة تقليدية لأن EXISTS يهتم فقط بـ "هل توجد صفوف"، وليس بقيم الأعمدة. استخدام SELECT * أو SELECT NULL له نفس التأثير.

مقارنة أداء الاستعلام الفرعي مقابل JOIN

عنصر المقارنة استعلام فرعي JOIN
سهولة القراءة أقرب للتفكير الطبيعي يتطلب فهم منطق الانضمام
الأداء فرق بسيط في الحالات البسيطة؛ الاستعلامات الفرعية المترابطة قد تكون أبطأ بشكل عام أفضل، مع محسّن متخصص
المرونة يمكن استخدام نتائج دوال التجميع كشروط يتطلب GROUP BY
يُوصى به "العثور على الأكبر"، "العثور على غير الموجود" "عرض مع الارتباط"، "دمج متعدد الجداول"
💡 نصيحة: قواعد البيانات الحديثة (مثل PostgreSQL، MySQL 8.0) تحتوي على محسّنات ذكية، والفارق في الأداء بين الاستعلامات الفرعية وJOINs ضئيل في العديد من السيناريوهات. أعطِ الأولوية لسهولة القراءة الأفضل، واستخدم EXPLAIN للتحليل والتحسين عند ظهور مشاكل الأداء.


2. الصيغة الأساسية/الاستخدام

صيغة استعلام WHERE فرعي

SQL
-- استعلام مُدرج (يُرجع قيمة واحدة)
SELECT column_name FROM table_name
WHERE column_name comparison_operator (SELECT aggregate_function FROM table_name);

-- استعلام متعدد الصفوف (يُرجع قائمة)
SELECT column_name FROM table_name
WHERE column_name IN (SELECT column_name FROM table_name WHERE condition);

صيغة استعلام FROM فرعي

SQL
SELECT column_name
FROM (SELECT ... FROM ... WHERE ...) AS alias
WHERE condition;
💡 نصيحة: الاسم المستعار لاستعلام FROM الفرعي مطلوب. قواعد البيانات المختلفة لديها متطلبات مختلفة، لكن إضافة اسم مستعار هو النهج الأكثر أمانًا.

صيغة استعلام SELECT المُدرج

SQL
SELECT column1, column2,
       (SELECT aggregate_function FROM table_name WHERE condition) AS alias
FROM table_name;
💡 نصيحة: إذا كان الاستعلام المُدرج يحتاج إلى الإشارة إلى أعمدة من الاستعلام الخارجي (استعلام مترابط)، يمكنك استخدام اسم الجدول المستعار مباشرة من الاستعلام الخارجي.

صيغة EXISTS

SQL
SELECT column_name FROM table_A a
WHERE EXISTS (SELECT 1 FROM table_B b WHERE b.foreign_key = a.primary_key);
💡 نصيحة: EXISTS هو "استعلام فرعي مترابط" — الاستعلام الفرعي يشير إلى أعمدة من الاستعلام الخارجي (مثل a.id)، لذلك يتم تنفيذ الاستعلام الفرعي مرة واحدة لكل صف في الاستعلام الخارجي.

💡 نصيحة: لمجموعات البيانات الكبيرة، EXISTS عادةً أكثر كفاءة من IN، لأن EXISTS يتوقف بمجرد العثور على أول تطابق، بينما IN يحتاج إلى إرجاع جميع النتائج.


مثال: العثور على موظفين برواتب فوق المتوسط (الصعوبة ⭐)

SQL
SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees)
ORDER BY salary DESC;
▶ جرّب الكود

الإخراج:

TEXT
name    salary
------  --------
Qian Qi 20000.00
Li Si   18000.00
Zhang San 15000.00
Wu Shi  14000.00

أولاً احسب متوسط الراتب (approximately 14000)، ثم قم بتصفية الموظفين فوق المتوسط.


مثال: العثور على أعلى موظف راتبًا في كل قسم (الصعوبة ⭐⭐)

SQL
SELECT e.name, d.name AS department, e.salary
FROM employees e
JOIN departments d ON e.department_id = d.id
WHERE e.salary = (
    SELECT MAX(e2.salary)
    FROM employees e2
    WHERE e2.department_id = e.department_id
);
▶ جرّب الكود

الإخراج:

TEXT
name    department  salary
------  ----------  --------
Qian Qi Technology  20000.00
Wang Wu Marketing   12000.00
Wu Shi  Finance     14000.00
💡 المفتاح: هذا استعلام فرعي مترابطe.department_id في الاستعلام الفرعي يشير إلى الصف الحالي في الاستعلام الخارجي. يتم تنفيذ الاستعلام الفرعي مرة واحدة لكل صف خارجي، للعثور على أعلى راتب في قسم ذلك الموظف، ثم يتحقق مما إذا كان الموظف الحالي هو الأعلى راتبًا.


مثال: استخدام جدول مُشتق لتحليل مستويات رواتب الأقسام (الصعوبة ⭐⭐⭐)

SQL
SELECT dept_name, emp_count, avg_salary,
       CASE 
           WHEN avg_salary >= 15000 THEN 'راتب مرتفع'
           WHEN avg_salary >= 12000 THEN 'راتب متوسط'
           ELSE 'يحتاج تحسين'
       END AS level
FROM (
    SELECT d.name AS dept_name, 
           COUNT(e.id) AS emp_count,
           AVG(e.salary) AS avg_salary
    FROM departments d
    LEFT JOIN employees e ON d.id = e.department_id
    GROUP BY d.id, d.name
) AS dept_stats
WHERE emp_count > 0
ORDER BY avg_salary DESC;
▶ جرّب الكود

الإخراج:

TEXT
dept_name  emp_count  avg_salary  level
---------  ---------  ----------  ----------
Technology 3          17666.67    راتب مرتفع
Finance    2          13500.00    راتب متوسط
Marketing  2          11500.00    يحتاج تحسين
💡 النهج: استعلام FROM الفرعي يحسب أولاً عدد موظفي كل قسم ومتوسط الراتب، ثم يقوم الاستعلام الرئيسي بتصنيف هذه الإحصائيات وتصفيةتها. نمط "التجميع أولاً، التحليل لاحقًا" هذا شائع جدًا في تطوير التقارير.


3. حالات الاستخدام الشائعة

الحالة 1: العثور على منتجات لم تُطلب قط

SQL
SELECT p.name, p.category, p.price
FROM products p
WHERE NOT EXISTS (
    SELECT 1 FROM orders o WHERE o.product_id = p.id
);

الإخراج (يعتمد على البيانات):

TEXT
name      category  price
--------  --------  -------
Mac Mini  Computer  4499.00
💡 المقارنة: نفس المتطلب تم تنفيذه باستخدام LEFT JOIN:

SQL
SELECT p.name, p.category, p.price
FROM products p
LEFT JOIN orders o ON p.id = o.product_id
WHERE o.id IS NULL;

كلا النهجين لهما أداء متشابه — اختر الذي يوفر سهولة قراءة أفضل.

الحالة 2: العثور على عملاء تجاوزت مبالغ طلباتهم المتوسط

SQL
SELECT customer_name, total_spent
FROM (
    SELECT o.customer_name, 
           SUM(o.quantity * p.price) AS total_spent
    FROM orders o
    JOIN products p ON o.product_id = p.id
    GROUP BY o.customer_name
) AS customer_totals
WHERE total_spent > (
    SELECT AVG(o.quantity * p.price)
    FROM orders o
    JOIN products p ON o.product_id = p.id
);

الإخراج:

TEXT
customer_name  total_spent
-------------  -----------
Xiao Li        14397.00
Xiao Gang      17998.00
Xiao Wang      11998.00
💡 النهج: استعلام FROM الفرعي يحسب إجمالي إنفاق كل عميل، استعلام WHERE الفرعي يحسب متوسط مبلغ الطلب، والاستعلام الرئيسي يُصفّر العملاء فوق المتوسط.


❓ أسئلة شائعة

س: كم مستوى يمكن أن تتداخل فيه الاستعلامات الفرعية؟ ج: لا يوجد حد نظري، لكن في الممارسة العملية، يُوصى بعدم تجاوز 3 مستويات. الكثير من المستويات تشير إلى منطق استعلام معقد للغاية — فكر في تقسيمه إلى عدة خطوات أو إعادة كتابته باستخدام JOINs.

س: هل يجب استخدام IN أم EXISTS؟ ج: عندما تكون مجموعة نتائج الاستعلام الفرعي صغيرة والجدول الخارجي كبير، IN أكثر كفاءة؛ عندما يكون الجدول الخارجي صغيرًا وجدول الاستعلام الفرعي كبير، EXISTS أكثر كفاءة (لأن EXISTS يتوقف عند أول تطابق). الفرق ضئيل في السيناريوهات البسيطة — استخدم EXPLAIN للمقارنة في الحالات المعقدة.

س: ماذا يحدث إذا أرجع الاستعلام المُدرج عدة صفوف؟ ج: سيرمي قاعدة البيانات خطأ. يجب أن يُرجع الاستعلام المُدرج بالضبط صفًا واحدًا وعمودًا واحدًا. إذا لم تكن متأكدًا من عدد الصفوف، استخدم LIMIT 1 أو دالة تجميع (مثل MAX, MIN) لضمان قيمة واحدة.

س: ما الفرق بين الاستعلامات الفرعية المترابطة وغير المترابطة؟ ج: الاستعلام الفرعي غير المترابط يتم تنفيذه بشكل مستقل مرة واحدة (مثل "العثور على متوسط الراتب")، وتُستخدم نتيجته بواسطة الاستعلام الرئيسي. الاستعلام الفرعي المترابط يشير إلى أعمدة من الاستعلام الخارجي ويتم تنفيذه مرة واحدة لكل صف خارجي (مثل "العثور على أعلى راتب في كل قسم"). الاستعلامات الفرعية المترابطة قد تكون بطيئة مع مجموعات البيانات الكبيرة — انتبه للأداء.


📖 ملخص


📝 تمارين

التمرين 1 (⭐): استخدم استعلامًا فرعيًا للعثور على جميع موظفي قسم "Technology" (تلميح: أولاً استخدم استعلامًا فرعيًا للعثور على معرف قسم Technology).

التمرين 2 (⭐⭐): استخدم استعلام FROM فرعي (جدول مُشتق) لحساب عدد طلبات كل عميل وإجمالي إنفاقه، ثم قم بتصفية العملاء الذين يتجاوز إجمالي إنفاقهم 5000.

التمرين 3 (⭐⭐⭐): استخدم EXISTS للعثور على "العملاء الذين طلبوا جميع المنتجات" — أي العملاء الذين لا يوجد لديهم "منتج غير مطلوب" (تلميح: منطق NOT EXISTS المزدوج، أو تنفيذ بطريقة أخرى).


الدرس التالي

👉 10-set-operations - عمليات المجموعات: تعلّم UNION وUNION ALL وINTERSECT وEXCEPT، وأتقن عمليات المجموعات على نتائج استعلام متعددة!

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%