بازگشت جانبی (Sideways Recursion) در گروههای محاسباتی DAX
آیتمهای محاسباتی DAX بازگشت کامل (Recursion) را فراهم نمیکنند. با این حال، یک نوع محدود از بازگشت وجود دارد که به آن بازگشت جانبی (Sideways Recursion) گفته میشود. در این مقاله، این موضوع پیچیده را با مثالهای عملی توضیح میدهیم. ابتدا بیایید بفهمیم بازگشت چیست و چرا پرداختن به آن اهمیت دارد.
بازگشت زمانی رخ میدهد که یک آیتم محاسباتی به خود ارجاع دهد، که این میتواند منجر به یک حلقه نامتناهی در اعمال آیتمهای محاسباتی شود .
برای روشن شدن موضوع، یک گروه محاسباتی Time Intelligence را در نظر بگیرید که دو آیتم محاسباتی به شکل زیر تعریف شدهاند:
YTD =
CALCULATE (
SELECTEDMEASURE (),
DATESYTD ( 'Date'[Date] )
)
SPLY =
CALCULATE (
SELECTEDMEASURE (),
SAMEPERIODLASTYEAR ( 'Date'[Date] )
)
نیاز داریم تا یک آیتم محاسباتی سوم اضافه کنیم که جمع سال به امروز سال گذشته (PYTD) را محاسبه کند. این مقدار را میتوان با ترکیب دو تابع Time Intelligence به دست آورد: DATESYTD
و SAMEPERIODLASTYEAR
. آیتم محاسباتی زیر این سناریو را حل میکند:
PYTD=
CALCULATE (
SELECTEDMEASURE (),
DATESYTD ( SAMEPERIODLASTYEAR ( 'Date'[Date] ) )
)
با توجه به سادگی این محاسبه، این راهحل از قبل بهینه است. با این حال، به عنوان یک چالش ذهنی، میتوانیم تلاش کنیم همان کد را به روش دیگری بنویسیم. در واقع، از قبل یک آیتم محاسباتی YTD وجود دارد که جمع سال به امروز را محاسبه میکند؛ بنابراین میتوان به جای ترکیب توابع Time Intelligence در یک فرمول، از این آیتم محاسباتی استفاده کرد. تعریف آیتم محاسباتی PYTD مشابه به شکل زیر است:
PYTD=
CALCULATE (
SELECTEDMEASURE (),
SAMEPERIODLASTYEAR ( 'Date'[Date] ),
'Time Intelligence'[Time calc] = "YTD"
)
آیتم محاسباتی، همان نتیجه تعریف قبلی را به دست میدهد، اما با استفاده از یک تکنیک متفاوت. تابع SAMEPERIODLASTYEAR
زمینه فیلتر را به سال گذشته منتقل میکند، در حالی که محاسبه سال به امروز (YTD) با استفاده از یک آیتم محاسباتی موجود در گروه محاسباتی Time calc به دست میآید: YTD. همانطور که پیشتر اشاره شد، در این مثال، کد کمتر قابل خواندن و پیچیدهتر است. با این حال، میتوان تصور کرد که در سناریوهای پیچیدهتر، توانایی فراخوانی آیتمهای محاسباتی تعریفشده قبلی بسیار کاربردی است—زیرا از تکرار همان کد در اندازهگیریها جلوگیری میکند.
این یک مکانیزم قدرتمند برای تعریف محاسبات پیچیده است، اما با سطحی از پیچیدگی همراه است که باید به خوبی درک شود: بازگشت (Recursion). همانطور که در آیتم محاسباتی PYTD مشاهده کردید، امکان تعریف یک آیتم محاسباتی بر اساس آیتم دیگری از همان گروه محاسباتی وجود دارد. به عبارت دیگر، در یک گروه محاسباتی، برخی آیتمها میتوانند بر اساس دیگر آیتمهای همان گروه تعریف شوند. اگر این ویژگی بدون هیچ محدودیتی در دسترس بود، میتوانست منجر به شرایط بسیار پیچیدهای شود که در آن آیتم محاسباتی A به B وابسته باشد، که آن هم به C وابسته است، و C ممکن است به A وابسته باشد. مثال ساختگی زیر این مسئله را نشان میدهد:
Loop A =
CALCULATE (
SELECTEDMEASURE (),
Infinite[Loop] = "Loop B"
)
Loop B =
CALCULATE (
SELECTEDMEASURE (),
Infinite[Loop] = "Loop A"
)
اگر در یک عبارت مانند مثال زیر استفاده شود، DAX قادر به اعمال آیتمهای محاسباتی نخواهد بود، زیرا آیتم A نیازمند اعمال آیتم B است، که آن هم به نوبه خود نیازمند A است و این روند ادامه مییابد:
CALCULATE (
[Sales Amount],
Infinite[Loop] = "Loop A"
)
برخی از زبانهای برنامهنویسی اجازه میدهند تا وابستگیهای دایرهای مشابه در تعریف عبارات (معمولاً در توابع) استفاده شود، که منجر به تعریفهای بازگشتی میشود. تعریف یک تابع بازگشتی یعنی تابع بر اساس خودش تعریف شده است. بازگشت (Recursion) قدرت بسیار بالایی دارد، اما برای توسعهدهندگان در هنگام نوشتن کد و برای بهینهساز مسیر اجرای بهینه، پیچیدگی زیادی ایجاد میکند.
به همین دلایل، DAX اجازه تعریف آیتمهای محاسباتی بازگشتی کامل را نمیدهد. در DAX، یک توسعهدهنده میتواند به آیتم محاسباتی دیگری از همان گروه محاسباتی ارجاع دهد، اما بدون اینکه همان آیتم محاسباتی دوباره ارجاع داده شود. به عبارت دیگر، میتوان با استفاده از CALCULATE
یک آیتم محاسباتی را فراخوانی کرد، اما آیتم محاسباتی فراخوانی شده نمیتواند به طور مستقیم یا غیرمستقیم آیتم محاسباتی اصلی را فراخوانی کند. این ویژگی بازگشت جانبی (Sideways Recursion) نامیده میشود. هدف آن اجرای بازگشت کامل نیست، بلکه امکان استفاده مجدد از آیتمهای محاسباتی پیچیده را بدون پیچیدگی و قدرت کامل بازگشت فراهم میکند.
توجه داشته باشید که بازگشت ممکن است به دلیل اعمال فیلتر یک Measure روی یک آیتم محاسباتی نیز رخ دهد، نه فقط بین آیتمهای محاسباتی. برای مثال، تعاریف زیر را در نظر بگیرید: Measures (Sales Amount, MA, MB) و آیتمهای محاسباتی (A و B):
Sales Amount = SUMX ( Sales, Sales[Quantity] * Sales[Net Price] )
MA = CALCULATE ( [Sales Amount], Infinite[Loop] = "A" )
MB = CALCULATE ( [Sales Amount], Infinite[Loop] = "B" )
Loop A = [MB]
Loop B = [MA]
آیتمهای محاسباتی به یکدیگر ارجاع نمیدهند. در عوض، آنها به یک Measure ارجاع میدهند که خود به نوبه خود به آیتمهای محاسباتی ارجاع دارد و همین باعث ایجاد یک حلقه نامتناهی میشود. میتوانیم این روند را با دنبال کردن اعمال آیتمهای محاسباتی گام به گام مشاهده کنیم. عبارت زیر را در نظر بگیرید:
CALCULATE (
[Sales Amount],
Infinite[Loop] = "Loop A"
)
اعمال آیتم محاسباتی A نتیجه زیر را تولید میکند:
CALCULATE (
CALCULATE ( [MB] )
)
با این حال، Measure MB به صورت داخلی هم به Sales Amount و هم به آیتم محاسباتی B ارجاع میدهد؛ این Measure معادل کد زیر است:
CALCULATE (
CALCULATE (
CALCULATE (
[Sales Amount],
Infinite[Loop] = "B"
)
)
)
در این مرحله، اعمال آیتم محاسباتی B نتیجه زیر را تولید میکند:
CALCULATE (
CALCULATE (
CALCULATE (
CALCULATE ( [MA] )
)
)
)
دوباره، Measure MA به صورت داخلی هم به Sales Amount و هم به آیتم محاسباتی A ارجاع میدهد و معادل کد زیر است:
CALCULATE (
CALCULATE (
CALCULATE (
CALCULATE (
CALCULATE (
[Sales Amount],
Infinite[Loop] = "A"
)
)
)
)
)
اکنون دوباره به عبارت اولیه بازگشتهایم و احتمالاً وارد یک حلقه نامتناهی از اعمال آیتمهای محاسباتی روی عبارت میشویم، هرچند آیتمهای محاسباتی به یکدیگر ارجاع نمیدهند. در واقع، آنها به یک Measure ارجاع میدهند که خود به آیتمهای محاسباتی ارجاع دارد. موتور DAX به اندازه کافی هوشمند است تا تشخیص دهد که در این حالت یک حلقه نامتناهی وجود دارد. بنابراین، DAX اعمال بازگشت را متوقف کرده و اجرای دوم همان آیتم محاسباتی را نادیده میگیرد. به همین دلیل، آخرین فیلتر روی Infinite[Loop] نادیده گرفته شده و عبارت بدون اعمال هیچ آیتم محاسباتی دیگری ارزیابی میشود. اگر این قابلیت وجود نداشت، DAX باید خطایی مبنی بر تشخیص وابستگی دایرهای صادر میکرد. بنابراین، بازگشت محدود به فراخوانی آیتمهای محاسباتی است که هنوز اعمال نشدهاند؛ به همین دلیل به آن بازگشت جانبی (Sideways Recursion) گفته میشود.
بازگشت جانبی میتواند به عبارات بسیار پیچیدهای منجر شود که خواندن آنها دشوار است و احتمال تولید نتایج غیرمنتظره وجود دارد. بیشتر پیچیدگی آیتمهای محاسباتی با بازگشت جانبی زمانی ظاهر میشود که Measureهایی وجود دارند که به صورت داخلی از CALCULATE
برای اعمال آیتمهای محاسباتی استفاده میکنند—در حالی که کاربران از طریق رابط کاربری ابزار، مانند Slicer در Power BI، آیتم محاسباتی را تغییر میدهند.
پیشنهاد ما این است که استفاده از بازگشت جانبی در کد خود را تا حد امکان محدود کنید، حتی اگر این به معنای تکرار همان کد در چند مکان باشد. تنها در گروههای محاسباتی مخفی میتوانید به طور ایمن روی بازگشت جانبی تکیه کنید، به طوری که توسط کد مدیریت شود و نه کاربران. به خاطر داشته باشید که کاربران Power BI میتوانند Measureهای خود را در گزارش تعریف کنند و بدون آگاهی از موضوع پیچیدهای مانند بازگشت، ممکن است خطا ایجاد کنند بدون اینکه دلیل آن را به درستی درک کنند.
برای استفاده مجدد از همان بخش کد DAX در چند عبارت مختلف، از توابع تعریفشده توسط کاربر در DAX (User-Defined Functions) استفاده کنید، زیرا آنها نیازی به هیچ نوع بازگشت ندارند و توسط موتور DAX به صورت بهینه اجرا میشوند.
دیدگاهتان را بنویسید