الدوال النافذة
الدوال النافذة
🌍 تشبيه من الحياة الواقعية
تخيّل كشف علامات فصل دراسي:
- GROUP BY — تجميع الفصل بالكامل حسب الجنس، وحساب متوسط الدرجات للذكور والإناث، بحيث يظهر صفان فقط
- دالة نافذة — على كشف علامات كل طالب، يُكتب إضافياً "ترتيبك بين الذكور" و"كم تزيد عن متوسط درجات الذكور"، مع الاحتفاظ بمعلومات كل شخص
الدوال النافذة مثل "عدسة مكبرة" — لا تقلل عدد الصفوف، لكنها تتيح لك رؤية إحصائيات تجميعية في كل صف.
🎯 المفاهيم الأساسية
ما هي الدالة النافذة
تقوم الدوال النافذة بعمليات تجميع أو ترتيب عبر "نافذة" من البيانات بدلاً من تقليل عدد الصفوف. يمكن لكل صف رؤية تفاصيله الخاصة بالإضافة إلى معلومات الملخص ضمن مجموعته.
جملة OVER
OVER() هي جوهر الدوال النافذة — إنها تحدد نطاق "النافذة":
function() OVER (
[PARTITION BY column] -- أي حقل يتم التقسيم (التجميع) بحسبه
[ORDER BY column] -- كيفية الترتيب داخل التقسيم
[ROWS/RANGE frame] -- اختياري: تقييد نطاق الصفوف بشكل أكبر
)
ROW_NUMBER / RANK / DENSE_RANK — دوال الترتيب
-- ROW_NUMBER: تسلسل متزايد بصرامة، بدون تعادل (1, 2, 3, 4)
ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC)
-- RANK: يسمح بالتعادل، مع فجوات (1, 2, 2, 4)
RANK() OVER (PARTITION BY department_id ORDER BY salary DESC)
-- DENSE_RANK: يسمح بالتعادل، بدون فجوات (1, 2, 2, 3)
DENSE_RANK() OVER (PARTITION BY department_id ORDER BY salary DESC)
| الدالة | يسمح بالتعادل | فجوات | نتيجة مثال |
|---|---|---|---|
| ROW_NUMBER | لا | — | 1, 2, 3, 4 |
| RANK | نعم | نعم | 1, 2, 2, 4 |
| DENSE_RANK | نعم | لا | 1, 2, 2, 3 |
NTILE — التقسيم إلى دلاء
تقسيم البيانات بالتساوي إلى N مجموعات:
-- تقسيم الموظفين إلى 4 مستويات حسب الراتب (أرباع)
NTILE(4) OVER (ORDER BY salary DESC)
LAG / LEAD — الوصول إلى الصفوف المجاورة
-- LAG: الوصول إلى الصف السابق
LAG(salary, 1) OVER (PARTITION BY department_id ORDER BY hire_date)
-- LEAD: الوصول إلى الصف التالي
LEAD(salary, 1) OVER (PARTITION BY department_id ORDER BY hire_date)
| الدالة | الاتجاه | حالة الاستخدام |
|---|---|---|
| LAG(col, n) | النظر n صفوف للخلف | حساب التغيير من فترة لأخرى، المقارنة مع السجل السابق |
| LEAD(col, n) | النظر n صفوف للأمام | التنبؤ بالسجل التالي |
SUM / AVG / COUNT OVER — دوال التجميع النافذة
-- كل صف يُظهر إجمالي راتب القسم
SUM(salary) OVER (PARTITION BY department_id)
-- كل صف يُظهر متوسط راتب القسم
AVG(salary) OVER (PARTITION BY department_id)
-- كل صف يُظهر عدد موظفي القسم
COUNT(*) OVER (PARTITION BY department_id)
PARTITION BY و ORDER BY في النافذة
-- PARTITION BY: تحديد نطاق النافذة (مشابه لـ GROUP BY، لكن لا يطيح الصفوف)
-- ORDER BY: الترتيب داخل النافذة، يؤثر على الترتيب والحسابات التراكمية
-- مثال: تجميع حسب القسم، ترتيب حسب الراتب تنازلياً داخل كل قسم
RANK() OVER (PARTITION BY department_id ORDER BY salary DESC)
-- مثال: مجموع تراكمي (مرتب حسب تاريخ التوظيف، تراكم الراتب صف بصف)
SUM(salary) OVER (ORDER BY hire_date)
الدوال النافذة مقابل GROUP BY
| الميزة | GROUP BY | الدالة النافذة |
|---|---|---|
| عدد الصفوف | يقل (يُدمج في صف واحد) | لا يتغير (جميع الصفوف محفوظة) |
| البيانات التفصيلية | تضيع | محفوظة |
| حالة الاستخدام | إحصائيات تجميعية | ترتيب، مقارنة، حسابات تراكمية |
| رؤية التفاصيل والملخص معاً | ❌ لا | ✅ نعم |
GROUP BY عندما تحتاج فقط إلى نتائج تجميعية.
📝 الأساسيات
-- الصيغة العامة للدالة النافذة
function() OVER (
[PARTITION BY col1, col2]
[ORDER BY col3 [ASC|DESC]]
[ROWS BETWEEN ... AND ...]
)
-- الترتيب
ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC)
RANK() OVER (PARTITION BY department_id ORDER BY salary DESC)
DENSE_RANK() OVER (PARTITION BY department_id ORDER BY salary DESC)
-- الصفوف المجاورة
LAG(column, offset, default) OVER (ORDER BY column)
LEAD(column, offset, default) OVER (ORDER BY column)
-- التجميع
SUM(column) OVER (PARTITION BY col)
AVG(column) OVER (PARTITION BY col)
COUNT(*) OVER (PARTITION BY col)
-- التقسيم إلى دلاء
NTILE(n) OVER (ORDER BY column)
- يمكن حذف
PARTITION BY، مما يعني أن النافذة تمتد على مجموعة النتائج بالكامل ORDER BYمطلوب لدوال الترتيب، واختياري للدوال التجميعية- لا يمكن استخدام الدوال النافذة في
WHEREأوHAVING؛ لفها في استعلام فرعي أو CTE
📌 أمثلة
مثال: ترتيب رواتب الموظفين داخل القسم (الصعوبة ⭐⭐)
SELECT
CONCAT(e.first_name, e.last_name) AS name,
d.department_name AS department,
e.salary,
ROW_NUMBER() OVER (PARTITION BY e.department_id ORDER BY e.salary DESC) AS row_num,
RANK() OVER (PARTITION BY e.department_id ORDER BY e.salary DESC) AS rank,
DENSE_RANK() OVER (PARTITION BY e.department_id ORDER BY e.salary DESC) AS dense_rank
FROM employees e
LEFT JOIN departments d ON e.department_id = d.department_id
ORDER BY d.department_name, salary DESC;
النتيجة:
name | department | salary | row_num | rank | dense_rank
-------+------------+----------+---------+------+-----------
Zhang Wei | Tech | 12000.00 | 1 | 1 | 1
Li Na | Tech | 9500.00 | 2 | 2 | 2
Wang Qiang| Sales | 8000.00 | 1 | 1 | 1
Zhao Min | Sales | 11000.00 | 2 | 2 | 2
Liu Yang | Marketing| 7500.00 | 1 | 1 | 1
Chen Jing | NULL | 9000.00 | 1 | 1 | 1
الشرح: الفرق بين دوال الترتيب الثلاثة يظهر بوضوح عند وجود قيم متعادلة. ROW_NUMBER يُعطي أرقاماً تسلسلية صارمة، وRANK يسمح بفجوات بعد التعادل، بينما DENSE_RANK لا يسمح بذلك.
مثال: حساب الفرق عن متوسط راتب القسم (الصعوبة ⭐⭐)
SELECT
CONCAT(e.first_name, e.last_name) AS name,
d.department_name AS department,
e.salary,
ROUND(AVG(e.salary) OVER (PARTITION BY e.department_id), 2) AS dept_avg,
ROUND(e.salary - AVG(e.salary) OVER (PARTITION BY e.department_id), 2) AS diff_from_avg
FROM employees e
LEFT JOIN departments d ON e.department_id = d.department_id
ORDER BY d.department_name, salary DESC;
النتيجة:
name | department | salary | dept_avg | diff_from_avg
-------+------------+----------+----------+--------------
Zhang Wei | Tech | 12000.00 | 10750.00 | 1250.00
Li Na | Tech | 9500.00 | 10750.00 | -1250.00
Zhao Min | Sales | 11000.00 | 9500.00 | 1500.00
Wang Qiang| Sales | 8000.00 | 9500.00 | -1500.00
Liu Yang | Marketing| 7500.00| 7500.00 | 0.00
Chen Jing | NULL | 9000.00 | 9000.00 | 0.00
الشرح: AVG() OVER() يحسب متوسط القسم في كل صف دون طي الصفوف، بحيث يمكن لكل موظف رؤية الفرق بين راتبه والمتوسط مباشرة.
مثال: LAG لنمو الطلبات من فترة لأخرى (الصعوبة ⭐⭐⭐)
SELECT
o.order_id,
o.order_date,
o.total_amount,
LAG(o.total_amount, 1) OVER (ORDER BY o.order_date) AS prev_order_amount,
ROUND(
(o.total_amount - LAG(o.total_amount, 1) OVER (ORDER BY o.order_date))
/ LAG(o.total_amount, 1) OVER (ORDER BY o.order_date) * 100,
2) AS growth_pct
FROM orders o
WHERE o.status != 'cancelled'
ORDER BY o.order_date;
النتيجة:
order_id | order_date | total_amount | prev_order_amount | growth_pct
---------+------------+--------------+-------------------+-----------
1001 | 2026-01-10 | 2500.00 | NULL | NULL
1002 | 2026-02-15 | 1800.00 | 2500.00 | -28.00
1003 | 2026-02-20 | 3200.00 | 1800.00 | 77.78
1004 | 2026-03-05 | 950.00 | 3200.00 | -70.31
1005 | 2026-03-10 | 4100.00 | 950.00 | 331.58
...
الشرح: LAG() تصل إلى قيمة "الصف السابق"، مما يجعلها مثالية لتحليل الاتجاهات مثل المقارنة من فترة لأخرى ومن سنة لأخرى. الصف الأول ليس لديه صف سابق، لذا يُرجع NULL.
🎬 تدريب على السيناريوهات
السيناريو 1: إيجاد أفضل موظف مبيعات في كل قسم
إيجاد الموظف בעל أعلى مبيعات في كل قسم.
WITH emp_sales AS (
SELECT
CONCAT(e.first_name, e.last_name) AS name,
d.department_name AS department,
COALESCE(SUM(o.total_amount), 0) AS total_sales,
RANK() OVER (PARTITION BY d.department_id ORDER BY COALESCE(SUM(o.total_amount), 0) DESC) AS dept_rank
FROM employees e
LEFT JOIN departments d ON e.department_id = d.department_id
LEFT JOIN orders o ON e.employee_id = o.employee_id AND o.status != 'cancelled'
GROUP BY e.employee_id, e.first_name, e.last_name, d.department_id, d.department_name
)
SELECT name, department, total_sales, dept_rank
FROM emp_sales
WHERE dept_rank = 1
ORDER BY total_sales DESC;
النقطة الأساسية: أولاً استخدم CTE + RANK() للترتيب، ثم استخدم WHERE لتصفية الترتيب الأعلى. لا يمكن استخدام الدوال النافذة مباشرة في WHERE؛ يجب لفها في استعلام فرعي أو CTE.
السيناريو 2: تتبع ترتيب توظيف الموظفين وتغييرات الرواتب
SELECT
CONCAT(e.first_name, e.last_name) AS name,
d.department_name AS department,
e.hire_date,
e.salary,
LAG(e.salary, 1) OVER (PARTITION BY e.department_id ORDER BY e.hire_date) AS prev_salary,
ROUND(e.salary - LAG(e.salary, 1) OVER (PARTITION BY e.department_id ORDER BY e.hire_date), 2) AS salary_change,
ROW_NUMBER() OVER (PARTITION BY e.department_id ORDER BY e.hire_date) AS hire_order
FROM employees e
LEFT JOIN departments d ON e.department_id = d.department_id
ORDER BY d.department_name, e.hire_date;
النقطة الأساسية: LAG() + PARTITION BY يمكنها تتبع التغييرات التسلسلية داخل مجموعة، مما يجعلها مناسبة لتحليل الاتجاهات.
❓ أسئلة شائعة
س: هل يمكن استخدام الدوال النافذة في جملة WHERE؟ ج: لا.
WHEREيُنفَّذ قبل الدوال النافذة، لذا لم يتم حساب الدالة النافذة بعد. تحتاج أولاً إلى حساب الدالة النافذة باستخدام استعلام فرعي أو CTE، ثم التصفية بـWHEREفي الاستعلام الخارجي.
س: متى أستخدم ROW_NUMBER مقابل RANK؟ ج: استخدم
ROW_NUMBERعندما تحتاج إلى "ترقيم فريد، بدون تعادل" (مثل: التصفح، إزالة التكرار)؛ استخدمRANKعندما تحتاج إلى "ترتيب حسب القيمة، مع السماح بالتعادل" (مثل: ترتيب الدرجات، ترتيب المبيعات).
س: ما الفرق بين PARTITION BY و GROUP BY؟ ج:
GROUP BYيدمج عدة صفوف في صف واحد (يقلل عدد الصفوف)؛PARTITION BYفقط يحدد نطاق النافذة دون تقليل الصفوف (كل صف محفوظ). يمكن استخدامهما معاً: أولاً التجميع بـGROUP BY، ثم استخدام الدوال النافذة لإضافة الترتيب.
س: ما أداء الدوال النافذة؟ ج: عادةً ما يكون أداء الدوال النافذة أفضل من استعلامات الربط الذاتي الفرعية. لمجموعات البيانات الكبيرة، أنشئ فهارس على الأعمدة المستخدمة في
PARTITION BYوORDER BY.
📖 ملخص
| الدالة | حالة الاستخدام | مثال |
|---|---|---|
| ROW_NUMBER | رقم تسلسلي فريد | التصفح، إزالة التكرار |
| RANK | ترتيب مع تعادل (فجوات) | ترتيب الدرجات |
| DENSE_RANK | ترتيب مع تعادل (بدون فجوات) | تصنيف المستويات |
| NTILE | تقسيم متساوي | تحليل النسب المئوية |
| LAG | الوصول إلى الصف السابق | حساب التغيير من فترة لأخرى |
| LEAD | الوصول إلى الصف التالي | التنبؤ بالاتجاهات |
| SUM/AVG/COUNT OVER | تجميع تراكمي/جماعي | تحليل مقارن |
OVER()يحدد نطاق النافذة، وPARTITION BYيقسم، وORDER BYيرتب- الدوال النافذة لا تقلل عدد الصفوف — تُضيف معلومات تجميعية مع الاحتفاظ بالتفاصيل
- لا يمكن وضع الدوال النافذة في
WHERE؛ لفها في استعلام فرعي أو CTE
📝 تمارين
- استخدم
ROW_NUMBER()لترقيم الموظفين حسب تاريخ التوظيف وإيجاد آخر موظف تم توظيفه. - استخدم
RANK()لترتيب الموظفين حسب الراتب داخل كل قسم وإيجاد أعلى موظفين راتباً في كل قسم. - استخدم
LAG()لحساب تغيير الراتب للموظفين داخل كل قسم، مرتبين حسب تاريخ التوظيف. - استخدم
SUM() OVER()لحساب المجموع التراكمي للمبيعات لكل طلب (مرتب حسب تاريخ الطلب).
الدرس التالي
👉 20-cte-temp-table - الجداول المؤقتة وCTE: تعلّم كيفية استخدام جمل WITH (CTE)، بما في ذلك CTE التكرارية، وCTE مقابل الاستعلامات الفرعية، وإنشاء واستخدامات الجداول المؤقتة.



