تدريب: استعلامات متعددة الجداول الشاملة
تدريب: استعلامات متعددة الجداول الشاملة
📋 متطلبات المشروع
هذا القسم يستخدم سيناريو نظام إدارة طلبات التجارة الإلكترونية لتطبيق مهارات الاستعلام عن الجداول المتعددة المكتسبة سابقًا بشكل شامل.
هيكل جداول قاعدة البيانات
نستخدم قاعدة بيانات موحدة من 4 جداول:
-- جدول الأقسام
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)
);
بيانات الاختبار
-- بيانات الأقسام
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: تقرير أداء طلبات الموظفين
المتطلب: عرض عدد طلبات كل موظف وإجمالي مبيعاته، بما في ذلك الموظفين بدون طلبات.
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;
النتيجة:
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
شرح الكود:
LEFT JOIN departments— انضمام جدول الأقسام للحصول على أسماء الأقسام، مع الاحتفاظ بالموظفين بدون قسمLEFT JOIN orders— انضمام جدول الطلبات، مع الاحتفاظ بالموظفين بدون طلباتCOALESCE(SUM(...), 0)— يحول NULL إلى 0 لتجنب عرض فراغاتGROUP BY— تجميع حسب الموظف، تجميع عدد الطلبات والمبلغ الإجمالي
الاستعلام 2: ترتيب مبيعات الأقسام
المتطلب: حساب إجمالي المبيعات حسب القسم وترتيبها.
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;
شرح الكود:
- سلسلة
LEFT JOIN: departments → employees → orders COUNT(DISTINCT e.employee_id)— إزالة تكرارات عدد الموظفين لتجنب العد المزدوج بسبب طلبات متعددةRANK()— دالة نافذة تقوم بالترتيب حسب إجمالي المبيعاتCOALESCE— يتعامل مع الأقسام بدون طلبات، عرض 0
الاستعلام 3: الموظفون ذوو الرواتب المرتفعة وتفاصيل طلباتهم
المتطلب: العثور على الموظفين الذين رواتبهم فوق متوسط الشركة، مع معلومات طلباتهم.
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;
شرح الكود:
- استعلام فرعي
(SELECT AVG(salary) FROM employees)— يحسب متوسط راتب الشركة INNER JOIN— يحتفظ فقط بالموظفين ذوي الرواتب المرتفعة الذين لديهم طلباتWHERE e.salary > (...)— يستخدم نتيجة الاستعلام الفرعي كشرط تصفيةORDER BYيرتب حسب الراتب تنازليًا أولاً، ثم حسب التاريخ تصاعديًا
الاستعلام 4: استعلام متقاطع — طلبات بدون منتجات مقابلة في القسم
المتطلب: العثور على طلبات حيث أن قسم الموظف ليس لديه منتجات.
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
);
شرح الكود:
INNER JOIN employees— يربط الطلبات بالموظفينLEFT JOIN departments— يحصل على أسماء الأقسامNOT IN (استعلام فرعي)— يستبعد الأقسام التي لديها منتجات- يدمج JOIN والاستعلام الفرعي للتصفية المختلطة
الاستعلام 5: استخدام UNION لدمج بيانات متعددة الأبعاد
المتطلب: إنشاء دليل شامل يدمج "الموظفين الذين لديهم طلبات" و"جميع الأقسام".
-- الموظفون الذين لديهم طلبات مكتملة
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;
شرح الكود:
- SELECT الأول — يستخدم استعلامًا فرعيًا لتصفية الموظفين الذين لديهم طلبات مكتملة
- SELECT الثاني — يحصل على جميع الأقسام
UNION ALL— يدمج مجموعتي النتائج، يحتفظ بجميع الصفوف- يضيف عمود ثابت "type" لتمييز مصادر البيانات
الاستعلام 6: تحليل شامل — توزيع حالات الطلبات وأداء الموظفين
المتطلب: حساب توزيع حالات الطلبات لكل موظف والعثور على الموظفين الذين تعاملوا مع طلبات "مكتملة".
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;
شرح الكود:
CASE WHENتجميع شرطي — يحول بيانات الصفوف إلى أعمدة (pivot)LEFT JOINيحتفظ بجميع البيانات المرتبطة- استعلام فرعي
IN (...)يُصفّر الموظفين بدون طلبات GROUP BYيجمع ويُجمّع حسب الموظف
❓ أسئلة شائعة
س: متى يجب استخدام INNER JOIN مقابل LEFT JOIN؟ ج: إذا كنت تحتاج إلى الاحتفاظ بجميع السجلات من الجدول الأيسر (حتى عندما لا يوجد تطابق في الجدول الأيمن)، استخدم
LEFT JOIN. إذا كنت فقط تحتاج إلى السجلات المتطابقة في كلا الجدولين، استخدمINNER JOIN. في الممارسة العملية، استعلامات التقارير عادةً تستخدمLEFT JOIN، بينما التحقق من البيانات يستخدمINNER JOIN.
س: أيهما أفضل أداءً، الاستعلام الفرعي أم JOIN؟ ج: محسّنات قواعد البيانات الحديثة عادةً تحول الاستعلامات الفرعية إلى JOINs للتنفيذ. للارتباطات البسيطة،
JOINأكثر بديهية؛ للتحقق من الوجود ("هل يوجد؟")، استعلاماتEXISTSأوINالفرعية أكثر دلالية. استخدمEXPLAINللتحقق من اختلافات الأداء.
س: كيف أُصحح أخطاء استعلامات الجداول المتعددة المعقدة؟ ج: ابنِ بشكل تصاعدي: ابدأ باستعلام فرعي داخلي، تأكد من صحة النتيجة، ثم لفه طبقة بطبقة. يمكنك أيضًا استخدام CTEs (
WITHclause) لتقسيم الاستعلامات المعقدة إلى عدة مجموعات نتائج مؤقتة قابلة للقراءة.
س: هل يمكن أن يحتوي SELECT بعد GROUP BY فقط على دوال تجميع وأعمدة مجمّعة؟ ج:نعم. الأعمدة غير المجمّعة في
SELECTيجب أن تظهر جميعها فيGROUP BY، وإلا سيرمي قاعدة البيانات خطأ. هذا متطلب معيار SQL.
📖 ملخص
| المهارة | التطبيق في هذا القسم |
|---|---|
| INNER JOIN | ربط الجداول بعلاقات متطابقة |
| LEFT JOIN | الاحتفاظ بجميع سجلات الجدول الأيسر |
| استعلام فرعي (WHERE IN) | يُستخدم كشرط تصفية |
| استعلام فرعي (مُدرج) | يُرجع قيمة واحدة للمقارنة |
| UNION ALL | دمج مجموعات نتائج من أبعاد مختلفة |
| CASE WHEN | تجميع شرطي (pivot) |
| دوال التجميع | COUNT، SUM، AVG، COALESCE |
| دوال النافذة | RANK() للترتيب |
نقاط التدريب الرئيسية:
- ابنِ الاستعلامات المعقدة بشكل تصاعدي من البسيط إلى المعقد
- تأكد من صحة استعلامات الجدول الواحد قبل إضافة انضمامات الجداول المتعددة
- استخدم
COALESCEللتعامل مع قيم NULL - استخدم
EXPLAINلتحليل أداء الاستعلام
📝 تمارين
- اكتب استعلامًا: العثور على معلومات القسم الذي ينتمي إليه الموظف ذو أعلى راتب، بما في ذلك اسم القسم والمدينة والميزانية.
- اكتب استعلامًا: عدد الأقسام وعدد الموظفين الإجمالي لكل مدينة، باستخدام
LEFT JOINلضمان عرض المدن بدون موظفين أيضًا. - اكتب استعلامًا: العثور على الموظفين الذين لم يعالجوا أي طلب، قم بالتنفيذ بكل من الاستعلام الفرعي (
NOT IN) وLEFT JOIN ... IS NULL، وقارن ما إذا كانت النتائج متسقة. - اكتب استعلامًا: تقسيم الموظفين إلى ثلاث مجموعات حسب الراتب — "مرتفع (>10000)", "متوسط (6000-10000)", "منخفض (<6000)" — وعد الأشخاص ومتوسط الراتب لكل مجموعة.
الدرس التالي
سندرس بعد ذلك دوال التجميع — أتقن COUNT وSUM وAVG وMAX وMIN، بالإضافة إلى الاستخدام المتقدم لـ GROUP BY وHAVING.



