استعلامات التجميع
استعلامات التجميع
🌍 تشبيه من العالم האמיתי
تخيل أنك معلم لديك كومة من كشوف الدرجات. تريد معرفة:
- متوسط درجة كل طالب → تجميع حسب الطالب، حساب المتوسط
- أعلى درجة لكل مادة → تجميع حسب المادة، إيجاد الأقصى
- نسبة النجاح لكل فصل → تجميع حسب الفصل، حساب النسبة
هذا ما يفعله GROUP BY: أولاً تقسيم البيانات إلى مجموعات، ثم إجراء الإحصائيات على كل مجموعة. الدوال التجميعية التي تعلمتها في الدرس السابق تحسب قيمة واحدة للجدول بالكامل، بينما GROUP BY يحسب القيم حسب الفئات.
🎯 المفاهيم الأساسية
الصيغة الأساسية لـ GROUP BY
SELECT column_name, aggregate_function(column_name)
FROM table_name
WHERE condition
GROUP BY column_name;
منطق التنفيذ: أولاً تجميع حسب عمود GROUP BY، ثم تطبيق الدالة التجميعية على كل مجموعة.
-- كم عدد الموظفين في كل قسم؟
SELECT department_id, COUNT(*) AS headcount
FROM employees
GROUP BY department_id;
الناتج:
department_id headcount
------------- ---------
1 3
2 2
3 2
NULL 1
SELECT إما في GROUP BY أو أن تكون ملفوفة بدالة تجميعية. وإلا، ستكون الدلالات غامضة وسيُبلّغ قاعدة البيانات عن خطأ.
التجميع بعد التجميع
النمط الأساسي لـ GROUP BY هو: تجميع → تجميع إحصائي.
-- متوسط الراتب لكل قسم
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id;
مجموعات تجميعية شائعة:
SELECT department_id,
COUNT(*) AS headcount,
SUM(salary) AS total_salary,
AVG(salary) AS avg_salary,
MAX(salary) AS highest_salary,
MIN(salary) AS lowest_salary
FROM employees
GROUP BY department_id;
التجميع بأعمدة متعددة
يمكنك التجميع حسب عدة أعمدة — تُعتبر الصفوف في نفس المجموعة فقط عندما تتطابق قيم جميع الأعمدة المحددة:
-- عدد الموظفين حسب القسم والحالة
SELECT department_id, status, COUNT(*) AS headcount
FROM employees
GROUP BY department_id, status;
الناتج:
department_id status headcount
------------- -------- ---------
1 active 3
2 active 1
2 inactive 1
3 active 2
GROUP BY + ORDER BY
يمكنك الترتيب بعد التجميع:
-- متوسط الراتب حسب القسم، مرتب من الأعلى إلى الأدنى
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
ORDER BY avg_salary DESC;
WHERE مقابل HAVING — تصفية الصفوف مقابل تصفية المجموعات
هذا أحد أهم المفاهيم في هذا الدرس:
| الميزة | WHERE | HAVING |
|---|---|---|
| يُصفّي | صفوف (قبل التجميع) | مجموعات (بعد التجميع) |
| توقيت التنفيذ | قبل GROUP BY |
بعد GROUP BY |
| يمكن استخدام الدوال التجميعية | ❌ لا | ✅ نعم |
| الاستخدام | يمكن استخدامه بمفرده | يجب استخدامه مع GROUP BY |
-- WHERE: تصفية الصفوف أولاً، ثم التجميع
-- إيجاد الأقسام التي لديها أكثر من 3 موظفين
SELECT department_id, COUNT(*) AS headcount
FROM employees
WHERE salary > 5000 -- أولاً استبعاد الصفوف التي رواتبها أقل من 5000
GROUP BY department_id;
-- HAVING: التجميع أولاً، ثم تصفية المجموعات
-- إيجاد الأقسام التي لديها 3 موظفين أو أكثر
SELECT department_id, COUNT(*) AS headcount
FROM employees
GROUP BY department_id
HAVING COUNT(*) >= 3; -- تصفية المجموعات التي عدد أعضائها أقل من 3
-- الجمع بين WHERE + HAVING
-- إيجاد الأقسام التي لديها موظفون تم تعيينهم في 2024 ومتوسط راتب > 10000
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
WHERE hire_date >= '2024-01-01' -- تصفية الصفوف أولاً
GROUP BY department_id
HAVING AVG(salary) > 10000; -- ثم تصفية المجموعات
ترتيب التنفيذ
يختلف ترتيب الكتابة وترتيب التنفيذ في استعلامات SQL:
ترتيب الكتابة: SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY
ترتيب التنفيذ: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY
فهم ترتيب التنفيذ أمر بالغ الأهمية لكتابة SQL صحيح:
FROM— تحديد مصدر البياناتWHERE— تصفية صف بصفGROUP BY— تجميعHAVING— تصفية المجموعاتSELECT— اختيار الأعمدة، حساب التعبيراتORDER BY— ترتيب
WHERE — لأنه عند تنفيذ WHERE، لم يتم تنفيذ GROUP BY بعد، لذا لا توجد نتائج تجميعية.
📝 الصيغة الأساسية
-- التجميع الأساسي
SELECT column_name, aggregate_function(column_name)
FROM table_name
GROUP BY column_name;
-- التجميع بأعمدة متعددة
SELECT column1, column2, aggregate_function(column_name)
FROM table_name
GROUP BY column1, column2;
-- WHERE + GROUP BY + HAVING
SELECT column_name, aggregate_function(column_name)
FROM table_name
WHERE row_level_condition
GROUP BY column_name
HAVING aggregate_level_condition
ORDER BY sort_column;
- لا يلزم أن تتطابق أعمدة
GROUP BYمع ترتيب الأعمدة فيSELECT، ولكن يجب أن تشمل جميع الأعمدة غير التجميعية - يمكن لـ
HAVINGاستخدام أسماء المستعارة للدوال التجميعية (مدعومة في MySQL و PostgreSQL)، لكن SQL القياسي يتطلب كتابة التعبير الكامل - تتجمع قيم
NULLمعًا فيGROUP BY
📌 أمثلة
مثال: إحصائيات الموظفين حسب القسم
SELECT d.department_name,
COUNT(e.employee_id) AS headcount,
AVG(e.salary) AS avg_salary,
SUM(e.salary) AS total_salary
FROM employees e
LEFT JOIN departments d ON e.department_id = d.department_id
GROUP BY d.department_name
ORDER BY total_salary DESC;
الناتج:
department_name headcount avg_salary total_salary
--------------- --------- ---------- ------------
Technology 3 17666.67 53000.00
Finance 2 13500.00 27000.00
Marketing 2 11500.00 23000.00
Administration 1 9000.00 9000.00
LEFT JOIN عرض الأقسام التي لا يوجد بها موظفون (بعدد 0). يقوم GROUP BY department_name بالتجميع حسب اسم القسم.
مثال: WHERE + GROUP BY + HAVING معًا
-- من بين الموظفين الذين تم تعيينهم في 2024، إيجاد الأقسام بمتوسط راتب > 12000
SELECT department_id,
COUNT(*) AS headcount,
AVG(salary) AS avg_salary
FROM employees
WHERE hire_date >= '2024-01-01'
GROUP BY department_id
HAVING AVG(salary) > 12000
ORDER BY avg_salary DESC;
الناتج:
department_id headcount avg_salary
------------- --------- ----------
1 2 18500.00
3 1 14000.00
WHERE hire_date >= '2024-01-01'— أولاً تصفية الموظفين الذين تم تعيينهم في 2024GROUP BY department_id— تجميع حسب القسمHAVING AVG(salary) > 12000— الاحتفاظ فقط بالمجموعات التي متوسط رواتبها > 12000ORDER BY avg_salary DESC— ترتيب حسب متوسط الراتب تنازليًا
مثال: تجميع بأعمدة متعددة + شروط معقدة
-- إحصائيات الموظفين حسب القسم والحالة، عرض المجموعات التي عدد أعضائها 2 أو أكثر فقط
SELECT d.department_name,
e.status,
COUNT(*) AS headcount,
MIN(e.salary) AS lowest_salary,
MAX(e.salary) AS highest_salary
FROM employees e
JOIN departments d ON e.department_id = d.department_id
WHERE e.salary > 0
GROUP BY d.department_name, e.status
HAVING COUNT(*) >= 2
ORDER BY d.department_name, headcount DESC;
الناتج:
department_name status headcount lowest_salary highest_salary
--------------- ------ --------- ------------- --------------
Finance active 2 12000.00 14000.00
Marketing active 2 10000.00 12000.00
Technology active 3 15000.00 20000.00
WHERE لاستبعاد الصفوف التي رواتبها صفر، ثم تجميع حسب القسم + الحالة، يقوم HAVING بتصفية المجموعات التي عدد أعضائها أقل من 2، وأخيرًا الترتيب.
🎬 سيناريوهات تطبيقية
السيناريو 1: تقرير المبيعات — إحصائيات الطلبات الشهرية
إنشاء تقرير مبيعات شهري، عرض الأشهر التي لديها 3 طلبات أو أكثر فقط.
SELECT
EXTRACT(YEAR FROM order_date) AS year,
EXTRACT(MONTH FROM order_date) AS month,
COUNT(*) AS order_count,
SUM(total_amount) AS total_amount,
AVG(total_amount) AS avg_order_amount
FROM orders
WHERE status IN ('completed', 'shipped')
GROUP BY EXTRACT(YEAR FROM order_date), EXTRACT(MONTH FROM order_date)
HAVING COUNT(*) >= 3
ORDER BY year, month;
WHERE بتصفية الطلبات المكتملة والمشحونة، تجميع حسب السنة والشهر للإحصائيات، يقوم HAVING بتصفية الأشهر التي عدد طلباتها أقل من 3.
السيناريو 2: تحليل الموارد البشرية — إيجاد الأsections ذات الرواتب المرتفعة
إيجاد الأقسام التي متوسط رواتبها أعلى من متوسط الرواتب في الشركة.
SELECT d.department_name,
COUNT(e.employee_id) AS headcount,
AVG(e.salary) AS dept_avg_salary
FROM departments d
JOIN employees e ON d.department_id = e.department_id
GROUP BY d.department_name
HAVING AVG(e.salary) > (SELECT AVG(salary) FROM employees)
ORDER BY dept_avg_salary DESC;
HAVING. أولاً حساب متوسط راتب كل قسم، ثم مقارنته بالمتوسط العام للشركة. هذا تطبيق مشترك للدوال التجميعية والاستعلامات الفرعية.
❓ أسئلة شائعة
س: هل يجب أن تظهر أعمدة SELECT في GROUP BY? ج: نعم، يجب أن تظهر جميع الأعمدة غير التجميعية في
GROUP BY. هذا متطلب معياري في SQL. على سبيل المثال،SELECT a, b, COUNT(*) FROM t GROUP BY aسيُسبب خطأ لأنbليست فيGROUP BYولا ملفوفة بدالة تجميعية.
س: هل يمكن لـ HAVING استخدام أسماء المستعارة من SELECT? ج: يدعم MySQL و PostgreSQL استخدام أسماء المستعارة في
HAVING، لكن SQL Server لا يدعم ذلك. للتوافق، يُوصى بكتابة التعبير الكامل:HAVING AVG(salary) > 10000بدلاً منHAVING avg_salary > 10000.
س: كيف تُعالج قيم NULL في GROUP BY? ج: تتجمع قيم
NULLمعًا. إذا كانdepartment_idيحتوي على قيم NULL، فسيتم وضع جميع الصفوف التي تحتوي على NULL في نفس المجموعة. يكون هذا مفيدًا عادةً، لكن انتبه للتمييز بين "لا يوجد قسم" و"معرّف القسم هو 0".
س: هل يمكن استخدام WHERE و HAVING معًا? ج: نعم، وهو شائع جدًا. يقوم
WHEREبتصفية الصفوف قبل التجميع، وHAVINGيصفّي المجموعات بعد التجميع. ترتيب التنفيذ:WHERE→GROUP BY→HAVING.
📖 ملخص
- يقوم
GROUP BYبتقسيم البيانات إلى مجموعات وتطبيق الدوال التجميعية على كل مجموعة - يجب أن تظهر الأعمدة غير التجميعية في
SELECTفيGROUP BY - التجميع بأعمدة متعددة: تجميع حسب تركيبات قيم عدة أعمدة
WHERE: يصفّي الصفوف قبل التجميع، لا يمكن استخدام الدوال التجميعيةHAVING: يصفّي المجموعات بعد التجميع، يمكن استخدام الدوال التجميعية- ترتيب التنفيذ:
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY - تتجمع قيم
NULLمعًا
📝 تمارين
التمرين 1 (⭐): استعلم عن عدد الموظفين ومتوسط الراتب لكل قسم، مرتب حسب متوسط الراتب من الأعلى إلى الأدنى.
التمرين 2 (⭐⭐): استعلم عن عدد الموظفين الذين تم تعيينهم بعد 2024 في كل قسم، عرض الأقسام التي لديها 2 موظفين أو أكثر فقط.
التمرين 3 (⭐⭐⭐): استعلم عن أعلى موظف راتبًا في كل قسم. اعرض اسم القسم واسم الموظف والراتب. تلميح: يمكن تحقيق ذلك باستخدام استعلام فرعي أو دالة النافذة ROW_NUMBER() (تُغطى لاحقًا).
الدرس التالي
👉 15-advanced-functions - الدوال المتقدمة: تعلّم الدوال المتقدمة في SQL، وإتقان معالجة النصوص، والحسابات الرقمية، وعمليات التواريخ، وغيرها من المهارات العملية!



