الدوال النافذة

الدوال النافذة

🌍 تشبيه من الحياة الواقعية

تخيّل كشف علامات فصل دراسي:

الدوال النافذة مثل "عدسة مكبرة" — لا تقلل عدد الصفوف، لكنها تتيح لك رؤية إحصائيات تجميعية في كل صف.


🎯 المفاهيم الأساسية

ما هي الدالة النافذة

تقوم الدوال النافذة بعمليات تجميع أو ترتيب عبر "نافذة" من البيانات بدلاً من تقليل عدد الصفوف. يمكن لكل صف رؤية تفاصيله الخاصة بالإضافة إلى معلومات الملخص ضمن مجموعته.

جملة OVER

OVER() هي جوهر الدوال النافذة — إنها تحدد نطاق "النافذة":

SQL
function() OVER (
    [PARTITION BY column]    -- أي حقل يتم التقسيم (التجميع) بحسبه
    [ORDER BY column]        -- كيفية الترتيب داخل التقسيم
    [ROWS/RANGE frame]       -- اختياري: تقييد نطاق الصفوف بشكل أكبر
)

ROW_NUMBER / RANK / DENSE_RANK — دوال الترتيب

SQL
-- 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 مجموعات:

SQL
-- تقسيم الموظفين إلى 4 مستويات حسب الراتب (أرباع)
NTILE(4) OVER (ORDER BY salary DESC)

LAG / LEAD — الوصول إلى الصفوف المجاورة

SQL
-- 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 — دوال التجميع النافذة

SQL
-- كل صف يُظهر إجمالي راتب القسم
SUM(salary) OVER (PARTITION BY department_id)

-- كل صف يُظهر متوسط راتب القسم
AVG(salary) OVER (PARTITION BY department_id)

-- كل صف يُظهر عدد موظفي القسم
COUNT(*) OVER (PARTITION BY department_id)

PARTITION BY و ORDER BY في النافذة

SQL
-- 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 عندما تحتاج فقط إلى نتائج تجميعية.


📝 الأساسيات

SQL
-- الصيغة العامة للدالة النافذة
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

📌 أمثلة

مثال: ترتيب رواتب الموظفين داخل القسم (الصعوبة ⭐⭐)

SQL
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;
▶ جرّب الكود

النتيجة:

TEXT
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 لا يسمح بذلك.

مثال: حساب الفرق عن متوسط راتب القسم (الصعوبة ⭐⭐)

SQL
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;
▶ جرّب الكود

النتيجة:

TEXT
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 لنمو الطلبات من فترة لأخرى (الصعوبة ⭐⭐⭐)

SQL
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;
▶ جرّب الكود

النتيجة:

TEXT
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: إيجاد أفضل موظف مبيعات في كل قسم

إيجاد الموظف בעל أعلى مبيعات في كل قسم.

SQL
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: تتبع ترتيب توظيف الموظفين وتغييرات الرواتب

SQL
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 تجميع تراكمي/جماعي تحليل مقارن

📝 تمارين

  1. استخدم ROW_NUMBER() لترقيم الموظفين حسب تاريخ التوظيف وإيجاد آخر موظف تم توظيفه.
  2. استخدم RANK() لترتيب الموظفين حسب الراتب داخل كل قسم وإيجاد أعلى موظفين راتباً في كل قسم.
  3. استخدم LAG() لحساب تغيير الراتب للموظفين داخل كل قسم، مرتبين حسب تاريخ التوظيف.
  4. استخدم SUM() OVER() لحساب المجموع التراكمي للمبيعات لكل طلب (مرتب حسب تاريخ الطلب).

الدرس التالي

👉 20-cte-temp-table - الجداول المؤقتة وCTE: تعلّم كيفية استخدام جمل WITH (CTE)، بما في ذلك CTE التكرارية، وCTE مقابل الاستعلامات الفرعية، وإنشاء واستخدامات الجداول المؤقتة.

Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%