نحوه اجرای تابع IF در DAX | آموزش بهینهسازی کدهای DAX در Power BI
DAX یک زبان تابعی (Functional) است. این یعنی بدون توجه به میزان پیچیدگی فرمول، هر Measure در نهایت تنها یک فراخوانی تابع (Function Call) است. سپس این تابع ممکن است توابع دیگری را فراخوانی کند و همین زنجیره، ساختارهای پیچیده و حرفهای DAX را شکل میدهد. با این حال، همیشه فقط یک تابع در سطح بالا (Top-Level) وجود دارد. این ویژگی از یک سو بسیار زیبا و منظم است، و از سوی دیگر میتواند درک آن را دشوار و چالشبرانگیز کند. در واقع، همین ماهیت تابعی است که DAX را به زبانی جذاب و منحصربهفرد تبدیل کرده است.
اما وقتی یک فرمول DAX اجرا میشود، ماهیت تابعی خود را از دست میدهد. چرا که در نهایت این فرمول باید به مجموعهای از کوئریهای سادهتر تبدیل شود تا توسط یکی از موتورهای DAX اجرا گردد: موتور ذخیرهسازی (Storage Engine) یا موتور فرمول (Formula Engine). در این مرحله، نحوه اجرای توابع بازنویسی میشود و به شکل بسیار سادهتری در میآید.
فهرست مطالب
معرفی الزامات محاسباتی در DAX
یکی از موضوعات جالب و مهم در DAX، درک عمیقتر فرآیند تبدیل خودکار مقادیر اندازهگیری (Measure) به اعداد نهایی است. در این مقاله تمرکز ما روی تابع IF خواهد بود؛ تابعی که احتمالاً یکی از پرکاربردترین توابع در DAX محسوب میشود. با این حال، همانطور که خواهید دید، رفتار آن به آن سادگی و شهودی که به نظر میرسد نیست.
در مثالی که در این گزارش آورده شده، دو Measure تعریف شدهاند:
Sales Amount (مقدار فروش)
Discounted Sales (فروش با تخفیف)
ایده پشت Discounted Sales این است که اگر مقدار فروش بیشتر از یک میلیون باشد، ۵٪ تخفیف روی آن اعمال شود.
Sales Amount = SUMX ( Sales, Sales[Quantity] * Sales[Net Price] )
Discounted Sales =
IF (
[Sales Amount] >= 1E6,
[Sales Amount] * 0.95,
[Sales Amount]
)
در این گزارش، مقادیر Measureها بر اساس ستون Product[Brand] فیلتر (Slice) شدهاند. بنابراین، یک روش طبیعی برای درک نحوه اجرای کوئری توسط DAX به این شکل خواهد بود:
ابتدا مقدار فروش (Sales Amount) برای هر برند محاسبه میشود، و سپس دستور IF روی هر سطر اجرا میگردد تا بررسی شود که آیا این مقدار بیشتر از یک میلیون است یا خیر.
اگر این شرط برقرار باشد، همان مقدار فروش به عنوان نتیجه بازگردانده میشود.
اما اگر مقدار فروش کمتر از یک میلیون باشد، ۵٪ تخفیف از آن کسر میشود و مقدار نهایی پس از اعمال تخفیف نمایش داده میشود.
به عبارت دیگر، تابع IF در این سناریو برای هر برند، یک بار اجرا میشود و منطق شرطی بر اساس مقدار هر برند ارزیابی میگردد.
با وجود اینکه این تصور شهودی به نظر میرسد، اما واقعیت با آن تفاوت دارد. این برداشت نادرست باعث سردرگمی قابلتوجهی میشود، بهویژه هنگام محاسبه مقادیر کل (Total).
در حقیقت، این تفکر اشتباه منجر به این تصور میشود که مجموع کلی، برابر است با مجموع مقادیر محاسبهشده برای هر برند بهصورت جداگانه. اما همانطور که در مقالهای دیگر نیز به تفصیل توضیح دادهایم، این یک اشتباه رایج در درک نحوه محاسبه Totals در Power BI است.
تحلیل نحوه اجرای DAX
هدف اصلی زبان DAX، سرعت بالا در اجرای محاسبات است. این به معنی کاهش تعداد کوئریهای اجرایی و بهکارگیری راهکارهایی است که از نظر آماری عملکرد بهتری دارند.
در فرمولی که نوشتهایم، مقدار Sales Amount در هر دو شاخهی تابع IF استفاده شده است. ولی در بسیاری از سناریوها، عبارات موجود در دو شاخه (then و else) با یکدیگر تفاوت اساسی دارند و ممکن است از Measureهای مختلفی استفاده کنند.
نکته مهم اینجاست که هر بار که DAX بخواهد Measure مربوط به Sales Amount را محاسبه کند، نیاز دارد جدول Sales را اسکن کند، و این ممکن است یک عملیات سنگین و زمانبر باشد. بنابراین، موتور DAX تلاش میکند تا تعداد دفعات اسکن جدول را به حداقل برساند.
نحوه عملکرد تابع IF در DAX
تابع IF در DAX سه آرگومان دارد:
شرط (if condition)
عبارت در صورت برقرار بودن شرط (then expression)
عبارت در صورت برقرار نبودن شرط (else expression)
برای محاسبه نتیجه، DAX مراحل زیر را طی میکند:
محاسبه اجزای شرط IF (در این مثال، Sales Amount برای همه برندها محاسبه میشود).
تقسیم برندها به دو گروه:
برندهایی که شرط برقرار است (Sales Amount ≥ 1 میلیون)
برندهایی که شرط برقرار نیست (Sales Amount < 1 میلیون)
محاسبه عبارت شاخه “then” برای گروه اول
محاسبه عبارت شاخه “else” برای گروه دوم
ترکیب نتایج و تولید خروجی نهایی
برای بررسی دقیقتر این رفتار، میتوانیم از Execution Plan (برنامه اجرایی کوئری) استفاده کنیم. کوئریای که Power BI برای تولید گزارش اجرا میکند، تقریباً مشابه کوئری سادهسازیشده زیر است (که در ادامه مقاله ارائه میشود).
EVALUATE
SUMMARIZECOLUMNS (
ROLLUPADDISSUBTOTAL ( 'Product'[Brand], "IsGrandTotalRowTotal" ),
"Sales Amount", [Sales Amount],
"Discounted Sales", [Discounted Sales]
)
زمانی که این فرمول اجرا میشود، موتور DAX پنج کوئری متفاوت در Storage Engine تولید میکند. یکی از این کوئریها (خط ۶) صرفاً لیستی از برندها از جدول Product
را بازمیگرداند که میتوان آن را نادیده گرفت.
اولین کوئری مهم، مقدار فروش (Sales Amount) را بر اساس Product[Brand]
بازیابی میکند.
گام دوم در اجرای فرمول، یعنی بررسی شرط تابع IF، توسط Formula Engine (موتور فرمول) انجام میشود.
در این مرحله، Formula Engine تشخیص میدهد کدام برندها شرط را برقرار میکنند و کدام برندها آن را نقض میکنند. سپس، براساس این دستهبندی، دو کوئری جداگانه به Storage Engine ارسال میکند.
نکته مهم در اینجاست که در بخش WHERE
این کوئریها، میتوان دید:
کوئری اول مربوط به گروه “then” است (برندهایی که شرط برقرار است)
کوئری دوم مربوط به گروه “else” است (برندهایی که شرط برقرار نیست)
در ادامه، این کوئری مربوط به شاخه “then” را مشاهده میکنیم:
این سازوکار دقیق نشان میدهد که برخلاف تصور رایج، تابع IF یک شرط ساده نیست که روی هر سطر جداگانه اعمال شود، بلکه DAX از یک ساختار بهینهسازیشده برای دستهبندی دادهها و کاهش بار پردازشی استفاده میکند.
WITH
$Expr0 := ( PFCAST ( 'Sales'[Quantity] AS INT ) * PFCAST ( 'Sales'[Net Price] AS INT ) )
SELECT
'Product'[Brand],
SUM ( @$Expr0 )
FROM 'Sales'
LEFT OUTER JOIN 'Product'
ON 'Sales'[ProductKey]='Product'[ProductKey]
WHERE
'Product'[Brand] IN ( 'Contoso', 'Adventure Works', 'The Phone Company' ) ;
و این هم کوئری مربوط به شاخه “else”
WITH
$Expr0 := ( PFCAST ( 'Sales'[Quantity] AS INT ) * PFCAST ( 'Sales'[Net Price] AS INT ) )
SELECT
'Product'[Brand],
SUM ( @$Expr0 )
FROM 'Sales'
LEFT OUTER JOIN 'Product'
ON 'Sales'[ProductKey]='Product'[ProductKey]
WHERE
'Product'[Brand] IN ( 'Wide World Importers', 'Northwind Traders', 'Southridge Video',
'Litware', 'Fabrikam', 'Proseware', 'A. Datum', 'Tailspin Toys' ) ;
آخرین کوئری که توسط Storage Engine اجرا میشود، مقدار Sales Amount (مقدار فروش) را بدون اعمال فیلتر یا برش بر اساس برند (Brand) بازیابی میکند.
این کوئری نقش بسیار مهمی دارد، چرا که از آن برای محاسبهی مقدار کل (Total) در پایین ماتریس استفاده میشود.
به عبارت دیگر، برخلاف سایر کوئریهایی که هرکدام برای برند خاصی مقدار را محاسبه میکردند، این کوئری کل فروش را بهصورت تجمیعی و در سطح کلی دادهها برمیگرداند تا مقدار نهایی در قسمت Total گزارش بهدرستی نمایش داده شود.
WITH
$Expr0 := ( PFCAST ( 'Sales'[Quantity] AS INT ) * PFCAST ( 'Sales'[Net Price] AS INT ) )
SELECT
SUM ( @$Expr0 )
FROM 'Sales';
برای درک بهتر نحوه اجرای تابع IF در DAX، پیشنهاد میشود فرمولهای مختلفی را در مدل دادهای خود امتحان کنید. این کار کمک میکند تا دقیقتر ببینید که DAX چگونه منطق شرطی را پیادهسازی میکند.
چالش عملکرد: چند بار اسکن جدول Sales
اگرچه این الگوریتم در بسیاری از سناریوهای عمومی بسیار کارآمد عمل میکند، اما در مثال ما نیاز به سه بار اسکن جدول Sales دارد (بهعلاوه یک بار دیگر برای محاسبه Total).
اما از دید یک توسعهدهنده حرفهای DAX، میدانیم که این محاسبه را میتوان با تنها یک بار اسکن جدول Sales انجام داد، چرا که مقدار Sales Amount در هر دو شاخهی تابع IF استفاده شده است.
راهکار پیشنهادی برای بهینهسازی: استفاده از متغیر (Variable)
برای بهبود عملکرد این کد، میتوانیم به DAX کمک کنیم تا کارآمدتر عمل کند؛ با استفاده از متغیر (VAR). این تکنیک باعث میشود مقدار Sales Amount
فقط یکبار محاسبه شود و سپس در هر دو شاخه استفاده شود، بدون نیاز به اسکن مجدد جدول.
Discounted Sales 2 =
VAR SalesAmount = [Sales Amount]
RETURN
IF (
SalesAmount >= 1E6,
SalesAmount * 0.95,
SalesAmount
)
بهینهسازی با استفاده از متغیر در DAX
با استفاده از متغیر (VAR)، به صورت واضح اعلام میکنیم که میخواهیم مقدار Measure Sales Amount را برای همه برندها محاسبه کنیم، بدون توجه به شرط تابع IF.
این اطلاعات به DAX اجازه میدهد که برنامه اجرایی کوئری (Query Plan) را تغییر دهد و شرط IF را در Formula Engine برای هر برند، با استفاده از مقدار موجود Sales Amount، اجرا کند.
همانطور که مشاهده میکنید، اکنون فقط یک کوئری Storage Engine وجود دارد که مقدار Sales Amount را بر اساس برند تجمیع میکند، در مقابل دو کوئری جداگانهای که در نسخه قبلی داشتیم.
همچنین دقت کنید که کوئری دوم Storage Engine که در این تصویر مشاهده میشود، معادل همان کوئری مجموع کل (Total) است که در مثال قبلی به آن اشاره شد.
به یاد داشته باشید که در این سناریوی خاص، استفاده از متغیر باعث کاهش تعداد دفعات اسکن جدول میشود.
اما این به این معنی نیست که همیشه استفاده از متغیر بهترین راهکار است.
بسته به عوامل مختلفی مانند:
پیچیدگی Measure
حجم مدل دادهای شما
تعداد مقادیر موجود در ستونهای مورد استفاده برای گروهبندی
ممکن است یک روش جایگزین بهتر و مناسبتر باشد.
بنابراین، بهبود کد DAX همیشه نیازمند آزمون و تست گسترده است.
نتیجهگیری
در این مقاله دیدیم که چگونه تابع IF زمانی که به عنوان یکی از توابع سطح بالا در یک Measure به کار میرود، اجرا میشود. البته ممکن است رفتارها متفاوت باشد، مخصوصاً اگر IF درون یک حلقه (Iteration) یا سناریوهای پیچیدهتر استفاده شود.
دیدگاهتان را بنویسید