SQL Window Functions: شغل تحليلي بسطر واحد بدل Subquery متشعّب
لو عندك جدول مبيعات وعايز تحسب الإجمالي التراكمي لكل يوم، أو ترتيب أعلى 3 منتجات داخل كل تصنيف، Self Join أو Correlated Subquery هيشتغلوا — بس هيبطّؤوا الاستعلام بشكل ملحوظ على ملايين الصفوف. الـ Window Functions بتحل نفس المشكلة بسطر واحد وبخطة تنفيذ أنضف.
المشكلة باختصار
الـ GROUP BY بيطوي الصفوف في صف واحد لكل مجموعة. لكن أحيانًا محتاج الصف الأصلي يفضل موجود ومعاه قيمة محسوبة على مجموعة حواليه — زي "إجمالي مبيعات هذا الشهر لحد النهارده" أو "ترتيب هذا المنتج داخل تصنيفه". ده بالظبط اللي الـ Window Functions اتعملت عشانه.
يعني إيه Window Function بالظبط؟
دالة تجميع (أو ترتيب) بتتنفذ على "نافذة" من الصفوف المرتبطة بالصف الحالي، بدون ما تدمج الصفوف. الصيغة العامة:
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) فيه مبيعات يوم بيوم. عايز نعرض لكل يوم: المبيعات، والإجمالي التراكمي من أول الشهر.
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:
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، الصفوف كلها راجعة.
ROW_NUMBER ضد RANK ضد DENSE_RANK
الثلاثة بيرتّبوا الصفوف داخل النافذة، بس كل واحد بيتعامل مع التعادل بطريقة مختلفة:
- ROW_NUMBER(): بيدي رقم فريد لكل صف حتى لو القيمة متساوية. استخدمه لما محتاج صف واحد بالظبط لكل مجموعة.
- RANK(): بيدي نفس الرقم للمتعادلين، وبيقفز في الأرقام بعدهم (1, 1, 3).
- DENSE_RANK(): نفس الرقم للمتعادلين، لكن بدون قفز (1, 1, 2).
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 بيرجع قيمة الصف السابق ضمن نفس الترتيب:
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