SQL: استعلامات التجميع
1. 🌍 تشبيه من العالم האמיתי
تخيل أنك معلم لديك كومة من كشوف الدرجات. تريد معرفة:
- متوسط درجة كل طالب → تجميع حسب الطالب، حساب المتوسط
- أعلى درجة لكل مادة → تجميع حسب المادة، إيجاد الأقصى
- نسبة النجاح لكل فصل → تجميع حسب الفصل، حساب النسبة
هذا ما يفعله GROUP BY: أولاً تقسيم البيانات إلى مجموعات، ثم إجراء الإحصائيات على كل مجموعة. الدوال التجميعية التي تعلمتها في الدرس السابق تحسب قيمة واحدة للجدول بالكامل، بينما GROUP BY يحسب القيم حسب الفئات.
2. 🎯 المفاهيم الأساسية
(1) الصيغة الأساسية لـ 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 أو أن تكون ملفوفة بدالة تجميعية. وإلا، ستكون الدلالات غامضة وسيُبلّغ قاعدة البيانات عن خطأ.
(2) التجميع بعد التجميع
النمط الأساسي لـ 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;
(3) التجميع بأعمدة متعددة
يمكنك التجميع حسب عدة أعمدة — تُعتبر الصفوف في نفس المجموعة فقط عندما تتطابق قيم جميع الأعمدة المحددة:
-- عدد الموظفين حسب القسم والحالة
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
(4) GROUP BY + ORDER BY
يمكنك الترتيب بعد التجميع:
-- متوسط الراتب حسب القسم، مرتب من الأعلى إلى الأدنى
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
ORDER BY avg_salary DESC;
(5) 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; -- ثم تصفية المجموعات
(6) ترتيب التنفيذ
يختلف ترتيب الكتابة وترتيب التنفيذ في استعلامات 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 بعد، لذا لا توجد نتائج تجميعية.
3. 📝 الصيغة الأساسية
-- التجميع الأساسي
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
4. 📌 أمثلة
▶ مثال: إحصائيات الموظفين حسب القسم
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، وأخيرًا الترتيب.
5. 🎬 سيناريوهات تطبيقية
(1) السيناريو 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) السيناريو 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() (تُغطى لاحقًا).
6. الدرس التالي
👉 15-advanced-functions - الدوال المتقدمة: تعلّم الدوال المتقدمة في SQL، وإتقان معالجة النصوص، والحسابات الرقمية، وعمليات التواريخ، وغيرها من المهارات العملية!