PostgreSQL: دوال النوافذ في PostgreSQL: دليل مفصل
آخر تحديث: 2026-08-26
1. ما ستتعلمه
- عبارة OVER ونموذج تنفيذ دوال النوافذ
- PARTITION BY للتجزئة و ORDER BY للترتيب
- أنواع الإطارات الثلاثة: ROWS / RANGE / GROUPS
- دوال الترتيب: ROW_NUMBER / RANK / DENSE_RANK / NTILE
- دوال الإزاحة: LAG / LEAD / FIRST_VALUE / LAST_VALUE / NTH_VALUE
- المجاميع التراكمية والمتوسطات المتحركة
- الفرق الأساسي بين دوال النوافذ ودوال ال��جميع
2. القصة
Charlie هو محلل نمو في منصة SaaS. مدير المنتج طلب منه إنتاج تقرير احتفاظ المستخدمين:
- عدد المستخدمين الجدد اليومي
- معدل الاحتفاظ في اليوم 7
- معدل الاحتفاظ في اليوم 30
- رتبة تسجيل كل مستخدم (الشخص رقم N الذي سجل في نفس اليوم)
النهج التقليدي يحتاج عدة عبارات SQL بالإضافة إلى جداول مؤقتة. بعد تعلم دوال النوافذ، يحصل Charlie على جميع الإحصائيات في عبارة SQL واحدة — لا GROUP BY لطي الصفوف؛ كل صف يحتفظ ببياناته الأصلية مع النتيجة المحسوبة.
3. المفهوم: أساسيات دوال النوافذ
(1) ما هي دالة النوافذ
دالة النوافذ تحسب عبر مجموعة من الصفوف المرتبطة ("النافذة") لكنها لا تطوي الصفوف — كل صف يحصل على نتيجة. هذا هو أكبر اختلاف لها عن دوال التجميع.
| الميزة | دالة التجميع | دالة النوافذ |
|---|---|---|
| عدد الصفوف | صفوف متعددة → صف واحد | عدد الصفوف لا يتغير |
| الصيغة | SUM(col) |
SUM(col) OVER (...) |
| GROUP BY | مطلوبة | غير مطلوبة |
| تحتفظ بالأعمدة الأصلية | لا | نعم |
| الاستخدام النموذجي | إحصائيات ملخصة | الترتيب، الإزاحة، المجاميع التراكمية |
(2) هيكل عبارة OVER
function_name() OVER (
[PARTITION BY expr]
[ORDER BY expr [ASC|DESC] [NULLS FIRST|NULLS LAST]]
[frame_clause]
)
flowchart TD
A[OVER] --> B[PARTITION BY]
B --> C[ORDER BY]
C --> D[عبارة الإطار]
D --> E{ROWS / RANGE / GROUPS}
E --> F[ROWS BETWEEN ... AND ...]
E --> G[RANGE BETWEEN ... AND ...]
E --> H[GROUPS BETWEEN ... AND ...]
B -.->|اختياري| C
C -.->|اختياري| D
style A fill:#e1f5fe
style B fill:#fff9c4
style C fill:#fff9c4
style D fill:#c8e6c9
(1) ▶ مثال
SELECT
order_id,
customer_id,
amount,
SUM(amount) OVER () AS total_all
FROM orders;
order_id | customer_id | amount | total_all
----------+-------------+--------+-----------
101 | 1 | 15000 | 750000
102 | 1 | 8000 | 750000
201 | 2 | 25000 | 750000
كل صف يعيد SUM الجدول بالكامل، وعدد الصفوف لم يتغير.
4. المفهوم: PARTITION BY و ORDER BY
(1) PARTITION BY للتجزئة
PARTITION BY تقسم البيانات إلى "أجزاء" مستقلة، ودالة النوافذ تحسب داخل كل جزء بشكل منفصل.
| العبارة | التأثير | التشبيه |
|---|---|---|
PARTITION BY col |
تجزئة حسب العمود | مثل GROUP BY، لكن بدون طي |
| بدون PARTITION BY | الجدول بالكامل جزء واحد | مثل GROUP BY بدون عمود تجميع |
(2) ▶ مثال
SELECT
order_id,
customer_id,
amount,
SUM(amount) OVER (PARTITION BY customer_id) AS customer_total
FROM orders
ORDER BY customer_id, order_id;
order_id | customer_id | amount | customer_total
----------+-------------+--------+----------------
101 | 1 | 15000 | 23000
102 | 1 | 8000 | 23000
201 | 2 | 25000 | 25000
301 | 3 | 12000 | 57000
302 | 3 | 45000 | 57000
(2) ORDER BY للترتيب
ORDER BY تحدد ترتيب الصفوف داخل الجزء وهي أساسية لدوال الترتيب والإزاحة.
| السيناريو | يحتاج ORDER BY؟ | السبب |
|---|---|---|
| ROW_NUMBER / RANK | مطلوبة | الترتيب يعتمد على الترتيب |
| LAG / LEAD | مطلوبة | الصفوف السابقة/اللاحقة تعتمد على الترتيب |
| SUM() OVER (PARTITION BY) | اختيارية | بدون ترتيب، تحسب إجمالي الجزء |
| FIRST_VALUE / LAST_VALUE | مطلوبة | القيمة الأولى/الأخيرة تعتمد على الترتيب |
(3) ▶ مثال
SELECT
order_id,
customer_id,
amount,
ROW_NUMBER() OVER (ORDER BY amount DESC) AS rn_desc
FROM orders;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(3) دمج PARTITION BY + ORDER BY
جزء أولاً، ثم ترتيب؛ الترتيب يحسب بشكل مستقل داخل كل جزء.
(4) ▶ مثال
SELECT
order_id,
customer_id,
amount,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY amount DESC
) AS rank_in_customer
FROM orders;
order_id | customer_id | amount | rank_in_customer
----------+-------------+--------+------------------
102 | 1 | 8000 | 2
101 | 1 | 15000 | 1
201 | 2 | 25000 | 1
302 | 3 | 45000 | 1
301 | 3 | 12000 | 2
5. المفهوم: عبارة الإطار
(1) أنواع الإطارات الثلاثة
عبارة الإطار تقرر أي صفوف "تراها" دالة النوافذ لحسابها.
| نوع الإطار | الحدود مبنية على | الأفضل لـ |
|---|---|---|
| ROWS | إزاحة الصف الفعلية | تحكم دقيق في الصفوف، مثلاً "آخر 3 صفوف" |
| RANGE | إزاحة القيمة المنطقية | الصفوف بنفس القيمة مجمعة، مثلاً "نفس المبلغ" |
| GROUPS | إزاحة مجموعة نفس القيمة | خاصة بـ PostgreSQL؛ تجمع حسب قيمة ORDER BY |
(2) كلمات حدود الإطار المفتاحية
| الكلمة المفتاحية | المعنى |
|---|---|
UNBOUNDED PRECEDING |
أول صف في الجزء |
UNBOUNDED FOLLOWING |
آخر صف في الجزء |
CURRENT ROW |
الصف الحالي |
N PRECEDING |
N صفوف / قيم قبل |
N FOLLOWING |
N صفوف / قيم بعد |
(3) قواعد الإطار الافتراضي
| لديه ORDER BY؟ | الإطار الافتراضي |
|---|---|
| نعم | RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW |
| لا | ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING |
(5) ▶ مثال
SELECT
order_date,
amount,
SUM(amount) OVER (
ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_sum
FROM daily_sales;
order_date | amount | running_sum
-------------+--------+-------------
2025-01-01 | 5000 | 5000
2025-01-02 | 8000 | 13000
2025-01-03 | 3000 | 16000
2025-01-04 | 12000 | 28000
(6) ▶ مثال
SELECT
order_date,
amount,
ROUND(AVG(amount) OVER (
ORDER BY order_date
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
), 2) AS moving_avg_3
FROM daily_sales;
Output:
result
----------
42.50
(1 row)
(7) ▶ مثال
SELECT
order_date,
amount,
SUM(amount) OVER (
ORDER BY order_date
RANGE BETWEEN INTERVAL '7 days' PRECEDING AND CURRENT ROW
) AS sum_last_7_days
FROM daily_sales;
Output:
result
----------
42.50
(1 row)
6. المفهوم: دوال الترتيب
(1) مقارنة دوال الترتيب الأربعة
| الدالة | معالجة نفس القيمة | المخرج | متتالية؟ |
|---|---|---|---|
| ROW_NUMBER | تزايد صارم | 1,2,3,4 | نعم |
| RANK | نفس الرتبة، تتخطى | 1,1,3,4 | لا |
| DENSE_RANK | نفس الرتبة، لا تتخطى | 1,1,2,3 | نعم |
| NTILE(N) | تقسيم إلى N مجموعات | 1,1,2,2,3,3 | — |
(8) ▶ مثال
SELECT
name,
score,
ROW_NUMBER() OVER (ORDER BY score DESC) AS rn,
RANK() OVER (ORDER BY score DESC) AS rnk,
DENSE_RANK() OVER (ORDER BY score DESC) AS drnk
FROM students;
name | score | rn | rnk | drnk
--------+-------+----+-----+------
Alice | 95 | 1 | 1 | 1
Bob | 90 | 2 | 2 | 2
Charlie| 90 | 3 | 2 | 2
Dave | 85 | 4 | 4 | 3
(2) تجميع NTILE
NTILE(N) تقسم الصفوف المرتبة إلى N مجموعات متساوية تقريباً، شائعة الاستخدام لتحليل الرباعيات.
(9) ▶ مثال
SELECT
customer_id,
total_spent,
NTILE(4) OVER (ORDER BY total_spent DESC) AS quartile
FROM customer_summary;
customer_id | total_spent | quartile
-------------+-------------+----------
12 | 500000 | 1
5 | 350000 | 1
8 | 280000 | 2
19 | 150000 | 2
3 | 90000 | 3
22 | 60000 | 3
41 | 25000 | 4
15 | 5000 | 4
7. المفهوم: دوال الإزاحة والقيمة
(1) مرجع دوال الإزاحة
| الدالة | التأثير | الاستخدام النموذجي |
|---|---|---|
| LAG(col, N, default) | قيمة الصف رقم N قبل الحالي | النمو بين الفترات |
| LEAD(col, N, default) | قيمة الصف رقم N بعد الحالي | التنبؤ، المقارنة |
| FIRST_VALUE(col) | القيمة الأولى في النافذة | مبلغ الطلب الأول |
| LAST_VALUE(col) | القيمة الأخيرة في النافذة | مبلغ الطلب الأخير |
| NTH_VALUE(col, N) | القيمة رقم N في النافذة | الطلب رقم N |
(10) ▶ مثال
SELECT
order_date,
daily_revenue,
LAG(daily_revenue, 1) OVER (ORDER BY order_date) AS prev_day,
daily_revenue - LAG(daily_revenue, 1) OVER (ORDER BY order_date) AS diff,
ROUND(
(daily_revenue - LAG(daily_revenue, 1) OVER (ORDER BY order_date))
* 100.0 / NULLIF(LAG(daily_revenue, 1) OVER (ORDER BY order_date), 0),
2) AS pct_change
FROM daily_revenue
ORDER BY order_date;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(11) ▶ مثال
SELECT
month,
revenue,
LEAD(revenue, 1) OVER (ORDER BY month) AS next_month_revenue
FROM monthly_revenue;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(2) ملاحظات على FIRST_VALUE / LAST_VALUE
الإطار الافتراضي لـ LAST_VALUE هو RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW، ليس الجزء بالكامل. يجب تحديد الإطار صراحةً للحصول على آخر صف في الجزء.
| الدالة | الإطار الافتراضي | كيفية الحصول على آخر صف في الجزء |
|---|---|---|
| FIRST_VALUE | حتى CURRENT ROW (صحيح بالمصادفة) | لا تغيير مطلوب |
| LAST_VALUE | حتى CURRENT ROW (ليس آخر صف!) | أضف ROWS BETWEEN ... AND UNBOUNDED FOLLOWING |
(12) ▶ مثال
SELECT
order_id,
customer_id,
amount,
FIRST_VALUE(amount) OVER (
PARTITION BY customer_id ORDER BY order_date
) AS first_order_amount,
LAST_VALUE(amount) OVER (
PARTITION BY customer_id ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS last_order_amount
FROM orders;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(13) ▶ مثال
SELECT
order_id,
customer_id,
amount,
NTH_VALUE(amount, 2) OVER (
PARTITION BY customer_id ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS second_order_amount
FROM orders;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
8. المفهوم: المجموع التراكمي والمتوسط المتحرك
(1) المجموع التراكمي
| الإطار | المعنى | SQL |
|---|---|---|
| الإطار الافتراضي | من بداية الجزء إلى الصف الحالي | SUM() OVER (ORDER BY col) |
| ROWS صريح | نفس ما سبق | ROWS UNBOUNDED PRECEDING |
| RANGE صريح | صفوف نفس القيمة مجمعة | RANGE UNBOUNDED PRECEDING |
(14) ▶ مثال
SELECT
month,
revenue,
SUM(revenue) OVER (ORDER BY month) AS running_revenue
FROM monthly_revenue;
month | revenue | running_revenue
----------+---------+----------------
2025-01 | 500000 | 500000
2025-02 | 620000 | 1120000
2025-03 | 580000 | 1700000
2025-04 | 710000 | 2410000
(2) المتوسط المتحرك
(15) ▶ مثال
SELECT
order_date,
daily_revenue,
ROUND(AVG(daily_revenue) OVER (
ORDER BY order_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
), 2) AS ma_7day
FROM daily_revenue;
Output:
result
----------
42.50
(1 row)
(3) مجموع تراكمي مجزأ
(16) ▶ مثال
SELECT
order_id,
customer_id,
order_date,
amount,
SUM(amount) OVER (
PARTITION BY customer_id
ORDER BY order_date
) AS cumulative_spent
FROM orders
ORDER BY customer_id, order_date;
Output:
result
----------
42.50
(1 row)
9. دوال النوافذ مقابل دوال التجميع
| البعد | دالة التجميع | دالة النوافذ |
|---|---|---|
| عدد الصفوف | مطوية إلى صف واحد | عدد الصفوف الأصلي محفوظ |
| الصيغة | SUM(col) |
SUM(col) OVER(...) |
| GROUP BY | مطلوبة | غير مطلوبة |
| حساب لكل صف | بعد واحد | نوافذ مختلفة متعددة ممكنة |
| الأداء | عادة أسرع | تحتاج فرز + تجزئة، أبطأ قليلاً |
| الأفضل لـ | تقارير ملخصة | الترتيب، الإزاحة، المجاميع التراكمية |
(17) ▶ مثال
نمط دالة التجميع:
SELECT customer_id, SUM(amount) AS total
FROM orders
GROUP BY customer_id;
Output:
result
----------
42.50
(1 row)
نمط دالة النوافذ (يحتفظ بالصفوف الأصلية):
SELECT
order_id,
customer_id,
amount,
SUM(amount) OVER (PARTITION BY customer_id) AS customer_total
FROM orders;
10. مثال شامل
تحليل احتفاظ المستخدمين لـ Charlie — عد المستخدمين الجدد اليوميين ومعدلات احتفاظهم في اليوم 7 / اليوم 30:
WITH first_login AS (
SELECT
user_id,
MIN(login_date) AS first_date,
ROW_NUMBER() OVER (PARTITION BY MIN(login_date) ORDER BY user_id) AS reg_rank
FROM user_logins
GROUP BY user_id
),
daily_new_users AS (
SELECT
first_date AS cohort_date,
COUNT(*) AS new_users
FROM first_login
GROUP BY first_date
),
retention_base AS (
SELECT
fl.first_date AS cohort_date,
fl.user_id,
ul.login_date,
ul.login_date - fl.first_date AS day_offset
FROM first_login fl
JOIN user_logins ul ON fl.user_id = ul.user_id
),
retention_count AS (
SELECT
cohort_date,
day_offset,
COUNT(DISTINCT user_id) AS retained_users
FROM retention_base
WHERE day_offset IN (0, 7, 30)
GROUP BY cohort_date, day_offset
)
SELECT
r.cohort_date,
n.new_users,
MAX(CASE WHEN r.day_offset = 0 THEN r.retained_users END) AS d0,
MAX(CASE WHEN r.day_offset = 7 THEN r.retained_users END) AS d7,
MAX(CASE WHEN r.day_offset = 30 THEN r.retained_users END) AS d30,
ROUND(
MAX(CASE WHEN r.day_offset = 7 THEN r.retained_users END) * 100.0
/ NULLIF(n.new_users, 0), 1
) AS day7_rate,
ROUND(
MAX(CASE WHEN r.day_offset = 30 THEN r.retained_users END) * 100.0
/ NULLIF(n.new_users, 0), 1
) AS day30_rate
FROM retention_count r
JOIN daily_new_users n ON r.cohort_date = n.cohort_date
GROUP BY r.cohort_date, n.new_users
ORDER BY r.cohort_date;
11. ترتيب التنفيذ
موقع دوال النوافذ في ترتيب تنفيذ SQL:
flowchart TD
A[FROM] --> B[WHERE]
B --> C[GROUP BY]
C --> D[HAVING]
D --> E["دوال النوافذ<br/>OVER / PARTITION / ORDER / FRAME"]
E --> F[SELECT]
F --> G[DISTINCT]
G --> H[ORDER BY]
H --> I[LIMIT]
style E fill:#c8e6c9
style D fill:#fff9c4
| الخطوة | العبارة | الوصف |
|---|---|---|
| 1 | FROM | تحديد مصدر البيانات |
| 2 | WHERE | تصفية الصفوف |
| 3 | GROUP BY | تجميع التجميع |
| 4 | HAVING | تصفية المجموعات |
| 5 | دوال النوافذ | حساب عبر النتيجة المصفاة |
| 6 | SELECT | اختيار أعمدة المخرجات |
| 7 | DISTINCT | إزالة التكرار |
| 8 | ORDER BY | الترتيب النهائي |
| 9 | LIMIT | تحديد الصفوف |
❓ أسئلة شائعة
WINDOW w AS (PARTITION BY ...)، ثم اكتب OVER w في دوال متعددة.📖 ملخص
- دوال النوافذ لا تطوي الصفوف؛ كل صف يعيد نتيجة، وعبارة OVER تعرف النافذة
- PARTITION BY تجزئ، ORDER BY ترتب، والإطار يتحكم في نطاق الحساب
- ROW_NUMBER / RANK / DENSE_RANK تتصرف بشكل مختلف؛ اختر بعناية
- LAG / LEAD تصل للصفوف السابقة/اللاحقة؛ انتبه للإطارات الافتراضية لـ FIRST_VALUE / LAST_VALUE
- ROWS تزيح بالصف الفعلي، RANGE بالقيمة المنطقية، GROUPS بالقيمة المجمعة
- المجاميع التراكمية والمتوسطات المتحركة هي حالات استخدام نموذجية لدوال النوافذ
- دوال النوافذ تنفذ بعد WHERE/GROUP BY ولا يمكن استخدامها في WHERE
- عبارة WINDOW تعيد استخدام تعريفات OVER، مما يقلل التكرار
📝 تمارين
-
⭐ استخدم ROW_NUMBER لاستعلام أفضل طلبين بأعلى مبلغ لكل عميل.
-
⭐ استخدم LAG لحساب فرق المبلغ بين الطلبات المتجاورة لكل عميل.
-
⭐⭐ احسب المتوسط المتحرك لـ 7 أيام للإيراد اليومي لكل منطقة.
-
⭐⭐ استخدم DENSE_RANK و NTILE(4) لتقسيم العملاء إلى 4 مستويات حسب إجمالي الإنفاق.
-
⭐⭐⭐ اكتب عبارة SQL واحدة تحسب معدل الاحتفاظ في اليوم 1 / اليوم 7 / اليوم 30 للمستخدمين الجدد اليوميين، باستخدام دوال النوافذ بدلاً من self-joins.