الدرس 12

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

الوحدة الرابعة: مواضيع متقدمة مدة القراءة: 25 دقيقة

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

الاستعلام الفرعي (Subquery) هو استعلام SELECT داخل استعلام آخر. يُستخدم للحصول على بيانات تُستخدم في الاستعلام الخارجي.

-- الشكل العام
SELECT column
FROM table
WHERE column = (SELECT column FROM table WHERE condition);

أنواع الاستعلامات الفرعية

1. Subquery في WHERE

إرجاع قيمة واحدة (Scalar Subquery)

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

-- الموظف الأعلى راتباً
SELECT name, salary
FROM employees
WHERE salary = (SELECT MAX(salary) FROM employees);

إرجاع قائمة (مع IN)

-- الموظفون في الأقسام الموجودة في الرياض
SELECT name, department_id
FROM employees
WHERE department_id IN (
    SELECT id
    FROM departments
    WHERE location = 'الرياض'
);

-- الموظفون الذين طلبوا منتجات
SELECT DISTINCT customer_name
FROM orders
WHERE product_id IN (
    SELECT id
    FROM products
    WHERE category = 'إلكترونيات'
);

NOT IN

-- الموظفون الذين ليسوا في قسم التقنية أو المبيعات
SELECT name
FROM employees
WHERE department_id NOT IN (
    SELECT id
    FROM departments
    WHERE name IN ('التقنية', 'المبيعات')
);

2. Subquery في SELECT

-- كل موظف مع متوسط راتب قسمه
SELECT name,
       salary,
       department_id,
       (SELECT AVG(salary)
        FROM employees e2
        WHERE e2.department_id = e1.department_id) AS dept_avg
FROM employees e1;

-- كل قسم مع عدد موظفيه
SELECT name,
       (SELECT COUNT(*)
        FROM employees
        WHERE department_id = d.id) AS employee_count
FROM departments d;

3. Subquery في FROM (Derived Table)

-- معالجة البيانات على مرحلتين
SELECT department, avg_salary
FROM (
    SELECT department_id AS department,
           AVG(salary) AS avg_salary
    FROM employees
    GROUP BY department_id
) AS dept_stats
WHERE avg_salary > 5000;

-- أعلى راتب في كل قسم
SELECT dept_salaries.department_id, dept_salaries.max_salary
FROM (
    SELECT department_id,
           MAX(salary) AS max_salary
    FROM employees
    GROUP BY department_id
) AS dept_salaries;

EXISTS و NOT EXISTS

EXISTS يتحقق من وجود صفوف في الاستعلام الفرعي.

-- الأقسام التي فيها موظفون
SELECT name
FROM departments d
WHERE EXISTS (
    SELECT 1
    FROM employees e
    WHERE e.department_id = d.id
);

-- الأقسام الفارغة (بدون موظفين)
SELECT name
FROM departments d
WHERE NOT EXISTS (
    SELECT 1
    FROM employees e
    WHERE e.department_id = d.id
);

-- المنتجات التي لم تُطلب أبداً
SELECT name
FROM products p
WHERE NOT EXISTS (
    SELECT 1
    FROM orders o
    WHERE o.product_id = p.id
);
EXISTS vs IN

EXISTS: أسرع عندما الجدول الخارجي صغير والداخلي كبير
IN: أسرع عندما الاستعلام الفرعي يُرجع قيماً قليلة

ALL و ANY

ALL - مقارنة مع جميع القيم

-- الموظفون الذين راتبهم أعلى من كل موظفي المبيعات
SELECT name, salary
FROM employees
WHERE salary > ALL (
    SELECT salary
    FROM employees
    WHERE department_id = 1
);
-- يجب أن يكون الراتب أعلى من الجميع

ANY (أو SOME) - مقارنة مع أي قيمة

-- الموظفون الذين راتبهم أعلى من أي موظف في المبيعات
SELECT name, salary
FROM employees
WHERE salary > ANY (
    SELECT salary
    FROM employees
    WHERE department_id = 1
);
-- يكفي أن يكون أعلى من واحد فقط

Correlated Subqueries (المترابطة)

الاستعلام الفرعي يعتمد على الاستعلام الخارجي.

-- الموظفون الذين راتبهم أعلى من متوسط قسمهم
SELECT name, salary, department_id
FROM employees e1
WHERE salary > (
    SELECT AVG(salary)
    FROM employees e2
    WHERE e2.department_id = e1.department_id
);

-- آخر طلب لكل عميل
SELECT *
FROM orders o1
WHERE order_date = (
    SELECT MAX(order_date)
    FROM orders o2
    WHERE o2.customer_name = o1.customer_name
);

أمثلة عملية متقدمة

المنتج الأكثر مبيعاً

SELECT name
FROM products
WHERE id = (
    SELECT product_id
    FROM orders
    GROUP BY product_id
    ORDER BY SUM(quantity) DESC
    LIMIT 1
);

الموظف الثاني في الراتب

SELECT name, salary
FROM employees
WHERE salary = (
    SELECT DISTINCT salary
    FROM employees
    ORDER BY salary DESC
    LIMIT 1 OFFSET 1
);

نسبة راتب كل موظف من الإجمالي

SELECT name,
       salary,
       ROUND(salary * 100.0 / (SELECT SUM(salary) FROM employees), 2) AS percentage
FROM employees
ORDER BY percentage DESC;

الأقسام مع أكثر من المتوسط العام للموظفين

SELECT d.name, COUNT(e.id) AS emp_count
FROM departments d
JOIN employees e ON d.id = e.department_id
GROUP BY d.id, d.name
HAVING COUNT(e.id) > (
    SELECT AVG(emp_count)
    FROM (
        SELECT COUNT(*) AS emp_count
        FROM employees
        GROUP BY department_id
    ) AS counts
);

Subquery vs JOIN

-- نفس النتيجة بطريقتين:

-- باستخدام Subquery:
SELECT name FROM employees
WHERE department_id IN (
    SELECT id FROM departments WHERE location = 'الرياض'
);

-- باستخدام JOIN:
SELECT e.name
FROM employees e
JOIN departments d ON e.department_id = d.id
WHERE d.location = 'الرياض';
متى نستخدم كل واحد؟
  • Subquery: عندما تحتاج قيمة واحدة أو قائمة للمقارنة
  • JOIN: عندما تحتاج أعمدة من كلا الجدولين في النتيجة
  • JOIN غالباً أسرع، لكن Subquery أوضح أحياناً

تمارين للتطبيق

  1. اعرض الموظفين الذين راتبهم أعلى من متوسط الشركة
  2. اعرض المنتجات التي سعرها أعلى من متوسط الأسعار
  3. اعرض الأقسام التي ليس فيها موظفون
  4. اعرض العميل الذي لديه أكثر عدد من الطلبات

الخلاصة

  • الاستعلامات الفرعية = استعلام داخل استعلام
  • تُستخدم في WHERE, SELECT, FROM
  • IN للقوائم، EXISTS للوجود
  • ALL للمقارنة مع الكل، ANY للمقارنة مع واحد
  • الاستعلامات المترابطة تعتمد على الخارجي
تهانينا!

أكملت جميع دروس الدورة! الآن لديك أساس قوي في SQL. استمر في التدريب باستخدام صفحة التجربة وطبّق ما تعلمته على مشاريع حقيقية.