تدريب: استعلامات متعددة الجداول الشاملة

تدريب: استعلامات متعددة الجداول الشاملة

📋 متطلبات المشروع

هذا القسم يستخدم سيناريو نظام إدارة طلبات التجارة الإلكترونية لتطبيق مهارات الاستعلام عن الجداول المتعددة المكتسبة سابقًا بشكل شامل.

هيكل جداول قاعدة البيانات

نستخدم قاعدة بيانات موحدة من 4 جداول:

SQL
-- جدول الأقسام
CREATE TABLE departments (
    department_id   INT PRIMARY KEY,
    department_name VARCHAR(50) NOT NULL,
    city            VARCHAR(50),
    budget          DECIMAL(12,2)
);

-- جدول الموظفين
CREATE TABLE employees (
    employee_id   INT PRIMARY KEY,
    first_name    VARCHAR(50) NOT NULL,
    last_name     VARCHAR(50) NOT NULL,
    email         VARCHAR(100),
    salary        DECIMAL(10,2),
    department_id INT,
    hire_date     DATE,
    FOREIGN KEY (department_id) REFERENCES departments(department_id)
);

-- جدول الطلبات
CREATE TABLE orders (
    order_id     INT PRIMARY KEY,
    customer_id  INT,
    employee_id  INT,
    order_date   DATE,
    total_amount DECIMAL(12,2),
    status       VARCHAR(20),
    FOREIGN KEY (employee_id) REFERENCES employees(employee_id)
);

-- جدول المنتجات
CREATE TABLE products (
    product_id   INT PRIMARY KEY,
    product_name VARCHAR(100) NOT NULL,
    price        DECIMAL(10,2),
    stock        INT,
    department_id INT,
    FOREIGN KEY (department_id) REFERENCES departments(department_id)
);

بيانات الاختبار

SQL
-- بيانات الأقسام
INSERT INTO departments VALUES (1, 'Technology', 'Beijing', 500000);
INSERT INTO departments VALUES (2, 'Sales', 'Shanghai', 300000);
INSERT INTO departments VALUES (3, 'Marketing', 'Guangzhou', 200000);
INSERT INTO departments VALUES (4, 'Finance', 'Shenzhen', 150000);

-- بيانات الموظفين
INSERT INTO employees VALUES (1, 'Zhang', 'Wei', 'zhangwei@co.com', 12000, 1, '2023-03-15');
INSERT INTO employees VALUES (2, 'Li', 'Na', 'lina@co.com', 9500, 1, '2023-06-20');
INSERT INTO employees VALUES (3, 'Wang', 'Qiang', 'wangqiang@co.com', 8000, 2, '2022-11-10');
INSERT INTO employees VALUES (4, 'Zhao', 'Min', 'zhaomin@co.com', 11000, 2, '2024-01-05');
INSERT INTO employees VALUES (5, 'Liu', 'Yang', 'liuyang@co.com', 7500, 3, '2024-04-18');
INSERT INTO employees VALUES (6, 'Chen', 'Jing', 'chenjing@co.com', 9000, NULL, '2025-02-28');

-- بيانات الطلبات
INSERT INTO orders VALUES (1001, 101, 3, '2026-05-10', 2500.00, 'completed');
INSERT INTO orders VALUES (1002, 102, 3, '2026-05-15', 1800.00, 'completed');
INSERT INTO orders VALUES (1003, 103, 4, '2026-06-01', 3200.00, 'shipped');
INSERT INTO orders VALUES (1004, 101, 1, '2026-06-05', 950.00, 'pending');
INSERT INTO orders VALUES (1005, 104, 4, '2026-06-10', 4100.00, 'completed');
INSERT INTO orders VALUES (1006, 105, 3, '2026-06-15', 2800.00, 'cancelled');

-- بيانات المنتجات
INSERT INTO products VALUES (1, 'Laptop', 6999.00, 50, 1);
INSERT INTO products VALUES (2, 'Wireless Mouse', 129.00, 200, 1);
INSERT INTO products VALUES (3, 'Office Desk', 899.00, 30, 2);
INSERT INTO products VALUES (4, 'Projector', 3500.00, 15, 3);
INSERT INTO products VALUES (5, 'Printer', 2200.00, 25, 1);

🏗️ تدريب الاستعلامات

الاستعلام 1: تقرير أداء طلبات الموظفين

المتطلب: عرض عدد طلبات كل موظف وإجمالي مبيعاته، بما في ذلك الموظفين بدون طلبات.

SQL
SELECT
    e.employee_id,
    CONCAT(e.first_name, e.last_name) AS name,
    d.department_name AS department,
    COUNT(o.order_id) AS order_count,
    COALESCE(SUM(o.total_amount), 0) AS total_sales
FROM employees e
LEFT JOIN departments d ON e.department_id = d.department_id
LEFT JOIN orders o ON e.employee_id = o.employee_id
GROUP BY e.employee_id, e.first_name, e.last_name, d.department_name
ORDER BY total_sales DESC;

النتيجة:

TEXT
employee_id | name     | department | order_count | total_sales
-----------+----------+------------+-------------+------------
         4 | Zhao Min | Sales      |           2 |     7300.00
         3 | Wang Qiang| Sales      |           3 |     7100.00
         1 | Zhang Wei | Technology |           1 |      950.00
         2 | Li Na    | Technology |           0 |        0.00
         5 | Liu Yang | Marketing  |           0 |        0.00
         6 | Chen Jing| NULL       |           0 |        0.00

شرح الكود:

  1. LEFT JOIN departments — انضمام جدول الأقسام للحصول على أسماء الأقسام، مع الاحتفاظ بالموظفين بدون قسم
  2. LEFT JOIN orders — انضمام جدول الطلبات، مع الاحتفاظ بالموظفين بدون طلبات
  3. COALESCE(SUM(...), 0) — يحول NULL إلى 0 لتجنب عرض فراغات
  4. GROUP BY — تجميع حسب الموظف، تجميع عدد الطلبات والمبلغ الإجمالي

الاستعلام 2: ترتيب مبيعات الأقسام

المتطلب: حساب إجمالي المبيعات حسب القسم وترتيبها.

SQL
SELECT
    d.department_name AS department,
    d.city AS city,
    COUNT(DISTINCT e.employee_id) AS employee_count,
    COUNT(o.order_id) AS total_orders,
    COALESCE(SUM(o.total_amount), 0) AS total_sales,
    RANK() OVER (ORDER BY COALESCE(SUM(o.total_amount), 0) DESC) AS ranking
FROM departments d
LEFT JOIN employees e ON d.department_id = e.department_id
LEFT JOIN orders o ON e.employee_id = o.employee_id
GROUP BY d.department_id, d.department_name, d.city
ORDER BY total_sales DESC;

شرح الكود:

  1. سلسلة LEFT JOIN: departments → employees → orders
  2. COUNT(DISTINCT e.employee_id) — إزالة تكرارات عدد الموظفين لتجنب العد المزدوج بسبب طلبات متعددة
  3. RANK() — دالة نافذة تقوم بالترتيب حسب إجمالي المبيعات
  4. COALESCE — يتعامل مع الأقسام بدون طلبات، عرض 0

الاستعلام 3: الموظفون ذوو الرواتب المرتفعة وتفاصيل طلباتهم

المتطلب: العثور على الموظفين الذين رواتبهم فوق متوسط الشركة، مع معلومات طلباتهم.

SQL
SELECT
    e.employee_id,
    CONCAT(e.first_name, e.last_name) AS name,
    e.salary AS salary,
    o.order_id,
    o.order_date AS order_date,
    o.total_amount AS order_amount,
    o.status AS order_status
FROM employees e
INNER JOIN orders o ON e.employee_id = o.employee_id
WHERE e.salary > (
    SELECT AVG(salary) FROM employees
)
ORDER BY e.salary DESC, o.order_date;

شرح الكود:

  1. استعلام فرعي (SELECT AVG(salary) FROM employees) — يحسب متوسط راتب الشركة
  2. INNER JOIN — يحتفظ فقط بالموظفين ذوي الرواتب المرتفعة الذين لديهم طلبات
  3. WHERE e.salary > (...) — يستخدم نتيجة الاستعلام الفرعي كشرط تصفية
  4. ORDER BY يرتب حسب الراتب تنازليًا أولاً، ثم حسب التاريخ تصاعديًا

الاستعلام 4: استعلام متقاطع — طلبات بدون منتجات مقابلة في القسم

المتطلب: العثور على طلبات حيث أن قسم الموظف ليس لديه منتجات.

SQL
SELECT
    o.order_id,
    CONCAT(e.first_name, e.last_name) AS employee_name,
    d.department_name AS department,
    o.total_amount AS order_amount
FROM orders o
INNER JOIN employees e ON o.employee_id = e.employee_id
LEFT JOIN departments d ON e.department_id = d.department_id
WHERE e.department_id IS NOT NULL
  AND e.department_id NOT IN (
      SELECT DISTINCT department_id FROM products WHERE department_id IS NOT NULL
  );

شرح الكود:

  1. INNER JOIN employees — يربط الطلبات بالموظفين
  2. LEFT JOIN departments — يحصل على أسماء الأقسام
  3. NOT IN (استعلام فرعي) — يستبعد الأقسام التي لديها منتجات
  4. يدمج JOIN والاستعلام الفرعي للتصفية المختلطة

الاستعلام 5: استخدام UNION لدمج بيانات متعددة الأبعاد

المتطلب: إنشاء دليل شامل يدمج "الموظفين الذين لديهم طلبات" و"جميع الأقسام".

SQL
-- الموظفون الذين لديهم طلبات مكتملة
SELECT
    CONCAT(first_name, last_name) AS name,
    'Employee' AS type,
    CAST(salary AS VARCHAR) AS details
FROM employees
WHERE employee_id IN (
    SELECT DISTINCT employee_id FROM orders WHERE status = 'completed'
)
UNION ALL
-- جميع الأقسام
SELECT
    department_name,
    'Department',
    CONCAT('Budget: ', CAST(budget AS VARCHAR))
FROM departments
ORDER BY type, name;

شرح الكود:

  1. SELECT الأول — يستخدم استعلامًا فرعيًا لتصفية الموظفين الذين لديهم طلبات مكتملة
  2. SELECT الثاني — يحصل على جميع الأقسام
  3. UNION ALL — يدمج مجموعتي النتائج، يحتفظ بجميع الصفوف
  4. يضيف عمود ثابت "type" لتمييز مصادر البيانات

الاستعلام 6: تحليل شامل — توزيع حالات الطلبات وأداء الموظفين

المتطلب: حساب توزيع حالات الطلبات لكل موظف والعثور على الموظفين الذين تعاملوا مع طلبات "مكتملة".

SQL
SELECT
    CONCAT(e.first_name, e.last_name) AS employee_name,
    d.department_name AS department,
    SUM(CASE WHEN o.status = 'completed' THEN 1 ELSE 0 END) AS completed,
    SUM(CASE WHEN o.status = 'shipped' THEN 1 ELSE 0 END) AS shipped,
    SUM(CASE WHEN o.status = 'pending' THEN 1 ELSE 0 END) AS pending,
    SUM(CASE WHEN o.status = 'cancelled' THEN 1 ELSE 0 END) AS cancelled,
    COUNT(o.order_id) AS total_orders
FROM employees e
LEFT JOIN departments d ON e.department_id = d.department_id
LEFT JOIN orders o ON e.employee_id = o.employee_id
WHERE e.employee_id IN (
    SELECT DISTINCT employee_id FROM orders
)
GROUP BY e.employee_id, e.first_name, e.last_name, d.department_name
ORDER BY total_orders DESC;

شرح الكود:

  1. CASE WHEN تجميع شرطي — يحول بيانات الصفوف إلى أعمدة (pivot)
  2. LEFT JOIN يحتفظ بجميع البيانات المرتبطة
  3. استعلام فرعي IN (...) يُصفّر الموظفين بدون طلبات
  4. GROUP BY يجمع ويُجمّع حسب الموظف

❓ أسئلة شائعة

س: متى يجب استخدام INNER JOIN مقابل LEFT JOIN؟ ج: إذا كنت تحتاج إلى الاحتفاظ بجميع السجلات من الجدول الأيسر (حتى عندما لا يوجد تطابق في الجدول الأيمن)، استخدم LEFT JOIN. إذا كنت فقط تحتاج إلى السجلات المتطابقة في كلا الجدولين، استخدم INNER JOIN. في الممارسة العملية، استعلامات التقارير عادةً تستخدم LEFT JOIN، بينما التحقق من البيانات يستخدم INNER JOIN.

س: أيهما أفضل أداءً، الاستعلام الفرعي أم JOIN؟ ج: محسّنات قواعد البيانات الحديثة عادةً تحول الاستعلامات الفرعية إلى JOINs للتنفيذ. للارتباطات البسيطة، JOIN أكثر بديهية؛ للتحقق من الوجود ("هل يوجد؟")، استعلامات EXISTS أو IN الفرعية أكثر دلالية. استخدم EXPLAIN للتحقق من اختلافات الأداء.

س: كيف أُصحح أخطاء استعلامات الجداول المتعددة المعقدة؟ ج: ابنِ بشكل تصاعدي: ابدأ باستعلام فرعي داخلي، تأكد من صحة النتيجة، ثم لفه طبقة بطبقة. يمكنك أيضًا استخدام CTEs (WITH clause) لتقسيم الاستعلامات المعقدة إلى عدة مجموعات نتائج مؤقتة قابلة للقراءة.

س: هل يمكن أن يحتوي SELECT بعد GROUP BY فقط على دوال تجميع وأعمدة مجمّعة؟ ج:نعم. الأعمدة غير المجمّعة في SELECT يجب أن تظهر جميعها في GROUP BY، وإلا سيرمي قاعدة البيانات خطأ. هذا متطلب معيار SQL.


📖 ملخص

المهارة التطبيق في هذا القسم
INNER JOIN ربط الجداول بعلاقات متطابقة
LEFT JOIN الاحتفاظ بجميع سجلات الجدول الأيسر
استعلام فرعي (WHERE IN) يُستخدم كشرط تصفية
استعلام فرعي (مُدرج) يُرجع قيمة واحدة للمقارنة
UNION ALL دمج مجموعات نتائج من أبعاد مختلفة
CASE WHEN تجميع شرطي (pivot)
دوال التجميع COUNT، SUM، AVG، COALESCE
دوال النافذة RANK() للترتيب

نقاط التدريب الرئيسية:


📝 تمارين

  1. اكتب استعلامًا: العثور على معلومات القسم الذي ينتمي إليه الموظف ذو أعلى راتب، بما في ذلك اسم القسم والمدينة والميزانية.
  2. اكتب استعلامًا: عدد الأقسام وعدد الموظفين الإجمالي لكل مدينة، باستخدام LEFT JOIN لضمان عرض المدن بدون موظفين أيضًا.
  3. اكتب استعلامًا: العثور على الموظفين الذين لم يعالجوا أي طلب، قم بالتنفيذ بكل من الاستعلام الفرعي (NOT IN) و LEFT JOIN ... IS NULL، وقارن ما إذا كانت النتائج متسقة.
  4. اكتب استعلامًا: تقسيم الموظفين إلى ثلاث مجموعات حسب الراتب — "مرتفع (>10000)", "متوسط (6000-10000)", "منخفض (<6000)" — وعد الأشخاص ومتوسط الراتب لكل مجموعة.

الدرس التالي

سندرس بعد ذلك دوال التجميع — أتقن COUNT وSUM وAVG وMAX وMIN، بالإضافة إلى الاستخدام المتقدم لـ GROUP BY وHAVING.

Web-Tutorial.com

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

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

100%