مقدمه ای بر زمینههای ارزیابی در DAX
در این مقاله، به یکی از پیچیدهترین موضوعات در DAX یعنی مفهوم زمینه ارزیابی (evaluation context) میپردازیم. DAX به عنوان یک ابزار قدرتمند برای پردازش و تجزیه و تحلیل دادهها، نیاز به یک زمینه ارزیابی (Evaluation Context) مناسب دارد تا بتواند دادهها را به درستی تفسیر و تحلیل کند. ما در این مقاله به تشریح مفاهیم بنیادی زمینه ارزیابی (Evaluation Context) و چگونگی تأثیرگذاری آن بر نتایج تحلیل میپردازیم. همچنین، با ارائه مثالهای عملی، نشان میدهیم که چگونه یک زمینه ارزیابی (Evaluation Context) مناسب میتواند به دقت و کارایی در پردازش دادهها کمک کند. این مقاله برای متخصصان و محققانی که در حوزه پردازش دادهها فعالیت میکنند، مفید خواهد بود.
از آنجاییکه درک این موضوع بسیار وابسته به درک مفهوم زمینه است، پس از مفهموم کلمه context شروع میکنیم.
مفهوم کلمه Context در DAX
شاید قبلاً کلمه context را شنیدهاید، اما شاید نه در رابطه با DAX. مفهوم context در زبان بسیار مهم است؛ یک کلمه یا عبارت میتواند در contextهای مختلف معانی متفاوتی داشته باشد. برای مثال، عبارت "حفظ کردن" را در نظر بگیرید. حالا، این عبارت چه معنایی میدهد؟ خوب، این بستگی به context دارد:
- اگر شخصی در یک کلاس درس ادبیات حضور داشته باشد و بگوید «شعری از مولانا را حفظ کردهام»، شکی در آن نیست که منظور این است که آن شعر را به خاطر سپرده است.
- اما اگر یک ورزشکار حرفهای از عبارت «رکورد خود را حفظ کردهام» استفاده کند احتمالاً متوجه میشوید که او به این معنا اشاره دارد که همچنان دارای رکورد یا جایگاه قبلی خود میباشد.
نکته موردنظر این است که یک عبارت مشابه میتواند در contextهای مختلف معانی متفاوتی داشته باشد. در DAX نیز یک فرمول (مانند یک مژر) میتواند در contextهای مختلف نتایج متفاوتی داشته باشد؛ بنابراین در DAX از اصطلاح زمینه ارزیابی (Evaluation Context) استفاده میشود. زمینه ارزیابی (Evaluation Context) به این معنی است که هر فرمول میتواند در شرایط مختلف نتایج متفاوتی داشته باشد. در DAX، دو نوع زمینه ارزیابی (Evaluation Context) یا شرایط وجود دارد که میتوانند باعث شوند یک فرمول نتایج متفاوتی داشته باشد: زمینه فیلتر (Filter Context) و زمینه سطر (Row Context).
مفهوم زمینه فیلتر (Filter Context) در DAX
هنگامی که یک مژر برای محاسبه تعداد کل محصولات مینویسید و آن را درون یک کارت در صفحهای خالی از نمودار قرار میدهید؛ از آنجاییکه هیچگونه فیلتری بر روی مژر اعمال نمیشود، کارت مقدار کل را نمایش میدهد.
ولی اگر همین مژر را درون یک ماتریس شامل گروههای مختلف محصول قرار دهید، این نمودار میتواند مدل داده را فیلتر کند و همان مژر نوشته شده در مرحله قبل، تعداد محصولات هر گروه را بصورت جداگانه محاسبه میکند. یعنی این مژر به ازای هر سطر در ماتریس (گروه محصول) فیلتر میشود.
درواقع نتیجه نهایی رفتار فیلترینگ همین است که یک مژر واحد میتواند به دلیل فیلترهایی که از نمودار میآیند، نتایج متفاوتی داشته باشد.
مثال قبل، تاثیر نمودارهای Power BI را در رفتار فیلترینگ بیان میکند. موارد دیگری هم موجب فعال شدن این رفتار میشوند که در ادامه به این موارد میپردازیم.
یکی دیگر از منابع فعال شدن رفتار فیلترینگ، روابط تعریف شده در مدل داده است. برای درک این نوع اثرگذاری به مثال زیر توجه کنید.
ماتریس زیر را در نظر بگیرید.
ستون Category در این ماتریس (شماره 1 در تصویر بالا) ستونی است که از جدول Products در مدل داده میآید. هنگامی که ستون Category توسط نمودار فیلتر میشود، کل جدول Products نیز فیلتر میشود. به همین دلیل است که میتوانید نتایج متفاوتی برای مژر [Total Number of Products] در هر سطر ماتریس ببینید. اما جدول Products جدول Customers را به صورت مستقیم یا غیرمستقیم فیلتر نمیکند. تصویر زیر نمای مدل را دوباره نشان میدهد تا بتوانید این موضوع را تایید کنید.

توجه داشته باشید که جدول Products (شماره 1 در تصویر بالا) بطور مستقیم جدول Sales (شماره 3) را از طریق رابطه فیلتر میکند. میتوانید این موضوع را بر اساس جهت رابطه از Products به Sales (شماره 2) تایید کنید. اما جدول Sales جدول Customers را فیلتر نمیکند؛ هیچ رابطهای از جدول Sales به جدول Customers وجود ندارد، بنابراین میدانید که جدول Customers فیلتر نمیشود. این به این معنی است که مژر [Total Number of Customers] برای هر سطر در نمودار ماتریس بالا، نتایج متفاوتی نشان نمیدهد.
این رفتار فیلترینگ که در اینجا توضیح داده شده است، یک نام رسمی دارد: زمینه فیلتر (Filter Context). زمینه فیلتر به هر نوع فیلترینگی اشاره دارد که بر مدل داده در Power BI اعمال میشود. زمینه فیلتر میتواند توسط یک نمودار (مانند یک ماتریس) و همچنین توسط تابع CALCULATE ایجاد شود. زمینه فیلتر اولیه، رفتار فیلترینگ طبیعی است که از یک نمودار یا گزارش میآید. زمینه فیلتر اولیه میتواند از موارد زیر حاصل شود:
• سطرهای یک نمودار، مانند یک ماتریس (شماره 1)
• ستونها در یک ماتریس (شماره 2)
• فیلترها در پنل فیلترها (شماره 3)
• نمودار اسلایسر (شماره 4)
• هر نمودار دیگر در صفحه، مانند یک نمودار ستونی (شماره 5)
زمینه فیلتر اولیه رفتار فیلترینگ طبیعی است که از ترکیب همه فیلترهای اعمال شده در یک گزارش حاصل میشود.
زمینه فیلتر ناشی از یک گزارش، جداول زیرین در مدل داده را فیلتر میکند. اگر جداول به هم متصل باشند، فیلترها بطور خودکار از سمت یک به سمت چند در روابط انتشار مییابند، اما نمیتوانند بطور خودکار از سمت چند به یک انتشار یابند. به همین دلیل است که توصیه میشود مدل داده را با استفاده از روش Collie layout سازماندهی کنید (همانطور که در تصویر زیر آورده شده است)، که در آن جداول را بهگونهای چیدمان میکنید که جداول Dimension در بالا و جداول Fact در پایین قرار بگیرند. با استفاده از این چیدمان، کمتر گیج میشوید و میتوانید بهراحتی فیلترها را مانند آبشار که در آن آب به سمت پایین جریان مییابد، تصور کنید: جداول بالایی جداول پایین را فیلتر میکنند، اما جداول پایین جداول بالایی را فیلتر نمیکنند.
این تنظیمات یک نشانه بصری فراهم میکند که نشان میدهد فیلترها از طریق روابط به سمت پایین جریان مییابند، اما از طریق روابط به سمت بالا جریان نمییابند.
بطور خلاصه، زمینه فیلتر از منابع زیر حاصل میشود:
• یک نمودار در یک گزارش
• پنل فیلترها در یک گزارش
• تابع &CALCULATE
اگر یک نمودار دارای فیلتر نداشته باشید و تابع CALCULATE را استفاده نکنید، پس زمینه فیلتر ندارید؛ به همین سادگی.
توجه: با استفاده از DAX، میتوان جداول Dimension (جداول بالایی در طرح چیدمان Collie) را بر اساس نتایج در یک جدول Fact (جداول پایین) فیلتر کرد، به جای اینکه فیلتر بهصورت خودکار منتشر شود. همچنین میتوان با تغییر روابط، جهت پیشفرض فیلتر را تغییر داد. اما بطور پیشفرض، فیلترها از سمت یک به سمت چند انتشار مییابند و این روش بهترین است، مگر اینکه دلیل خوبی برای تغییر آن وجود داشته باشد.
مفهوم زمینه سطر (Row Context) در DAX
در مقالهای ستونهای محاسباتی را معرفی کردیم و توضیح دادیم که یک ستون محاسباتی مهارت خاصی دارد: توانایی ارزیابی سطر به سطر. هنگامی که یک ستون محاسباتی در DAX مینویسید، یک فرمول واحد برای کل ستون مینویسید و این فرمول یک بار برای هر سطر جدول ارزیابی میشود.
تصویر زیر، یک جدول را در نمای داده (نه یک ماتریس در گزارش) نشان میدهد.
ستون سوم در جدول بالا یک ستون محاسباتی است که با یک فرمول واحد تعریف شده است:
با وجود یک فرمول واحد، نتیجه متفاوتی برای هر سطر در ستون جدید این جدول وجود دارد. این فرمول واحد به دلیل این که ستون محاسباتی توانایی ارزیابی سطر به سطر را دارد، کار میکند. این فرمول بارها و بارها و برای هر سطر جدول ارزیابی میشود و اغلب نتایج متفاوتی را برای هر سطر برمیگرداند. باید دوباره به این جمله بازگردیم: هر فرمول بر اساس context به نتایج متفاوتی منجر میشود. در مورد ستونهای محاسباتی، این context زمینه سطر نامیده میشود. زمینه سطر صرفاً آگاهی و توانایی DAX برای انجام ارزیابی سطر به سطر است که اغلب نتایج متفاوتی را در هر بار ارزیابی برمیگرداند.
برخی توابع DAX خاص نیز توانایی ارزیابی سطر به سطر را دارند (یعنی دارای زمینه سطر هستند). همه توابعی که در انتهای نام خود دارای حرف X هستند، مانند SUMX و AVERAGEX، دارای زمینه سطر هستند، درست مانند یک ستون محاسباتی. (برخی توابع دیگر نیز این ویژگی را دارند مانند توابع FILTER، ALL، ADDCOLUMNS و ... .)
وقتی به زمینه سطر فکر میکنید، تصور کنید که تابع (یا ستون محاسباتی) جدول را سطر به سطر پیمایش میکند و در هر لحظه روی یک سطر توقف میکند و مقادیر موجود در تقاطع بین ستونها و سطر جاری را انتخاب میکند و سپس عملیات تابع را بر روی این مقادیر جاری انجام میدهد. در تصویر بالا سطر جاری با کادر رنگی مشخص شده و عملیات تابع روی مقادیر موجود در هر ستونِ مربوط سطر جاری (شماره 1 و شماره 2) انجام میشود. مژرهای معمولی نمیتوانند این کار را انجام دهند؛ فقط توابعی که دارای زمینه سطر هستند و ستونهای محاسباتی، میتوانند این کار را انجام دهند.
به مژر زیر دقت کنید:
این مژر یک خطا برمیگرداند؛ این نحوه نوشتن مژر در DAX درست نیست. این فرمول شامل ستونهای برهنه است—ستونهایی که هیچ تابعی اطراف آنها نیست. در DAX، نوشتن یک مژر که دارای ستونهای برهنه باشد، قانونی نیست. چنین مژری خطا برمیگرداند. هر فرمولی در DAX میتواند نتایج متفاوتی را بر اساس context داشته باشد؛ در این مورد، نتیجه یک خطا است زیرا برای این فرمول بعنوان یک مژر هیچ زمینه سطری وجود ندارد.
بیایید به مژر SUMX که در زیر نشان داده شده است، نگاهی بیندازیم.
تابع SUMX ابتدا زمینه سطر را بر روی جدول Sales ایجاد میکند (شماره 1 در تصویر بالا). سپس این تابع جدول را سطر به سطر پیمایش میکند. در هر سطر، مقدار موجود را که در تقاطع بین ستون Sales[ExtendedAmount] (شماره 2) و سطر جاری قرار دارد، انتخاب میکند و آن را به مقداری که در تقاطع بین ستون Sales[TaxAmt] (شماره 3) و سطر جاری قرار دارد، اضافه میکند. این کار را برای هر سطر جدول (شماره 1) انجام میدهد و سپس همه این مقادیر را جمع میکند (شماره 4). ستونهای مشابه از مثال مژر قبلی اکنون در داخل تابع SUMX که دارای زمینه سطر است، نتایج معتبر برمیگردانند.
اگر نمیتوانید تصور کنید که تابعSUMXدر فرمول بالا چه کاری انجام میدهد، میتوانید یک فرمول آزمایشی به عنوان یک ستون محاسباتی بنویسید تا بطور بهتر بتوانید ببینید چگونه کار میکند. یک ستون محاسباتی و یک تابع SUMX اساساً کار مشابهی انجام میدهند؛ تنها تفاوت این است که یک ستون محاسباتی نتایج سطر به سطر را بطور دائمی در جدول ذخیره میکند، در حالی که تابعSUMX این کار را انجام نمیدهد.
توجه: یکی از چیزهایی که اغلب در مورد فرمول بالا از چالشبرانگیز است، این است که «چرا فرمول دو بار به نام جدول اشاره میکند—یک بار در آرگومان ورودی اول و بار دوم در آرگومان ورودی دوم؟» به یاد داشته باشید که بهترین شیوه همیشه این است که نام ستون را با پیشوند نام جدول کامل کنید. از نظر فنی، فرمول بالا بدون وجود نام جدول در کنار ستونهای ExtendedAmount (شماره 2) و TaxAmt (شماره 3)، نیز کار میکند، اما ذکر کردن نام جدول در نگارش فرمول، بهترین شیوه است که وضوح و خوانایی کد را تضمین میکند.
زمینه سطر بطور خودکار زمینه فیلتر را ایجاد نمیکند.
این نکته بسیار مهم است که باید بطور واضح آن را درک کنید: زمینه سطر بطور خودکار زمینه فیلتر را ایجاد نمیکند. زمینه سطر و زمینه فیلتر دو مفهوم کاملاً مستقل هستند. برای درک بهتر این موضوع، میتوانید یک ستون محاسباتی جدید در جدول Products ایجاد کنید. در نمای داده، به پنل Fields بروید، روی جدول Products راست کلیک کنید (شماره 1 در تصویر زیر) و گزینه New Column (شماره 2) را انتخاب کنید.
یک ستون محاسباتی جدید به جدول اضافه کنید، اما هنوز Enter را فشار ندهید:
فکر میکنید چه مقداری در هر سطر این ستون جدید ظاهر میشود وقتی Enter را فشار میدهید؟ آیا انتظار دارید که مجموع برای محصول در هر سطر باشد؟ آیا انتظار دارید که مجموع برای همه محصولات باشد؟ خب، پاسخ ممکن است شما را غافلگیر کند و مستقیماً با این نکته مرتبط است که زمینه سطر بطور خودکار زمینه فیلتر را ایجاد نمیکند. Enter را فشار دهید و به ستون جدید در نمای داده نگاه کنید.
همانطور که در شکل زیر میبینید، مقدار برای هر سطر جدول یکسان است. هیچ فیلتری بر روی جدول Sales (یا هر جدول دیگری) بعنوان نتیجه این فرمول وجود ندارد و بنابراین پاسخ همیشه برای هر سطر یکسان است. در این فرمول زمینه سطر وجود دارد زیرا یک ستون محاسباتی است: سطرها بهصورت یک به یک ارزیابی میشوند. اما این زمینه سطر زمینه فیلتر را ایجاد نمیکند. دوباره تأکید میشود که زمینه سطر هیچ ارتباطی با زمینه فیلتر ندارد؛ زمینه سطر و زمینه فیلتر مفاهیم کاملاً مستقل هستند. در ابتدای این مطلب یاد گرفتید که زمینه فیلتر فقط از سه منبع میآید: یک نمودار، پنل فیلترها یا تابع CALCULATE. در ستون محاسباتی مثال نشان داده شده، هیچ نموداری یا تابع CALCULATEوجود ندارد، بنابراین به هیچ وجه زمینه فیلتری وجود ندارد. با توجه به اینکه زمینه فیلتر وجود ندارد، جدول Sales کاملاً بدون فیلتر است و بنابراین SUM(Sales[ExtendedAmount]) باید نتیجه جدول Sales بدون فیلتر را برگرداند. به ظاهر اینگونه به نظر نمیرسد، اما همینطور کار میکند.
معرفی انتقال زمینه (Context Transition) در DAX
این امکان در DAX وجود دارد که زمینه سطر یک ستون محاسباتی را به زمینه فیلتر تبدیل کنید، از طریق فرآیندی به نام انتقال زمینه. برای انجام این کار، کافی است فرمول را در داخل تابع CALCULATE قرار دهید، همانطور که در زیر نشان داده شده است:
وقتی این کار را انجام میدهید، زمینه سطر موجود در ستون محاسباتی به یک زمینه فیلتر معادل تبدیل میشود. سپس تابع CALCULATE به فیلتر Power BI میگوید که یک فیلتر جدید بر روی جدول Products اضافه کند، و سپس قبل از اینکه محاسبه تکمیل شود این فیلتر از طریق رابطه به جدول Sales منتقل میشود، و این کار برای هر سطر جدول تکرار میشود. بنابراین، ممکن است به ازای هر سطر در این ستون محاسباتی جدید مقادیر متفاوتی داشته باشید. مقدار بازگشتی در هر سطر از این ستون جدید در جدول Products برابر با مجموع فروش برای همان محصول است.
توجه: برخی از سطرها خالی هستند زیرا هیچ فروشی برای آن محصول ثبت نشده است.
میتوانید فرمول را به این شکل تصور کنید:
این شبهکد صرفاً برای توضیح است، اما نشان میدهد که مفهوم انتقال زمینه در هر جایی که زمینه سطر وجود دارد کار میکند—یعنی در ستونهای محاسباتی و همچنین در توابع پیماشگر مانند SUMX و توابع دیگری که در تعدادی از آنها قبلا در همین مقاله معرفی شدند. درواقع وجود هیچ فیلتری در داخل CALCULATE لازم نیست، بلکه صرف استفاده از تابع CALCULATE انتقال زمینه را انجام میدهد و یک زمینه فیلتر جدید ایجاد میکند که معادل با زمینه سطر جاری میباشد. هرچند که درصورت لزوم میتوانید فیلترهای اضافی را در داخل CALCULATE استفاده کنید، اما توجه داشته باشید که هیچ فیلتری برای فعال کردن انتقال زمینه لازم نیست.
اثر ضمنی و پنهان CALCULATE
حالا که آموختید میتوانید از CALCULATE برای تبدیل زمینه سطر به زمینه فیلتر استفاده کنید، یک نکته دیگر نیز باید بدانید. به این مژر فکر کنید:
حالا به اولین نسخه ستون محاسباتی در بالا فکر کنید. چه اتفاقی میافتد وقتی ستون جدیدی در جدول Products اضافه کنیم، به این شکل؟
مقدار ۲۹.۴ میلیون دلار در تمام طول ستون جدید در جدول Products به دست خواهد آمد. چرا؟ زیرا در یک ستون محاسباتی زمینه سطر وجود دارد، اما زمینه فیلتر وجود ندارد. بنابراین جدول Sales کاملاً بدون فیلتر است و در نتیجه SUM (Sales[ExtendedAmount]) باید برای هر سطر ۲۹.۴ میلیون دلار برگرداند.
حالا به این مژر برگردید:
توجه کنید که فرمول این مژر با فرمول ستون محاسباتی (اولین مثال در بالا اما با نام متفاوت) یکسان است. بنابراین اگر فرمول داخل مژر با فرمول در ستون محاسباتی یکسان باشد، میتوانید تصور کنید که میتوانید فرمول در ستون محاسباتی را با مژر واقعی جایگزین کنید، به این شکل:
اگر مژر [Total Sales] همین فرمول را دارد، آیا نتیجه یکسان نخواهد بود؟ خب، در واقع نه؛ نتیجهای متفاوت از قبل به دست میآید، همانطور که در زیر نشان داده شده است.
برگردید و دوباره نگاه کنید. خلاصهای از آنچه پیدا میکنید این است:
این ستون محاسباتی ۲۹.۴ میلیون دلار را در تمام طول ستون برمیگرداند. در این ستون زمینه سطر وجود دارد، اما زمینه فیلتر وجود ندارد، بنابراین فرمول باید برای هر سطر در جدول ۲۹.۴ میلیون دلار برگرداند.
ستون محاسباتی بعدی مجموع فروش به ازای هر محصول در جدول Products را برمیگرداند (یک عدد متفاوت برای هر محصول):
در این ستون به دلیل وجود تابع CALCULATE ، زمینه سطر وجود دارد و زمینه سطر به یک زمینه فیلتر معادل از طریق فرآیند انتقال زمینه تبدیل میشود؛ در اینجا هم زمینه سطر و هم زمینه فیلتر وجود دارد. CALCULATE زمینه سطر را از جدول Products به یک زمینه فیلتر معادل تبدیل میکند، و این زمینه فیلتر جدید برای هر سطر در ستون محاسباتی به جدول Sales منتقل میشود.
ستون محاسباتی بعدی نیز مجموع فروش به ازای هر محصول در جدول Products را برمیگرداند:
ستون محاسباتی Total Sales Column (Ver 3) دقیقاً همان نتیجهای را که Total Sales Column (Ver 2) دارد، برمیگرداند. اگر داخل مژر [Total Sales] را نگاه کنید، نمیتوانید تابع CALCULATE را ببینید؛ این مژر شامل CALCULATEنیست. اما یک CALCULATE ضمنی وجود دارد که نمیتوانید آن را ببینید. نکته مهم اینست که هر مژر بطور خودکار یک CALCULATE ضمنی دارد که توسط Power BI اضافه میشود؛ به همین دلیل است که Total Sales Column (Ver 3) مانند Total Sales Column (Ver 2) رفتار میکند و نه مانند Total Sales Column (Ver 1).
توجه: اگر در این مرحله هستید و کاملاً انتقال زمینه را درک نکردهاید، نگران نباشید، تنها نیستید. این یکی از سختترین موضوعات در زبان DAX برای یادگیری و درک کامل است. چند شب روی آن فکر کنید، تمرین کنید و سپس به این مقاله برگردید و دوباره آن را بخوانید (همراه مقاله دیگر در مورد تابع CALCULATE ). ممکن است نیاز داشته باشید که این محتوا را چندین بار بخوانید تا کاملاً جا بیفتد. پس ناامید نشوید و استمرار داشته باشید.
خلاصه و جمعبندی
در این مقاله، مفهوم بنیادین زمینه ارزیابی (Evaluation Context) در DAX به عنوان کلیدی برای درک نحوه ارزیابی فرمولها و دستیابی به نتایج دقیق مورد بررسی قرار گرفت. زمینه ارزیابی (Evaluation Context) تعیین میکند که یک فرمول در چه شرایطی ارزیابی میشود و شامل دو نوع اصلی است: Filter Context و Row Context.
Filter Context: ناشی از فیلترهای اعمال شده بر روی مدل داده است، چه از طریق عناصر بصری گزارش (مانند نمودارها و اسلایسرها) و چه از طریق توابع DAX مانند CALCULATE . Filter Context تعیین میکند که کدام زیرمجموعه از دادهها در محاسبات مورد استفاده قرار میگیرد.
Row Context: مربوط به توانایی DAX در ارزیابی سطر به سطر در ستونهای محاسباتی و توابعی مانند SUMX است. Row Context به فرمول اجازه میدهد تا در هر سطر از جدول، مقادیر مربوط به همان سطر را در محاسبات لحاظ کند.
یکی از نکات کلیدی در درک زمینههای ارزیابی، مفهوم انتقال زمینه (Context Transition) است. این فرآیند به وسیله تابع CALCULATE انجام میشود و به DAX اجازه میدهد تا Row Context را به Filter Context تبدیل کند. این تبدیل برای انجام محاسبات پیچیدهتر که نیازمند فیلتر کردن دادهها بر اساس مقادیر موجود در سطرها هستند، ضروری است.
در نهایت، این موضوع را مدنظر داشته باشید که مژرها به طور ضمنی دارای یک تابع CALCULATE هستند. این بدان معناست که حتی اگر در تعریف یک مژر از CALCULATE استفاده نشود، DAX به طور خودکار یک Context Transition را اعمال میکند. درک این رفتار ضمنی برای جلوگیری از اشتباهات رایج در نوشتن فرمولهای DAX و دستیابی به نتایج صحیح ضروری است.
منبع کتاب:
Matt Allington - Supercharge Power BI_ Power BI is Better When You Learn To Write DAX (2021, Holy Macro! Books)