PostgreSQL: دوال التجميع والتجميع في PostgreSQL
آخر تحديث: 2026-08-26
1. ما ستتعلمه
- دوال التجميع الأساسية الخمس: COUNT / SUM / AVG / MAX / MIN
- GROUP BY للتجميع بعمود واحد ومتعدد الأعمدة
- عبارة HAVING لتصفية النتائج المجمعة
- ميزات PostgreSQL: GROUPING SETS / ROLLUP / CUBE تجميع متعدد الأبعاد
- ميزة PostgreSQL: عبارة FILTER للتجميع الشرطي
- استخدام DISTINCT داخل دوال التجميع
- كيفية تعامل دوال التجميع مع NULL
2. القصة
Bob هو محلل بيانات في منصة تجارة إلكترونية عبر الحدود. مع اقتراب نهاية العام، يطلب منه المدير التنفيذي إنتاج تقرير تحليل مبيعات سنوي:
- عد الطلبات وإيرادات المبيعات لكل منطقة
- تحليل متقاطع عبر أبعاد متعددة (ربع السنة + المنطقة)
- حساب فرق الإيرادا�� بين الأشهر التي بها طلبات والتي بدونها
- إنتاج بيانات ملخصة لجميع الأبعاد دفعة واحدة
يجد Bob أن GROUP BY العادي ي��كنه التجميع حسب بعد واحد فقط في كل مرة، مما يجبره على كتابة عبارات SQL متعددة وUNION بينها. حتى تعلم GROUPING SETS وعبارة FILTER في PostgreSQL، واللتان تسمحان لعبارة SQL واحدة بتلبية كل المتطلبات.
3. المفهوم
(1) نظرة عامة على دوال التجميع
تقوم دوال التجميع بضغط عدة صفوف إدخال في صف إخراج واحد وهي حجر الزاوية في تحليل البيانات.
SELECT
COUNT(*) AS total_rows,
COUNT(amount) AS non_null_count,
SUM(amount) AS total_amount,
AVG(amount) AS avg_amount,
MAX(amount) AS max_amount,
MIN(amount) AS min_amount
FROM orders;
total_rows | non_null_count | total_amount | avg_amount | max_amount | min_amount
------------+----------------+--------------+------------+------------+------------
100 | 95 | 12500000 | 131578.95 | 500000 | 1200
(2) دوال التجميع الخمس بالتفصيل
| الدالة | الغرض | سلوك NULL | نوع الإرجاع |
|---|---|---|---|
COUNT(*) |
عد كل الصفوف (بما فيها NULL) | يشمل NULL | bigint |
COUNT(col) |
عد الصفوف غير NULL | يتجاهل NULL | bigint |
SUM(col) |
المجموع | يتجاهل NULL؛ الكل NULL يرجع NULL | نفس نوع الإدخال |
AVG(col) |
المتوسط | يتجاهل NULL | numeric |
MAX(col) / MIN(col) |
القيمة القصوى / الدنيا | يتجاهل NULL | نفس نوع الإدخال |
(1) ▶ مثال
SELECT
COUNT(*) AS all_rows,
COUNT(discount) AS rows_with_discount
FROM orders;
all_rows | rows_with_discount
----------+-------------------
100 | 42
58 صفًا لديهم خصم NULL، لذا COUNT(discount) يستبعدهم.
(2) ▶ مثال
SELECT
SUM(discount) AS total_discount,
AVG(discount) AS avg_discount
FROM orders
WHERE region = 'NA';
total_discount | avg_discount
----------------+--------------------
125000 | 2976.1904761904762
AVG يحسب المتوسط للصفوف غير NULL فقط: 125000 / 42 ≈ 2976.19، وليس 125000 / 100.
(3) ▶ مثال
SELECT
MAX(created_at) AS latest_order,
MIN(created_at) AS earliest_order
FROM orders;
latest_order | earliest_order
------------------------+------------------------
2025-12-28 15:30:00 | 2025-01-03 09:12:00
(4) ▶ مثال
SELECT
COUNT(*) AS cnt,
SUM(amount) AS total
FROM orders
WHERE region = 'ANTARCTICA';
cnt | total
-----+-------
0 |
COUNT يرجع 0 لمجموعة فارغة، بينما SUM يرجع NULL لمجموعة فارغة — فخ كلاسيكي.
(3) GROUP BY للتجميع
يقوم GROUP BY بتجميع الصفوف حسب العمود (الأعمدة) المحدد، منتجًا صف تجميع واحد لكل مجموعة.
SELECT
region,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
GROUP BY region;
region | order_count | total_amount
--------+-------------+--------------
EU | 35 | 4200000
NA | 45 | 5800000
APAC | 20 | 2500000
(5) ▶ مثال
SELECT
region,
EXTRACT(QUARTER FROM created_at)::int AS quarter,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
GROUP BY region, EXTRACT(QUARTER FROM created_at)
ORDER BY region, quarter;
region | quarter | order_count | total_amount
--------+---------+-------------+--------------
APAC | 1 | 5 | 620000
APAC | 2 | 6 | 780000
APAC | 3 | 4 | 500000
APAC | 4 | 5 | 600000
EU | 1 | 8 | 950000
EU | 2 | 9 | 1100000
...
(4) HAVING تصفية المجموعات
WHERE يصفي الصفوف قبل التجميع؛ HAVING يصفي المجموع��ت بعد التجميع.
| العبارة | متى تطبق | تسمح بدوال التجميع |
|---|---|---|
| WHERE | قبل GROUP BY | لا |
| HAVING | بعد GROUP BY | نعم |
(6) ▶ مثال
SELECT
region,
SUM(amount) AS total_amount
FROM orders
GROUP BY region
HAVING SUM(amount) > 3000000
ORDER BY total_amount DESC;
region | total_amount
--------+--------------
NA | 5800000
EU | 4200000
إجمالي APAC البالغ 2,500,000 تمت تصفيته.
(7) ▶ مثال
SELECT
region,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
WHERE amount >= 5000
GROUP BY region
HAVING COUNT(*) >= 10
ORDER BY total_amount DESC;
Output:
count
-------
5
(1 row)
أولاً يتم تصفية الطلبات تحت 5,000، ثم تتم إزالة المناطق التي لديها أقل من 10 طلبات في المجموعة.
4. النقاط الرئيسية
(1) استخدام DISTINCT داخل دوال التجميع
(8) ▶ مثال
SELECT
COUNT(DISTINCT customer_id) AS unique_customers,
COUNT(*) AS total_orders
FROM orders;
unique_customers | total_orders
------------------+--------------
780 | 1000
(9) ▶ مثال
SELECT
SUM(DISTINCT bonus) AS unique_bonus_total
FROM employee_targets;
Output:
result
----------
42.50
(1 row)
(2) عبارة FILTER (ميزة PostgreSQL)
تسمح لك عبارة FILTER بتجميع نفس مجموعة الصفوف تحت شروط مختلفة دون كتابة تعبيرات CASE WHEN متعددة.
SELECT
region,
COUNT(*) FILTER (WHERE amount >= 10000) AS high_value_orders,
COUNT(*) FILTER (WHERE amount < 10000) AS low_value_orders,
SUM(amount) FILTER (WHERE quarter = 1) AS q1_revenue,
SUM(amount) FILTER (WHERE quarter = 2) AS q2_revenue
FROM orders
GROUP BY region;
| الأسلوب | الصيغة | قابلية القراءة | الأداء |
|---|---|---|---|
| CASE WHEN | SUM(CASE WHEN ... THEN x ELSE 0 END) |
مقبولة | مسح واحد |
| FILTER | SUM(x) FILTER (WHERE ...) |
ممتازة | مسح واحد |
(10) ▶ مثال
SELECT
region,
COUNT(DISTINCT EXTRACT(MONTH FROM created_at))
FILTER (WHERE amount > 0) AS months_with_orders,
12 - COUNT(DISTINCT EXTRACT(MONTH FROM created_at))
FILTER (WHERE amount > 0) AS months_without_orders
FROM orders
WHERE EXTRACT(YEAR FROM created_at) = 2025
GROUP BY region;
Output:
count
-------
5
(1 row)
(11) ▶ مثال
SELECT
region,
SUM(amount) FILTER (WHERE EXTRACT(YEAR FROM created_at) = 2024) AS revenue_2024,
SUM(amount) FILTER (WHERE EXTRACT(YEAR FROM created_at) = 2025) AS revenue_2025
FROM orders
GROUP BY region;
Output:
result
----------
42.50
(1 row)
(3) GROUPING SETS / ROLLUP / CUBE (ميزة PostgreSQL)
تنتج نتائج تجميعية عبر أبعاد متعددة في استعلام واحد، دون الحاجة لكتابة عبارات SQL متعددة وUNION بينها.
(12) ▶ مثال
SELECT
region,
EXTRACT(QUARTER FROM created_at)::int AS quarter,
SUM(amount) AS total_amount
FROM orders
GROUP BY GROUPING SETS (
(region, EXTRACT(QUARTER FROM created_at)),
(region),
(EXTRACT(QUARTER FROM created_at)),
()
)
ORDER BY region NULLS LAST, quarter NULLS LAST;
region | quarter | total_amount
--------+---------+--------------
APAC | 1 | 620000
APAC | 2 | 780000
APAC | 3 | 500000
APAC | 4 | 600000
APAC | | 2500000
EU | 1 | 950000
...
| 1 | 2200000
...
| | 12500000
NULL يعني أن ذلك البعد على المستوى التلخيصي؛ استخدم دالة GROUPING() للتمييز بين NULL الحقيقي وNULL على مستوى التلخيص.
(13) ▶ مثال
SELECT
region,
EXTRACT(QUARTER FROM created_at)::int AS quarter,
SUM(amount) AS total_amount,
GROUPING(region) AS g_region,
GROUPING(quarter) AS g_quarter
FROM orders
GROUP BY ROLLUP (region, EXTRACT(QUARTER FROM created_at))
ORDER BY region NULLS LAST, quarter NULLS LAST;
Output:
result
----------
42.50
(1 row)
| الصيغة | GROUPING SETS المكافئة | أبعاد الإخراج |
|---|---|---|
ROLLUP(a, b) |
(a,b), (a), () |
هرمي: تفصيل → مجموع فرعي → مجموع كلي |
CUBE(a, b) |
(a,b), (a), (b), () |
تقاطع كامل: كل التركيبات |
GROUPING SETS((a),(b)) |
(a), (b) |
أي تركيبة مخصصة |
(14) ▶ مثال
SELECT
region,
EXTRACT(QUARTER FROM created_at)::int AS quarter,
SUM(amount) AS total_amount
FROM orders
GROUP BY CUBE (region, EXTRACT(QUARTER FROM created_at))
ORDER BY region NULLS LAST, quarter NULLS LAST;
Output:
result
----------
42.50
(1 row)
(4) ملخص سلوك NULL لدوال التجميع
| السيناريو | COUNT(*) | COUNT(col) | SUM | AVG | MAX/MIN |
|---|---|---|---|---|---|
| لديه قيم غير NULL | يعد كل الصفوف | يعد غير NULL فقط | يجمع متجاهلاً NULL | يحسب المتوسط متجاهلاً NULL | القيم القصوى متجاهلاً NULL |
| الكل NULL | يعد الصفوف | 0 | NULL | NULL | NULL |
| مجموعة نتائج فارغة | 0 | 0 | NULL | NULL | NULL |
5. تطبيق عملي
(15) ▶ مثال
SELECT
category,
SUM(amount) AS total_amount
FROM orders
GROUP BY category
ORDER BY total_amount DESC
LIMIT 3;
Output:
result
----------
42.50
(1 row)
(16) ▶ مثال
SELECT
region,
COUNT(DISTINCT customer_id) FILTER (
WHERE EXTRACT(YEAR FROM created_at) = 2024
) AS customers_2024,
COUNT(DISTINCT customer_id) FILTER (
WHERE EXTRACT(YEAR FROM created_at) = 2025
) AS customers_2025,
ROUND(
COUNT(DISTINCT customer_id) FILTER (WHERE EXTRACT(YEAR FROM created_at) = 2025)::numeric
/ NULLIF(
COUNT(DISTINCT customer_id) FILTER (WHERE EXTRACT(YEAR FROM created_at) = 2024),
0
) * 100, 1
) AS retention_rate
FROM orders
GROUP BY region;
Output:
count
-------
5
(1 row)
(17) ▶ مثال
SELECT
EXTRACT(MONTH FROM created_at)::int AS month_num,
SUM(amount) FILTER (WHERE EXTRACT(YEAR FROM created_at) = 2024) AS revenue_2024,
SUM(amount) FILTER (WHERE EXTRACT(YEAR FROM created_at) = 2025) AS revenue_2025
FROM orders
GROUP BY EXTRACT(MONTH FROM created_at)
ORDER BY month_num;
Output:
result
----------
42.50
(1 row)
(18) ▶ مثال
SELECT
region,
category,
SUM(amount) AS total_amount,
COUNT(*) FILTER (WHERE amount >= 50000) AS big_deals,
GROUPING(region) AS g_region,
GROUPING(category) AS g_category
FROM orders
GROUP BY ROLLUP (region, category)
ORDER BY region NULLS LAST, category NULLS LAST;
Output:
count
-------
5
(1 row)
(19) ▶ مثال
SELECT
customer_id,
COUNT(*) AS order_count,
SUM(amount) AS total_spent
FROM orders
GROUP BY customer_id
HAVING COUNT(*) >= 5 AND SUM(amount) >= 100000
ORDER BY total_spent DESC;
Output:
count
-------
5
(1 row)
6. مثال شامل
تحليل المبيعات السنوي لـ Bob — عبارة SQL واحدة تنتج تقريرًا كامل الأبعاد:
SELECT
region,
EXTRACT(QUARTER FROM created_at)::int AS quarter,
SUM(amount) AS total_revenue,
COUNT(*) AS order_count,
COUNT(DISTINCT customer_id) AS unique_customers,
SUM(amount) FILTER (WHERE amount >= 50000) AS big_deal_revenue,
COUNT(*) FILTER (WHERE amount >= 50000) AS big_deal_count,
AVG(amount) AS avg_order_value,
GROUPING(region) AS g_region,
GROUPING(quarter) AS g_quarter
FROM orders
WHERE EXTRACT(YEAR FROM created_at) = 2025
GROUP BY ROLLUP (region, EXTRACT(QUARTER FROM created_at))
ORDER BY region NULLS LAST, quarter NULLS LAST;
region | quarter | total_revenue | order_count | unique_customers | big_deal_revenue | big_deal_count | avg_order_value | g_region | g_quarter
--------+---------+---------------+-------------+------------------+------------------+----------------+-----------------+----------+-----------
APAC | 1 | 620000 | 5 | 4 | 120000 | 1 | 124000.00 | 0 | 0
APAC | 2 | 780000 | 6 | 5 | 250000 | 2 | 130000.00 | 0 | 0
APAC | 3 | 500000 | 4 | 3 | 50000 | 1 | 125000.00 | 0 | 0
APAC | 4 | 600000 | 5 | 4 | 100000 | 1 | 120000.00 | 0 | 0
APAC | | 2500000 | 20 | 12 | 520000 | 5 | 125000.00 | 0 | 1
EU | 1 | 950000 | 8 | 7 | 350000 | 3 | 118750.00 | 0 | 0
...
| | 12500000 | 100 | 780 | 5000000 | 45 | 125000.00 | 1 | 1
7. تدفق التنفيذ
ترتيب التنفيذ الكامل لاستعلام تجميعي:
flowchart TD
A[FROM] --> B[WHERE]
B --> C[GROUP BY]
C --> D[HAVING]
D --> E["دوال التجميع<br/>COUNT/SUM/AVG/MAX/MIN"]
E --> F[SELECT]
F --> G[ORDER BY]
G --> H[LIMIT]
style A fill:#e1f5fe
style C fill:#fff9c4
style D fill:#fff9c4
style E fill:#c8e6c9
| الخطوة | العبارة | الوصف |
|---|---|---|
| 1 | FROM | تحديد مصدر البيانات |
| 2 | WHERE | تصفية الصفوف (قبل التجميع) |
| 3 | GROUP BY | تجميع الصفوف |
| 4 | دوال التجميع | حساب التجميع لكل مجموعة |
| 5 | HAVING | تصفية المجموعات (بعد التجميع) |
| 6 | SELECT | اختيار أعمدة الإخراج |
| 7 | ORDER BY | الترتيب |
| 8 | LIMIT | تحديد عدد الصفوف |
❓ أسئلة شائعة
📖 ملخص
- دوال التجميع الخمس COUNT/SUM/AVG/MAX/MIN تتعامل مع NULL بشكل مختلف
- GROUP BY يجمع حسب العمود؛ HAVING يصفي النتائج المجمعة
- WHERE يصفي الصفوف قبل التجميع؛ HAVING يصفي المجموعات بعد التجميع
- عبارة FILTER في PostgreSQL تستبدل CASE WHEN بصيغة أنظف
- GROUPING SETS / ROLLUP / CUBE تنتج ملخصات متعددة الأبعاد في استعلام واحد
- دالة GROUPING() تميز NULL الحقيقي عن NULL على مستوى التلخيص
- COUNT(*) يرجع 0 لمجموعة فارغة؛ SUM/AVG يرجعان NULL
📝 تمارين
- ⭐ عد عدد الطلبات والمبلغ الإجمالي لكل منطقة في جدول orders، مرتبة تنازليًا حسب المبلغ.
- ⭐ ابحث عن قيم customer_id التي لديها أكثر من 10 طلبات ومتوسط مبلغ أعلى من 50,000 دولار.
- ⭐⭐ باستخدام عبارة FILTER، أخرج مبيعات كل منطقة للربع الأول حتى الرابع في عبارة SQL واحدة.
- ⭐⭐ استخدم CUBE لحساب ملخص تقاطع كامل الأبعاد على (region, category)، واستخدم GROUPING() لتمييز صفوف الملخص.
- ⭐⭐⭐ اكتب عبارة SQL واحدة تخرج: ملخص المنطقة، ملخص الفئة، تفصيل المنطقة+الفئة، وال��جمالي الكلي للجدول — بمسح جدول orders مرة واحدة فقط.