الاستعلامات الفرعية
الاستعلامات الفرعية
تخيّل أنك في سوبر ماركت وتسأل الموظف: "ما أغلى منتج لديكم؟" يتحقق الموظف ويقول: "MacBook Pro." ثم تتابع: "من هم العملاء الذين اشتروا هذا المنتج؟" — ما فعلته للتو هو استعلام فرعي: استخدام إجابة سؤال لطرح السؤال التالي. تعمل الاستعلامات الفرعية في SQL بنفس الطريقة: استخدام نتيجة استعلام كشرط أو مصدر بيانات لاستعلام آخر.
1. المفاهيم الأساسية
ما هو الاستعلام الفرعي؟
الاستعلام الفرعي (يُسمى أيضًا الاستعلام الداخلي أو المتداخل) هو استعلام مُضمَّن داخل بيان 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، ويُحدد ديناميكيًا شرط التصفية.
-- العثور على الموظفين الذين رواتبهم فوق المتوسط
SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);
يمكن للاستعلامات الفرعية أن تُرجع:
- قيمة واحدة (استعلام مُدرج): يُستخدم مع عوامل المقارنة مثل
=,>,< - عدة قيم (استعلام متعدد الصفوف): يُستخدم مع عوامل مثل
IN,ANY,ALL
-- إرجاع عدة قيم: مطابقة مع IN
SELECT name, salary
FROM employees
WHERE department_id IN (
SELECT id FROM departments WHERE location = 'Beijing'
);
استعلام FROM فرعي (جدول مُشتق)
يظهر الاستعلام الفرعي في شرط FROM، مما يُنشئ فعليًا جدول مؤقت (يُسمى أيضًا جدول مُشتق). يجب إعطاؤه اسمًا مستعارًا.
-- أولاً احسب متوسط راتب كل قسم، ثم قم بالتصفية منه
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;
AS dept_avg أعلاه)، وإلا سيرمي SQL خطأ.
استعلام SELECT فرعي (استعلام مُدرج)
يظهر الاستعلام الفرعي في قائمة أعمدة SELECT كـ عمود محسوب. في كل مرة يعالج فيها الاستعلام الرئيسي صفًا، يتم تنفيذ الاستعلام الفرعي مرة واحدة.
-- عرض راتب كل موظف ومتوسط راتب الشركة
SELECT name, salary,
(SELECT AVG(salary) FROM employees) AS avg_salary
FROM employees;
الإخراج:
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 يتحقق مما إذا كان الاستعلام الفرعي يُرجع على الأقل صفًا واحدًا. لا يهتم بالقيم المُرجعة — فقط "هل توجد نتائج".
-- العثور على الأقسام التي لديها موظفين
SELECT d.name
FROM departments d
WHERE EXISTS (
SELECT 1 FROM employees e WHERE e.department_id = d.id
);
-- العثور على الأقسام بدون موظفين
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 |
| يُوصى به | "العثور على الأكبر"، "العثور على غير الموجود" | "عرض مع الارتباط"، "دمج متعدد الجداول" |
EXPLAIN للتحليل والتحسين عند ظهور مشاكل الأداء.
2. الصيغة الأساسية/الاستخدام
صيغة استعلام WHERE فرعي
-- استعلام مُدرج (يُرجع قيمة واحدة)
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 فرعي
SELECT column_name
FROM (SELECT ... FROM ... WHERE ...) AS alias
WHERE condition;
صيغة استعلام SELECT المُدرج
SELECT column1, column2,
(SELECT aggregate_function FROM table_name WHERE condition) AS alias
FROM table_name;
صيغة EXISTS
SELECT column_name FROM table_A a
WHERE EXISTS (SELECT 1 FROM table_B b WHERE b.foreign_key = a.primary_key);
a.id)، لذلك يتم تنفيذ الاستعلام الفرعي مرة واحدة لكل صف في الاستعلام الخارجي.
مثال: العثور على موظفين برواتب فوق المتوسط (الصعوبة ⭐)
SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees)
ORDER BY salary DESC;
الإخراج:
name salary
------ --------
Qian Qi 20000.00
Li Si 18000.00
Zhang San 15000.00
Wu Shi 14000.00
أولاً احسب متوسط الراتب (approximately 14000)، ثم قم بتصفية الموظفين فوق المتوسط.
مثال: العثور على أعلى موظف راتبًا في كل قسم (الصعوبة ⭐⭐)
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
);
الإخراج:
name department salary
------ ---------- --------
Qian Qi Technology 20000.00
Wang Wu Marketing 12000.00
Wu Shi Finance 14000.00
e.department_id في الاستعلام الفرعي يشير إلى الصف الحالي في الاستعلام الخارجي. يتم تنفيذ الاستعلام الفرعي مرة واحدة لكل صف خارجي، للعثور على أعلى راتب في قسم ذلك الموظف، ثم يتحقق مما إذا كان الموظف الحالي هو الأعلى راتبًا.
مثال: استخدام جدول مُشتق لتحليل مستويات رواتب الأقسام (الصعوبة ⭐⭐⭐)
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;
الإخراج:
dept_name emp_count avg_salary level
--------- --------- ---------- ----------
Technology 3 17666.67 راتب مرتفع
Finance 2 13500.00 راتب متوسط
Marketing 2 11500.00 يحتاج تحسين
3. حالات الاستخدام الشائعة
الحالة 1: العثور على منتجات لم تُطلب قط
SELECT p.name, p.category, p.price
FROM products p
WHERE NOT EXISTS (
SELECT 1 FROM orders o WHERE o.product_id = p.id
);
الإخراج (يعتمد على البيانات):
name category price
-------- -------- -------
Mac Mini Computer 4499.00
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: العثور على عملاء تجاوزت مبالغ طلباتهم المتوسط
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
);
الإخراج:
customer_name total_spent
------------- -----------
Xiao Li 14397.00
Xiao Gang 17998.00
Xiao Wang 11998.00
❓ أسئلة شائعة
س: كم مستوى يمكن أن تتداخل فيه الاستعلامات الفرعية؟ ج: لا يوجد حد نظري، لكن في الممارسة العملية، يُوصى بعدم تجاوز 3 مستويات. الكثير من المستويات تشير إلى منطق استعلام معقد للغاية — فكر في تقسيمه إلى عدة خطوات أو إعادة كتابته باستخدام JOINs.
س: هل يجب استخدام IN أم EXISTS؟ ج: عندما تكون مجموعة نتائج الاستعلام الفرعي صغيرة والجدول الخارجي كبير، IN أكثر كفاءة؛ عندما يكون الجدول الخارجي صغيرًا وجدول الاستعلام الفرعي كبير، EXISTS أكثر كفاءة (لأن EXISTS يتوقف عند أول تطابق). الفرق ضئيل في السيناريوهات البسيطة — استخدم EXPLAIN للمقارنة في الحالات المعقدة.
س: ماذا يحدث إذا أرجع الاستعلام المُدرج عدة صفوف؟ ج: سيرمي قاعدة البيانات خطأ. يجب أن يُرجع الاستعلام المُدرج بالضبط صفًا واحدًا وعمودًا واحدًا. إذا لم تكن متأكدًا من عدد الصفوف، استخدم
LIMIT 1أو دالة تجميع (مثلMAX,MIN) لضمان قيمة واحدة.
س: ما الفرق بين الاستعلامات الفرعية المترابطة وغير المترابطة؟ ج: الاستعلام الفرعي غير المترابط يتم تنفيذه بشكل مستقل مرة واحدة (مثل "العثور على متوسط الراتب")، وتُستخدم نتيجته بواسطة الاستعلام الرئيسي. الاستعلام الفرعي المترابط يشير إلى أعمدة من الاستعلام الخارجي ويتم تنفيذه مرة واحدة لكل صف خارجي (مثل "العثور على أعلى راتب في كل قسم"). الاستعلامات الفرعية المترابطة قد تكون بطيئة مع مجموعات البيانات الكبيرة — انتبه للأداء.
📖 ملخص
- الاستعلام الفرعي هو استعلام متداخل داخل بيان SQL آخر؛ يتم تنفيذ الاستعلام الداخلي أولاً، وتُستخدم نتيجته بواسطة الاستعلام الخارجي
- استعلام WHERE فرعي: يُستخدم للتصفية الديناميكية مع عوامل مثل
IN,=,> - استعلام FROM فرعي (جدول مُشتق): يعمل كمصدر بيانات مؤقت؛ يجب أن يكون له اسم مستعار؛ مناسب لـ "التجميع أولاً، التحليل لاحقًا"
- استعلام SELECT المُدرج: يعمل كعمود محسوب، يتم تنفيذه مرة واحدة لكل صف خارجي
- EXISTS/NOT EXISTS: يهتم فقط بما إذا كان الاستعلام الفرعي يحتوي على نتائج؛ مناسب للعثور على "ارتباطات موجودة/مفقودة"
- استعلام فرعي مقابل JOIN: أعطِ الأولوية لسهولة القراءة؛ استخدم EXPLAIN لتحليل مشاكل الأداء
📝 تمارين
التمرين 1 (⭐): استخدم استعلامًا فرعيًا للعثور على جميع موظفي قسم "Technology" (تلميح: أولاً استخدم استعلامًا فرعيًا للعثور على معرف قسم Technology).
التمرين 2 (⭐⭐): استخدم استعلام FROM فرعي (جدول مُشتق) لحساب عدد طلبات كل عميل وإجمالي إنفاقه، ثم قم بتصفية العملاء الذين يتجاوز إجمالي إنفاقهم 5000.
التمرين 3 (⭐⭐⭐): استخدم EXISTS للعثور على "العملاء الذين طلبوا جميع المنتجات" — أي العملاء الذين لا يوجد لديهم "منتج غير مطلوب" (تلميح: منطق NOT EXISTS المزدوج، أو تنفيذ بطريقة أخرى).
الدرس التالي
👉 10-set-operations - عمليات المجموعات: تعلّم UNION وUNION ALL وINTERSECT وEXCEPT، وأتقن عمليات المجموعات على نتائج استعلام متعددة!



