طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 122 · برمجة قواعد البيانات
الدرس 25 · الوحدة 11: تطوير استعلامات محسّنة باستخدام البرمجة الإجرائية

دوال النوافذ: الترتيب والمجاميع المتراكمة

ترتيب ومجاميع متراكمة ومقارنات بالصف السابق، في استعلام واحد بلا حلقات. ⏱ نحو 45 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تشرح الفرق بين دالة النافذة و GROUP BY.
  • ترتّب الصفوف بـ ROW_NUMBER و RANK و DENSE_RANK.
  • تحسب المجموع المتراكم والمتوسط المتحرك وفرق الصف السابق.
  • تستخرج أعلى N في كل مجموعة دون حلقات.

١التجميع دون أن تختفي الصفوف

GROUP BY يدمج الصفوف في صف لكل مجموعة. أما دالة النافذة فتحسب على مجموعة من الصفوف وتُبقي كل صف كما هو، وتضيف إليه الناتج. علامتها OVER (…):

query.sql
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;
النتيجة · 6 صفوف
namecategory_idpriceمتوسط_التصنيفالفرق
حاسب محمول132001876.671323.33
جهاز لوحي114501876.67-426.67
شاشة 27 بوصة19801876.67-896.67
سماعة رأس2210141.6768.33
لوحة مفاتيح2140141.67-1.67
فأرة لاسلكية275141.67-66.67

PARTITION BY category_id: النافذة هي منتجات التصنيف نفسه. كل منتج بقي في صفه، ومعه متوسط تصنيفه.

٢الترتيب: ROW_NUMBER و RANK و DENSE_RANK

query.sql
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;
النتيجة · 8 صفوف
nameordersrow_numberrankdense_rank
سارة3111
خالد2222
نورة2322
فهد2422
ريم1553
عبدالله1653
هند1753
ماجد0884

ترتيب العملاء بعدد طلباتهم. عند التعادل: row_number يعطي أرقامًا مختلفة، و rank يكرر الرقم ثم يقفز (1، 2، 2، 2، 5)، و dense_rank يكرر بلا قفز (1، 2، 2، 2، 3).

أشهر استخدام: أعلى N في كل مجموعة. كانت تحتاج حلقة، وصارت سطرين:

query.sql
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;
النتيجة · 5 صفوف
category_idnameprice
1حاسب محمول3200
2سماعة رأس210
3تعلم SQL85
4أقلام ملونة18
5مكعبات بناء120

أغلى منتج في كل تصنيف. لا تُكتب دالة النافذة في WHERE مباشرة، لذلك نحسبها في استعلام فرعي ثم نصفّي.

٣المتراكم والسابق والمتحرك

مع ORDER BY داخل OVER تصبح النافذة «من البداية حتى الصف الحالي»، فيصير المجموع متراكمًا. وLAG يجلب قيمة الصف السابق:

query.sql
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;
النتيجة · 11 صفًّا
dayrevenueمتراكمعن_اليوم_السابقمتوسط_آخر_3
2025-09-0133503350NULL3350
2025-09-033753725-29751863
2025-09-102153940-1601313
2025-09-12157055101355720
2025-09-201385648-1432641
2025-10-011960760818221223
2025-10-022107818-1750769
2025-10-0532001101829901790
2025-10-0728511303-29151232
2025-10-0924011543-451242
2025-10-1130401458328001188

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:

الخلاصة

تحدٍّ برمجي: أغلى منتجين في كل تصنيف

متوسط

اعرض لكل تصنيف أغلى منتجين، مع ترتيب كل منهما في تصنيفه.

المطلوب:
  • الأعمدة: category (اسم التصنيف) و name و price و pos (1 أو 2).
  • استخدم row_number() OVER (PARTITION BY … ORDER BY price DESC).
  • رتّب بـ category_id ثم pos.
👀 اعرض النتيجة المطلوبة
النتيجة المطلوبة
categorynamepricepos
أجهزةحاسب محمول32001
أجهزةجهاز لوحي14502
ملحقاتسماعة رأس2101
ملحقاتلوحة مفاتيح1402
كتبتعلم SQL851
كتبأساسيات البرمجة602
قرطاسيةأقلام ملونة181
قرطاسيةدفتر ملاحظات122
ألعابمكعبات بناء1201
ألعابلغز ألف قطعة652
✓ التحقق الآلي من حلّك:
  • الكود يعمل بلا أخطاء
  • عشرة صفوف: منتجان لكل تصنيف بالترتيب الصحيح
  • دالة نافذة مقسّمة

💡 تلميح 1
ابدأ من مثال «أغلى منتج في كل تصنيف»، واجعل الشرط pos <= 2.
💡 تلميح 2
أضف ربطًا مع categories لاسم التصنيف.

حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.

solution.sql
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;
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

📝 معلم؟ أرسل ملاحظة للمؤلف عن هذا الدرس