دوال النوافذ: الترتيب والمجاميع المتراكمة
ترتيب ومجاميع متراكمة ومقارنات بالصف السابق، في استعلام واحد بلا حلقات. ⏱ نحو 45 دقيقة.
- تشرح الفرق بين دالة النافذة و
GROUP BY. - ترتّب الصفوف بـ
ROW_NUMBERوRANKوDENSE_RANK. - تحسب المجموع المتراكم والمتوسط المتحرك وفرق الصف السابق.
- تستخرج أعلى
Nفي كل مجموعة دون حلقات.
١التجميع دون أن تختفي الصفوف
GROUP BY يدمج الصفوف في صف لكل مجموعة. أما دالة النافذة فتحسب على مجموعة من الصفوف وتُبقي كل صف كما هو، وتضيف إليه الناتج. علامتها OVER (…):
SELECT name, category_id, price, round(avg(price) OVER (PARTITION BY category_id), 2) AS متوسط_التصنيف, price - round(avg(price) OVER (PARTITION BY category_id), 2) AS الفرقFROM productsWHERE category_id IN (1, 2)ORDER BY category_id, price DESC;
| name | category_id | price | متوسط_التصنيف | الفرق |
|---|---|---|---|---|
| حاسب محمول | 1 | 3200 | 1876.67 | 1323.33 |
| جهاز لوحي | 1 | 1450 | 1876.67 | -426.67 |
| شاشة 27 بوصة | 1 | 980 | 1876.67 | -896.67 |
| سماعة رأس | 2 | 210 | 141.67 | 68.33 |
| لوحة مفاتيح | 2 | 140 | 141.67 | -1.67 |
| فأرة لاسلكية | 2 | 75 | 141.67 | -66.67 |
PARTITION BY category_id: النافذة هي منتجات التصنيف نفسه. كل منتج بقي في صفه، ومعه متوسط تصنيفه.
٢الترتيب: ROW_NUMBER و RANK و DENSE_RANK
SELECT c.name, count(o.id) AS orders, row_number() OVER (ORDER BY count(o.id) DESC, c.id) AS row_number, rank() OVER (ORDER BY count(o.id) DESC) AS rank, dense_rank() OVER (ORDER BY count(o.id) DESC) AS dense_rankFROM customers c LEFT JOIN orders o ON o.customer_id = c.idGROUP BY c.id, c.nameORDER BY row_number;
| name | orders | row_number | rank | dense_rank |
|---|---|---|---|---|
| سارة | 3 | 1 | 1 | 1 |
| خالد | 2 | 2 | 2 | 2 |
| نورة | 2 | 3 | 2 | 2 |
| فهد | 2 | 4 | 2 | 2 |
| ريم | 1 | 5 | 5 | 3 |
| عبدالله | 1 | 6 | 5 | 3 |
| هند | 1 | 7 | 5 | 3 |
| ماجد | 0 | 8 | 8 | 4 |
ترتيب العملاء بعدد طلباتهم. عند التعادل: row_number يعطي أرقامًا مختلفة، و rank يكرر الرقم ثم يقفز (1، 2، 2، 2، 5)، و dense_rank يكرر بلا قفز (1، 2، 2، 2، 3).
أشهر استخدام: أعلى N في كل مجموعة. كانت تحتاج حلقة، وصارت سطرين:
SELECT category_id, name, price FROM ( SELECT category_id, name, price, row_number() OVER (PARTITION BY category_id ORDER BY price DESC) AS rn FROM products) tWHERE rn = 1ORDER BY category_id;
| category_id | name | price |
|---|---|---|
| 1 | حاسب محمول | 3200 |
| 2 | سماعة رأس | 210 |
| 3 | تعلم SQL | 85 |
| 4 | أقلام ملونة | 18 |
| 5 | مكعبات بناء | 120 |
أغلى منتج في كل تصنيف. لا تُكتب دالة النافذة في WHERE مباشرة، لذلك نحسبها في استعلام فرعي ثم نصفّي.
٣المتراكم والسابق والمتحرك
مع ORDER BY داخل OVER تصبح النافذة «من البداية حتى الصف الحالي»، فيصير المجموع متراكمًا. وLAG يجلب قيمة الصف السابق:
WITH daily AS ( SELECT o.order_date AS day, sum(i.qty * i.unit_price) AS revenue FROM orders o JOIN order_items i ON i.order_id = o.id WHERE o.status <> 'cancelled' GROUP BY o.order_date)SELECT day, revenue, sum(revenue) OVER (ORDER BY day) AS متراكم, revenue - lag(revenue) OVER (ORDER BY day) AS عن_اليوم_السابق, round(avg(revenue) OVER (ORDER BY day ROWS BETWEEN 2 PRECEDING AND CURRENT ROW), 0) AS متوسط_آخر_3FROM daily ORDER BY day;
| day | revenue | متراكم | عن_اليوم_السابق | متوسط_آخر_3 |
|---|---|---|---|---|
| 2025-09-01 | 3350 | 3350 | NULL | 3350 |
| 2025-09-03 | 375 | 3725 | -2975 | 1863 |
| 2025-09-10 | 215 | 3940 | -160 | 1313 |
| 2025-09-12 | 1570 | 5510 | 1355 | 720 |
| 2025-09-20 | 138 | 5648 | -1432 | 641 |
| 2025-10-01 | 1960 | 7608 | 1822 | 1223 |
| 2025-10-02 | 210 | 7818 | -1750 | 769 |
| 2025-10-05 | 3200 | 11018 | 2990 | 1790 |
| 2025-10-07 | 285 | 11303 | -2915 | 1232 |
| 2025-10-09 | 240 | 11543 | -45 | 1242 |
| 2025-10-11 | 3040 | 14583 | 2800 | 1188 |
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW إطار من 3 صفوف: الحالي واثنان قبله، أي متوسط متحرك.
| الدالة | تعطي |
|---|---|
row_number()، rank()، dense_rank() | رقم الترتيب |
lag(col) / lead(col) | قيمة الصف السابق / التالي |
first_value(col) | أول قيمة في النافذة |
ntile(4) | تقسيم الصفوف إلى 4 مجموعات متساوية (أرباع) |
sum / avg / count … OVER | تجميع على النافذة |
توقّع وتحقّق
الفرق بين GROUP BY ودالة النافذة:
عند التعادل، أي دالة تكرر الرقم بلا قفز؟
قيمة الصف السابق:
لكل مجموعة نافذة مستقلة نكتب داخل OVER:
الخلاصة
- دالة النافذة:
f() OVER(PARTITION BY…ORDER BY…) وتُبقي الصفوف. row_numberفريد،rankيقفز بعد التعادل،dense_rankلا يقفز.sum…OVER(ORDER BY…) متراكم، وlag/leadللسابق والتالي، وROWS BETWEENللإطار.- أعلى
Nلكل مجموعة:row_numberفي استعلام فرعي ثمWHERE.
تحدٍّ برمجي: أغلى منتجين في كل تصنيف
متوسطاعرض لكل تصنيف أغلى منتجين، مع ترتيب كل منهما في تصنيفه.
- الأعمدة:
category(اسم التصنيف) وnameوpriceوpos(1 أو 2). - استخدم
row_number() OVER (PARTITION BY … ORDER BY price DESC). - رتّب بـ category_id ثم pos.
👀 اعرض النتيجة المطلوبة
| category | name | price | pos |
|---|---|---|---|
| أجهزة | حاسب محمول | 3200 | 1 |
| أجهزة | جهاز لوحي | 1450 | 2 |
| ملحقات | سماعة رأس | 210 | 1 |
| ملحقات | لوحة مفاتيح | 140 | 2 |
| كتب | تعلم SQL | 85 | 1 |
| كتب | أساسيات البرمجة | 60 | 2 |
| قرطاسية | أقلام ملونة | 18 | 1 |
| قرطاسية | دفتر ملاحظات | 12 | 2 |
| ألعاب | مكعبات بناء | 120 | 1 |
| ألعاب | لغز ألف قطعة | 65 | 2 |
- الكود يعمل بلا أخطاء
- عشرة صفوف: منتجان لكل تصنيف بالترتيب الصحيح
- دالة نافذة مقسّمة
💡 تلميح 1
pos <= 2.💡 تلميح 2
حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
SELECT category, name, price, pos FROM ( SELECT c.id AS cid, c.name AS category, p.name, p.price, row_number() OVER (PARTITION BY p.category_id ORDER BY p.price DESC) AS pos FROM products p JOIN categories c ON c.id = p.category_id) tWHERE pos <= 2ORDER BY cid, pos;