في هذه الصفحة
المقدمة
المقال ده هو الجزء الأول من سلسلة مقالات هنتكلم فيها عن sql window functions ، في المقال ده هنشرح ازاي الـ window functions بتشتغل وايه الفرق بينها و بين الـ aggregate functions وهنتكلم عن 3 window functions مشهورين وهم: RANK, DESNE_RANK, ROW_NUMBER، و هنشوف خلال المقال امتى الـ functions دي ممكن تكون مفيدة وايه المشاكل اللي بتحلها.
عشان تقرأ المقال وتستفيد منه محتاج تكون عارف الـ aggregate functions و group by، دي مصادر بالعربي للمواضيع دي:
- 29 - | MS SQL Server For Beginners | - | Aggregate Functions | - |Summary Functions|
- 30 - | MS SQL Server For Beginners | - | Group by .. Having|
ما هي ال Window Function ؟
عشان نعرف يعني ايه window function هنبدأ بمثال، في المثال ده عندنا جدول فيه بيانات موظفين في شركة
واتطلب مننا نحسب متوسط المرتبات لكل قسم، حاجة زي كدة نقدر نعملها عن طريقة إننا نستخدم AVG ونعمل group by الـ departmentID، وده الكود اللي هينفذ المطلوب:
SELECT departmentId, AVG(salary) AS average_salary
FROM employee
GROUP BY departmentId
ودي النتيجة اللي هتطلع:
ولكن لو اللي طلب مننا نحسب متوسط المرتبات لكل قسم لما شاف النتيجة دي قال إنه مش عايز الشكل ده اللي هو الـ ID بتاع كل قسم وجنبه متوسط المرتبات فيه ولكن هو عايز يبقى قدامه بيانات الموظفين كلها تكون ظاهرة هي ومتوسط المرتبات لكل قسم الاتنين في نفس الجدول بمعنى إني عايز قدام كل موظف متوسط المرتبات في القسم اللي هو شغال فيه، باختصار عايز النتيجة تكون بالشكل ده:
شكل زي ده صعب نوصله باستخدام AVG مع group by، لان AVG دي aggregate function ومعنى كدة إنها بتعمل عملية معينة اللي هي المتوسط على مجموعة صفوف وترجع النتيجة في صف واحد بس، ولما أستخدم group by ده هيخلي عندي صف واحد بس لكل قسم، ففي المثال ده عندي 3 أقسام فالنتيجة ظاهر فيها 3 صفوف بس، فالسؤال هنا: ازاي أحسب متوسط المرتبات لكل قسم و في نفس الوقت النتيجة يبقى فيها نفس عدد صفوف الجدول الأصلي ويبقى عندي قدام كل موظف متوسط المرتبات في القسم اللي هو شغال فيه؟ دي الحاجة اللي الـ window functions بتعملها لنا.
الـ window function هي function بتعمل عملية معينة على مجموعة صفوف في الجدول من غير ما البيانات اللي في الجدول الأصلي تروح مني زي ما بيحصل لما أستخدم aggregate function.
الشكل ده بيوضح ازاي الـ window function والـ aggregate function مختلفين عن بعض:
بعد ما فهمنا الفكرة الأساسية للـ window functions هنعرف دلوقتي ازاي نكتبهم في SQL. الخطوة الأولى هتكون إننا هنعرض كل الأعمدة اللي في الجدول:
SELECT *
FROM employee
ومع كل الأعمدة هنعرض متوسط المرتبات بس المرة دي هنستخدم AVG كـ window function، عشان أقول إن الـ function دي window function بستخدم كلمة OVER:
SELECT *, AVG(salary) OVER() AS average_salary
FROM employee
لو عملنا run للكود ده هيظهر لنا العمود الزيادة فيه متوسط المرتبات بس للشركة كلها مش متوسط المرتبات لكل قسم زي ما إحنا عايزين، عشان أقول إني عايز متوسط المرتبات ده يكون لكل قسم بستخدم كلمة PARTITION BY جوا OVER وبحدد العمود اللي عايز أقسّم الصفوف بناءاََ عليه، بما إني عايز متوسط المرتبات لكل قسم هقول PARTITION BY departmentID:
SELECT *, AVG(salary) OVER(PARTITION BY departmentId) AS average_salary
FROM employee
ممكن نفكر في PARTITION BY دي إنها زي GROUP BY بس بنستخدمها مع الـ window functions بس وإني لو استخدمت OVER من غير PARTITION BY هيطلعلي قيمة واحدة بس متكررة لكل صف بس لو قولت PARTITION BY departmentID هيطلعلي قيمة لكل قسم والقيمة دي هتبقى هتظهر مع كل موظف على حسب القسم اللي هو شغال فيه.
بعد ما نعمل run للكود اللي فوق هتظهر لنا النتيجة اللي كانت مطلوبة وهي بيانات كل موظف و قدام كل موظف متوسط المرتبات في القسم اللي هو شغال فيه:
ميزة الجدول ده هو إننا نقدر نشوف بيانات الموظف كلها وكمان متوسط المرتبات للقسم اللي هو شغال فيه وبالتالي فيه تفاصيل أكتر من إن يبقى بس قدامي الـ ID بتاع القسم ومتوسط المرتبات فيه، مثلاََ في الجدول ده أقدر بسهولة أشوف الموظف ده مرتبه أعلى ولا أقل من المتوسط
من مميزات الـ window function كمان إننا نقدر نحسب أكتر من حاجة في نفس الـ query، في جدول الموظفين مثلاََ ممكن أحسب متوسط المرتبات في الشركة كلها ومتوسط المرتبات لكل قسم، وأعلى مرتب في الشركة كلها وأعلى مرتب لكل قسم، كل ده في نفس الـ query، بالشكل ده:
SELECT *,
AVG(salary) OVER() AS overall_average_salary,
AVG(salary) OVER(PARTITION BY departmentId) AS department_average_salary,
MAX(salary) OVER() AS overall_max_salary,
MAX(salary) OVER(PARTITION BY departmentId) AS department_max_salary
FROM employee
ملخص المعلومات اللي عرفناها لحد دلوقت:
- الـ window function بتعمل عملية معينة على مجموعة صفوف ولكن النتيجة بيكون فيها نفس عدد صفوف الجدول الأصلي
- عشان أقول إن الـ function دي window function بستخدم OVER
- لو استخدمت OVER بس الـ window function هتتحسب على الجدول كله
- بستخدم PARTITION BY عشان أقول إني عايز الـ window function تتحسب لكل مجموعة صفوف مع بعض زي مثلاََ الصفوف اللي ليها نفس الـ departmentID
- ممكن أستخدم أكتر من window function في نفس الـ query
Ranking Functions
مع GROUP BY نقدر نستخدم بس MIN, MAX, SUM, AVG, COUNT. بس نقدر نستخدم functions أكتر من كدة بكتير كـ window functions، في الصورة دي أمثلة على الـ functions دي
في المقال ده هنتكلم عن RANK, DENSE_RANK, ROW_NUMBER، وفي المقالات الجاية هنتكلم عن functions تانية.
هنرجع تاني لجدول الموظفين بس المرة دي عايزين ندي لكل موظف رقم عبارة عن ترتيبه في المرتبات يعني لو بيقبض أعلى مرتب يبقى قدامه 1 تاني أعلى مرتب يبقى قدامه 2 وهكذا.
بما إننا عايزين رقم لكل موظف فهنستخدم window function، وعشان ندي لكل موظف رقم على حسب ترتيبه في المرتبات فممكن نعمل كدة ب 3 طرق:
- ROW_NUMBER():
الـ function دي بتدي رقم لكل صف، أول صف بتديله رقم 1 تاني صف رقم 2 تالت 3 وهكذا ومفيش صفين أو اتنين موظفين بياخدوا نفس الرقم حتى لو بياخدوا نفس المرتب. ممكن تفكر فيها إنها زي Loop بتبدأ عند أول صف وجواها counter بيبدأ من 1 وفي كل لفة بينقل عالصف اللي بعده ويزود الـ counter بواحد
- DENSE_RANK():
الـ function دي بردو بتدي رقم لكل صف، وبردو بتبدأ من أول 1. طيب إيه الاختلاف بينها وبين ROW_NUMBER؟ هنشوف في الأنيميشن ده إيه الفرق بينهم
زي ما إحنا شايفين dense_rank ادت رقم واحد لأعلى مرتب زيها زي row_number ونفس الكلام مع تاني أعلى مرتب، الاختلاف ظهر لما جينا عند الموظف التالت ولقينا مرتبه هو كمان بيساوي تاني أعلى مرتب هنا dense_rank ادته رقم 2 بردو واللي بعده خد رقم 3. dense_rank فكرتها شبه ترتيب الأوائل في المدرسة لما يكون أكتر من طالب جايبين نفس المجموع بيبقى ترتيبهم التاني والتاني مكرر وبعدين التالت وهكذا. فالاختلاف بين dense_rank و row_number هو إن dense_rank بتراعي إن فيه صفوف متساوية في المرتب وبتديهم نفس الرقم أو نفس الترتيب بينما row_number ملهاش دعوة هي مجرد بتدي رقم لكل صف وخلاص.
- RANK:
زي ما عملنا مع dense_rank هنشوف أنميشن الأول وبعدين نشرح الفرق بين rank وdense_rank و row_number.
هنلاحظ هنا إن rank زي dense_rank الموظفين اللي بياخدوا نفس المرتب بتديهم نفس الرقم بس فيه اختلاف وهو إن rank بعد ما ادت رقم 2 لتاني وتالت موظف جت عند الموظف الرابع ادته رقم 4 يعني rank قالت ان تاني أعلى مرتب فيه 2 موظفين بياخدوه يبقى الاتنين موظفين دول في المركز التاني بس المركز اللي بعد كدة هيبقى المركز الرابع عكس dense_rank اللي خلت الموظفين الاتنين دول في المركز التاني وجت عند الموظف اللي بعدهم خلته في المركز التالت. هنلاحظ تاني بردو إن كان عندنا 3 موظفين بياخدوا نفس المرتب rank ادتهم كلهم رقم 7 وجت عند الموظف اللي بعدهم ادته رقم 10. ف rank كإنها بتقول أنا عندي 3 قد بعض في المركز السابع فهدي كل واحد فيهم رقم 7 بس المركز الثامن والتاسع مش هيبقوا موجودين هبدأ أعد تاني من أول 10.
مقارنة بين rank , dense_rank, row_number:
بعد ما فهمنا فكرة RANK, DENSE_RANK, ROW_NUMBER هنشوف إزاي نكتبهم في SQL، زي ما قولنا قبل كدة إن دول window functions وإن عشان أقول إن دي window function بستخدم OVER، أنا هنا عايز ترتيب الموظف في المرتبات بالنسبة للشركة كلها فالبتالي مش هستخدم PARTITION BY جوا OVER. فالكود هيبقى بالشكل ده لو هستخدم ROW_NUMBER مثلا:
SELECT *, ROW_NUMBER() OVER() AS salary_rank
FROM employee
بس لو جينا نعمل RUN للكود هيطلعلنا الـ ERROR ده:
الايرور بيقولنا إن ROW_NUMBER لازم تستخدمها مع OVER ومعاها ORDER BY، إحنا في الكود استخدمنا OVER فمش دي المشكلة، زي ما قولنا إننا هنستخدم ROW_NUMBER, RANK, DENSE_RANK عشان تدي رقم لكل موظف على حسب ترتيبه في المرتبات بس في الكود اللي كتبناه مفيش أي حاجة ليها علاقة بترتيب المرتبات، محتاجين نقول للـ function الترتيب اللي بناءاََ عليه هتدي رقم لكل صف، وهنا ييجي دور حاجة كمان نقدر نستخدمها جوا OVER وهي ORDER BY، بنستخدم ORDER BY عشان نقول للـ window function على الترتيب اللي تمشي بيه، احنا هنا عايزين الـ function تمشي من أعلى مرتب للأقل فهنقول ORDER BY SALARY DESC، والكود هيبقى بالشكل ده:
SELECT *, ROW_NUMBER() OVER(ORDER BY salary DESC) AS salary_rank
FROM employee
لو عملنا RUN للكود هيطلع لنا النتيجة زي ما كنا متوقعين:
ونفس الكود ده ينطبق على RANK, DENSE_RANK بس هنغير اسم الـ FUNCTION بس:
SELECT *,
ROW_NUMBER() OVER(ORDER BY salary DESC) AS salary_rank_row_number,
RANK() OVER(ORDER BY salary DESC) AS salary_rank,
DENSE_RANK() OVER(ORDER BY salary DESC) AS salary_dense_rank
FROM employee
فيه ملحوظتين محتاجين نقولهم:
1- لازم نستخدم ORDER BY جوة OVER مع الـ ranking functions. نقدر نستخدم ORDER BY جوة OVER مع أي window function عموماََ بس مش اجباري.
2- rank, dense_rank,row_number مش بياخدوا أي arguments.
Problem 1: Department top 3 salaries
ممكن حد يفكر إيه الاستفادة اللي هناخدها لما ندي رقم لكل موظف على حسب ترتيبه في المرتبات، ده ممكن يكون مفيد في حاجات كتيرة مثلاََ نقدر نعرض أعلى 5 موظفين بياخدوا مرتبات في الشركة، أو نقدر بسهولة نعرف تاني أو تالت أعلى مرتب في الشركة، ممكن كمان لو استخدمنا PARTITION BY جوة OVER نعرف ترتيب مرتبات الموظفين جوا كل قسم مش بس جوا الشركة كلها وده اللي هنحاول نعمله دلوقت. هنحل مع بعض سؤال Department Top Three Salaries على leetcode. السؤال بيقولنا إن معانا جدولين، جدول فيه بيانات الموظفين وجدول فيه بيانات الأقسام. ومطلوب مننا نعرف الموظفين اللي بياخدوا أعلى 3 مرتبات في كل قسم، ده مثال على الجدولين اللي معانا والنتيجة المطلوبة.
المطلوب هنا شبه المثال اللي قولناه من شوية لما كنا عايزين ندي رقم لكل موظف عبارة عن ترتيبه في مرتبات الشركة بس الفرق إنه عايز ترتيب المرتبات يكون يكون لكل قسم وكمان عايز يعرف الناس اللي بتاخد أعلى 3 مرتبات بس. هنبدأ نفس بداية المثال اللي فات، بما إننا عايزين رقم لكل موظف فهنستخدم window function وزي ما قولنا عندنا 3 طرق ندي بيهم رقم لكل موظف اللي هما RANK, DENSE_RANK, ROW_NUMBER، والسؤال هنا: إيه الطريقة اللي هتحل السؤال ده؟ هنقرأ السؤال ممكن يكون فيه حاجة توضح لنا ممكن نستخدم إيه.
في السؤال بيقول إنه عايز يعرف الـ high earners في كل قسم وإن الـ high earner ده حد بيقبض مرتب من أعلى 3 مرتبات unique في القسم، يعني إيه الكلام ده؟ تعالوا نشوف المثال اللي اداهولنا تاني
تعالوا نركز على قسم واحد بس في المثال وهو القسم اللي الـ ID بتاعته 1، إيه أعلى 3 أرقام مختلفين في العمود بتاع المرتب؟ 90000 و 85000 و 70000، المطلوب إننا نجيب كل اللي بيقبضوا أي مرتب من ال3 مرتبات دول، لو عندي أكتر من موظف بيقبض 90000 مطلوب نعرضهم كلهم ونفس الكلام للي بيقبضوا 85000 و 70000.
فهنا ممكن نستخدم DENSE_RANK لأنها هتيجي عند كل اللي بيقبضوا 90000 وتديهم رقم 1 وكل اللي بيقبضوا 85000 وتديهم رقم 2 واللي بيقبضوا 70000 تديهم رقم 3 وبالتالي لو استخدمنا DENSE_RANK هنكون عارفين إن الموظفين اللي مطلوب نعرفهم DENSE_RANK هتديهم رقم من 1 لـ 3، لو استخدمنا ROW_NUMBER مثلا مش هنبقى عارفين الموظفين اللي مطلوب نعرفهم هياخدوا أرقام من كام لكام لأن ROW_NUMBER بتدي رقم مختلف لكل موظف حتى لو ليهم نفس المرتب، ولو استخدمنا RANK هيكون عندنا نفس المشكلة لأن مثلاََ لو عندي 3 موظفين بيقبضوا 90000 هياخدوا رقم واحد وموظف واحد بيقبض 85000 فالموظف ده RANK هتديله رقم 4 و اللي بيقبضوا 70000 RANK هتديهم رقم 5 فهنا RANK خلت الموظفين اللي بياخدوا أعلى 3 مرتبات ياخدوا أرقام من 1 لـ 5 وrange الأرقام ده هيختلف كل مرة على حسب عدد الناس اللي بتقبض نفس المرتب. عشان كدة DENSE_RANK هي الطريقة المناسبة هنا.
بعد ما عرفنا ليه هنستخدم DENSE_RANK هنبدأ نكتب الحل
- بما إننا عايزين نعرف ترتيب المرتبات في كل قسم فهنقول PARTITION BY departmentId
- عشان نقوله رتب من أعلى مرتب لأقل مرتب هنقول ORDER BY salary desc
SELECT name , salary, departmentId,
DENSE_RANK() OVER(PARTITION BY departmentId ORDER BY salary DESC) AS salary_rank
FROM Employee
لو عملنا run للكود هتطلع النتيجة دي:
مننساش إننا عايزين نعرف بس الموظفين اللي بيقبضوا أعلى 3 مرتبات اللي هما واخدين أرقام من 1 ل 3 وبالتالي الجدول اللي طلع ده محتاجين نكتب query تفلتر منه الصفوف اللي مش عايزينها، إيه الحاجة اللي بنستخدمها عشان نكتب query على نتيجة query تانية؟ subquery.
هنحط الـ query اللي كتبناها دي بين قوسين جوا الـ From، وبعدين نعمل شرط نقوله عايزين بس الموظفين اللي واخدين رقم أقصى حاجة 3 :
SELECT *
FROM (
SELECT name , salary, departmentId,
DENSE_RANK() OVER(PARTITION BY departmentId ORDER BY salary DESC) AS salary_rank
FROM Employee
) AS ranked_salaries
WHERE salary_rank <= 3
باقي بس خطوة أخيرة وهي إننا محتاجين اسم القسم مش الـ ID لأن ده مطلوب في السؤال، فهنعمل JOIN لجدول الأقسام على الـ departmentID ونعرض اسم القسم، وبكدة نكون وصلنا لحل السؤال كامل:
SELECT d.name as Department, ranked_salaries.name as Employee, salary as Salary
FROM (
SELECT name , salary, departmentId,
DENSE_RANK() OVER(PARTITION BY departmentId ORDER BY salary DESC) AS salary_rank
FROM Employee
) AS ranked_salaries
JOIN Department d
ON ranked_salaries.departmentId = d.id
WHERE salary_rank <= 3
توضيح للكود بيشتغل ازاي:
ملخص خطوات حل السؤال:
- المطلوب كان نعرف الموظفين اللي بيقبضوا مرتب من أعلى 3 مرتبات في كل قسم
- استخدمنا DENSE_RANK لأنها هتدي لكل موظف من المطلوب نعرفهم رقم من 1 ل 3 وبالتالي range الأرقام معروف فنقدر نكتب شرط يطلع الموظفين دول بس في النتيجة
- جوا OVER استخدمنا PARTITION BY departmentId لأننا عايزين نعرف أعلى مرتبات لكل قسم، واستخدمنا ORDER BY salary DESC عشان نقول لـ DENSE_RANK تمشي بالترتيب من أعلى لأقل مرتب
- بعد ما كتبنا الـ query اللي فيها DENSE_RANK، استخدمناها كـ subquery في query تانية
- الـ query التانية هدفها إننا نكتب شرط إننا عايزين بس الموظفين اللي واخدين أرقام أقل من أو يساوي 3
- عملنا JOIN لجدول الـ department عشان نجيب منه أسماء الأقسام
Problem 2: Product Prices
مثال كمان على استخدامات للـ ranking functions، الجدول اللي في الصورة ده معمول عشان نتابع تغيرات الأسعار في كل منتج، عندنا لكل منتج سعره والتاريخ اللي اتحدد فيه السعر ده:
ممكن نستخدم الـ ranking functions هنا عشان نعرف ايه سعر كل منتج دلوقتي، عن طريق إننا نستخدم ROW_NUMBER مثلاََ وجوا OVER نقوله PARTITION BY product_id و ORDER BY price_date DESC عشان نقول لـ ROW_NUMBER تشتغل من أحدث تاريخ لأقدم تاريخ، فهنا ROW_NUMBER هتبدأ من أحدث تاريخ اللي هو عنده أجدد سعر للمنتج وتديله رقم واحد والتاريخ اللي بعده رقم 2 وهكذا.
SELECT product_id, price,
RANK() OVER(PARTITION BY product_id ORDER BY price_date DESC) AS price_rank
FROM ProductPriceHistory
لما نعمل run دي النتيجة اللي هتطلع:
وهنعمل زي ما عملنا في السؤال اللي فات ونستخدم الكود ده كـ subquery ونكتب شرط إننا عايزين الصفوف اللي خدت رقم 1 بس اللي هي فيها أحدث سعر لكل منتج.
-- most recent price per product
SELECT product_id, price
FROM (
SELECT product_id, price,
RANK() OVER(PARTITION BY product_id ORDER BY price_date DESC) AS price_rank
FROM ProductPriceHistory
) AS subquery
WHERE price_rank = 1
لما نعمل run هيظهر لنا كل منتج وأحدث سعر ليه:
توضيح للكود:
ملحوظة: ممكن هنا نستخدم RANK أو DENSE_RANK مش شرط ROW_NUMBER، لأن الهدف هو إننا نعرف أحدث سعر بس فمحتاجين الصفوف اللي هتاخد رقم واحد بس وهنفلتر الباقي، في الحالة دي أي function منهم هتنفع لأن زي ما قولنا الاختلافات بين RANK, DENSE_RANK, ROW_NUMBER بتظهر لما يلاقوا قيمتين زي بعض، في المثال ده احنا بنرتب بالتاريخ اللي اتغير فيه سعر المنتج، وطبيعي مفيش منتج سعره هيتغير أكتر من مرة في نفس اليوم وبالتالي لكل منتج مفيش صفين فيهم نفس التاريخ، وبالتالي كلهم هيشتغلوا زي بعض هنا هيدوا رقم 1 لأحدث تاريخ ورقم أكبر من 1 للباقي، وبالتالي نقدر نكتب شرط إننا عايز الصفوف اللي واخدة رقم 1 بس.
ممكن نفس الكود اللي كتبناه ده بس نغير ترتيب التواريخ بدل من الأحدث للأقدم يبقى العكس ونعرف أقدم سعر لكل منتج بدل أحدث سعر:
-- oldest price per product
SELECT product_id, price
FROM (
SELECT product_id, price,
RANK() OVER(PARTITION BY product_id ORDER BY price_date ASC) AS price_rank
FROM ProductPriceHistory
) AS subquery
WHERE price_rank = 1
ممكن في الـ subquery نغير الترتيب بدل ما يبقى ORDER BY price_date DESC نخليها ORDER BY price DESC وبالتالي ROW_NUMBER هتشتغل من أعلى سعر للأقل، وبالتالي الكود ده يحسب لكل منتج ايه أعلى سعر وصله
SELECT product_id, price
FROM (
SELECT product_id, price,
RANK() OVER(PARTITION BY product_id ORDER BY price DESC) AS price_rank
FROM ProductPriceHistory
) AS subquery
WHERE price_rank = 1
ممكن كمان نعرف أعلى سعر لكل منتج في سنة معينة، مثلاََ لو عايزين نعرف أعلى سعر لكل منتج في سنة 2023 ممكن في الـ subquery نقوله عايزين الصفوف بتاعة سنة 2023 بس، وباقي الكود هيفضل زي ما هو:
SELECT product_id, price
FROM (
SELECT product_id, price,
RANK() OVER(PARTITION BY product_id ORDER BY price DESC) AS price_rank
FROM ProductPriceHistory
WHERE YEAR(price_date) = 2023
) AS subquery
WHERE price_rank = 1
في الختام
وبكدة نكون وصلنا لنهاية المقال، في المقال ده عرفنا يعني إيه window function وفهمنا إزاي بتشتغل واتعلمنا 3 window functions وهم RANK, DENSE_RANK, ROW_NUMBER وشوفنا ايه الفرق بينهم واستخدامات ليهم. أتمنى يكون المقال مفيد وفي الأجزاء الجاية إن شاء الله هنتكلم عن window functions تانية و هنشوف استخدامات ومشاكل أكتر الـ window functions بتحلها.
المصادر
- SQL Ranking Window Functions | ROW_NUMBER, RANK, DENSE_RANK, NTILE, CUME_DIST, PERCENT_RANK
- SQL Rank Functions OverSimplified | by abhinaya rajaram | CodeX | Medium
- SQL Window Functions in 10 Minutes
- OVER clause (Transact-SQL) - SQL Server | Microsoft Learn
- RANK (Transact-SQL) - SQL Server | Microsoft Learn
- DENSE_RANK (Transact-SQL) - SQL Server | Microsoft Learn
- ROW_NUMBER (Transact-SQL) - SQL Server | Microsoft Learn