معرفی محاسبات مبتنی بر تقویم (Calendar-based Time Intelligence) در DAX
از زمان اولین انتشار DAX در سال 2010، این زبان مجموعهای از توابع Time Intelligence را برای سادهسازی محاسباتی مانند Year-to-Date (YTD)، Year-over-Year (YOY) و غیره ارائه داده است. با این حال، این محاسبات تنها از تقویم میلادی (Gregorian Calendar) پشتیبانی میکردند و سایر تقویمها مانند 4-4-5، ISO یا سایر تقویمهای غیرمیلادی را پوشش نمیدادند.
در نسخه کلاسیک Time Intelligence، ستونهای جدول تاریخ (Date Table) برای توابع ناشناخته بودند؛ تنها استثنا، ستون اصلی تاریخ (معمولاً Date[Date]
) بود.
تقویمهای سفارشی در Time Intelligence
توابع Calendar-based Time Intelligence برای پشتیبانی از انواع مختلف تقویمها طراحی شدهاند. این توابع بر ستونهای موجود در جدول تاریخ تکیه میکنند تا مشخص کنند:
سال از کجا شروع و کجا تمام میشود،
سال به چه بخشهایی تقسیم میشود (فصل، ماه و/یا هفته).
به همین دلیل، جدول تاریخ باید شامل ستونهای متعددی با معانی مشخص باشد و توسعهدهنده باید این ستونها را به سطوح تقویم مرتبط کند. این ستونها هم برای انسان و هم برای موتور DAX معنا دارند.
مثال: SAMEPERIODLASTYEAR
در نسخه کلاسیک، تابع SAMEPERIODLASTYEAR مقدار یک Measure را برای دوره مشابه در سال گذشته نمایش میدهد. این کار با شناسایی دوره زمانی انتخابشده و سپس انتقال فیلتر بر اساس ستون تاریخ به عقب (یک سال) انجام میشود.
اما در Calendar-based Time Intelligence، جدول تاریخ باید ستونی داشته باشد که سال را مشخص کند. در این حالت، موتور DAX میتواند فیلتر روی آن ستون را تغییر دهد تا دادههای سال قبل بازیابی شوند. البته این فرآیند در عمل بسیار پیچیدهتر است، اما همین توضیح برای درک مفهوم کلی کافی است.
معرفی تقویمها (Calendars)
در Calendar-based Time Intelligence فرض نمیشود که سال حتماً میلادی است. رفتار توابع بر اساس محتوای ستونهای جدول تاریخ تعیین میشود. به همین دلیل جدول تاریخ باید ستونهایی داشته باشد که نشان دهند سال به چه بخشهایی تقسیم شده است؛ سپس توسعهدهنده باید این ستونها را برچسبگذاری کند تا DAX از معنای آنها آگاه شود.
این ارتباط از طریق Calendar Objects تعریف میشود. هر Calendar Object ارتباط بین ستونهای جدول تاریخ و سطوح زمانی مانند سال، فصل، ماه و روز را ذخیره میکند.
چند تقویم روی یک جدول تاریخ
ما میتوانیم چند تقویم روی یک جدول تاریخ بسازیم. به عنوان مثال:
تقویم میلادی (Gregorian)
تقویم ISO
تقویم مالی (Fiscal Calendar)
اما وجود چندین تقویم در یک جدول میتواند چالشبرانگیز باشد؛ زیرا هنگام حذف فیلتر توسط توابع Calendar-based، همه تقویمها درگیر میشوند، حتی اگر فقط یکی از تقویمها در تابع استفاده شده باشد.
نمونه ساختار سلسلهمراتبی تقویم میلادی
یک تقویم میلادی معمولاً شامل سلسلهمراتب زیر است:
سال (Year)
فصل (Quarter)
ماه (Month)
روز (Date)
در سمت چپ سلسلهمراتب و در سمت راست نمونهای از مقادیر قابل مشاهده است.
نقش تقویم در سلسلهمراتب تاریخ
یک تقویم (Calendar) سطوح سلسلهمراتبی (Hierarchy Levels) را به ستونهای جدول تاریخ Date Table متصل میکند. این Mapping به توسعهدهندگان اجازه میدهد هر نوع تقویمی را ایجاد کنند. برای مثال: ساخت یک تقویم با سیزده ماه به کمک Calendarها امکانپذیر است، در حالی که با Time Intelligence کلاسیک چنین چیزی ممکن نبود.
تقویم هفتگی: رایجترین کاربرد
هرچند تقویمهای ۱۳ ماهه چندان رایج نیستند، اما یکی از پرکاربردترین تقویمهایی که نیازمند Calendar-based Time Intelligence است، تقویم هفتگی است.
در تقویم میلادی معمولی، هفتهها را نمیتوان بهسادگی به ماهها یا فصلها تجمیع کرد، زیرا برخی هفتهها بین دو ماه قرار میگیرند.
به همین دلیل، ایجاد سلسلهمراتبی که هم شامل هفتهها و هم ماهها باشد، مطابق با تعریف تقویم میلادی امکانپذیر نیست.
با این حال، میتوان هفتهها را بهعنوان موجودیت اصلی تقویم تعریف کرد و مفاهیمی مانند سال، فصل و ماه را بر اساس هفتهها بازتعریف نمود. در این حالت، هر هفته به یک “دوره” (که گاهی معادل یک ماه در نظر گرفته میشود) و یک فصل خاص تعلق دارد. ما در ادامه فصل، یک تقویم هفتگی ایجاد خواهیم کرد.
استفاده از تقویمها در DAX
در توابع Time Intelligence کلاسیک، وجود یک ستون تاریخ در جدول Date برای عملکرد آنها کافی بود.
اما در توابع Calendar-based Time Intelligence، کافی نیست که جدول تاریخ صرفاً در مدل باشد؛ بلکه باید ستونها را به دستهبندی مناسب در تقویمها Associate کرد.
یک جدول تاریخ میتواند چندین تقویم را در خود جای دهد. حتی یک ستون میتواند در چندین تقویم استفاده شود. با این حال، باید دقت کافی داشت که یک ستون همیشه به یک Category (دستهبندی) ثابت مرتبط باشد.
مثال: اگر ستون
Date[Month]
به دستهی Month in Year مرتبط شد، نمیتوان همان ستون را در تقویمی دیگر به دستهای متفاوت نسبت داد.
دستهبندی ستونها (Column Categories)
هر Column Category معنای ستون در تقویم را مشخص میکند و رفتار مورد انتظار آن ستون را نسبت به سایر ستونهای همان تقویم تعریف مینماید.
در جدول زیر دستهبندیهای موجود برای ستونها آورده شده است:
ضرورت تعریف دستهبندیها (Categories)
لازم نیست همه دستهبندیها (Column Categories) در یک تقویم تعریف شوند. با این حال، بهتر است برای ستونهایی از جدول تاریخ (Date Table) که در پیمایش سلسلهمراتبی تقویم (Hierarchical Navigation) نقش دارند، دستهبندی مشخص شود.
توابع Time Intelligence مبتنی بر تقویم حتی میتوانند با تقویمهای ناقص (Incomplete Calendars) نیز کار کنند؛ یعنی در شرایطی که همه دستهبندیها ستون متناظر نداشته باشند.
ارتباط میان دستهبندیها
بین دستهبندیهای مختلف رابطه وجود دارد. ساختار سلسلهمراتب به این صورت است که هر سطح (Level) بخشی از سطح بالاتر است:
Quarter بخشی از Year است.
Month بخشی از Quarter است.
Day بخشی از Month است.
از میان تمام سطوح، سطح Week (هفته) حالت ویژهای دارد. دلیل آن این است که یک هفته ممکن است با یک ماه همپوشانی داشته باشد یا نداشته باشد؛ این موضوع بستگی به نحوه تعریف تقویم دارد.
دستهبندیهای کامل و ناقص (Complete vs. Partial Categories)
دستهبندیهایی که در ستون اول برجسته شدهاند، طبیعیترین سلسلهمراتب زمانی را تشکیل میدهند:
Year → Quarter → Month → Week → Date.
به این گروه، Complete Categories گفته میشود، زیرا هر مقدار در این دستهبندیها یک نقطه زمانی یکتا را مشخص میکند.
مثال: عبارت Q1 2025 بهطور دقیق یک فصل را مشخص میکند.
بنابراین، اگر جدول تاریخ شامل ۵ سال داده باشد، ستونی که به دسته Quarter مرتبط است باید ۲۰ مقدار یکتا داشته باشد.
در مقابل، دستهبندیهایی که بهتنهایی نمیتوانند یک نقطه زمانی یکتا را نشان دهند، Partial Categories نامیده میشوند.
مثال Partial Category
یک ستون با تنها ۴ مقدار یکتا (Q1 تا Q4) بدون در نظر گرفتن سال، نمیتواند زمان دقیقی را تعریف کند. در این حالت:
دسته صحیح آن ستون Quarter of Year است،
زیرا برای شناسایی دقیق، نیاز داریم مقدار سال (Year) را هم مشخص کنیم.
به بیان دیگر، مقدار Q1 بهتنهایی معنای زمانی ندارد؛ ولی ترکیب آن با سال، مانند Q1 2025، یک نقطه زمانی مشخص را میسازد.
تعریف تقویم در DAX انعطافپذیر است:
میتوان تنها از Year و Quarter of Year استفاده کرد،
یا فقط Quarter،
یا ترکیبی از همه این دستهها.
توابع Calendar-based Time Intelligence در همه این حالتها کار میکنند. آنها تلاش میکنند تا جای ممکن از دستههای کامل استفاده کنند و در صورت نیاز به سراغ دستههای ناقص بروند.
ستون اصلی و ستونهای وابسته (Primary vs. Associated Columns)
هر دستهبندی در تقویم شامل:
یک ستون اصلی (Primary Column): ستونی که در خروجی توابع Time Intelligence استفاده میشود.
ستونهای وابسته (Associated Columns): ستونهایی با معنای یکسان که مکمل ستون اصلی هستند.
به عنوان مثال:
دسته Month in Year ممکن است ستونی شامل نام کامل ماهها (January تا December) داشته باشد.
در عین حال، ممکن است ستونی شامل نامهای کوتاه (Jan تا Dec) نیز وجود داشته باشد.
در این حالت، یکی از ستونها بهعنوان Primary Column انتخاب میشود و دیگری بهعنوان Associated Column تعریف خواهد شد.
ضرورت قابلیت مرتبسازی (Sorting)
ستونهایی که در تقویم تگگذاری میشوند باید قابلیت مرتبسازی (Sortable) داشته باشند، یا بهصورت مستقیم یا از طریق خاصیت Sort By Column.
مرتبسازی حیاتی است، زیرا بسیاری از محاسبات Time Intelligence نیاز دارند دورههای قبل یا بعد از دوره جاری را شناسایی کنند.
هر ستونی که در خاصیت Sort By Column تعریف شود، بهطور خودکار بهعنوان ستون وابسته در نظر گرفته میشود؛ حتی اگر صریحاً چنین برچسبی نخورده باشد.
تعریف تقویمها (Defining Calendars)
رابط کاربری برای تعریف تقویمها ممکن است با گذشت زمان تغییر کند و بسته به ابزار مورد استفاده متفاوت باشد.
با این حال، یک روش استاندارد و مستقل از ابزار (Agnostic) برای نمایش تعریف یک تقویم، بررسی نمایش TMDL آن است.
createOrReplace
ref table Date
calendar Gregorian
calendarColumnGroup = year
primaryColumn: Year
calendarColumnGroup = quarter
primaryColumn: 'Year Quarter Number'
associatedColumn: 'Year Quarter'
calendarColumnGroup = month
primaryColumn: 'Year Month Number'
associatedColumn: 'Year Month'
associatedColumn: 'Year Month Short'
calendarColumnGroup = date
primaryColumn: Date
دستهبندی ستونها در تقویم (calendarColumnGroup)
هر Category در تقویم بهعنوان یک شیء calendarColumnGroup تعریف میشود و باید دقیقاً یک primaryColumn داشته باشد.
هر associatedColumn نیز در یک خط جداگانه برای هر ستون وابسته تعریف میشود.
تمام ستونهایی که در ویژگی sort-by-column برای ستونهای primaryColumn یا associatedColumn مشخص شده باشند، بهطور خودکار بهعنوان associatedColumn در نظر گرفته میشوند.
سایر ستونهای جدول تاریخ (Date Table) که جزو هیچیک از calendarColumnGroupها نیستند، ستونهای filter-keep محسوب میشوند.
معرفی ستونهای مرتبط با زمان (Time-related Columns)
هنگامی که توسعهدهندگان از توابع Calendar-based Time Intelligence استفاده میکنند، دیگر عمل خودکار REMOVEFILTERS روی ستون Date انجام نمیشود.
در این رویکرد جدید:
توابع تنها روی ستونهایی عمل میکنند که در تقویم Tag شدهاند.
این توابع با توجه به معنای ستونها (Semantics) فیلترها را اضافه یا حذف میکنند.
هر ستون در جدول تاریخ که در هیچ تقویمی Tag نشده باشد، بهعنوان filter-keep column در نظر گرفته میشود. در مقابل، تمام ستونهایی که در تقویم Tag شده باشند، ستونهای filter-clear هستند.
این موضوع باعث میشود که مدیریت ستونهای filter-keep در Calendar-based Time Intelligence بسیار سادهتر از رویکرد کلاسیک باشد.
نیاز به حذف فیلتر روی ستونهای خاص
ممکن است ستونهایی وجود داشته باشند که بخواهیم فیلتر آنها را بهطور اجباری حذف کنیم، حتی اگر به هیچ دستهبندی در تقویم تعلق نداشته باشند.
نمونههایی از این ستونها:
ستون فصلها (Spring, Summer, Fall, Winter)
فاز ماه (Moon Phase)
ستونهای متنی مانند: “Today”، “Tomorrow”، “Current Month”
برای این کار میتوان چنین ستونهایی را به تقویم اضافه کرد، بدون آنکه به هیچ Category اختصاص داده شوند.
تعریف Time-related Columns
ستونی که در تقویم اضافه شود اما به هیچ دستهبندی نسبت داده نشود، یک Time-related Column محسوب میشود.
این ستونها تحت مدیریت خاصی قرار میگیرند.
بیشتر توابع Calendar-based Time Intelligence فیلتر این ستونها را حذف میکنند.
اما استثنایی وجود دارد: توابع لترال شیفت (Lateral-shifting) مانند DATEADD و SAMEPERIODLASTYEAR فیلترهای ستونهای Time-related را نگه میدارند.
تعریف در TMDL
در TMDL، یک Time-related Column با یک calendarColumnGroup تعریف میشود که به هیچ Category متصل نیست و فقط شامل ستونهای مرتبط است.
createOrReplace
ref table Date
calendar Gregorian
calendarColumnGroup = year
primaryColumn: Year
calendarColumnGroup = monthOfYear
primaryColumn: 'Month Number'
associatedColumn: Month
calendarColumnGroup
column: Period
column: Season
درک رفتار توابع Time Intelligence
توابع Time Intelligence برای محاسبهی یک دورهی زمانی دیگر، Filter Context روی جدول Date را تغییر میدهند. برای رسیدن به این هدف، این توابع معمولاً سه گام را انجام میدهند:
تشخیص دورهی زمانی انتخابشدهی فعلی.
تعیین سطحی که باید جابهجا/گسترش داده شود (مثلاً سال، فصل، ماه، هفته یا روز).
گسترش یا جابهجایی انتخاب فعلی طبق پارامترهای ورودی تابع.
توجه: پارامترهای هر تابع میتوانند بر گام دوم و سوم اثر بگذارند.
درک رفتار Calendar-based Time Intelligence
توابع Calendar-based Time Intelligence به دانش از پیش کدنویسیشده دربارهی ساختار تقویم متکی نیستند. ساختار سال توسط دستهبندی ستونها (Column Categories) در تعریف تقویم مشخص میشود. بنابراین، مهم است الگوریتمی را که این توابع برای تعیین فیلتر جدید بهکار میبرند و نحوهی اعمال آن فیلتر بر Filter Context موجود را درک کنیم.
نمیخواهیم خواننده را نگران کنیم؛ در بیشتر سناریوها، رفتار این توابع طبیعی و قابلدرک است و نتایج بدیهی تولید میکنند. بااینحال، در برخی موارد ممکن است نتایج غیرمنتظره دیده شود—میخواهیم خواننده از این موارد آگاه باشد. علاوه بر این، میتوان با استفاده از پارامترهای خاص، رفتار الگوریتم را تغییر داد. به بیان دیگر، توابع Calendar-based از توابع کلاسیک منعطفتر هستند و طبیعتاً این انعطافپذیری مقداری پیچیدگی برای یادگیری به همراه دارد.
نمونه: ساخت Measure «همان دوره در سال گذشته» با توابع Calendar-based
بیایید یک Measure بسازیم که مقدار یک Measure دیگر را برای همان دوره در سال گذشته با استفاده از توابع Calendar-based محاسبه کند:
Sales SPLY Calendar =
CALCULATE (
[Sales Amount],
SAMEPERIODLASTYEAR ( 'Gregorian' )
)
مقایسه Calendar-based SPLY با نسخه کلاسیک
Measure ساختهشده با استفاده از تقویم، یعنی:
Same SPLY Calendar
دقیقاً همان نتیجهای را تولید میکند که نسخه کلاسیک Sales SPLY به دست میآورد. این موضوع در شکل زیر نشان داده شده است.
درک الگوریتم جابهجایی در Calendar-based Time Intelligence
این بار موتور DAX توانست فوریه 2020 (29 روز) را به فوریه 2019 (28 روز) منتقل کند، بدون آنکه به دانشی از پیش تعریفشده درباره ساختار تقویم متکی باشد. برای درک بهتر، لازم است الگوریتم را بررسی کنیم.
روی سلول فوریه 2020 تمرکز کنیم:
Filter Context شامل سه ستون است:
Year = 2020
Quarter = Q1
Month = February
به یاد داشته باشید:
ستون Quarter با دستهبندی Quarter of Year برچسبگذاری شده است.
ستون Month با دستهبندی Month of Year برچسبگذاری شده است.
ترکیب این فیلترها یک نقطه یکتا در شبکه تقویم (Calendar Lattice) ایجاد میکند: فوریه 2020. این مقدار با ترکیب Year و Month of Year مشخص میشود و معادل همان سطحی از دستهبندی Month است که قبلاً در سلسلهمراتب تقویم نشان داده شد.
الگوریتم “Same Distance from Parent”
تابع نیاز دارد یک سال به عقب جابهجا شود.
برای Year: محاسبه ساده است → سال قبل از 2020 برابر 2019 است.
برای Month: الگوریتم باید تشخیص دهد که فوریه دومین ماه سال 2020 است و سپس همان موقعیت را در سال 2019 پیدا کند. دومین ماه سال 2019 → فوریه 2019.
به این ترتیب، Filter Context جدید شامل فوریه 2019 میشود.
این الگوریتم به نام “Same Distance from Parent” شناخته میشود.
چون تابع در سطح Year جابهجایی را انجام میدهد، Year نقش Parent دارد.
بنابراین، ماه بازگرداندهشده همان ماهی است که در ابتدای سال قبلی در همان فاصله قرار دارد.
این روش پیامدهای مهمی دارد که در ادامه فصل بهطور کامل بررسی خواهد شد.
درک نحوه حذف فیلترها (Filter Clearing)
تا اینجا تابع SAMEPERIODLASTYEAR توانسته هدف را شناسایی کند: فوریه 2019.
اما کافی نیست که فقط فیلتر جدید اعمال شود، زیرا با Filter Context موجود در تضاد خواهد بود.
اما همانطور که گفتیم، این روش برای ستونهای Filter-keep مشکلات جدی ایجاد میکند.
به همین دلیل، توابع Calendar-based Time Intelligence از یک الگوریتم پیچیدهتر برای تصمیمگیری استفاده میکنند:
DAX ابتدا مجموعهای از وابستگیهای دستهبندی ستون (Column Category Dependencies) برای ستون «X» را تعیین میکند.
Year هیچ وابستگیای ندارد.
Month of Year وابسته به Quarter of Year و Month of Quarter است.
پس از شناسایی وابستگیها، دستور REMOVEFILTERS فقط روی ستونهایی اعمال میشود که در همان موقعیت عمودی بهعنوان «X» یا یکی از وابستگیهای آن قرار دارند.
مدیریت ستونهای Time-related
در این سناریو:
فیلترهای روی Week of Month، Day of Month و Day of Week تغییر نمیکنند—even اگر وجود داشته باشند.
دلیل: این دستهها ستونهای مرتبط با زمان (Time-related Columns) هستند و در این شرایط بهعنوان Filter-keep Columns عمل میکنند.
پاکسازی فیلتر در تقویمهای متعدد
پاکسازی فیلتر (Filter Clearing) در تمام تقویمهای موجود در جدول تاریخ اعمال میشود. اگر جدول شامل چندین تقویم باشد، فیلتر از هر ستونی که در هر تقویمی Tag شده باشد و در همان سطحی قرار گیرد که پاکسازی انجام میشود، حذف خواهد شد.
به عبارت دیگر:
اگر جدول تاریخ شامل یک تقویم ISO باشد که ستونی به نام ISO Month دارد و آن ستون در دستهبندی Month of Year قرار گرفته است، در همین مثال روی آن نیز دستور REMOVEFILTERS اجرا میشود—even اگر به تقویم جاری تعلق نداشته باشد.
این دقیقاً همان دلیلی است که استفاده از چند تقویم روی یک جدول تاریخ میتواند گیجکننده باشد. امکان ساخت چند تقویم وجود دارد، اما توسعهدهندگان باید با دقت بسیار مشخص کنند کدام فیلترها حذف و کدامها نگهداری خواهند شد.
درک جابهجایی افقی و سلسلهمراتبی (Lateral vs. Hierarchical Shift)
در این فصل دو تابع را بررسی کردیم: SAMEPERIODLASTYEAR و DATESYTD. انتخاب این دو تابع دلیل خاصی داشت:
آنها دو دسته متفاوت از توابع Time Intelligence را نشان میدهند.
SAMEPERIODLASTYEAR → جابهجایی افقی (Lateral Shift)
این تابع انتخاب فعلی در Filter Context را گرفته و آن را یک سال به عقب میبرد.اگر انتخاب اصلی در سطح ماه باشد، SAMEPERIODLASTYEAR همچنان در سطح ماه عمل میکند، تنها فیلتر را یک سال عقب میبرد.
جابهجایی افقی سطح جزئیات (Granularity) را تغییر نمیدهد.
تنها تابع دیگری که جابهجایی افقی انجام میدهد DATEADD است که در واقع درون SAMEPERIODLASTYEAR استفاده میشود.
DATESYTD → جابهجایی سلسلهمراتبی (Hierarchical Shift)
این تابع سطح انتخاب را تغییر میدهد.اگر انتخاب اصلی تنها یک تاریخ باشد، فیلتر جدید میتواند شامل تاریخها، ماهها و سالها شود.
DATESYTD معمولاً بازه انتخابشده را گسترش میدهد.
توابع دیگری مثل NEXTMONTH همیشه یک ماه برمیگردانند—even اگر انتخاب اولیه یک فصل یا یک سال بوده باشد.
همه توابع Time Intelligence به جز DATEADD و SAMEPERIODLASTYEAR از نوع Hierarchical Shift هستند.
تفاوت کلیدی: مدیریت ستونهای مرتبط با زمان (Time-related Columns)
یک Time-related Column ستونی است که در تقویم Tag شده اما به هیچ دستهبندی سلسلهمراتبی متصل نشده باشد.
در Lateral Shift (مثل SAMEPERIODLASTYEAR, DATEADD):
فیلتر روی ستونهای Time-related حفظ میشود.در Hierarchical Shift (مثل DATESYTD یا TOTALYTD):
فیلتر روی ستونهای Time-related پاک میشود.
به بیان دیگر، ستونهای Time-related رفتاری دوگانه دارند:
در جابهجایی افقی → Filter-Keep
در جابهجایی سلسلهمراتبی → Filter-Clear
این موضوع در بخش جدید مربوط به DATEADD و SAMEPERIODLASTYEAR با جزئیات بیشتری توضیح داده خواهد شد.
جمعبندی
در این مقاله ، با اصول اولیهی محاسبات Calendar-based Time Intelligence در DAX آشنا شدیم.
یاد گرفتیم چگونه یک Calendar را در جدول تاریخ تعریف کنیم.
چگونه ستونها را به دستهبندیهای صحیح نسبت دهیم.
و چگونه توابع Time Intelligence را با تقویم صحیح به کار بگیریم.
دیدگاهتان را بنویسید