نگاهی سریع به SQL Window Functions
چرا آشنایی با SQL Window Functions از نان شب واجبتر است ؟

در زندگی روزانه برخی از رویدادها و درک برخی مفاهیم هستند که بعد از آشنایی با آنها، زندگی به دو قسمت قبل و بعد تقسیم میشود. در دنیای فنی این نقاط عطف، مفاهیم، کتابخانهها و ابزاری هستند که بعد از آشنایی با آنها تا مدتها انگشت حسرت خواهیم گزید که ای دل غافل، چرا قبلاً با این موضوع، آشنا نشده بودم.
هر چند در گذشته در مقاله شما هم هنوز از  SQL-92  استفاده میکنید؟ به ضرورت آشنایی با مفاهیم جدید SQL پرداخته بودم اما بخش SQL Window Functions از دید خودم هم مغفول مانده بود. قابلیتی که در SQL-2003 به مجموعه استاندارد SQL اضافه شده است و امروزه در تمامی بانکهای اطلاعاتی رابطهای رایج، قابل استفاده و بسیار کار راهانداز است. سناریوهای زیر را در نظر بگیرید :
- میزان فروش هر ماه را به دست آوردهاید، اما درصد فروش ماهیانه ( از فروش کل سال ) را هم لازم دارید.
- تعداد کلیک هر محصول را به صورت روزانه دارید و میخواهید در هر روز، رتبه فروش هر کالا را محاسبه کنید.
- میزان پرداختی به هر کارمند از ابتدای سال را به صورت تجمعی نیاز دارید (هر ماه به مجموع ماههای قبل اضافه شود )
- تیم قهرمان لیگ برتر در بیست سال اخیر را قرار است نمایش دهید اما همزمان، تیم دوم و اختلاف امتیاز این تیم با تیم دوم را هم می خواهید نمایش دهید.
- قصد اعلام نمرات دانشجویان را دارید اما میخواهید هر دانشجو رتبه مطلق و رتبه نرمال شده خود در کلاس را ببیند که رتبه مطلق، همان ترتیب نزولی نمرات است و رتبه نرمال شده هم به این صورت اعمال می شود که تمام نمرات بین دو عدد ۹۰ و ۱۰۰، دو عدد ۸۰ و ۹۰ و … در یک رده قرار میگیرند. یعنی نفری که ۹۸ گرفته است و نفری که ۹۲ گرفته است هر دو رتبه نرمال یک را مشاهده میکنند اما اولی رتبه مطلق ۱ و دومی رتبه مطلق ۲ را خواهد دید.
تمام این سناریوها که در دنیای امروز و نیازهای رو به رشد اطلاعاتی آن، برای کاربردهای روزانه بسیار محتمل به نظر می رسند، با توابع پنجرهای SQL به راحتی قابل پیادهسازی و اجرا هستند. کاری که انجام آن با توابع تجمعی  SQL پیچیده خواهد بود . 
توابع تجمعی SQL مانند AVG,SUM,COUNT که اغلب به همراه دستور GROUP BY به کار میروند، سطرها را براساس مقادیر ستون موجود در بخش GROUP BY گروهبندی میکنند و سپس توابع تجمعی را بر روی گروهها اعمال کرده و به ازای هر گروه، یک خروجی را تولید میکنند. در مقابل، در بسیاری از سناریوها و کاربردها، نیاز داریم هر سطر با آمار و دادههای مجموعهای مشابه از سطرها (مثلا تمام رکوردهای یک روز، یک محصول، یک برچسب و …) مقایسه شده و محاسباتی برپایه آنها انجام شده، به کاربر نمایش داده شود.

اینجاست که نیاز به توابعی داریم که :
- سطرها را به گروهها یا بخشهایی تقسیم کنند (Partition)
- در هر بخش، سطرها را بر اساس معیار مورد نیاز ما مرتب کنند (Order By)
- بعد از مرتبسازی، مبنای محاسبات و اعمال تابع را مشخص کند (از ابتدا تا انتها، از ابتدای بخش تا رکورد جاری، از رکورد جاری تا انتهای بخش – Window Frame)
- محاسبات یا تابع مورد نیاز را بر روی بخشهای مرتب ایجاد شده و براساس مبنای مشخص شده، انجام دهد (Window Function)

SQL  به همراه یک مثال کاربردیبهتر است با یک مثال این موارد را به صورت عملیاتی مشاهده کنیم. فرض کنید میخواهیم میزان مسافت پیموده شده یک ماشین را از ابتدای ماه به صورت تجمعی محاسبه کنیم. یعنی هر روز را با مجموع پیمایش روزهای قبل جمع کنیم تا مجموع کل تا آنروز محاسبه شود. یعنی یک خروجی مشابه با شکل زیر نیاز داریم :

در این جا، مبنای گروهبندی یا Partitioning کل رکوردها (اندازه Window برابر کل رکوردها)، مرتب سازی بر اساس فیلد Day و تابعی که باید محاسبه شود تابع SUM است (که در اینجا مبنای اعمال آن از ابتدای پارتیشن تا رکورد جاری است). کد زیر، دستور SQL معادل خروجی تصویر فوق به کمک توابع پنجرهای است :
در کد فوق ساختار اصلی استفاده از توابع پنجرهای را میبینید.
- تابع پنجرهای درقسمت Selectنوشته میشود.
- کلمه کلیدی Overکه نشاندهنده استفاده از توابع پنجرهای است.
- کلمه کلیدی Partition ByدرونOverکه ملاک اصلی گروهبندی سطرها را مشخص میکند که البته در این مثال، اعمال نشده است بنابراین کل سطرها، یک گروه یا بخش را تشکیل میدهند.
- کلمه کلیدی Order Byکه ترتیب سطرها درون هر پارتیشن (بخش) را مشخص میکند .
- کلمه کلیدی Rows که مبنای محاسبه و اندازه پنجره را مشخص میکند (Window Frame که اگر مشخص نشود، تمام سطرهای یک گروه و یا بسته به تابع مورد استفاده، از ابتدای پارتیشن تا رکورد جاری خواهد بود) و در مثال بعدی قابل مشاهده است.
قبل از اینکه مثالهای دیگری را با هم ببینیم، بهتر است روندی که هنگام اعمال و استفاده از توابع پنجرهای در پشت صحنه اتفاق میافتد را به صورت تصویری مشاهده کنیم. فرض کنید می خواهیم میانگین دو روزه زمان رکابزنی هر دوچرخهسوار را نمایش دهیم. در این حالت، گروه بندی براساس شناسه دوچرخهسوار، مرتب سازی براساس تاریخ و تابع پنجرهای مورد نیاز، تابع میانگین خواهد بود. روالی که پشت صحنه انجام میشود به صورت زیر است ( عبارت ROWS BETWEEN 1 PRECEDING AND CURRENT ROW تعیین کننده اندازه قاب پنجره مورد نیاز برای اعمال تابع فوق خواهد بود.) :

در مثالی دیگر، میخواهیم میانگین سه روز گذشته را به ازای هر روز، مشخص کنیم . یعنی یک خروجی مشابه زیر داشته باشیم :

در اینجا باز هم گروهبندی یا Partitioning نداریم اما با استفاده از Rows به تعیین بازه اعمال تابع تجمعی به ازای هر سطر می پردازیم. کد زیر خروجی بالا را برای ما تولید خواهد کرد : 
همانطور که احتمالاً حدس زدهاید کلمه کلیدی PRECEDING برای بیان سطرهای قبل از سطر جاری به کار می رود و کلمه مقابل آن، Following است که تعیین کننده سطرهای بعد از سطر جاری برای اعمال تابع تجمعی است. 
مثالی دیگر را با هم مرور میکنیم. یک سایت تجاری داریم که چندین کمپین تبلیغاتی بر روی آن در حال اجراست. تعداد کلیکها و استقبال کاربران در هر روز را ذخیره کردهایم. حال میخواهیم تعداد کل کلیکها به ازای هر کمپین را به دست آوریم :
این کار را به راحتی و به کمک عبارت Partition By به صورت زیر انجام میدهیم : 
اگر میخواستیم همین کار را با SQL رایج انجام دهیم باید کد زیر را مینوشتیم : 
که به وضوح، کد قبلی خواناتر و سادهتر است. البته ممکن است بپرسید این خروجی که با توابع تجمعی معمولی SQL به راحتی قابل محاسبه بود و نیاز نبود که مجموع هر ماه، در هر سطر تکرار شود؟ حق با شماست اما این محاسبات را برای این انجام میدهیم که بتوانیم مثلاً بین مقدار هر سطر و مجموع محاسبه شده یک نسبت منطقی برقرار کرده، آنرا نمایش دهیم مثلاً درصد فروش هر روز از فروش کل ماه را هم بتوانیم به ازای هر سطر و به عنوان یک ستون جدید نمایش دهیم. 

حال فرض کنید در همین مثال، می خواهیم بدانیم در روز اول هر کمپین، چه تعداد کلیک داشته ایم. (کدام کمپینها در برخورد اول، مشتری پسندتر بودهاند). در اینجا ملاک گروهبندی ما کمپین و مرتب سازی ما براساس تاریخ خواهد بود . تابع First_Value  به صورت زیر میتواند پاسخگوی این نیاز ما باشد : 

اگر به میزان کلیک روز قبل و بعد هم در کنار اطلاعات روز جاری نیاز داریم ، میتوانیم از دو تابع LAG و LEAD که اولی سطر قبلی و دومی سطر بعدی را برمیگرداند ، استفاده کنیم (پیش فرض آن یک سطر قبل یا بعد است اما می توانید عددی هم به عنوان پارامتر دوم به آن بدهید که مثلا دو سطر قبل را برگرداند). نام ستونی که میخواهیم اطلاعات آن از یک سطر قبل یا بعد برگردانده شود، پارامتر اصلی این دو تابع است. دقت کنید که این تابع بعد از اعمال Order By در هر پارتیشن، محاسبه میشود. 

امیدواریم تا اینجا دید مناسبی به توابع پنجرهای پیدا کرده باشید. به عنوان آخرین مثال، فرض کنید میخواهیم رتبه هر سطر را در هر پارتیشن به دست آوریم. شکل و کد زیر پیاده سازی و خروجی مربوطه را به ما نشان میدهند.

تفاوت توابع رتبه بندی را در شکل فوق می توانید مشاهده کنید.
اگر به این مبحث علاقهمند شدهاید توصیه میکنم برای تکمیل مبحث به این آموزش جمع و جور و کامل سایت LearnSQL با عنوان برگه مرور (تقلب) توابع پنجرهای مراجعه کنید. این سایت البته یک آموزش تعاملی خوب هم در همین زمینه دارد. این مقاله هم به کمک مثالهای کاربردی، سعی در آشنایی بیشتر خواننده با توابع پنجره ای دارد .
 
				
بسیار مفید و شیوا
مطلب بسیار کامل و قابل فهم و شیوایی بود. سپاس
سلام
امروز برای window functions s سوالی برایم پیش اومده بود که با مطالب استاد بزرگوار مواجه شدم
تشکر از استاد بنایی عزیز و گرامی
سال ۱۳۸۴ در مقطع کارشناسی کامپیوتر سعادتی نصیب ماشد تا دانشجوی استاد گرامی آقای بنایی باشیم
موفق و پیروز باشید
سلام استاد اگر بخواهیم بخش بندی براساس ماههای شمسی باشد باید چکار کنیم؟
سلام. تاریخ فارسی را به عنوان یک فیلد باید به جدول اضافه کنیم تا بتوانیم این گروه بندی را انجام دهیم. اگر تاریخ موجود در جداول شما ، میلادی باشد، بعید می دانم روش سریع و مناسبی برای تبدیل آن به فارسی در اس کیو ال و گروه بندی بر اساس آن وجود داشته باشد .
سلام و روزبخیر
مطلب خیلی مفید بود و دید خیلی خوبی در رابطه با توابع پنجره ای به من داد
ممنون واقعا
بسیار عالی بود