PostgreSQL: دوال التجميع والتجميع في PostgreSQL

آخر تحديث: 2026-08-26

1. ما ستتعلمه


2. القصة

Bob هو محلل بيانات في منصة تجارة إلكترونية عبر الحدود. مع اقتراب نهاية العام، يطلب منه المدير التنفيذي إنتاج تقرير تحليل مبيعات سنوي:

يجد Bob أن GROUP BY العادي ي��كنه التجميع حسب بعد واحد فقط في كل مرة، مما يجبره على كتابة عبارات SQL متعددة وUNION بينها. حتى تعلم GROUPING SETS وعبارة FILTER في PostgreSQL، واللتان تسمحان لعبارة SQL واحدة بتلبية كل المتطلبات.


3. المفهوم

(1) نظرة عامة على دوال التجميع

تقوم دوال التجميع بضغط عدة صفوف إدخال في صف إخراج واحد وهي حجر الزاوية في تحليل البيانات.

SQL
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;
TEXT 📖 للعرض فقط
 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) ▶ مثال

SQL
SELECT
  COUNT(*)       AS all_rows,
  COUNT(discount) AS rows_with_discount
FROM orders;
TEXT 📖 للعرض فقط
 all_rows | rows_with_discount
----------+-------------------
      100 |                 42

58 صفًا لديهم خصم NULL، لذا COUNT(discount) يستبعدهم.

(2) ▶ مثال

SQL
SELECT
  SUM(discount)  AS total_discount,
  AVG(discount)  AS avg_discount
FROM orders
WHERE region = 'NA';
TEXT 📖 للعرض فقط
 total_discount |     avg_discount
----------------+--------------------
         125000 | 2976.1904761904762

AVG يحسب المتوسط للصفوف غير NULL فقط: 125000 / 42 ≈ 2976.19، وليس 125000 / 100.

(3) ▶ مثال

SQL
SELECT
  MAX(created_at) AS latest_order,
  MIN(created_at) AS earliest_order
FROM orders;
TEXT 📖 للعرض فقط
     latest_order      |    earliest_order
------------------------+------------------------
 2025-12-28 15:30:00   | 2025-01-03 09:12:00

(4) ▶ مثال

SQL
SELECT
  COUNT(*)  AS cnt,
  SUM(amount) AS total
FROM orders
WHERE region = 'ANTARCTICA';
TEXT 📖 للعرض فقط
 cnt | total
-----+-------
   0 |

COUNT يرجع 0 لمجموعة فارغة، بينما SUM يرجع NULL لمجموعة فارغة — فخ كلاسيكي.

(3) GROUP BY للتجميع

يقوم GROUP BY بتجميع الصفوف حسب العمود (الأعمدة) المحدد، منتجًا صف تجميع واحد لكل مجموعة.

SQL
SELECT
  region,
  COUNT(*)    AS order_count,
  SUM(amount) AS total_amount
FROM orders
GROUP BY region;
TEXT 📖 للعرض فقط
 region | order_count | total_amount
--------+-------------+--------------
 EU     |          35 |      4200000
 NA     |          45 |      5800000
 APAC   |          20 |      2500000

(5) ▶ مثال

SQL
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;
TEXT 📖 للعرض فقط
 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) ▶ مثال

SQL
SELECT
  region,
  SUM(amount) AS total_amount
FROM orders
GROUP BY region
HAVING SUM(amount) > 3000000
ORDER BY total_amount DESC;
TEXT 📖 للعرض فقط
 region | total_amount
--------+--------------
 NA     |      5800000
 EU     |      4200000

إجمالي APAC البالغ 2,500,000 تمت تصفيته.

(7) ▶ مثال

SQL
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:

TEXT 📖 للعرض فقط
 count 
-------
     5
(1 row)

أولاً يتم تصفية الطلبات تحت 5,000، ثم تتم إزالة المناطق التي لديها أقل من 10 طلبات في المجموعة.


4. النقاط الرئيسية

(1) استخدام DISTINCT داخل دوال التجميع

(8) ▶ مثال

SQL
SELECT
  COUNT(DISTINCT customer_id) AS unique_customers,
  COUNT(*)                    AS total_orders
FROM orders;
TEXT 📖 للعرض فقط
 unique_customers | total_orders
------------------+--------------
              780 |          1000

(9) ▶ مثال

SQL
SELECT
  SUM(DISTINCT bonus) AS unique_bonus_total
FROM employee_targets;

Output:

TEXT 📖 للعرض فقط
  result  
----------
   42.50
(1 row)

(2) عبارة FILTER (ميزة PostgreSQL)

تسمح لك عبارة FILTER بتجميع نفس مجموعة الصفوف تحت شروط مختلفة دون كتابة تعبيرات CASE WHEN متعددة.

SQL
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) ▶ مثال

SQL
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:

TEXT 📖 للعرض فقط
 count 
-------
     5
(1 row)

(11) ▶ مثال

SQL
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:

TEXT 📖 للعرض فقط
  result  
----------
   42.50
(1 row)

(3) GROUPING SETS / ROLLUP / CUBE (ميزة PostgreSQL)

تنتج نتائج تجميعية عبر أبعاد متعددة في استعلام واحد، دون الحاجة لكتابة عبارات SQL متعددة وUNION بينها.

(12) ▶ مثال

SQL
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;
TEXT 📖 للعرض فقط
 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) ▶ مثال

SQL
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:

TEXT 📖 للعرض فقط
  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) ▶ مثال

SQL
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:

TEXT 📖 للعرض فقط
  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) ▶ مثال

SQL
SELECT
  category,
  SUM(amount) AS total_amount
FROM orders
GROUP BY category
ORDER BY total_amount DESC
LIMIT 3;

Output:

TEXT 📖 للعرض فقط
  result  
----------
   42.50
(1 row)

(16) ▶ مثال

SQL
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:

TEXT 📖 للعرض فقط
 count 
-------
     5
(1 row)

(17) ▶ مثال

SQL
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:

TEXT 📖 للعرض فقط
  result  
----------
   42.50
(1 row)

(18) ▶ مثال

SQL
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:

TEXT 📖 للعرض فقط
 count 
-------
     5
(1 row)

(19) ▶ مثال

SQL
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:

TEXT 📖 للعرض فقط
 count 
-------
     5
(1 row)

6. مثال شامل

تحليل المبيعات السنوي لـ Bob — عبارة SQL واحدة تنتج تقريرًا كامل الأبعاد:

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;
TEXT 📖 للعرض فقط
 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. تدفق التنفيذ

ترتيب التنفيذ الكامل لاستعلام تجميعي:

100%
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(*) و COUNT(1)؟
ج إنهما متكافئان تمامًا في PostgreSQL. COUNT(*) هو الشكل الموصى به، لأن معناه أوضح.
س لماذا يرجع SUM القيمة NULL بدلاً من 0 لمجموعة فارغة؟
ج ينص معيار SQL على أن SUM على إدخال كله NULL أو فارغ يرجع NULL. إذا كنت بحاجة إلى 0، استخدم COALESCE(SUM(col), 0).
س هل يمكن استخدام HAVING بدون دالة تجميع؟
ج نعم — HAVING region = 'NA' صحيح من حيث الصيغة، لكن مثل هذا الشرط ينتمي إلى WHERE لأداء أفضل.
س هل عبارة FILTER بنفس سرعة CASE WHEN؟
ج أساسًا نفس الشيء — كلاهما يمسح البيانات مرة واحدة فقط. FILTER أوضح في القراءة وهو الشكل الموصى به في PostgreSQL.
س كيف يختلف GROUPING SETS عن عبارات SQL المتعددة مع UNION؟
ج GROUPING SETS يمسح الجدول مرة واحدة فقط، بينما عبارات SQL المتعددة مع UNION تمسحه عدة مرات. فجوة الأداء كبيرة عند أحجام البيانات الكبيرة.
س هل يمكن ظهور أعمدة غير مجمعة في SELECT بعد GROUP BY؟
ج ليس في وضع PostgreSQL الصارم. أي عمود غير مجمع في SELECT يجب أن يظهر أيضًا في GROUP BY، وإلا يحدث خطأ.

📖 ملخص


📝 تمارين

  1. ⭐ عد عدد الطلبات والمبلغ الإجمالي لكل منطقة في جدول orders، مرتبة تنازليًا حسب المبلغ.
  2. ⭐ ابحث عن قيم customer_id التي لديها أكثر من 10 طلبات ومتوسط مبلغ أعلى من 50,000 دولار.
  3. ⭐⭐ باستخدام عبارة FILTER، أخرج مبيعات كل منطقة للربع الأول حتى الرابع في عبارة SQL واحدة.
  4. ⭐⭐ استخدم CUBE لحساب ملخص تقاطع كامل الأبعاد على (region, category)، واستخدم GROUPING() لتمييز صفوف الملخص.
  5. ⭐⭐⭐ اكتب عبارة SQL واحدة تخرج: ملخص المنطقة، ملخص الفئة، تفصيل المنطقة+الفئة، وال��جمالي الكلي للجدول — بمسح جدول orders مرة واحدة فقط.
Web-Tutorial.com

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

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

100%