أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

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

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول

SQL Window Functions: احسب Running Total وRanking بدون Self Join

📅 ١٩ أبريل ٢٠٢٦⏱ 4 دقائق قراءة
SQL Window Functions: احسب Running Total وRanking بدون Self Join

SQL Window Functions: شغل تحليلي بسطر واحد بدل Subquery متشعّب

لو عندك جدول مبيعات وعايز تحسب الإجمالي التراكمي لكل يوم، أو ترتيب أعلى 3 منتجات داخل كل تصنيف، Self Join أو Correlated Subquery هيشتغلوا — بس هيبطّؤوا الاستعلام بشكل ملحوظ على ملايين الصفوف. الـ Window Functions بتحل نفس المشكلة بسطر واحد وبخطة تنفيذ أنضف.

المشكلة باختصار

الـ GROUP BY بيطوي الصفوف في صف واحد لكل مجموعة. لكن أحيانًا محتاج الصف الأصلي يفضل موجود ومعاه قيمة محسوبة على مجموعة حواليه — زي "إجمالي مبيعات هذا الشهر لحد النهارده" أو "ترتيب هذا المنتج داخل تصنيفه". ده بالظبط اللي الـ Window Functions اتعملت عشانه.

شاشة بها أكواد استعلامات SQL وأرقام تحليلات على قاعدة بيانات

يعني إيه Window Function بالظبط؟

دالة تجميع (أو ترتيب) بتتنفذ على "نافذة" من الصفوف المرتبطة بالصف الحالي، بدون ما تدمج الصفوف. الصيغة العامة:

SQL
FUNCTION(column) OVER (
  PARTITION BY group_column
  ORDER BY sort_column
  ROWS BETWEEN ... AND ...
)

الكلمة المفتاحية هنا OVER. هي اللي بتفرق بين SUM(amount) العادي اللي بيحتاج GROUP BY، وبين SUM(amount) OVER (...) اللي بيسيب كل صف مكانه.

مثال عملي: Running Total لمبيعات يومية

افترض جدول sales(sale_date, amount) فيه مبيعات يوم بيوم. عايز نعرض لكل يوم: المبيعات، والإجمالي التراكمي من أول الشهر.

SQL
SELECT
  sale_date,
  amount,
  SUM(amount) OVER (
    ORDER BY sale_date
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS running_total
FROM sales
WHERE sale_date >= DATE_TRUNC('month', CURRENT_DATE)
ORDER BY sale_date;

بدون Window Function كان لازم Self Join على نفس الجدول بشرط s2.sale_date <= s1.sale_date، وده بتعقيد O(n²) تقريبًا. على جدول فيه 500 ألف صف، Self Join ممكن ياخد 8–12 ثانية؛ الاستعلام فوق بيخلص في أقل من ثانية على نفس البيانات في PostgreSQL 16 مع فهرس على sale_date (قياس تقريبي على جهاز تطوير 8GB RAM).

PARTITION BY: قسّم الحساب حسب مجموعة

لو عايز الإجمالي التراكمي لكل عميل على حدة، ضيف PARTITION BY:

SQL
SELECT
  customer_id,
  sale_date,
  amount,
  SUM(amount) OVER (
    PARTITION BY customer_id
    ORDER BY sale_date
  ) AS customer_running_total
FROM sales
ORDER BY customer_id, sale_date;

الـ PARTITION BY بيعمل "إعادة تصفير" للحساب عند كل قيمة جديدة من customer_id. ركز: ده مش GROUP BY، الصفوف كلها راجعة.

محرر كود يعرض استعلام SQL متعدد الأسطر مع دوال تجميع

ROW_NUMBER ضد RANK ضد DENSE_RANK

الثلاثة بيرتّبوا الصفوف داخل النافذة، بس كل واحد بيتعامل مع التعادل بطريقة مختلفة:

  • ROW_NUMBER(): بيدي رقم فريد لكل صف حتى لو القيمة متساوية. استخدمه لما محتاج صف واحد بالظبط لكل مجموعة.
  • RANK(): بيدي نفس الرقم للمتعادلين، وبيقفز في الأرقام بعدهم (1, 1, 3).
  • DENSE_RANK(): نفس الرقم للمتعادلين، لكن بدون قفز (1, 1, 2).
SQL
SELECT
  category_id,
  product_id,
  revenue,
  ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY revenue DESC) AS rn,
  RANK()       OVER (PARTITION BY category_id ORDER BY revenue DESC) AS rnk,
  DENSE_RANK() OVER (PARTITION BY category_id ORDER BY revenue DESC) AS dense
FROM product_stats;

أشهر استخدام: أعلى 3 منتجات في كل تصنيف. لفّ الاستعلام في CTE وشرطه WHERE rn <= 3.

LAG وLEAD: قارن الصف الحالي بالصف السابق

عايز تحسب نسبة التغيّر الشهرية؟ LAG بيرجع قيمة الصف السابق ضمن نفس الترتيب:

SQL
SELECT
  month,
  revenue,
  LAG(revenue, 1) OVER (ORDER BY month) AS prev_month,
  ROUND(
    (revenue - LAG(revenue, 1) OVER (ORDER BY month))
    / NULLIF(LAG(revenue, 1) OVER (ORDER BY month), 0) * 100,
    2
  ) AS growth_pct
FROM monthly_revenue;

لاحظ NULLIF علشان تتفادى القسمة على صفر لو الشهر الأول قيمته صفر.

الأداء وtrade-offs

الـ Window Functions بتستفيد من Index على أعمدة PARTITION BY وORDER BY. على PostgreSQL، الـ planner بيعمل Sort مرة واحدة ويعيد استخدامها لكل الصفوف، بدل Scan متكرر زي الـ Correlated Subquery.

بتكسب: أداء O(n log n) بدل O(n²)، وقابلية قراءة أعلى بكتير. بتخسر: استهلاك ذاكرة أكبر لو النافذة كبيرة بدون فهرس مناسب، واستعلامات معقدة ممكن تبقى صعبة الـ debug لو كبرت. الافتراض هنا إنك على PostgreSQL 11+ أو MySQL 8+ أو SQL Server 2012+.

متى لا تستخدم Window Functions

لو المشكلة تجميعية بحتة (مجموع المبيعات لكل شهر فقط بدون ما تحتاج الصف الأصلي)، الـ GROUP BY العادي أبسط وأسرع. كمان على MySQL 5.7 أو أقدم — مش مدعومة أصلاً، هتحتاج ترقية أو حل مختلف. ولو بتشتغل على جدول صغير (أقل من 10 آلاف صف) وسرعة الاستعلام مش مشكلة، الفرق مش هيبان والقراءة هي اللي تحدد.

الخطوة التالية

افتح الاستعلام الأثقل عندك اللي فيه Self Join لغرض تحليلي، وحاول تستبدله بـ Window Function. شغّل EXPLAIN ANALYZE قبل وبعد، وقارن الـ total execution time. لو اللي اتحسن بنسبة 50% أو أكتر، روّح الباقي من الاستعلامات اللي على نفس النمط.

مصادر

  • PostgreSQL Documentation — Window Functions: postgresql.org/docs/current/tutorial-window.html
  • PostgreSQL — Window Function Calls Syntax: postgresql.org/docs/current/sql-expressions.html
  • MySQL 8.0 Reference Manual — Window Functions: dev.mysql.com/doc/refman/8.0/en/window-functions.html
  • Microsoft Learn — OVER Clause (Transact-SQL): learn.microsoft.com/en-us/sql/t-sql/queries/select-over-clause-transact-sql
  • Use The Index, Luke — Paging with Window Functions: use-the-index-luke.com/sql/partial-results/window-functions

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة