<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	xmlns:webfeeds="http://webfeeds.org/rss/1.0">

<channel>
	<title>SQLBI</title>
	<atom:link href="https://www.sqlbi.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.sqlbi.com</link>
	<description>Business Intelligence with passion</description>
	<lastBuildDate>Wed, 23 Sep 2026 10:00:53 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.2</generator>
            <webfeeds:icon>https://www.sqlbi.com/logo/icon.svg</webfeeds:icon>
            <webfeeds:logo>https://www.sqlbi.com/logo/icon.svg</webfeeds:logo>
            <webfeeds:accentColor>ec5d5d</webfeeds:accentColor>
            <webfeeds:related layout="card" target="browser" />
            <webfeeds:analytics id="UA-7095697-2" engine="GoogleAnalytics" />
        	<item>
		<title>Start using DAX with AI</title>
		<link>https://www.sqlbi.com/tv/start-using-dax-with-ai/</link>
					<comments>https://www.sqlbi.com/tv/start-using-dax-with-ai/#respond</comments>
		
		<dc:creator><![CDATA[Marco Russo]]></dc:creator>
		<pubDate>Wed, 23 Sep 2026 10:00:42 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<category><![CDATA[Power BI]]></category>
		<category><![CDATA[AI]]></category>
		<guid isPermaLink="false">https://www.sqlbi.com/?post_type=video&#038;p=904180</guid>

					<description><![CDATA[<figure><img src="https://i3.ytimg.com/vi/KtO0BRKHBv4/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure>The time has come: in 2026, using AI to write DAX has started to be effective. Latest news and the new courses are at https://www.sqlbi.com/ai/ We also created three new video courses that should not become obsolete in two weeks!&#8230;]]></description>
										<content:encoded><![CDATA[<figure><img src="https://i3.ytimg.com/vi/KtO0BRKHBv4/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure><p>The time has come: in 2026, using AI to write DAX has started to be effective.<br />
Latest news and the new courses are at <a href="https://www.sqlbi.com/ai/">https://www.sqlbi.com/ai/</a></p>
<p>We also created three new video courses that should not become obsolete in two weeks!<br />
𝗔𝗜 𝗳𝗼𝗿 𝗣𝗼𝘄𝗲𝗿 𝗕𝗜: 𝗜𝗻𝘁𝗿𝗼 (𝗳𝗿𝗲𝗲!)<br />
𝗗𝗔𝗫 𝘄𝗶𝘁𝗵 𝗔𝗜: 𝗘𝘀𝘀𝗲𝗻𝘁𝗶𝗮𝗹𝘀<br />
𝗗𝗔𝗫 𝘄𝗶𝘁𝗵 𝗔𝗜: 𝗦𝗰𝗲𝗻𝗮𝗿𝗶𝗼𝘀</p>
<p>Marco wrote a long post to recap what has changed between 2023 and 2026 - see the link below!</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/tv/start-using-dax-with-ai/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>The hidden complexity of a daily average in DAX</title>
		<link>https://www.sqlbi.com/tv/the-hidden-complexity-of-a-daily-average-in-dax/</link>
					<comments>https://www.sqlbi.com/tv/the-hidden-complexity-of-a-daily-average-in-dax/#respond</comments>
		
		<dc:creator><![CDATA[Alberto Ferrari]]></dc:creator>
		<pubDate>Tue, 22 Sep 2026 10:00:00 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<guid isPermaLink="false">http://www.sqlbi.com/?post_type=video&#038;p=903648</guid>

					<description><![CDATA[<figure><img src="https://i.ytimg.com/vi/ajsAULaG_3s/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure>A simple daily average can hide complexities and return different values. We explore several edge-case scenarios to build the desired calculation using AI to accelerate the development.]]></description>
										<content:encoded><![CDATA[<figure><img src="https://i.ytimg.com/vi/ajsAULaG_3s/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure><p>A simple daily average can hide complexities and return different values. We explore several edge-case scenarios to build the desired calculation using AI to accelerate the development.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/tv/the-hidden-complexity-of-a-daily-average-in-dax/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Where we stand on AI and DAX in 2026</title>
		<link>https://www.sqlbi.com/blog/marco/2026/09/22/where-we-stand-on-ai-and-dax-in-2026/</link>
					<comments>https://www.sqlbi.com/blog/marco/2026/09/22/where-we-stand-on-ai-and-dax-in-2026/#respond</comments>
		
		<dc:creator><![CDATA[Marco Russo]]></dc:creator>
		<pubDate>Tue, 22 Sep 2026 08:30:38 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[DAX]]></category>
		<guid isPermaLink="false">https://www.sqlbi.com/?post_type=blogpost&#038;p=903942</guid>

					<description><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/timeline-dax-ai-2023-2026-thumbnail.png" class="webfeedsFeaturedVisual" /></figure>The time has come: in 2026, using AI to write DAX has started to be effective. Let me recap what has changed between 2023 and 2026, and what we are doing with our three new courses to best assist you&#8230;]]></description>
										<content:encoded><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/timeline-dax-ai-2023-2026-thumbnail.png" class="webfeedsFeaturedVisual" /></figure><p>The time has come: in 2026, <strong>using AI to write DAX has started to be effective</strong>. Let me recap what has changed between 2023 and 2026, and what we are doing with our three new courses to best assist you through these changes.<br />
<span id="more-903942"></span></p>
<p><img decoding="async" class="nozoom" style="float: right; margin: 0 0 10px 10px; border: none !important;" src="https://www.sqlbi.com/wp-content/uploads/icon-developer.png" alt="Laptop with code brackets" width="150" height="150" /></p>
<p>I started my career as a developer: Basic, Assembler, Pascal, C, C++, Delphi, C#, and many other specific domain languages along the way. Writing code is magical and time-consuming. Over time, I shifted my focus to data analytics, but I never stopped coding. Much less, and with much less time, with many more compromises. In the last two years, the support of AI to improve developer productivity has been absolutely amazing, from completing one line of code to writing complete files and now managing entire repos. This should be the introduction to another article, because this one is about DAX: why does my second life as a developer matter?</p>
<p>The reason is that, while I adopted AI relatively early to write software, I was not enthusiastic about using AI to write DAX code. Fourteen months ago, in July 2025, I wrote a <a href="https://www.sqlbi.com/blog/marco/2025/07/28/a-few-thoughts-about-newsletter-300-and-ai/">post for the 300th edition of our Newsletter</a> to clarify the position of SQLBI on AI. The message was that AI speeds up specific tasks, that the end-to-end time savings were smaller than what you would have expected from faster prototype creation, and that we would write about AI only when we saw clear, measurable benefits.</p>
<p>You might think that it was a conflict-of-interest type of situation: after all, I have been teaching DAX for a living for many years. But no, that was not the reason, because in teaching, my goal has always been to help people achieve their goals faster and more efficiently. Other topics simply become important as technology evolves. I waited, simply because AI was not ready for DAX. Hey, have you heard people say DAX is hard? Well, I don't think it is hard; it's just different, and AI learns by example, adapting and generalizing concepts across languages. But with DAX, that wasn't entirely possible, because it’s unique (and beautiful!) in its own way.</p>
<p>Well, I have big news. <strong>The wait is over; the time has come. </strong>Writing DAX with AI started to make sense this year. The acceleration we've seen in DAX proficiency with the new frontier models released in the last six months has been incredible. We are not at the “perfect” level; performance could still be a weakness, but we are well past the point of considering it helpful. Today, we use AI every day to transform data, create and modify semantic models, and draft long, complex DAX code! We published two articles in August about using AI to write DAX.</p>
<p>So, I have news about our upcoming training and about clarifying our position at SQLBI on using AI to write DAX: long-time readers of this blog know that we have been cautious about using AI to write DAX. Clearly, something changed, and if you are interested, I want to recap the journey since the first experiments with AI in DAX and explain our current position. Sit down, relax, and enjoy the read.</p>
<h2>The new courses</h2>
<p>We recently created three courses for different steps in using AI to write DAX:</p>
<ul>
<li><a href="https://www.sqlbi.com/p/ai-for-powerbi-intro-video-course/"><strong>AI for Power BI: Intro</strong><img decoding="async" class="nozoom" style="float: right; margin: 0 0 10px 10px; border: none !important;" src="https://www.sqlbi.com/wp-content/uploads/ai-for-powerbi-intro-email.png" alt="AI for Power BI: Intro (logo)" width="100" /></a> to understand the basics of AI in Power BI. This is not about Copilot; it is about using agents, skills, plugins, and MCP servers to manipulate Power BI models and reports. If you already use your Claude Code connected to Power BI, you probably already have this knowledge. If you are still using ChatGPT in a web browser by copying and pasting DAX code, you definitely need to learn the foundational concepts that apply to this new way of working, regardless of the specific tool. The tool choice doesn’t really matter, as long as it connects to MCP servers.</li>
<li><a href="https://www.sqlbi.com/p/dax-with-ai-essentials-video-course/"><img decoding="async" class="nozoom" style="float: right; margin: 0 0 10px 10px; border: none !important;" src="https://www.sqlbi.com/wp-content/uploads/dax-with-ai-essentials.png" alt="DAX with AI: Essentials (logo)" width="100" /><strong>DAX with AI: Essentials</strong></a> to learn how to read the DAX code generated by AI tools. If you already know DAX, you will see how to write correct prompts and how to actually use the agents to write measures. If you already know the AI tools but you are new to DAX, you will learn how to read DAX even if you are not able to write it.</li>
<li><a href="https://www.sqlbi.com/p/dax-with-ai-scenarios-video-course/"><img decoding="async" class="nozoom" style="float: right; margin: 0 0 10px 10px; border: none !important;" src="https://www.sqlbi.com/wp-content/uploads/dax-with-ai-scenarios.png" alt="DAX with AI: Scenarios (logo)" width="100" /><strong>DAX with AI: Scenarios</strong></a> to deal with more complex scenarios where your work is mainly to control the direction of the development, which includes writing DAX code and modifying the model so that it generates the correct result for the business problem described. This course can be seen as a library of business scenarios you can use to create a specific solution or to learn generic techniques applied to different use cases. The course also includes foundational concepts (specific data modeling and DAX techniques) which you can apply to several scenarios. You can pick only the foundational concepts you need for the use case you are interested in, or you can just go through the whole course from A to Z. You have options.</li>
</ul>
<p>The courses are built with two distinct profiles in mind. The first is someone who doesn't know DAX, uses an LLM (or would like to), and wants better results; this person needs to learn enough DAX to understand what the model produces. The second is someone who already knows DAX and mainly uses AI to copy-and-paste DAX code from a chat window. Our new courses are more complete for the first profile, while they provide tremendous guidance on the right combination of tools and prompts to be more efficient and effective for the second profile. That profile will also use this as an opportunity to validate their DAX knowledge in a new context.</p>
<p>In these courses, we don't teach AI tools because they change so often that a course can become obsolete before it is even published. This also means it doesn’t matter whether you use Visual Studio Code, Claude, Codex, other tools, or one day a chat integrated into Power BI (we all would like that, right?). We provide help installing it on separate pages that we'll keep up to date, but the courses focus on using AI effectively for DAX development.</p>
<p>We focus on how you write the request, we offer reasoning about the problem, we give instructions, and we check the result. Many AI videos for Power BI assume you already know the tools or show what's possible without explaining what matters when you use it for work. We focused on teaching without worrying about the demo effect.</p>
<p>But what about all the other courses? <strong>Are they still relevant?</strong> Yes, you will just use those skills in a different way. But let’s ask different AI models; I wrote the following prompt:</p>
<blockquote><p><strong>Is it worth attending a DAX training now that AI can write good DAX code?</strong></p></blockquote>
<p>Here are the answers from three frontier models – no cherry picking!</p>
<p><!-- Complete AI responses. --></p>
<div id="sqlbi-dax-ai-answers" style="box-sizing: border-box; width: 100%; max-width: 100%; margin: 28px 0; border: 1px solid #ccd1d6; border-radius: 6px; background: #fff; color: #222; font-family: inherit; font-size: 16px; line-height: 1.65; overflow-wrap: anywhere;">
<div style="display: flex; flex-wrap: wrap; gap: 0; margin: 0; padding: 0; background: #f3f4f5; border-bottom: 1px solid #ccd1d6; border-radius: 6px 6px 0 0; overflow: hidden;" data-ai-tabs="" aria-label="Choose an AI response"><a id="sqlbi-ai-tab-gpt" style="box-sizing: border-box; flex: 1 1 180px; display: block; margin: 0; padding: 13px 12px; border: 0; border-top: 3px solid transparent; background: transparent; color: #454b52; font: inherit; font-size: 14px; font-weight: 600; line-height: 1.4; text-align: center; text-decoration: none; cursor: pointer; outline-offset: -3px;" href="#sqlbi-ai-panel-gpt" data-ai-tab="">GPT-6 Astra Light</a><br />
<a id="sqlbi-ai-tab-fable" style="box-sizing: border-box; flex: 1 1 180px; display: block; margin: 0; padding: 13px 12px; border: 0; border-top: 3px solid transparent; background: transparent; color: #454b52; font: inherit; font-size: 14px; font-weight: 600; line-height: 1.4; text-align: center; text-decoration: none; cursor: pointer; outline-offset: -3px;" href="#sqlbi-ai-panel-fable" data-ai-tab="">Fable 5.1 High</a><br />
<a id="sqlbi-ai-tab-grok" style="box-sizing: border-box; flex: 1 1 180px; display: block; margin: 0; padding: 13px 12px; border: 0; border-top: 3px solid transparent; background: transparent; color: #454b52; font: inherit; font-size: 14px; font-weight: 600; line-height: 1.4; text-align: center; text-decoration: none; cursor: pointer; outline-offset: -3px;" href="#sqlbi-ai-panel-grok" data-ai-tab="">Grok 4.6 Expert</a></div>
<section id="sqlbi-ai-panel-gpt" style="box-sizing: border-box; padding: 22px clamp(14px,4%,28px);" data-ai-panel="" aria-labelledby="sqlbi-ai-tab-gpt">
<h3 style="margin: 0 0 1em; font-size: 1.15em;" data-ai-fallback-heading="">GPT-6 Astra Light</h3>
<div style="display: block; box-sizing: border-box; width: 100%; max-width: none; float: none; margin: 0 0 22px; padding: 11px 14px; border-left: 3px solid #aab2ba; background: #f3f4f5; color: #555e67; font-family: Arial,sans-serif; font-size: 13px; line-height: 1.5;" role="note">Original conversation available at <a style="color: #9e281f; text-decoration: underline; text-underline-offset: 2px;" href="https://chatgpt.com/share/6aad0916-1508-83eb-bb5a-20c15e8a3e0f" target="_blank" rel="noopener noreferrer">ChatGPT (opens in a new tab)</a>.<br />
<span style="font-size: 12px;">Editorial source note — not part of the AI response.</span></div>
<div data-ai-answer="">
<p style="margin: 0 0 1em; padding: 0;">Yes—<strong>but the reason to attend DAX training in 2026 is different from what it was a few years ago</strong>.</p>
<p style="margin: 0 0 1em; padding: 0;">AI has substantially reduced the value of learning DAX syntax by heart. It has <strong>not</strong> eliminated the value of understanding how DAX actually evaluates expressions.</p>
<p style="margin: 0 0 1em; padding: 0;">Microsoft’s own current guidance reflects this. Copilot can generate DAX queries and measures, but Microsoft specifically warns that it can struggle with variables, calculation groups, newer DAX syntax, and other cases, and recommends understanding/reviewing generated code rather than blindly accepting it. <a style="color: #9e281f; text-decoration: underline; text-underline-offset: 2px; font-size: .85em;" href="https://learn.microsoft.com/en-us/power-bi/create-reports/copilot-semantic-models?utm_source=chatgpt.com">Microsoft Learn</a></p>
<h3 style="margin: 1.5em 0 .65em; padding: 0; font-size: 1.15em; line-height: 1.4; font-weight: bold; color: #222;">What AI changes</h3>
<div style="max-width: 100%; overflow-x: auto; margin: 0 0 1em;" tabindex="0" role="region" aria-label="Comparison table">
<table style="min-width: 520px; width: 100%; margin: 0 0 1em; border-collapse: collapse; table-layout: fixed; font-size: .94em;">
<thead>
<tr>
<th style="padding: 10px; border: 1px solid #d8dde2; background: #f3f4f5; text-align: left; vertical-align: top; font-weight: bold;" scope="col">Skill</th>
<th style="padding: 10px; border: 1px solid #d8dde2; background: #f3f4f5; text-align: left; vertical-align: top; font-weight: bold;" scope="col">Before AI</th>
<th style="padding: 10px; border: 1px solid #d8dde2; background: #f3f4f5; text-align: left; vertical-align: top; font-weight: bold;" scope="col">With good AI</th>
<th style="padding: 10px; border: 1px solid #d8dde2; background: #f3f4f5; text-align: left; vertical-align: top; font-weight: bold;" scope="col">Training value</th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Remembering function names</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Important</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Mostly unnecessary</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Low</td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Writing routine measures</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Important</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">AI is very good</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Low–medium</td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Looking up syntax</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Important</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Nearly automated</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Low</td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Filter context</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Essential</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Still essential</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;"><strong>Very high</strong></td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Context transition</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Essential</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">AI can write it, but you must understand consequences</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;"><strong>Very high</strong></td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Data-model/DAX interaction</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Essential</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">AI often lacks enough model intent</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;"><strong>Very high</strong></td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Debugging wrong totals</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Difficult</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">AI helps, but doesn't remove reasoning</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;"><strong>High</strong></td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Choosing between valid DAX patterns</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Expert skill</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">AI can propose alternatives</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;"><strong>High</strong></td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Performance optimization</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Expert skill</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">AI helps inconsistently</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;"><strong>High</strong></td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Reviewing AI-generated DAX</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Didn't exist</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">Increasingly important</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;"><strong>Very high</strong></td>
</tr>
</tbody>
</table>
</div>
<p style="margin: 0 0 1em; padding: 0;">The key distinction is:</p>
<p style="margin: 0 0 1em; padding: 0;"><strong>Writing DAX ≠ understanding DAX.</strong></p>
<p style="margin: 0 0 1em; padding: 0;">Suppose you ask AI:</p>
<blockquote style="display: block; box-sizing: border-box; width: 100%; max-width: none; float: none; margin: 0 0 1em; padding: 12px 16px 0; border-left: 3px solid #ccd1d6; font: inherit; color: inherit;">
<p style="margin: 0 0 1em; padding: 0;">"Calculate sales for customers whose lifetime sales exceed €10,000, preserving the product filter but ignoring the current customer selection."</p>
</blockquote>
<p style="margin: 0 0 1em; padding: 0;">A modern model can probably produce plausible DAX immediately.</p>
<p style="margin: 0 0 1em; padding: 0;">The difficult questions are now:</p>
<ul style="margin: 0 0 1em; padding: 0 0 0 1.5em; list-style: disc outside;">
<li style="margin: 0 0 .6em; padding: 0;">
<p style="margin: 0 0 1em; padding: 0;">Is it computing customer lifetime sales in the intended context?</p>
</li>
<li style="margin: 0 0 .6em; padding: 0;">
<p style="margin: 0 0 1em; padding: 0;">Should the customer filter be removed with <code style="font-family: Consolas,Monaco,monospace; font-size: .94em; color: inherit;">ALL</code>, <code style="font-family: Consolas,Monaco,monospace; font-size: .94em; color: inherit;">REMOVEFILTERS</code>, <code style="font-family: Consolas,Monaco,monospace; font-size: .94em; color: inherit;">ALLSELECTED</code>, or something else?</p>
</li>
<li style="margin: 0 0 .6em; padding: 0;">
<p style="margin: 0 0 1em; padding: 0;">When is the measure evaluated?</p>
</li>
<li style="margin: 0 0 .6em; padding: 0;">
<p style="margin: 0 0 1em; padding: 0;">Does context transition occur?</p>
</li>
<li style="margin: 0 0 .6em; padding: 0;">
<p style="margin: 0 0 1em; padding: 0;">Will totals behave correctly?</p>
</li>
<li style="margin: 0 0 .6em; padding: 0;">
<p style="margin: 0 0 1em; padding: 0;">What happens with many-to-many relationships?</p>
</li>
<li style="margin: 0 0 .6em; padding: 0;">
<p style="margin: 0 0 1em; padding: 0;">Does the expression scale to a 500-million-row model?</p>
</li>
</ul>
<p style="margin: 0 0 1em; padding: 0;">Those aren't primarily syntax questions. They are <strong>semantic-model reasoning questions</strong>.</p>
<p style="margin: 0 0 1em; padding: 0;">Microsoft still describes understanding context as critical both for producing dynamic calculations and for troubleshooting formulas. <a style="color: #9e281f; text-decoration: underline; text-underline-offset: 2px; font-size: .85em;" href="https://learn.microsoft.com/en-us/dax/dax-overview?utm_source=chatgpt.com">Microsoft Learn</a> And its intermediate DAX training continues to emphasize <code style="font-family: Consolas,Monaco,monospace; font-size: .94em; color: inherit;">CALCULATE</code>, filter context, filter modifiers and context transition rather than merely teaching function syntax. <a style="color: #9e281f; text-decoration: underline; text-underline-offset: 2px; font-size: .85em;" href="https://learn.microsoft.com/en-us/training/modules/dax-power-bi-modify-filter/?utm_source=chatgpt.com">Microsoft Learn</a></p>
<h3 style="margin: 1.5em 0 .65em; padding: 0; font-size: 1.15em; line-height: 1.4; font-weight: bold; color: #222;">So is a serious DAX course still worth the money?</h3>
<p style="margin: 0 0 1em; padding: 0;">I'd divide people into three groups.</p>
<table style="width: 100%; margin: 0 0 1em; border-collapse: collapse; table-layout: fixed; font-size: .94em;">
<thead>
<tr>
<th style="padding: 10px; border: 1px solid #d8dde2; background: #f3f4f5; text-align: left; vertical-align: top; font-weight: bold;" scope="col">Your situation</th>
<th style="padding: 10px; border: 1px solid #d8dde2; background: #f3f4f5; text-align: left; vertical-align: top; font-weight: bold;" scope="col">Value of intensive DAX training</th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">You make reports and mostly need SUM, ratios, YTD, basic comparisons</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">★★☆☆☆</td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">You develop production Power BI semantic models</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">★★★★☆</td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">You design complex models, troubleshoot DAX, optimize calculations, or advise other developers</td>
<td style="padding: 10px; border: 1px solid #d8dde2; text-align: left; vertical-align: top;">★★★★★</td>
</tr>
</tbody>
</table>
<p style="margin: 0 0 1em; padding: 0;">For the first group, I'd probably learn interactively with AI plus Microsoft Learn rather than spend several days in advanced classroom training.</p>
<p style="margin: 0 0 1em; padding: 0;">For the second and third groups, the economics have arguably become <strong>better</strong>, not worse: AI lets you skip a lot of mechanical coding and spend more of your time understanding the difficult parts.</p>
<p style="margin: 0 0 1em; padding: 0;">A course such as SQLBI's current <strong>Mastering DAX</strong> reflects that distinction. The 2026 curriculum includes filter/row context, table functions, semi-additive calculations, calculation groups, advanced relationships, window functions, time intelligence and what happens "inside CALCULATE"—topics considerably deeper than simply learning how to type measures. <a style="color: #9e281f; text-decoration: underline; text-underline-offset: 2px; font-size: .85em;" href="https://www.sqlbi.com/p/mastering-dax-workshop-berlin-jun-15-17-2026/">SQLBI</a> The new video course also explicitly spends significant time building an understanding of filter context from the ground up. <a style="color: #9e281f; text-decoration: underline; text-underline-offset: 2px; font-size: .85em;" href="https://www.sqlbi.com/blog/marco/2026/04/01/the-third-edition-of-the-mastering-dax-video-course-unplugged/">SQLBI</a></p>
<h3 style="margin: 1.5em 0 .65em; padding: 0; font-size: 1.15em; line-height: 1.4; font-weight: bold; color: #222;">The skill stack I think matters now</h3>
<pre style="margin: 0 0 1em; padding: 14px; background: #f3f4f5; border: 1px solid #dde1e5; border-radius: 4px; overflow-x: auto; white-space: pre; font-size: .9em; line-height: 1.6; color: #222;"><code style="font-family: Consolas,Monaco,monospace; font-size: .94em; color: inherit;">                    2020                         2026
             ─────────────────             ─────────────────
                 DAX syntax                    DAX semantics
               function recall                 filter context
              pattern knowledge               model reasoning
                   ↓                               ↓
             write the code               describe the intent
                                                ↓
                                           AI writes code
                                                ↓
                                      YOU verify correctness
                                                ↓
                                      YOU diagnose edge cases</code></pre>
<p style="margin: 0 0 1em; padding: 0;">The interesting consequence is that <strong>AI makes shallow DAX knowledge less valuable and deep DAX knowledge more valuable</strong>.</p>
<p style="margin: 0 0 1em; padding: 0;">Someone who knows only syntax is increasingly replaceable by AI.</p>
<p style="margin: 0 0 1em; padding: 0;">Someone who understands evaluation context, relationships, expanded tables, lineage, context transition and engine behavior can use AI as an extremely fast DAX assistant—and can recognize when it produces something subtly wrong.</p>
<p style="margin: 0 0 1em; padding: 0;">So if the proposed training is essentially <strong>"here are 100 DAX functions"</strong>, I would be reluctant to invest much time in it.</p>
<p style="margin: 0 0 1em; padding: 0;">If it is training in <strong>how DAX works</strong>, particularly something at the level of Mastering DAX, I think it remains highly relevant. The objective shouldn't be <em>"after three days I can write DAX without AI."</em> It should be:</p>
<blockquote style="display: block; box-sizing: border-box; width: 100%; max-width: none; float: none; margin: 0 0 1em; padding: 12px 16px 0; border-left: 3px solid #ccd1d6; font: inherit; color: inherit;">
<p style="margin: 0 0 1em; padding: 0;"><strong>"After three days I can tell AI what calculation I actually need, understand what it generated, and know when it is wrong."</strong></p>
</blockquote>
<p style="margin: 0 0 1em; padding: 0;">That is probably the more durable DAX skill now.</p>
</div>
</section>
<section id="sqlbi-ai-panel-fable" style="box-sizing: border-box; padding: 22px clamp(14px,4%,28px);" data-ai-panel="" aria-labelledby="sqlbi-ai-tab-fable">
<h3 style="margin: 0 0 1em; font-size: 1.15em;" data-ai-fallback-heading="">Fable 5.1 High</h3>
<div style="display: block; box-sizing: border-box; width: 100%; max-width: none; float: none; margin: 0 0 22px; padding: 11px 14px; border-left: 3px solid #aab2ba; background: #f3f4f5; color: #555e67; font-family: Arial,sans-serif; font-size: 13px; line-height: 1.5;" role="note">Original conversation available at <a style="color: #9e281f; text-decoration: underline; text-underline-offset: 2px;" href="https://claude.ai/share/39c907ae-68d3-496e-a9bf-793e09e69998" target="_blank" rel="noopener noreferrer">Claude (opens in a new tab)</a>.<br />
<span style="font-size: 12px;">Editorial source note — not part of the AI response.</span></div>
<div data-ai-answer="">
<p style="margin: 0 0 1em; padding: 0;">Yes, though the reason has shifted. AI has made the <em>typing</em> of DAX cheap, and that changes what training is for.</p>
<p style="margin: 0 0 1em; padding: 0;">The part AI handles well is translating a clear intent into syntax: "total sales for the same period last year, only for products in the current filter" becomes a working measure in seconds. What it does not do is know whether that measure is the right one for your model, and you will not know either unless you understand the underlying mechanics.</p>
<p style="margin: 0 0 1em; padding: 0;">A few things that still require you to actually understand DAX:</p>
<p style="margin: 0 0 1em; padding: 0;">Evaluation context. Most wrong DAX is not syntactically wrong; it returns a plausible number that is subtly incorrect because of filter context, context transition, or an unexpected interaction with the relationship. AI-generated code fails this way just as often as human code, and the failure is invisible unless you can reason about what the engine is doing. Reviewing a measure is a skill, and it is the same skill as writing one.</p>
<p style="margin: 0 0 1em; padding: 0;">Specification. To ask for the right thing, you have to know what "the right thing" is in DAX terms. "Same period last year" is ambiguous in a fiscal calendar with 53-week years; "ranking" hides a dozen decisions about ties and filters. The training gives you the vocabulary to ask precisely and to recognise when the answer solved a different problem.</p>
<p style="margin: 0 0 1em; padding: 0;">Modeling and performance. Much of what looks like a DAX problem is a data model problem. AI will happily write a 40-line measure to work around a missing relationship or a badly shaped fact table, and it will run slowly. Knowing when to fix the model instead is judgment, not code generation.</p>
<p style="margin: 0 0 1em; padding: 0;">Accountability. If the number in the CFO's report is wrong, "the AI wrote it" is not an answer. Someone has to be able to verify it.</p>
<p style="margin: 0 0 1em; padding: 0;">So the honest reframe is: training is now less about learning to produce code and more about learning to evaluate it, direct it, and know when to distrust it. That is arguably more valuable, not less, because the leverage is higher — one person who understands DAX plus an AI can do the work of several who don't, while several who don't plus an AI mostly produce confident errors faster.</p>
<p style="margin: 0 0 1em; padding: 0;">Where I'd be more skeptical: a course that is mainly syntax drills and function reference is worth less than it was. Look for training built around how the engine evaluates expressions and how to reason about results, and ideally one that acknowledges AI as a tool you'll be using alongside it.</p>
</div>
</section>
<section id="sqlbi-ai-panel-grok" style="box-sizing: border-box; padding: 22px clamp(14px,4%,28px);" data-ai-panel="" aria-labelledby="sqlbi-ai-tab-grok">
<h3 style="margin: 0 0 1em; font-size: 1.15em;" data-ai-fallback-heading="">Grok 4.6 Expert</h3>
<div style="display: block; box-sizing: border-box; width: 100%; max-width: none; float: none; margin: 0 0 22px; padding: 11px 14px; border-left: 3px solid #aab2ba; background: #f3f4f5; color: #555e67; font-family: Arial,sans-serif; font-size: 13px; line-height: 1.5;" role="note">Original conversation available at <a style="color: #9e281f; text-decoration: underline; text-underline-offset: 2px;" href="https://grok.com/share/bGVnYWN5LWNvcHk_5092e858-6f64-4b22-885f-7ed9f666df06" target="_blank" rel="noopener noreferrer">Grok (opens in a new tab)</a>.<br />
<span style="font-size: 12px;">Editorial source note — not part of the AI response.</span></div>
<div data-ai-answer="">
<p style="margin: 0 0 1em; padding: 0;"><strong>Yes — if you will own models or numbers that matter.</strong> AI is a strong junior pair-programmer for DAX, not a replacement for understanding it.</p>
<p style="margin: 0 0 1em; padding: 0;">AI (Power BI Copilot, ChatGPT, Claude, etc.) is now good at <em>writing</em> common DAX: simple aggregations, basic <code style="font-family: Consolas,Monaco,monospace; font-size: .94em; color: inherit;">CALCULATE</code> filters, standard time intelligence. That part of the job is cheaper than it was two years ago. What it is still weak at is the part that actually breaks reports. <a style="color: #9e281f; text-decoration: underline; text-underline-offset: 2px; font-size: .85em;" href="https://lets-viz.com/blogs/dax-pattern-breaks-ai-copilots-2026/">Lets-viz</a></p>
<h3 style="margin: 1.5em 0 .65em; padding: 0; font-size: 1.15em; line-height: 1.4; font-weight: bold; color: #222;">What AI still gets wrong</h3>
<p style="margin: 0 0 1em; padding: 0;">Independent 2026 benchmarks are consistent:</p>
<ul style="margin: 0 0 1em; padding: 0 0 0 1.5em; list-style: disc outside;">
<li style="margin: 0 0 .6em; padding: 0;">Copilot on structured DAX tests: about <strong>62.5% fully reliable</strong>. The rest looks plausible and is wrong in a subtle way. <a style="color: #9e281f; text-decoration: underline; text-underline-offset: 2px; font-size: .85em;" href="https://lets-viz.com/blogs/dax-pattern-breaks-ai-copilots-2026/">Lets-viz</a></li>
<li style="margin: 0 0 .6em; padding: 0;">Broader LLM tests: models often get the <em>intent</em> right and the <em>DAX grammar or evaluation</em> wrong. One large benchmark found far more syntax failures than logic failures; <code style="font-family: Consolas,Monaco,monospace; font-size: .94em; color: inherit;">RANKX</code> alone caused a huge share of compile errors. <a style="color: #9e281f; text-decoration: underline; text-underline-offset: 2px; font-size: .85em;" href="https://maxanatsko.com/blog/i-tested-70-ai-models-on-dax-heres-what-actually-works">Maxanatsko</a></li>
<li style="margin: 0 0 .6em; padding: 0;">The pattern that reliably breaks copilots is <strong>context transition</strong> — iterators like <code style="font-family: Consolas,Monaco,monospace; font-size: .94em; color: inherit;">SUMX</code> / <code style="font-family: Consolas,Monaco,monospace; font-size: .94em; color: inherit;">AVERAGEX</code> / <code style="font-family: Consolas,Monaco,monospace; font-size: .94em; color: inherit;">RANKX</code> that reference measures instead of columns. Custom fiscal calendars and messy many-to-many models are also frequent failure modes. <a style="color: #9e281f; text-decoration: underline; text-underline-offset: 2px; font-size: .85em;" href="https://lets-viz.com/blogs/dax-pattern-breaks-ai-copilots-2026/">Lets-viz</a></li>
</ul>
<p style="margin: 0 0 1em; padding: 0;">The dangerous cases are not red-error formulas. They are measures that total correctly on one visual and lie on another because grain, filter context, or a relationship was misunderstood.</p>
<p style="margin: 0 0 1em; padding: 0;">Microsoft’s own docs say Copilot can use the wrong functions, mishandle variables, and that inexperienced authors must validate everything before using it. <a style="color: #9e281f; text-decoration: underline; text-underline-offset: 2px; font-size: .85em;" href="https://learn.microsoft.com/en-us/power-bi/create-reports/copilot-semantic-models">Learn.microsoft</a></p>
<h3 style="margin: 1.5em 0 .65em; padding: 0; font-size: 1.15em; line-height: 1.4; font-weight: bold; color: #222;">Why that still justifies training</h3>
<p style="margin: 0 0 1em; padding: 0;">DAX is not Excel-with-funny-names. The hard skill is <strong>evaluation context</strong>: row context, filter context, context transition, grain, and how relationships change a number. That is also what AI is worst at.</p>
<p style="margin: 0 0 1em; padding: 0;">SQLBI (Marco Russo and Alberto Ferrari) shipped a <em>third</em> edition of Mastering DAX in 2026 <em>after</em> LLMs arrived. Their reasons to still learn it: generated DAX still has to be validated; a short correct formula is often cheaper than a long prompt; knowing the language makes you write better prompts; and “the moment you stop thinking, you become disposable.” Your value is the corrections the model still needs — in DAX, often more than 5–10%, especially if you also care about performance. <a style="color: #9e281f; text-decoration: underline; text-underline-offset: 2px; font-size: .85em;" href="https://www.sqlbi.com/articles/mastering-dax-video-course-3rd-edition/">Sqlbi</a></p>
<p style="margin: 0 0 1em; padding: 0;">The same pattern shows up across practitioner write-ups in 2026:</p>
<ul style="margin: 0 0 1em; padding: 0 0 0 1.5em; list-style: disc outside;">
<li style="margin: 0 0 .6em; padding: 0;">Copilot quality tracks <strong>model quality</strong>. Clean names, relationships, and measure descriptions make AI useful. A messy model makes it confidently wrong. <a style="color: #9e281f; text-decoration: underline; text-underline-offset: 2px; font-size: .85em;" href="https://towardsdatascience.com/the-power-bi-developers-survival-guide-to-microsoft-fabric/">Towardsdatascience</a></li>
<li style="margin: 0 0 .6em; padding: 0;">In Fabric, DAX and the semantic layer got <em>more</em> important, not less: Copilot, Direct Lake, and data agents all sit on measures you define. Bad DAX pollutes every AI feature downstream. <a style="color: #9e281f; text-decoration: underline; text-underline-offset: 2px; font-size: .85em;" href="https://towardsdatascience.com/the-power-bi-developers-survival-guide-to-microsoft-fabric/">Towardsdatascience</a></li>
</ul>
<p style="margin: 0 0 1em; padding: 0;">So the job is shifting from “type the formula” to:</p>
<ol style="margin: 0 0 1em; padding: 0 0 0 1.5em; list-style: decimal outside;">
<li style="margin: 0 0 .6em; padding: 0;">Specify the business rule precisely (grain, filters, what a blank means).</li>
<li style="margin: 0 0 .6em; padding: 0;">Keep the model AI-readable.</li>
<li style="margin: 0 0 .6em; padding: 0;">Check results at multiple grains (row, group, total).</li>
<li style="margin: 0 0 .6em; padding: 0;">Catch inefficient iterators before they hit production.</li>
<li style="margin: 0 0 .6em; padding: 0;">Know when the generated pattern is the wrong pattern.</li>
</ol>
<p style="margin: 0 0 1em; padding: 0;">You cannot do 3–5 if you only know how to paste.</p>
<h3 style="margin: 1.5em 0 .65em; padding: 0; font-size: 1.15em; line-height: 1.4; font-weight: bold; color: #222;">When training is <em>not</em> worth much</h3>
<p style="margin: 0 0 1em; padding: 0;">Skip a multi-day DAX workshop if:</p>
<ul style="margin: 0 0 1em; padding: 0 0 0 1.5em; list-style: disc outside;">
<li style="margin: 0 0 .6em; padding: 0;">You only build simple dashboards (<code style="font-family: Consolas,Monaco,monospace; font-size: .94em; color: inherit;">SUM</code>, <code style="font-family: Consolas,Monaco,monospace; font-size: .94em; color: inherit;">COUNT</code>, basic YoY) and someone else owns the model.</li>
<li style="margin: 0 0 .6em; padding: 0;">You will never be the person who has to explain a number to finance or audit.</li>
<li style="margin: 0 0 .6em; padding: 0;">The course is a function catalog (“here are 80 DAX functions”) rather than context and modeling.</li>
</ul>
<p style="margin: 0 0 1em; padding: 0;">For that profile, Copilot plus a good model and a short “how to verify a measure” habit is enough.</p>
<h3 style="margin: 1.5em 0 .65em; padding: 0; font-size: 1.15em; line-height: 1.4; font-weight: bold; color: #222;">What is still worth paying for</h3>
<p style="margin: 0 0 1em; padding: 0;">Invest in training that covers the parts AI does not internalize:</p>
<ul style="margin: 0 0 1em; padding: 0 0 0 1.5em; list-style: disc outside;">
<li style="margin: 0 0 .6em; padding: 0;">Filter context, <code style="font-family: Consolas,Monaco,monospace; font-size: .94em; color: inherit;">CALCULATE</code>, <code style="font-family: Consolas,Monaco,monospace; font-size: .94em; color: inherit;">KEEPFILTERS</code>, context transition</li>
<li style="margin: 0 0 .6em; padding: 0;">Grain and why a measure can be right in the detail and wrong in the total</li>
<li style="margin: 0 0 .6em; padding: 0;">Star-schema modeling (this multiplies AI accuracy more than any prompt trick)</li>
<li style="margin: 0 0 .6em; padding: 0;">How to test a measure (DAX Query View, different slicer combinations, blank handling)</li>
<li style="margin: 0 0 .6em; padding: 0;">Performance: when <code style="font-family: Consolas,Monaco,monospace; font-size: .94em; color: inherit;">SUMX</code> is necessary vs lazy</li>
<li style="margin: 0 0 .6em; padding: 0;">How to use Copilot as a draft + review loop, not as an oracle</li>
</ul>
<p style="margin: 0 0 1em; padding: 0;">That is a different syllabus from “learn DAX because you cannot remember <code style="font-family: Consolas,Monaco,monospace; font-size: .94em; color: inherit;">SAMEPERIODLASTYEAR</code>.” Syntax training is largely obsolete. Conceptual training is not.</p>
<p style="margin: 0 0 1em; padding: 0;"><strong>Practical rule:</strong> if a wrong number would change a decision, you still need enough DAX to distrust the model. AI made mediocre DAX cheaper. It made <em>good</em> DAX more valuable, because someone has to own the last 20%.</p>
</div>
</section>
</div>
<p><script>
(function () {
  "use strict";
  var root = document.getElementById("sqlbi-dax-ai-answers");
  if (!root || root.getAttribute("data-ai-ready")) return;
  var tabs = Array.prototype.slice.call(root.querySelectorAll("[data-ai-tab]"));
  var panels = Array.prototype.slice.call(root.querySelectorAll("[data-ai-panel]"));
  var headings = root.querySelectorAll("[data-ai-fallback-heading]");
  var tablist = root.querySelector("[data-ai-tabs]");
  var selected = 0;
  function select(index, focus) {
    selected = index;
    tabs.forEach(function (tab, i) {
      var active = i === index;
      tab.setAttribute("aria-selected", String(active));
      tab.tabIndex = active ? 0 : -1;
      tab.style.backgroundColor = active ? "#fff" : "transparent";
      tab.style.color = active ? "#9e281f" : "#454b52";
      tab.style.borderTopColor = active ? "#b53026" : "transparent";
      panels[i].hidden = !active;
      panels[i].style.display = active ? "block" : "none";
    });
    if (focus) tabs[index].focus();
  }
  tablist.setAttribute("role", "tablist");
  tabs.forEach(function (tab, index) {
    tab.setAttribute("role", "tab");
    tab.setAttribute("aria-controls", panels[index].id);
    panels[index].setAttribute("role", "tabpanel");
    panels[index].tabIndex = 0;
    panels[index].style.maxHeight = "640px";
    panels[index].style.overflowY = "auto";
    tab.addEventListener("click", function (event) {
      event.preventDefault();
      select(index, false);
    });
    tab.addEventListener("keydown", function (event) {
      var next = index;
      if (event.key === "ArrowRight") next = (index + 1) % tabs.length;
      else if (event.key === "ArrowLeft") next = (index + tabs.length - 1) % tabs.length;
      else if (event.key === "Home") next = 0;
      else if (event.key === "End") next = tabs.length - 1;
      else if (event.key !== " " && event.key !== "Enter") return;
      event.preventDefault();
      select(next, true);
    });
  });
  Array.prototype.forEach.call(headings, function (heading) { heading.style.display = "none"; });
  var initial = panels.findIndex(function (panel) { return "#" + panel.id === window.location.hash; });
  select(initial < 0 ? 0 : initial, false);
  root.setAttribute("data-ai-ready", "true");
  window.addEventListener("beforeprint", function () {
    tablist.style.display = "none";
    panels.forEach(function (panel) {
      panel.hidden = false;
      panel.style.display = "block";
      panel.style.maxHeight = "none";
      panel.style.overflowY = "visible";
    });
    Array.prototype.forEach.call(headings, function (heading) { heading.style.display = "block"; });
  });
  window.addEventListener("afterprint", function () {
    tablist.style.display = "flex";
    panels.forEach(function (panel) { panel.style.maxHeight = "640px"; panel.style.overflowY = "auto"; });
    Array.prototype.forEach.call(headings, function (heading) { heading.style.display = "none"; });
    select(selected, false);
  });
}());
</script></p>
<p>The classroom courses on DAX do not change. If you attend one, you will learn to write and read DAX, and you need to read it to check the code the AI writes. Of course, we are also integrating into the in-person courses how to use the AI tools in the context of DAX development.</p>
<p>And now, the full story behind this change.</p>
<p><a href="https://www.sqlbi.com/wp-content/uploads/timeline-dax-ai-2023-2026.svg"><img fetchpriority="high" decoding="async" style="width: 100%; height: auto;" src="https://www.sqlbi.com/wp-content/uploads/timeline-dax-ai-2023-2026.svg" alt="Timeline of what SQLBI published about writing DAX with AI from March 2023 to September 2026, with our assessment by period: not ready until early 2026, promising from February 2026, productive from August 2026" width="1600" height="610" /></a></p>
<p><em>What we published about writing DAX with AI, 2023 to 2026, and our assessment by period. The details and the links are in the table at the end of the post.</em></p>
<h2><strong>What we said, and why we said it</strong></h2>
<p><img decoding="async" class="nozoom" style="float: left; margin: 0 10px 10px 0; border: none !important;" src="https://www.sqlbi.com/wp-content/uploads/icon-hourglass.png" alt="Hourglass" width="150" height="150" /></p>
<p>In March 2023, we published an unplugged video where we <a href="https://www.sqlbi.com/tv/writing-dax-with-chatgpt-4-unplugged-50/">wrote DAX measures with ChatGPT-4</a>. A reader asked how ChatGPT could produce good DAX if most DAX on the internet is inaccurate, and my answer was that it wouldn't. In May 2024, we <a href="https://www.sqlbi.com/tv/writing-dax-with-chatgpt-4o-unplugged-58/">repeated the test with ChatGPT-4o</a>. The result was interesting, but not ready for prime time.</p>
<p>The reason was never opposition to AI. I would not go back two years and write .NET code without Copilot. However, in 2023 and 2024, and for most of 2025, you couldn't think of writing DAX with AI without being able to read it well, which implies you could also write it. We did not want to suggest AI to write DAX: telling someone who cannot understand DAX (as many people were at the time) to blindly trust DAX measures generated by AI would have been a risk we did not want to promote.</p>
<p>In August 2025, we ran an experiment we did not publish: We tried to use AI to generate exercises for our courses. It gave ideas, it wrote good descriptive text, and the exercises were correct in their English. But they were also wrong. They repeated the example just shown instead of testing whether the student had understood the concept in a different scenario – and in several cases, they told the student to do the wrong thing.</p>
<p>That experiment fairly summarizes 2025. Productivity was low, given the time and skill required to get something usable. My logic is simple: if a tool saves me time, it works; if it doesn't, it doesn't make me more productive, it isn't ready, and I can wait for another version.</p>
<h2><strong>What changed in 2026</strong></h2>
<p><img loading="lazy" decoding="async" class="nozoom" style="float: right; margin: 0 0 10px 10px; border: none !important;" src="https://www.sqlbi.com/wp-content/uploads/icon-student-robot.png" alt="Robot wearing a graduation cap" width="150" height="150" /></p>
<p>In February 2026, I saw the first sign. I sent Alberto a chat where a model asked for an example to explain a DAX concept and got it badly wrong. When I pointed out the error, it corrected it, corrected a second problem I hadn't noticed, and explained why. The correction showed that the AI had started to reason over the model rather than over the syntax. I like to say that AI failed the exam, but it was a very promising student.</p>
<p>Since Opus 4.8, we have seen big improvements, and the current generation of models (Opus 5, Fable, Sol, Astra, Grok 4.6, and others) reaches a level of DAX-writing quality we can no longer ignore. In my experience, two capabilities have changed. The first is generalization: the ability to abstract a concept and apply it to a different problem, which wasn't there two years ago and wasn't this refined six months ago. The second is the ability to work for one or two hours on a task without going off track. The sentence, "AI only repeats what it has read," could have been true in 2024, but it is definitely not true anymore. The latest models write DAX at a level above most of the students who leave our classroom courses. Not always, not for every problem, but the productivity improvement is definitely here.</p>
<p>Recently, we used AI to produce the exercises for the video courses, the task that had failed in August 2025. This time, <strong>we designed every exercise and let the model execute it; we reviewed everything before publishing</strong>. The result: more exercises, clearer descriptions, in less time. When I <a href="https://www.sqlbi.com/blog/marco/2026/07/08/generative-ai-guidelines-at-sqlbi-2026-update/">updated our generative AI guidelines</a> on July 8, 2026, I wrote that we used AI for DAX analysis and "rarely in DAX coding". Two months later, that sentence was already outdated, which shows how fast things change.</p>
<h2><strong>Why we did not say this earlier</strong></h2>
<p><img loading="lazy" decoding="async" class="nozoom" style="float: left; margin: 0 10px 10px 0; border: none !important;" src="https://www.sqlbi.com/wp-content/uploads/icon-u-turn.png" alt="U-turn road sign" width="150" height="150" /></p>
<p>I expect the objection: "A year ago you said AI was not ready for DAX, and now you sell courses about it." All true, but there are no contradictions between this and our other statements. To make life easier for those looking for these statements, I included a table at the end of the article.</p>
<p>When there was a lot of hype and the tools did not work, we said it was too early. Now the tools are not perfect, but they are at a level where, if you check what they are doing, they save a lot of time. We kept applying the same rules for evaluating technologies, and the tools evolved. In July 2025, we said we would discuss AI to write DAX only when we saw clear, measurable benefits: we see them now.</p>
<p>Another reason is specific to training. A generic LLM course in 2025 would have filled a classroom, and it would have been obsolete within six months. <strong>It was too early for us to produce content about it</strong>. I don't know whether September 2026 is early, late, or on time, but it is the moment when we have something to teach that we expect to still be valid in a year or more.</p>
<p>I know, it looks like we made a U-turn instead of simply applying our judgment about the maturity of the tools. However, I stand by that, which was preferable than telling people to write DAX with AI too early. Today, it's not always 100% correct, and performance isn't always ideal. But the productivity gains are significant enough to justify its use. <strong>You should still be able to read DAX</strong> and validate the measures, but you probably don't have to be an advanced DAX expert as if you had to write all the code. That makes a big difference.</p>
<h2><strong>What works today</strong></h2>
<p>In August 2026, we published two articles that illustrate the capabilities of AI in DAX. The first is <a href="https://www.sqlbi.com/articles/creating-dax-functions-with-ai-to-remove-duplicated-code/">creating DAX functions with AI to remove duplicated code</a>: when two measures share most of their code, in any language you move the common part into a function. With user-defined functions, you can do that in DAX too, and AI can write the function and replace the corresponding code in existing measures. Without AI, you can do this with some automated tools, but they stop at the first small difference between the two functions, whereas AI can handle those differences by moving parameters and adjusting the existing code to fit the new function.</p>
<p>When I write a measure, I have to look for the edge cases where the business rule could fail. This takes a lot of time, which is why almost nobody creates such tests. The AI connected to the semantic model finds the cases and writes the DAX queries in about ten minutes while I do something else. I have to check the expected values, because the tests must be validated at some point. In this case, the time saved doesn't shorten DAX writing, but it makes it possible to do validation work that was usually ignored.</p>
<p>These are two examples where we see <strong>clear and measurable benefits</strong>. When writing DAX code for an existing model, you have to be careful. When AI generates the model, the outcome is usually better. We are at the point where it can save time and money, depending on your skills. I can give many examples where writing a DAX formula takes less time than writing the prompt for it, at least for someone who knows DAX. At the same time, if I write complex calculations or transformations in DAX, it can save me time typing the formula if I review it carefully later. For someone who doesn't know DAX well, the quality of the DAX it produces usually addresses the question (if the requirements are clear), even if the code isn't ideal or bulletproof from a performance standpoint.</p>
<h2><strong>What still requires</strong><strong> your attention</strong></h2>
<p><strong>Nothing is perfect</strong>, neither are humans. We still have to pay attention to the prompt, the context, and, in particular, the model used in this work. There is a huge difference between the latest frontier models and what was released a year ago or more. Evaluating specific models is a topic for another article, and it will probably only be valid for a few weeks. So I prefer to focus on what you need to watch out for.</p>
<p>Set the goal, define the requirements clearly, identify edge cases, create tests, and make your model and measures more robust than what you would have done in the past: not because it was unnecessary, but because it was too expensive. We now have tools at our disposal that save us enough time to then focus on improving the quality of the solution we create. We will write more about this in future articles, and we have started incorporating these best practices into our new courses.</p>
<h2><strong>Conclusions</strong></h2>
<p><img loading="lazy" decoding="async" class="nozoom" style="float: right; margin: 0 0 10px 10px; border: none !important;" src="https://www.sqlbi.com/wp-content/uploads/icon-keep-thinking.png" alt="Light bulb with a gear inside" width="150" height="150" /></p>
<p>We have used the same rule for many years: <strong>we use a tool when it saves time in our work</strong>, we say what does not work as long as it does not work, and we say it works when it starts to work. When it comes to writing DAX with AI, that moment came in 2026: using AI today for DAX development is productive, and we can recommend it.</p>
<p>I also have another recommendation: Use AI to improve your productivity, but <strong>do not stop thinking</strong>. Your added value is in the corrections you apply to the output, and in DAX, they are probably more than the 5 to 10% that apply to other languages.</p>
<p>There has never been a better time to <strong>Enjoy DAX!</strong></p>
<p>&nbsp;</p>
<h2><strong>Timeline: what we published, and when</strong></h2>
<table>
<thead>
<tr>
<td><strong>When</strong></td>
<td><strong>What</strong></td>
<td><strong>What we said</strong></td>
</tr>
</thead>
<tbody>
<tr>
<td>March<br />
2023</td>
<td><a href="https://www.sqlbi.com/tv/writing-dax-with-chatgpt-4-unplugged-50/">Writing DAX with ChatGPT-4 (Unplugged #50)</a></td>
<td>A reader asked how ChatGPT could produce good DAX if most DAX on the internet is inaccurate. My reply: "Indeed, it will not."</td>
</tr>
<tr>
<td>May<br />
2024</td>
<td><a href="https://www.sqlbi.com/tv/writing-dax-with-chatgpt-4o-unplugged-58/">Writing DAX with ChatGPT-4o (Unplugged #58)</a></td>
<td>The same test, one year later, with a similar outcome.</td>
</tr>
<tr>
<td>November<br />
2024</td>
<td><a href="https://www.sqlbi.com/tv/compare-dax-optimizer-vs-chatgpt-vs-copilot-for-power-bi/">Compare DAX Optimizer vs ChatGPT vs Copilot for Power BI</a></td>
<td>Feature-by-feature comparison; deterministic results as one of the criteria.</td>
</tr>
<tr>
<td>July<br />
2025</td>
<td><a href="https://www.sqlbi.com/blog/marco/2025/07/28/a-few-thoughts-about-newsletter-300-and-ai/">A few thoughts about newsletter #300 and AI</a></td>
<td>"AI speeds up specific tasks dramatically, but the full end-to-end time savings aren't always as impressive as the initial prototypes suggest."</td>
</tr>
<tr>
<td>July<br />
2026</td>
<td><a href="https://www.sqlbi.com/blog/marco/2026/07/08/generative-ai-guidelines-at-sqlbi-2026-update/">Generative AI guidelines at SQLBI (2026 update)</a></td>
<td>"We can use AI for DAX analysis and rarely in DAX coding." And: "If you stop thinking, you become disposable."</td>
</tr>
<tr>
<td>August<br />
2026</td>
<td><a href="https://www.sqlbi.com/articles/creating-dax-functions-with-ai-to-remove-duplicated-code/">Creating DAX functions with AI to remove duplicated code</a></td>
<td>"AI is a great tool to author DAX code. Clearly, the code needs to be validated thoroughly before putting it in production."</td>
</tr>
<tr>
<td>August<br />
2026</td>
<td><a href="https://www.sqlbi.com/tv/testing-dax-measures-by-using-ai/">Testing DAX measures by using AI</a></td>
<td>"Use AI to explore the model and generate the repetitive code; keep the definition of what is correct behavior under human control."</td>
</tr>
<tr>
<td>September<br />
2026</td>
<td><a href="https://www.sqlbi.com/p/ai-for-power-bi-intro-video-course/">AI for Power BI: Intro</a><br />
<a href="https://www.sqlbi.com/p/dax-with-ai-essentials-video-course/">DAX with AI: Essentials</a><br />
<a href="https://www.sqlbi.com/p/dax-with-ai-scenarios-video-course/">DAX with AI: Scenarios</a></td>
<td>Launch three new courses about using AI with Power BI and DAX.</td>
</tr>
</tbody>
</table>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/blog/marco/2026/09/22/where-we-stand-on-ai-and-dax-in-2026/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>The hidden complexity of a daily average in DAX</title>
		<link>https://www.sqlbi.com/articles/the-hidden-complexity-of-a-daily-average-in-dax/</link>
					<comments>https://www.sqlbi.com/articles/the-hidden-complexity-of-a-daily-average-in-dax/#respond</comments>
		
		<dc:creator><![CDATA[Alberto Ferrari]]></dc:creator>
		<pubDate>Mon, 21 Sep 2026 20:00:08 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<category><![CDATA[Power BI]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[Data modeling]]></category>
		<guid isPermaLink="false">https://www.sqlbi.com/?p=903719</guid>

					<description><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/C0308-head.png" class="webfeedsFeaturedVisual" /></figure>A simple daily average masks a level of complexity that may not be immediately apparent. In this article, we use AI to create a daily average, and then to analyze several edge-case scenarios to build a sound calculation. Computing an&#8230;]]></description>
										<content:encoded><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/C0308-head.png" class="webfeedsFeaturedVisual" /></figure><p>A simple daily average masks a level of complexity that may not be immediately apparent. In this article, we use AI to create a daily average, and then to analyze several edge-case scenarios to build a sound calculation.<span id="more-903719"></span></p>
<p>Computing an average daily sales amount is straightforward: you should iterate over the dates, compute sales for each day, and average the results. An AI assistant can generate DAX code that works in just a few seconds; whether it computes the number you need is a different question.</p>
<p>In this article, we ask AI to generate every measure, function, and validation query. We start with a simple average and test it against more demanding scenarios. Our job is to explain the business requirements, challenge the proposed code, and verify the results. Each test reveals a missing detail from the original request.</p>
<p>The prompts below show a conversation with an AI agent. They describe how to get the DAX code from the AI. They are not a verbatim transcript: a different agent might produce different expressions.</p>
<p>The goal is not to test the assistant or demonstrate complex DAX code. The goal is to show that AI makes it easier to write code. In doing so, it leaves more time for humans to focus on inspecting border cases, refining the formula, and producing better calculations. The very same scenario would be challenging for a human, too.</p>
<p>We start with the following prompt:</p>
<blockquote><p>Create a measure Average Daily Sales (Selling Days) that computes the average daily sales amount by iterating over Date and computing [Sales Amount]</p></blockquote>
<p>A plausible answer is the following, although your execution result may differ:</p>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
Average Daily Sales (Selling Days) =
AVERAGEX ( &#039;Date&#039;, &#x5B;Sales Amount] )
</pre>
<p>The <em>Date</em> table contains one row per day. DAX evaluates <em>Sales Amount</em> for each date visible in the filter context, as it iterates the <em>Date</em> table reference. We can ask the assistant to iterate over the <em>Date[Date]</em> column:</p>
<blockquote><p>Rewrite the measure to iterate over the distinct values of Date[Date], instead of the entire Date table. Keep the existing blank-handling behavior and explain whether this change affects the denominator in this model.</p></blockquote>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
Average Daily Sales (Selling Days) =
AVERAGEX ( VALUES ( &#039;Date&#039;&#x5B;Date] ), &#x5B;Sales Amount] )
</pre>
<p>The measure now works on a single-column table, and it seems a bit more efficient. However, both versions have the same issue: AVERAGEX excludes blank results from the average but considers zero as a relevant value. The two different table expressions produce the same behavior and result.</p>
<p>The following figure shows a simple average over five rows, with the two possible calculations. AVERAGEX performs the calculation on the left, and it ignores blanks.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0308.01.01-Blank-changes-the-denominator.svg" width="800" /></p>
<p>To make the issue more evident, look at the following matrix, where we filtered one color (Brown) and expanded January 2024.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0308-A.png" width="650" /></p>
<p>The value shown for January 2024 is the same as 01/19/2024, the only day with sales.</p>
<p><em>Product</em> filters <em>Sales</em>; therefore, it filters its results, but it does not filter <em>Date</em> through the single-direction relationships. January shows 31 days, but Sales Amount is blank for 30 of them. We can ask AI for a query that makes the denominator visible:</p>
<blockquote><p>Show me a DAX query for January 2024 that compares all product colors with the color Brown. Return the color, sales amount, the number of dates with a nonblank Sales Amount, and the average over those dates. Use the Sales Amount measure and query-scoped definitions for any additional measures. Do not change the model. Explain why a date without sales is excluded from this average.</p></blockquote>
<p>The query generated should return a result very similar to the following one.</p>
<table width="100%">
<tbody>
<tr>
<td width="106"><strong>January 2024</strong></td>
<td width="141"><strong>Sales Amount</strong></td>
<td width="141"><strong>Days with sales</strong></td>
<td width="141"><strong>Average Daily Sales (Selling Days)</strong></td>
</tr>
<tr>
<td width="106">All colors</td>
<td width="141">188,419.28</td>
<td width="141">27</td>
<td width="141">6,978.49</td>
</tr>
<tr>
<td width="106">Brown</td>
<td width="141">8,454.80</td>
<td width="141">1</td>
<td width="141">8,454.80</td>
</tr>
</tbody>
</table>
<p>In January 2024, total sales are 188,419.28 across 27 days. The first measure returns 6,978.49. January has 31 days, but four have a blank <em>Sales Amount</em> and are excluded from the denominator. For Brown products, things are even worse: only one day has sales. In both cases, the number of considered days is less than 31.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0308.01.02-Less-sales-can-produce-a-larger-average.svg" width="800" /></p>
<p>There is no arithmetic error here. The measure computes average sales over days with a non-blank <em>Sales Amount</em>. For this sample, these are the days with sales. If this is the intended business definition, the measure is correct. However, if we want average sales per calendar day, the Brown result should be 8,454.80 divided by 31, or 272.74: the denominator should include days without sales.</p>
<p>To obtain the latter definition, we include an additional requirement in the prompt:</p>
<blockquote><p>
Generate a revised measure named Average Daily Sales (Calendar Days). Divide the total Sales Amount by the number of visible rows in Date, which has one row per day. A product filter must change the amount without removing days from the denominator. Use safe division, preserve a blank amount for now, and do not yet adjust for the beginning or end of the available history.</p></blockquote>
<p>The generated measure does not have an iterator anymore:</p>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
Average Daily Sales (Calendar Days) =
DIVIDE ( &#x5B;Sales Amount], COUNTROWS ( &#039;Date&#039; ) )
</pre>
<p>Because <em>Date</em> contains one row per day, COUNTROWS returns the number of visible days. A filter on <em>Product</em> changes the numerator and not the denominator, because <em>Date</em> does not receive filters from other tables. The result now corresponds to the more refined requirements.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0308-B.png" width="700" /></p>
<p>Another option would have been to replace blanks with zero inside AVERAGEX. For a period with sales, that would solve the denominator problem, even though the measure generated would be slower.</p>
<p>However, neither approach tells us whether all visible dates should participate in the calculation, in case the available calendar goes beyond the date range for which we have transactions.</p>
<p>To evaluate that condition, we ask AI to inspect the dates with the following prompt:</p>
<blockquote><p>
Generate a DAX query returning one row with the number of Date rows, the minimum and maximum Date[Date], the number of Sales rows, the minimum and maximum Sales[Order Date], the distinct count of order dates, and Sales Amount. Also count blank order dates and distinct order dates missing from Date. Query the model without modifying it. Report the date ranges without assuming that the first and last transaction prove data completeness.</p></blockquote>
<p>The query shows that the calendar runs from January 1, 2022 through December 31, 2026. The first transaction is on May 21, 2022, and the last is on March 21, 2026.</p>
<table>
<tbody>
<tr>
<td><strong>Field</strong></td>
<td><strong>Result</strong></td>
</tr>
<tr>
<td><strong>Date Rows</strong></td>
<td>1,826</td>
</tr>
<tr>
<td><strong>First Calendar Date</strong></td>
<td>January 1, 2022</td>
</tr>
<tr>
<td><strong>Last Calendar Date</strong></td>
<td>December 31, 2026</td>
</tr>
<tr>
<td><strong>Sales Rows</strong></td>
<td>3,996</td>
</tr>
<tr>
<td><strong>First Order Date</strong></td>
<td>May 21, 2022</td>
</tr>
<tr>
<td><strong>Last Order Date</strong></td>
<td>March 21, 2026</td>
</tr>
<tr>
<td><strong>Distinct Order Dates</strong></td>
<td>880</td>
</tr>
<tr>
<td><strong>Sales Amount</strong></td>
<td>4,373,105.53</td>
</tr>
<tr>
<td><strong>Blank Order Date Rows</strong></td>
<td>0</td>
</tr>
<tr>
<td><strong>Distinct Order Dates Missing from Date</strong></td>
<td>0</td>
</tr>
</tbody>
</table>
<p>The calendar in the <em>Date</em> table has months without transactions. Even if we reduce the calendar from May 2022 to March 2026, May 2022 (the first month with sales) contains 11 observed days, and March 2026 (the last month with sales) contains 21. Dividing sales by 31 days in either month understates the average.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0308.01.03-Incomplete-months-need-a-smaller-day-count.svg" width="800" /></p>
<p>The problem occurs at both ends of the available history. It also affects totals that include dates before or after that history, in case we did not reduce the calendar.</p>
<p>Notice the assumption we just introduced. The first transaction does not prove that data collection started on that date, and the last transaction does not prove that later dates are unavailable. It is possible that there were no sales on other days.</p>
<p>For the following example, we use the first and last transaction dates as the boundaries of a continuous observation period. We consider every date within that period available, including dates without transactions. In a production model, it is better to use exact data-availability dates when transaction dates cannot be used reliably.</p>
<p>Before improving the formula, we can ask AI to challenge the definition:</p>
<blockquote><p>
Review this average daily sales calculation before proposing another formula. Find small examples that distinguish days without sales from days outside the available data. Include product filters, incomplete first and last months, an empty period, nonconsecutive date selections, and totals. State the expected denominator and identify any business rule that the data alone cannot establish.</p></blockquote>
<p>This is a useful role for AI. Instead of asking only for implementation, we also ask it to find cases that might disprove it. A proposed test becomes useful when we can state its expected result independently and then execute it.</p>
<p>The AI result is a table containing several edge case scenarios that must be carefully inspected and covered by our new rule.</p>
<table>
<tbody>
<tr>
<td><strong>Test</strong></td>
<td><strong>Small example</strong></td>
<td><strong>Expected denominator</strong></td>
</tr>
<tr>
<td><strong>Day without sales</strong></td>
<td>Select May 22, 2022, which has no transactions but lies inside coverage.</td>
<td>1 day</td>
</tr>
<tr>
<td><strong>Day outside coverage</strong></td>
<td>Select May 20, 2022, before assumed coverage starts.</td>
<td>0 days</td>
</tr>
<tr>
<td><strong>Product filter</strong></td>
<td>Select January 2024 and Brown products. Brown has sales on one day, but the entire month is observed.</td>
<td>31 days, not 1</td>
</tr>
<tr>
<td><strong>Incomplete first month</strong></td>
<td>Select May 2022. Count May 21–31, including days without sales.</td>
<td>11 days, not 31</td>
</tr>
<tr>
<td><strong>Incomplete last month</strong></td>
<td>Select March 2026. Count March 1–21, even though its first transaction is March 3.</td>
<td>21 days, not 31</td>
</tr>
<tr>
<td><strong>Observed period without product sales</strong></td>
<td>Select March 2026 and Green products. Green has no sales that month.</td>
<td>21 days</td>
</tr>
<tr>
<td><strong>Period entirely outside coverage</strong></td>
<td>Select April 2026. No selected dates fall inside the coverage.</td>
<td>0 days</td>
</tr>
<tr>
<td><strong>Empty date selection</strong></td>
<td>Apply a filter that selects no dates.</td>
<td>0 days</td>
</tr>
<tr>
<td><strong>Nonconsecutive dates</strong></td>
<td>Select May 21, 22, and 28, 2022. Include the blank sales result on May 22, but exclude unselected dates between them.</td>
<td>3 days, not 8</td>
</tr>
<tr>
<td><strong>Nonconsecutive months</strong></td>
<td>Select January and March 2024. February is not selected.</td>
<td>62 days, not 91</td>
</tr>
<tr>
<td><strong>Total across unequal periods</strong></td>
<td>Select February and March 2026 together. February contributes 28 days; March contributes 21.</td>
<td>49 days</td>
</tr>
</tbody>
</table>
<p>After analyzing the scenarios, we end up with the following requirements for our average:</p>
<ul>
<li>Count the selected dates that fall within the observation period.</li>
<li>Include days without sales within that period.</li>
<li>Use the same observation period for every product, customer, and store selection.</li>
<li>Return zero when there are days observed but no sales for the current selection.</li>
<li>Return blank when there are no observed days selected.</li>
</ul>
<p>The difference between zero and blank matters: zero means we observed a time period and found no sales; blank means we want to ignore that period for the average.</p>
<p>A possible mistake would be to compute the first and last sale after removing only the <em>Date</em> filter, because it is incompatible with our requirements. With that approach, a product selection can move the boundaries. In our sample file, the last sale of a Green product is February 12, 2026, whereas other products have sales through March 21, 2026. Selecting Green should not make March unavailable. However, for our requirements, the March average is zero over 21 days observed.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0308.01.04-Zero-and-blank-answer-different-questions.svg" width="800" /></p>
<p>For the same reason, we must not use each month’s first and last transaction as that month’s boundaries. March 2026 has its first transaction on March 3, but March 1 and March 2 are inside the overall observation period, so they must be counted.</p>
<p>We now have enough information to create the request for the complete measure:</p>
<blockquote><p>
Generate a measure named Average Daily Sales using these rules:<br />
- Define one inclusive observation period from the first to the last Sales[Order Date], ignoring all ordinary report filters when finding those boundaries.<br />
- Count only currently visible, nonblank Date[Date] values inside that period.<br />
- Preserve nonconsecutive selections.<br />
- Compute Sales Amount over exactly those dates while preserving the other report filters.<br />
- Include dates without sales in the denominator.<br />
- Return zero for an observed selection without sales, and blank if there are no observed dates or no boundary dates.<br />
- Use variables to make the steps readable.</p></blockquote>
<p>The resulting code is considerably longer than the first expression we obtained at the beginning of the article:</p>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
Average Daily Sales =
VAR FirstObservedDate =
    CALCULATE ( MIN ( Sales&#x5B;Order Date] ), REMOVEFILTERS () )
VAR LastObservedDate =
    CALCULATE ( MAX ( Sales&#x5B;Order Date] ), REMOVEFILTERS () )
VAR ObservedDates =
    FILTER (
        VALUES ( &#039;Date&#039;&#x5B;Date] ),
        NOT ISBLANK ( &#039;Date&#039;&#x5B;Date] )
            &amp;&amp; &#039;Date&#039;&#x5B;Date] &gt;= FirstObservedDate
            &amp;&amp; &#039;Date&#039;&#x5B;Date] &lt;= LastObservedDate
    )
VAR NumberOfDays = COUNTROWS ( ObservedDates )
VAR Amount =
    CALCULATE ( &#x5B;Sales Amount], KEEPFILTERS ( ObservedDates ) )
RETURN
    IF (
        NOT ISBLANK ( FirstObservedDate )
            &amp;&amp; NOT ISBLANK ( LastObservedDate )
            &amp;&amp; NumberOfDays &gt; 0,
        DIVIDE ( COALESCE ( Amount, 0 ), NumberOfDays )
    )
</pre>
<p><em>FirstObservedDate</em> and <em>LastObservedDate</em> are evaluated after removing any report filters. Therefore, a filter on products or months in a matrix row does not move the boundaries. REMOVEFILTERS does not bypass row-level security: the interval is shared within the data visible to the current security context.</p>
<p><em>ObservedDates</em> starts from <em>VALUES ( 'Date'[Date] )</em>, which contains the dates visible in the current filter context. FILTER keeps only those dates within the observation period and excludes a possible blank member.</p>
<p>The code considers only the selected dates. For example, if the user selects January and March, the denominator must not include February. If it counted the elapsed days between the first and last selected date, it would include February, which the user could have excluded. The same principle applies to a selection of individual days or weekdays.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0308.01.05-Count-selected-dates-inside-the-observation-period.svg" width="800" /></p>
<p>The <em>Average Daily Sales</em> measure then counts <em>ObservedDates</em> and evaluates <em>Sales Amount</em> over the same set. KEEPFILTERS intersects this set with the current context. Filters applied to <em>Product</em>, <em>Customer</em>, and <em>Store</em> continue to affect the amount.</p>
<p>Finally, the measure returns a result only when the boundaries exist and at least one observed day is selected. COALESCE explicitly returns zero for an observed period without sales.</p>
<p>At this point, the AI has generated considerably more DAX than the original request had suggested. If we need the same rule in several measures, we can ask AI to place the logic in a DAX user-defined function (UDF). The following prompt must be executed in the same LLM session we previously used, so it implicitly references the measure <em>Average Daily Sales</em> which the AI created before:</p>
<blockquote><p>Generate a DAX user-defined function named Contoso.AveragePerObservedDay that encapsulates the observed-date selection, day counting, amount evaluation, and zero-versus-blank policy of the revised measure. Accept three parameters: an amount expression, the first observed date, and the last observed date. The caller must supply the boundaries.</p></blockquote>
<p>The result is a reusable function that encapsulates the complex logic and can be reused in different measures:</p>
<div class="dax-code-title">Function</div>
<pre class="brush: dax; title: ; snippet: Function; notranslate">
Contoso.AveragePerObservedDay =
    (
        AmountExpression : SCALAR EXPR,
        FirstObservedDate : SCALAR VAL,
        LastObservedDate : SCALAR VAL
    ) =&gt;
        VAR ObservedDates =
            FILTER (
                VALUES ( &#039;Date&#039;&#x5B;Date] ),
                NOT ISBLANK ( &#039;Date&#039;&#x5B;Date] )
                    &amp;&amp; &#039;Date&#039;&#x5B;Date] &gt;= FirstObservedDate
                    &amp;&amp; &#039;Date&#039;&#x5B;Date] &lt;= LastObservedDate
            )
        VAR NumberOfDays = COUNTROWS ( ObservedDates )
        VAR Amount =
            CALCULATE ( AmountExpression, KEEPFILTERS ( ObservedDates ) )
        RETURN
            IF (
                NOT ISBLANK ( FirstObservedDate )
                    &amp;&amp; NOT ISBLANK ( LastObservedDate )
                    &amp;&amp; NumberOfDays &gt; 0,
                DIVIDE ( COALESCE ( Amount, 0 ), NumberOfDays )
            )
</pre>
<p>The function is intentionally tied to the <em>Date</em> table that must exist in the model. It accepts <em>AmountExpression</em> as the expression to compute within the two observation boundaries specified in the following arguments. The caller decides where the boundaries come from, while the function implements the selection, counting, and empty-result rules.</p>
<p>Once the function is in place, we ask the AI to generate the measure that invokes the function:</p>
<blockquote><p>Generate a measure named Average Daily Sales (Function) that calls Contoso.AveragePerObservedDay with Sales Amount. Preserve the business behavior of Average Daily Sales.</p></blockquote>
<p>The measure generated should be very close to the following one:</p>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
Average Daily Sales (Function) =
Contoso.AveragePerObservedDay (
    &#x5B;Sales Amount],
    CALCULATE ( MIN ( Sales&#x5B;Order Date] ), REMOVEFILTERS () ),
    CALCULATE ( MAX ( Sales&#x5B;Order Date] ), REMOVEFILTERS () )
)
</pre>
<p>Other additive amounts using the same date relationship, observation period, and zero policy can reuse the function. A balance or a distinct customer count would need a separate analysis: dividing an aggregate by a day count is not generally equivalent to averaging its daily values.</p>
<h2>Conclusions</h2>
<p>Although it looks like a simple task, computing an average hides a lot of complexity. This is true for simple calculations, but it is also true (and harder) for more complex calculations. When you define a calculation, you always need to ask yourself how the calculation will behave in border-case scenarios, typically at the beginning or at the end of a time period, at the subtotal and total levels, with different selections, or with blank values in some of the columns or partial calculations.</p>
<p>This is where AI shines, because we can delegate several tasks: perform deeper analysis, try different versions of the code, and find examples that may prove the measure correct or disprove it, showing that some fixes are still needed.</p>
<p>Before the advent of AI, BI professionals spent much of their time writing and debugging DAX code. Today, AI can reduce that time, helping us improve the quality and soundness of measures and calculations.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/articles/the-hidden-complexity-of-a-daily-average-in-dax/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		      <dc:creator><![CDATA[Marco Russo]]></dc:creator>
	</item>
		<item>
		<title>Writing DAX at the correct granularity</title>
		<link>https://www.sqlbi.com/tv/writing-dax-at-the-correct-granularity/</link>
					<comments>https://www.sqlbi.com/tv/writing-dax-at-the-correct-granularity/#respond</comments>
		
		<dc:creator><![CDATA[Alberto Ferrari]]></dc:creator>
		<pubDate>Tue, 08 Sep 2026 10:00:00 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<guid isPermaLink="false">http://www.sqlbi.com/?post_type=video&#038;p=903191</guid>

					<description><![CDATA[<figure><img src="https://i.ytimg.com/vi/hIdtNrp047M/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure>A DAX calculation can produce convincing values at the detail level and fail at the total. The problem is often that the business rule was evaluated at the wrong granularity. This video introduces four grains, or levels of granularity, to&#8230;]]></description>
										<content:encoded><![CDATA[<figure><img src="https://i.ytimg.com/vi/hIdtNrp047M/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure><p>A DAX calculation can produce convincing values at the detail level and fail at the total. The problem is often that the business rule was evaluated at the wrong granularity. This video introduces four grains, or levels of granularity, to identify before writing your DAX formula.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/tv/writing-dax-at-the-correct-granularity/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Writing DAX at the correct granularity</title>
		<link>https://www.sqlbi.com/articles/writing-dax-at-the-correct-granularity/</link>
					<comments>https://www.sqlbi.com/articles/writing-dax-at-the-correct-granularity/#respond</comments>
		
		<dc:creator><![CDATA[Alberto Ferrari]]></dc:creator>
		<pubDate>Mon, 07 Sep 2026 20:00:23 +0000</pubDate>
				<category><![CDATA[Data Modeling]]></category>
		<category><![CDATA[DAX]]></category>
		<category><![CDATA[Power BI]]></category>
		<category><![CDATA[Data modeling]]></category>
		<guid isPermaLink="false">https://www.sqlbi.com/?p=902997</guid>

					<description><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/C0307-1.png" class="webfeedsFeaturedVisual" /></figure>A DAX calculation can produce convincing values at the detail level and still fail at the total. The problem is often not the total itself: the business rule was evaluated at the wrong granularity. This article introduces four grains, or&#8230;]]></description>
										<content:encoded><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/C0307-1.png" class="webfeedsFeaturedVisual" /></figure><p>A DAX calculation can produce convincing values at the detail level and still fail at the total. The problem is often not the total itself: the business rule was evaluated at the wrong granularity. This article introduces four grains, or levels of granularity, that should be identified before writing your DAX formula.<br />
<span id="more-902997"></span><br />
DAX code is often short. Understanding the question that the code must answer may be much harder.</p>
<p>Consider a calculation that multiplies a quantity by a rate. The formula appears simple, yet it is correct only if the quantity and the rate are evaluated while the rate has a clear meaning. If you perform the calculation after combining different rates, DAX must aggregate the rates somehow: AVERAGE, MIN, or MAX can return a number. However, returning a number does not make that number meaningful.</p>
<p>The principle to remember is the following:</p>
<blockquote><p>Evaluate the business rule at the lowest grain where all its inputs have one unambiguous meaning; aggregate only afterward.</p></blockquote>
<p>There are four grains involved in applying this principle:</p>
<ul>
<li>The source-table grain.</li>
<li>The business-rule grain.</li>
<li>The iteration or evaluation grain.</li>
<li>The requested output grain.</li>
</ul>
<p>These grains can be the same; in that case, the code is simple to write. The interesting problems arise when they differ. To showcase the scenario, we use a very simple model about producing lemonade.</p>
<h2>Introducing the lemonade model</h2>
<p>The sample model describes lemonade production. It contains two tables joined by a regular one-to-many relationship.<br />
<img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0307-1.png" alt="" width="600" /></p>
<p>Recipe contains one row for each recipe. Ingredient rates are properties of a recipe; they are not additive values, and as such, they cannot be aggregated.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0307-2.png" alt="" width="260" /></p>
<p>Batch contains one row for each production batch, and each batch, expressed in liters produced, follows a recipe.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0307-3.png" alt="" width="200" /></p>
<p>Water is additive. Therefore, the <em>Total Water</em> is straightforward, and it is already visible in the report:</p>
<div class="dax-code-title">Measure in Batch table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Batch; notranslate">
Total Water = SUM ( Batch&#x5B;Water Liters] )
</pre>
<p>However, the business question we want to answer is: “How many sugar spoons and how many liters of lemon juice should we allocate to prepare the three batches?”</p>
<p>The ingredient requirements are business rules:</p>
<ul>
<li>Sugar spoons are water liters multiplied by the sugar rate of the recipe.</li>
<li>Lemon juice liters are water liters multiplied by the lemon percentage of the recipe.</li>
</ul>
<p>The calculations are easy for an individual batch. For example, B01 uses 10 liters of water and the Sweet recipe, so it requires 30 spoons of sugar and 2.5 liters of lemon juice. Difficulty appears when multiple recipes are aggregated.</p>
<p>The following measures look reasonable at first sight. <em>Total Water</em> computes the total water, whereas the two percentages need to be aggregated, and AVERAGE may seem like a good idea:</p>
<div class="dax-code-title">Measure in Batch table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Batch; notranslate">
Wrong Sugar = &#x5B;Total Water] * AVERAGE ( Recipe&#x5B;Sugar Spoons per Liter] )
</pre>
<div class="dax-code-title">Measure in Batch table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Batch; notranslate">
Wrong Lemon = &#x5B;Total Water] * AVERAGE ( Recipe&#x5B;Lemon Juice % of Water] )
</pre>
<p>Both measures produce the expected value when the report shows one recipe in each row, but the total is clearly wrong. The value is correct at both the batch and recipe levels. Above the recipe level, it is not correct.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0307-4.png" alt="" width="350" /></p>
<p>At the total level, <em>Wrong Sugar</em> computes 14 liters multiplied by the unweighted average of 1 and 3 spoons per liter. The result is 28. <em>Wrong Lemon</em> computes 14 multiplied by the average of 10% and 25%, returning 2.45 liters.</p>
<p>Neither calculation represents the production plan. The batches contain 12 liters of Sweet lemonade and only 2 liters of Classic lemonade. A simple average gives both recipes the same weight, producing an incorrect result.</p>
<p>By default, Power BI does not sum the visible rows. A measure is evaluated independently in every cell. In the total cell, the requested output contains both recipes, and the formula explicitly asks DAX to average their rates. DAX answers that question correctly; it is the question that is wrong, because it does not consider the different granularities at play.</p>
<p>The four grains identify where the mistake occurs.</p>
<table width="100%">
<tbody>
<tr>
<td width="30%"><strong>Grain</strong></td>
<td width="35%"><strong>Question</strong></td>
<td width="34%"><strong>Lemonade example</strong></td>
</tr>
<tr>
<td width="30%">Source-table grain</td>
<td width="35%">What does one row in each table represent?</td>
<td width="34%">One production batch in Batch; one recipe in Recipe</td>
</tr>
<tr>
<td width="30%">Business-rule grain</td>
<td width="35%">At what grain do all inputs to the rule have one meaning?</td>
<td width="34%">Recipe, because one recipe determines one sugar rate and one lemon juice percentage</td>
</tr>
<tr>
<td width="30%">Iteration or evaluation grain</td>
<td width="35%">What rows does DAX evaluate before aggregating?</td>
<td width="34%">Batch rows, recipe rows, or one row for the entire filter context</td>
</tr>
<tr>
<td width="30%">Requested output grain</td>
<td width="35%">What grouping does the report request for the current cell?</td>
<td width="34%">Batch, recipe, or total</td>
</tr>
</tbody>
</table>
<p>The source grain is a property of the data, not of the DAX code. In this model, Batch and Recipe have different grains. The model works because every batch identifies its recipe through a relationship.</p>
<p>The business-rule grain is the level at which the rule can be evaluated without inventing an aggregation for any of its inputs. In the current model, you can first sum water by recipe because the ingredient rate is constant within each recipe.</p>
<p>This produces the following intermediate result.</p>
<table width="100%">
<tbody>
<tr>
<td width="16%"><strong>Recipe</strong></td>
<td width="16%"><strong>Water liters</strong></td>
<td width="15%"><strong>Sugar rate</strong></td>
<td width="18%"><strong>Sugar spoons</strong></td>
<td width="16%"><strong>Lemon rate</strong></td>
<td width="16%"><strong>Lemon liters</strong></td>
</tr>
<tr>
<td width="16%">Classic</td>
<td width="16%">2</td>
<td width="15%">1.00</td>
<td width="18%">2</td>
<td width="16%">10%</td>
<td width="16%">0.20</td>
</tr>
<tr>
<td width="16%">Sweet</td>
<td width="16%">12</td>
<td width="15%">3.00</td>
<td width="18%">36</td>
<td width="16%">25%</td>
<td width="16%">3.00</td>
</tr>
</tbody>
</table>
<p>Only after applying the rule to each row of this table is it safe to aggregate. The correct totals are 38 sugar spoons and 3.20 liters of lemon juice.</p>
<p>Evaluating at a finer grain is also safe, as we show with the next measure. In our example, computing the rule once for every batch produces the same result because the rate is constant for all batches of the same recipe.<br />
However, evaluating below the necessary grain can perform more work than required. A useful operational interpretation of the central rule is therefore:</p>
<blockquote><p>Never evaluate above the business-rule grain. A finer grain can be correct; the coarsest grain that still preserves an unambiguous meaning is usually more efficient.</p></blockquote>
<p>The DAX expression defines the evaluation and the granularity: the table passed to the SUMX iterator declares the grain of the calculation. For example, the following measure evaluates the sugar rule at the Batch granularity. It multiplies the water liters in each <em>Batch</em> row by the number of sugar spoons per liter defined in the corresponding recipe:</p>
<div class="dax-code-title">Measure in Batch table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Batch; notranslate">
Sugar Spoons at batch grain =
SUMX (
    Batch,
    Batch&#x5B;Water Liters] *
        RELATED ( Recipe&#x5B;Sugar Spoons per Liter] )
)
</pre>
<p>This formula is correct. However, you can perform the same calculation at the business-rule grain.</p>
<div class="dax-code-title">Measure in Batch table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Batch; notranslate">
Sugar Spoons =
SUMX (
    SUMMARIZE (
        Batch,
        Recipe&#x5B;Recipe],
        Recipe&#x5B;Sugar Spoons per Liter]
    ),
    &#x5B;Total Water] * Recipe&#x5B;Sugar Spoons per Liter]
)
</pre>
<p>SUMMARIZE builds one row for each recipe visible in the current filter context. SUMX iterates those rows. <em>Total Water</em> invokes the context transition because it is a measure, so it returns the water for the current recipe. The rate comes from the recipe itself, which is the same iterated row. Finally, SUMX aggregates the recipe results, only after the rule has been applied.</p>
<p>The lemon calculation follows the same structure:</p>
<div class="dax-code-title">Measure in Batch table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Batch; notranslate">
Total Lemon =
SUMX (
    SUMMARIZE (
        Batch,
        Recipe&#x5B;Recipe],
        Recipe&#x5B;Lemon Juice % of Water]
    ),
    &#x5B;Total Water] * Recipe&#x5B;Lemon Juice % of Water]
)
</pre>
<p>In this specific calculation, grouping only by <em>Recipe[Lemon Juice % of Water]</em> would also be correct and more efficient. Recipes with the same percentage can be combined because the percentage is the only non-additive input the rule uses. However, grouping by recipe as well can be simpler for an initial implementation, and it is easier to change if the rule later needs other recipe properties. Similarly, <em>Sugar Spoons</em> could aggregate by <em>Recipe[Sugar Spoons per Liter]</em>, using a single multiplication for all recipes with the same number of sugar spoons per liter.</p>
<p>The source-grain and business-grain versions return the same values at the recipe rows and at the total. Both formulas evaluate the multiplication before combining rows with different rates.</p>
<p>The query generated for the report determines the requested output grain. A matrix can request a value for each batch, each recipe, or the entire production plan. It can also change after a user drills down or adds a field to the visual.</p>
<p>A robust measure does not rely on the output grain to produce correct results. In order to make the rule unambiguous, the measure enforces its own evaluation grain internally:</p>
<ul>
<li>On a recipe row, the iterator sees one recipe.</li>
<li>At the grand total, the iterator sees both recipes and sums two results.</li>
<li>If the visual does not display Recipe at all, the measure still evaluates the rule by recipe.</li>
</ul>
<p>This is why you should not fix a total by blindly iterating the rows currently visible in a visual. The visual grain is a presentation choice. The business-rule grain belongs in the measure and should remain valid when the visual changes.</p>
<h2>Recognizing the same problem in budgeting</h2>
<p>Budgeting scenarios are a larger version of the lemonade example. Actual sales might exist by transaction, product, and day, while a budget is defined by product category and month. The source tables have different grains, the requested report can use yet another grain, and an allocation rule introduces its own grain.</p>
<p>There are only two correct choices when a report requests detail below the budget grain:</p>
<ul>
<li>Do not display the budget at unsupported detail.</li>
<li>Define an explicit allocation rule that creates that detail.</li>
</ul>
<p>Repeating a category budget for every product is not allocation. It duplicates the value. Similarly, computing one allocation factor after categories or periods have already been combined evaluates the rule at a grain above its valid level.</p>
<p>The <a href="https://www.daxpatterns.com/budget/">Budget pattern</a> makes the business choices explicit and computes prior-year sales at the forecast grain before allocating the forecast. The article, <a href="https://www.sqlbi.com/articles/budget-and-other-data-at-different-granularities-in-powerpivot/">Budget and Other Data at Different Granularities in PowerPivot</a> illustrates the related modeling problem with daily sales and monthly budgets. In both cases, the complexity comes from creating meaning across grains: the DAX formula in the model measure is the final representation of those choices.</p>
<p>The four-grain framework scales to these scenarios:</p>
<table width="100%">
<tbody>
<tr>
<td width="32%"><strong>Grain</strong></td>
<td width="67%"><strong>Typical budgeting example</strong></td>
</tr>
<tr>
<td width="32%">Source-table grain</td>
<td width="67%">Sales by transaction and day; budget by category and month</td>
</tr>
<tr>
<td width="32%">Business-rule grain</td>
<td width="67%">The category-month or other governed allocation group</td>
</tr>
<tr>
<td width="32%">Iteration or evaluation grain</td>
<td width="67%">The groups over which allocation factors are computed and applied</td>
</tr>
<tr>
<td width="32%">Requested output grain</td>
<td width="67%">Product, day, territory, subtotal, or grand total requested by the report</td>
</tr>
</tbody>
</table>
<p>The developer must decide whether the requested output is supported directly, needs an allocation, or should return blank. You cannot delegate that decision to AVERAGE or the total row of a matrix.</p>
<h2>A checklist for grain-aware DAX</h2>
<p>Before writing a measure, answer these questions:</p>
<ol>
<li>What does one row mean in every source table used by the calculation?</li>
<li>Which keys make every rate, threshold, percentage, status, or other rule input unambiguous?</li>
<li>Which inputs are additive before the rule is applied, and which are not?</li>
<li>What virtual table should an iterator enumerate to express the business-rule grain?</li>
<li>Is the chosen evaluation grain independent of the fields currently displayed in the visual?</li>
<li>Has any upstream transformation aggregated away a key required by the rule?</li>
<li>What should happen when the report requests detail below the grain supported by the data?</li>
<li>Can the result be verified by manually computing a few business-grain rows and aggregating them?</li>
</ol>
<p>The last test can be very effective. Do not start by checking whether the grand total equals the visible rows. First, build the small intermediate table at the business-rule grain, validate each row, and only then validate its aggregation.</p>
<h2>Conclusion</h2>
<p>The hardest part of many DAX calculations is deciding where to evaluate the business rule. The source grain describes the available data. The business-rule grain describes where the rule has meaning. The iteration grain describes what the DAX code actually does. The requested output grain describes what the report asks for. Correct results require these four grains to be compatible, not necessarily identical.</p>
<p>The lemonade model is a simple example to show what could go wrong, because it has only two recipes and three batches. In a real model, the same mistake can be hidden behind thousands of products, changing rates, allocations, and several dimensions.</p>
<p>The rule of thumb is to evaluate the business rule at the lowest grain where all its inputs have one unambiguous meaning, and aggregate only afterward.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/articles/writing-dax-at-the-correct-granularity/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		      <dc:creator><![CDATA[Marco Russo]]></dc:creator>
	</item>
		<item>
		<title>Matrix Totals - The Whiteboard #13</title>
		<link>https://www.sqlbi.com/tv/matrix-totals-the-whiteboard-13/</link>
					<comments>https://www.sqlbi.com/tv/matrix-totals-the-whiteboard-13/#respond</comments>
		
		<dc:creator><![CDATA[Marco Russo]]></dc:creator>
		<pubDate>Wed, 02 Sep 2026 12:44:43 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<category><![CDATA[Power BI]]></category>
		<guid isPermaLink="false">http://www.sqlbi.com/?post_type=video&#038;p=902818</guid>

					<description><![CDATA[<figure><img src="https://i.ytimg.com/vi/jbGEXZ85Nr0/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure>Let's talk about matrix totals in this episode of The Whiteboard! Special guest: the SQLBI Whiteboard! Learn abstract DAX concepts in a more interactive way with "The Whiteboard" series. Read more: https://www.sqlbi.com/blog/marco/2022/07/14/the-whiteboard-video-series-on-sqlbi-youtube-channel/ #thewhiteboard]]></description>
										<content:encoded><![CDATA[<figure><img src="https://i.ytimg.com/vi/jbGEXZ85Nr0/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure><p>Let's talk about matrix totals in this episode of The Whiteboard!<br />
Special guest: the SQLBI Whiteboard!</p>
<p>Learn abstract DAX concepts in a more interactive way with "The Whiteboard" series.<br />
Read more: https://www.sqlbi.com/blog/marco/2022/07/14/the-whiteboard-video-series-on-sqlbi-youtube-channel/</p>
<p>#thewhiteboard</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/tv/matrix-totals-the-whiteboard-13/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>SQLBI Whiteboard</title>
		<link>https://www.sqlbi.com/tools/sqlbi-whiteboard/</link>
					<comments>https://www.sqlbi.com/tools/sqlbi-whiteboard/#respond</comments>
		
		<dc:creator><![CDATA[SQLBI]]></dc:creator>
		<pubDate>Thu, 27 Aug 2026 11:29:59 +0000</pubDate>
				<guid isPermaLink="false">https://www.sqlbi.com/?post_type=tool&#038;p=902955</guid>

					<description><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/SQLBI.Whiteboard.svg" class="webfeedsFeaturedVisual" /></figure>SQLBI Whiteboard is a free, open-source whiteboard for Windows 11 designed for teaching DAX, SQL, and data modeling, with pen ink, live application capture, and syntax-highlighted code on the same canvas. Pen-first ink — Low-latency, pressure-aware strokes with palm rejection,&#8230;]]></description>
										<content:encoded><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/SQLBI.Whiteboard.svg" class="webfeedsFeaturedVisual" /></figure><p>SQLBI Whiteboard is a free, open-source whiteboard for Windows 11 designed for teaching DAX, SQL, and data modeling, with pen ink, live application capture, and syntax-highlighted code on the same canvas.<br />
<span id="more-902955"></span></p>
<ul dir="ltr">
<li><strong>Pen-first ink</strong> — Low-latency, pressure-aware strokes with palm rejection, rear-eraser support, and a barrel-button laser pointer, alongside full touch and mouse support on an unbounded canvas.</li>
<li><strong>Live application capture</strong> — Embed a live view of any running window or display on the board — a Power BI report, SSMS, anything — then freeze it and draw over it.</li>
<li><strong>DAX and SQL snippets</strong> — Paste code into containers with live syntax highlighting, and format DAX or T-SQL with one key.</li>
<li><strong>Boards are files</strong> — Documents are local <code>.wboard</code> files with no account and no cloud service, and Markdown-based <code>.wimport</code> recipes build a prepared board from a text file.</li>
</ul>
<p>SQLBI Whiteboard is a standalone application rather than a collaboration service: there is no shared canvas, no tenant, and no sign-in. It is designed for the room, the projector, the recorded lesson, and the pen — the way we teach.</p>
<p>Download it from the Microsoft Store, or as an installer or portable ZIP from GitHub, where the source code is available under the MIT license. Documentation, guide, and FAQ are at <a href="https://whiteboard.sqlbi.com">whiteboard.sqlbi.com</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/tools/sqlbi-whiteboard/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Testing DAX measures by using AI</title>
		<link>https://www.sqlbi.com/tv/testing-dax-measures-by-using-ai/</link>
					<comments>https://www.sqlbi.com/tv/testing-dax-measures-by-using-ai/#respond</comments>
		
		<dc:creator><![CDATA[Marco Russo]]></dc:creator>
		<pubDate>Tue, 25 Aug 2026 10:00:00 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<guid isPermaLink="false">http://www.sqlbi.com/?post_type=video&#038;p=902139</guid>

					<description><![CDATA[<figure><img src="https://i.ytimg.com/vi/qYQpl5Hjdk0/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure>How to use AI to identify relevant test cases in a semantic model and create repeatable DAX tests for a measure.]]></description>
										<content:encoded><![CDATA[<figure><img src="https://i.ytimg.com/vi/qYQpl5Hjdk0/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure><p>How to use AI to identify relevant test cases in a semantic model and create repeatable DAX tests for a measure.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/tv/testing-dax-measures-by-using-ai/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Testing DAX measures by using AI</title>
		<link>https://www.sqlbi.com/articles/testing-dax-measures-by-using-ai/</link>
					<comments>https://www.sqlbi.com/articles/testing-dax-measures-by-using-ai/#respond</comments>
		
		<dc:creator><![CDATA[Marco Russo]]></dc:creator>
		<pubDate>Mon, 24 Aug 2026 20:00:44 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<category><![CDATA[Power BI]]></category>
		<category><![CDATA[AI]]></category>
		<category><![CDATA[semantic model]]></category>
		<guid isPermaLink="false">https://www.sqlbi.com/?p=902664</guid>

					<description><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/C0306-cover.png" class="webfeedsFeaturedVisual" /></figure>This article describes how to use AI to identify relevant test cases in a semantic model and create repeatable DAX tests for a measure. A DAX measure is correct only when it produces the expected result in every situation of&#8230;]]></description>
										<content:encoded><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/C0306-cover.png" class="webfeedsFeaturedVisual" /></figure><p>This article describes how to use AI to identify relevant test cases in a semantic model and create repeatable DAX tests for a measure.<br />
<span id="more-902664"></span></p>
<p>A DAX measure is correct only when it produces the expected result in every situation of filter context and available data. A value being correct in a report for a particular product or customer does not prove that the measure works for other selections. This matters when the measure implements business logic that depends on the active filters.</p>
<p>We usually validate this logic by creating report pages that isolate a few customers and transactions. This is useful during development because we can see the data and we can reason about the result. However, manual validation is difficult to repeat after every change. It is also easy to forget a filter combination that previously exposed a problem.</p>
<p>AI can help with the repetitive part of this work. Once connected to the semantic model, the AI assistant can explore the data, find representative cases, and generate DAX queries that reproduce the required filter contexts. The expected results still require human validation. The role of AI is to find and automate the cases, not to define the business requirement.</p>
<h2>Understanding the measure to test</h2>
<p>We start from the <a href="https://www.daxpatterns.com/new-and-returning-customers/">New and returning customers</a> pattern and, in particular, from the <strong>Dynamic relative</strong> implementation of the <em># New Customers</em> measure. The pattern takes into account all the report filters. For example, if the report filters the Audio category, a customer is considered new on the first date when they purchase an Audio product. An earlier purchase in another category does not make the customer exist for the Audio category.</p>
<p>The same rule applies to brands. If the filter contains Contoso and Northwind Traders, the two brands form one selection. The customer is new on their first purchase date across the union of those brands. The measure must not compute a separate first purchase date for each brand.</p>
<p>This behavior makes the measure a good candidate for regression testing. A modification can preserve the result of a simple case while changing how the measure handles a category filter or a multiple-brand selection. Therefore, testing only a straightforward positive result is not enough.</p>
<h2>Identifying four types of tests</h2>
<p>We want tests that can detect both incorrect inclusions and incorrect exclusions. For this reason, we manually identify four types of cases before asking AI to automate them:</p>
<ul>
<li>A <strong>positive test</strong>checks that a qualifying customer is counted. This is the simplest valid case and it confirms that the main path of the calculation works.</li>
<li>A <strong>negative test</strong>checks that a customer without a qualifying purchase in the selected context is not counted. This detects filters that are ignored or incorrectly removed.</li>
<li>A <strong>prevent false positive test</strong>uses a case that looks valid under a narrower filter but is invalid under the filter being tested. This detects a measure that counts too many customers.</li>
<li>A <strong>prevent false negative test</strong>uses a valid case with an earlier purchase outside the current selection. This detects a measure that counts too few customers because it evaluates the first purchase over the wrong set of products.</li>
</ul>
<p>We manually created the following report pages, which isolate one case for each purpose. Read the details of each example to understand the type of tests implemented, which are intended to evaluate possible regressions in case of changes to the measure. You can skip the details of each case if you just want to see how to automate this process.</p>
<h3>Testing a positive case</h3>
<p>Customer 362, Cindy Ramos, purchased Audio products from both Contoso and Northwind Traders on April 9, 2007. This is the first purchase date in the selected two-brand Audio context. Therefore, <em># New Customers</em> must return 1.</p>
<p>The report also shows later purchases when all brands and categories are visible. Those later transactions do not affect the expected result on April 9, 2007.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0306-01.png" width="800" /></p>
<p>This test confirms that a customer with a qualifying first purchase is included. However, it does not prove that the measure excludes transactions outside the selection.</p>
<h3>Testing a negative case</h3>
<p>Customer 11317, Deanna Sara, made a purchase on July 9, 2007. That transaction was for Fabrikam in Cameras and camcorders, so it is outside the selected Contoso and Northwind Traders Audio context. The expected value of <em># New Customers</em> for that date and selection is 0.</p>
<p>The same customer first purchased Audio from Northwind Traders on July 23, 2007. The report returns 1 on that later date under the two-brand Audio filter, but it must not return 1 on July 9.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0306-02.png" width="800" /></p>
<p>This test detects a measure that removes or ignores the category and brand filters while finding the first purchase.</p>
<h3>Preventing a false positive</h3>
<p>The same customer provides a more subtle case. Deanna Sara bought Audio from Northwind Traders on July 23, 2007, and from Contoso on August 14, 2007. If the report selects only Contoso, the customer is new on August 14 because that is her first Contoso Audio purchase.</p>
<p>The expected result changes when the report selects both Contoso and Northwind Traders. The first purchase in that union occurred on July 23. Therefore, the customer is not new on August 14, and # New Customers must return 0.<br />
<img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0306-03.png" width="800" /></p>
<p>This case detects an implementation that evaluates the first purchase separately for every selected brand. This implementation could pass the positive and negative tests. However, it still counts the same customer as new more than once within a multiple-brand selection.</p>
<h3>Preventing a false negative</h3>
<p>Customer 175, Nicholas Robinson, made a purchase on January 18, 2007, outside the selected Audio context. The first purchase in the Contoso and Northwind Traders Audio selection occurred on October 27, 2007. Consequently, # New Customers must return 1 on October 27.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0306-04.png" width="800" /></p>
<p>This case detects a measure that uses the first purchase across all products. That behavior would be correct for a dynamic absolute calculation, but it is incorrect for the dynamic relative measure we are testing.</p>
<h2>Asking AI to create the tests</h2>
<p>Manually finding these cases requires repeated filtering and inspection of transaction histories. We can automate the discovery and generation process by connecting an AI assistant to Power BI Desktop through the <a href="https://learn.microsoft.com/en-us/power-bi/developer/mcp/">Power BI Modeling MCP server</a>. The local server lets the assistant inspect the semantic model and execute or validate DAX queries against it.</p>
<p>We used the following prompt:</p>
<blockquote><p>I want to create tests for the # New Customers measure. It is dynamic, which takes into account all the filters in the report for the calculation. Therefore, if the report filters one category (Audio, for example), a customer is reported as new the first time they buy a product of the Audio category. Find cases for one and two brands selected that show positive, negative, and prevent false positive and false negative. Create the DAX queries to validate the test. The query should return one or more row with the test name, the status, and a description of the test result (with an explanation of the issue found). We'll assign each query to a function and run one or more tests together by using UNION of their results.</p></blockquote>
<p>The prompt provides the semantic rule, the dimensions that must vary, the four test purposes, and the required output contract. This information is important. A generic request to test the measure would leave the assistant free to choose cases that do not exercise the relevant filter behavior.</p>
<p>The assistant inspected the model and generated eight tests: four for a single selected brand and four for a two-brand selection. We manually verified the selected transactions before keeping their expected values. This step prevents a circular test in which the measure under test also determines the expected result.</p>
<h2>Organizing tests with DAX functions</h2>
<p>The generated query uses <a href="https://learn.microsoft.com/en-us/dax/best-practices/dax-user-defined-functions">DAX user-defined functions</a> to keep every test independent and give every result the same shape. DAX user-defined functions (UDF) can be defined and evaluated in DAX query view by using the FUNCTION keyword in a DEFINE block. They require compatibility level 1702 or higher and are generally available in Power BI Desktop and the Power BI service starting from the June 2026 release. A common TestResult function compares the actual and expected values and returns one row with the test name, status, and description.</p>
<p>Each test function applies filters for one customer, category, brand selection, and date. TREATAS reproduces the filter context of the report, whereas COALESCE converts a blank result to zero when the customer must not be counted.</p>
<p>The following extract shows the common result function and the two-brand false-positive test:</p>
<div class="dax-code-title">Function</div>
<pre class="brush: dax; title: ; snippet: Function; notranslate">
// Creates the standard one-row result returned by every test.
TestResult = (
    testName : STRING,
    actualValue : INT64,
    expectedValue : INT64,
    passDescription : STRING,
    failureExplanation : STRING
) =&gt;
    ROW (
        &quot;Test name&quot;, testName,
        &quot;Status&quot;, IF ( actualValue = expectedValue, &quot;PASS&quot;, &quot;FAIL&quot; ),
        &quot;Description&quot;,
            IF (
                actualValue = expectedValue,
                passDescription,
                failureExplanation
                    &amp; &quot; Expected &quot; &amp; FORMAT ( expectedValue, &quot;0&quot; )
                    &amp; &quot;, got &quot; &amp; FORMAT ( actualValue, &quot;0&quot; ) &amp; &quot;.&quot;
            )
    )
</pre>
<div class="dax-code-title">Function</div>
<pre class="brush: dax; title: ; snippet: Function; notranslate">
// Guards against calculating the first purchase separately for each brand.
Test_NewCustomers_2Brands_NoFalsePositive = () =&gt;
    VAR Actual =
        COALESCE (
            CALCULATE (
                &#x5B;# New Customers],
                TREATAS ( { 11317 }, Customer&#x5B;CustomerKey] ),
                TREATAS ( { &quot;Audio&quot; }, Product&#x5B;Category] ),
                TREATAS (
                    { &quot;Contoso&quot;, &quot;Northwind Traders&quot; },
                    Product&#x5B;Brand]
                ),
                TREATAS ( { DATE ( 2007, 8, 14 ) }, &#039;Date&#039;&#x5B;Date] )
            ),
            0
        )
    RETURN
        TestResult (
            &quot;2 brands - prevent false positive&quot;,
            Actual,
            0,
            &quot;Customer 11317 is not counted because the first purchase &quot;
                &amp; &quot;in the selected union was on 2007-07-23.&quot;,
            &quot;The first date was evaluated per brand instead of across &quot;
                &amp; &quot;the selected brand union.&quot;
        )
</pre>
<p>The final EVALUATE statement combines the selected functions with UNION. We can add or remove function calls to run the complete suite or only the tests related to a specific change:</p>
<div class="dax-code-title">Function</div>
<pre class="brush: dax; title: ; snippet: Function; notranslate">
EVALUATE
UNION (
    Test_NewCustomers_1Brand_Positive (),
    Test_NewCustomers_1Brand_Negative (),
    Test_NewCustomers_1Brand_NoFalsePositive (),
    Test_NewCustomers_1Brand_NoFalseNegative (),
    Test_NewCustomers_2Brands_Positive (),
    Test_NewCustomers_2Brands_Negative (),
    Test_NewCustomers_2Brands_NoFalsePositive (),
    Test_NewCustomers_2Brands_NoFalseNegative ()
)
</pre>
<p>The sample Power BI file you can download below, contains the helper function and all eight test functions.</p>
<h2>Reading the test results</h2>
<p>Running the query against the original measure produces one row for each test. All eight tests return PASS.</p>
<table>
<thead>
<tr>
<td><strong>Test name</strong></td>
<td><strong>Status</strong></td>
<td><strong>Validated behavior</strong></td>
</tr>
</thead>
<tbody>
<tr>
<td>1 brand - positive</td>
<td>PASS</td>
<td>Customer 362 is counted on the first Contoso Audio purchase date.</td>
</tr>
<tr>
<td>1 brand - negative</td>
<td>PASS</td>
<td>Customer 11317 is not counted on a date with only a Northwind Traders Audio purchase.</td>
</tr>
<tr>
<td>1 brand - prevent false positive</td>
<td>PASS</td>
<td>Customer 4626 is not counted on a repeat Contoso Audio purchase.</td>
</tr>
<tr>
<td>1 brand - prevent false negative</td>
<td>PASS</td>
<td>Customer 175 is counted despite an earlier purchase outside Contoso Audio.</td>
</tr>
<tr>
<td>2 brands - positive</td>
<td>PASS</td>
<td>Customer 362 is counted on the first purchase date in the selected brand union.</td>
</tr>
<tr>
<td>2 brands - negative</td>
<td>PASS</td>
<td>Customer 11317 is not counted for a purchase outside both selected Audio brands.</td>
</tr>
<tr>
<td>2 brands - prevent false positive</td>
<td>PASS</td>
<td>The later Contoso purchase is not treated as an initial purchase.</td>
</tr>
<tr>
<td>2 brands - prevent false negative</td>
<td>PASS</td>
<td>An earlier purchase outside the two-brand Audio selection does not suppress the valid result.</td>
</tr>
</tbody>
</table>
<p>A failing row reports the expected and actual values and includes an explanation of the likely issue. Therefore, the query provides the first piece of information required to investigate a regression when it detects one.</p>
<h2>Keeping the tests useful</h2>
<p>The test query is deterministic after we select the customers, dates, filters, and expected values. The AI discovery process does not need to be deterministic because it is only used to create the initial suite. Once the tests are reviewed, they become ordinary DAX code that we can store with the project and execute again.</p>
<p>These tests depend on the sample data. If transactions or dimension values change, we must review the cases or run them against a stable test model. To implement tests, we always need controlled input data, regardless of whether we use AI to create these tests.</p>
<p>Moreover, the measure passing all eight tests does not prove that the measure is correct for every possible context. The goal of this article was to provide a more efficient way to create tests, but the completeness of the test suite depends on the calculation implemented and the model complexity that could affect the DAX expression.</p>
<h2>Conclusions</h2>
<p>Testing a DAX measure requires more than comparing a few totals. We must reproduce the filter contexts that define the business rules and include cases that detect both false positives and false negatives.</p>
<p>AI connected to the semantic model can reduce the effort required to find those cases and write the corresponding DAX queries. However, we must verify the expected results independently before accepting the tests. Use AI to explore the model and generate the repetitive code; keep the definition of what is correct behavior under human control.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/articles/testing-dax-measures-by-using-ai/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		      <dc:creator><![CDATA[Alberto Ferrari]]></dc:creator>
	</item>
		<item>
		<title>Creating DAX functions with AI to remove duplicated code</title>
		<link>https://www.sqlbi.com/tv/creating-dax-functions-with-ai-to-remove-duplicated-code/</link>
					<comments>https://www.sqlbi.com/tv/creating-dax-functions-with-ai-to-remove-duplicated-code/#respond</comments>
		
		<dc:creator><![CDATA[Alberto Ferrari]]></dc:creator>
		<pubDate>Tue, 11 Aug 2026 10:00:00 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<guid isPermaLink="false">https://www.sqlbi.com/?post_type=video&#038;p=901936</guid>

					<description><![CDATA[<figure><img src="https://i.ytimg.com/vi/sp8Bx2yL3lw/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure>How to leverage AI to refactor existing DAX code, remove duplicated parts, and centralize the common code in user-defined functions.]]></description>
										<content:encoded><![CDATA[<figure><img src="https://i.ytimg.com/vi/sp8Bx2yL3lw/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure><p>How to leverage AI to refactor existing DAX code, remove duplicated parts, and centralize the common code in user-defined functions.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/tv/creating-dax-functions-with-ai-to-remove-duplicated-code/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Creating DAX functions with AI to remove duplicated code</title>
		<link>https://www.sqlbi.com/articles/creating-dax-functions-with-ai-to-remove-duplicated-code/</link>
					<comments>https://www.sqlbi.com/articles/creating-dax-functions-with-ai-to-remove-duplicated-code/#respond</comments>
		
		<dc:creator><![CDATA[Marco Russo]]></dc:creator>
		<pubDate>Mon, 10 Aug 2026 20:00:00 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<category><![CDATA[Power BI]]></category>
		<category><![CDATA[AI]]></category>
		<guid isPermaLink="false">https://www.sqlbi.com/?p=901861</guid>

					<description><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/C0302.png" class="webfeedsFeaturedVisual" /></figure>Code written by AI can contain many duplicate parts. User-defined functions are a great tool to simplify complex measures; this article shows how to leverage AI to build better DAX code. AI is getting better and better at generating DAX&#8230;]]></description>
										<content:encoded><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/C0302.png" class="webfeedsFeaturedVisual" /></figure><p>Code written by AI can contain many duplicate parts. User-defined functions are a great tool to simplify complex measures; this article shows how to leverage AI to build better DAX code.</p>
<p><span id="more-901861"></span></p>
<p>AI is getting better and better at generating DAX code, and as of today, it is pretty common to see rather complex DAX code that is entirely AI-generated. Sometimes AI-generated code requires refactoring or adjustment to make it more performant or reusable.</p>
<p>In this article, we show the process of first obtaining DAX code that actually works with AI, and then refactoring it to produce a better version. How do we refactor it? With AI, of course. AI can be a great tool if you ask it the right questions.</p>
<p>As an example, we are going to use the <a href="https://www.daxpatterns.com/new-and-returning-customers/">New Customer</a> pattern. The prompt is deliberately simple and somewhat ambiguous, as it does not contain any clarification about what we mean by “new” customer, or how we want to generate the code. However, we are not interested in the algorithm but, rather, in the type of DAX code generated:</p>
<blockquote><p>Create two measures: I want to compute the new customers and then the sales of new customers</p></blockquote>
<p>AI quickly answers with two measures:</p>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
# New Customers = 
VAR FirstVisibleDate =
    MIN ( &#039;Date&#039;&#x5B;Date] )
VAR LastVisibleDate =
    MAX ( &#039;Date&#039;&#x5B;Date] )
VAR CustomersInCurrentPeriod =
    VALUES ( Sales&#x5B;CustomerKey] )
VAR NewCustomers =
    FILTER (
        CustomersInCurrentPeriod,
        VAR CurrentCustomerKey = Sales&#x5B;CustomerKey]
        VAR FirstPurchaseDate =
            CALCULATE (
                MIN ( Sales&#x5B;Order Date] ),
                REMOVEFILTERS (),
                TREATAS ( { CurrentCustomerKey }, Customer&#x5B;CustomerKey] )
            )
        RETURN
            FirstPurchaseDate &gt;= FirstVisibleDate
                &amp;&amp; FirstPurchaseDate &lt;= LastVisibleDate
    )
RETURN
    COUNTROWS ( NewCustomers )
</pre>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
Sales New Customers = 
VAR FirstVisibleDate =
    MIN ( &#039;Date&#039;&#x5B;Date] )
VAR LastVisibleDate =
    MAX ( &#039;Date&#039;&#x5B;Date] )
VAR CustomersInCurrentPeriod =
    VALUES ( Sales&#x5B;CustomerKey] )
VAR NewCustomers =
    FILTER (
        CustomersInCurrentPeriod,
        VAR CurrentCustomerKey = Sales&#x5B;CustomerKey]
        VAR FirstPurchaseDate =
            CALCULATE (
                MIN ( Sales&#x5B;Order Date] ),
                REMOVEFILTERS (),
                TREATAS ( { CurrentCustomerKey }, Customer&#x5B;CustomerKey] )
            )
        RETURN
            FirstPurchaseDate &gt;= FirstVisibleDate
                &amp;&amp; FirstPurchaseDate &lt;= LastVisibleDate
    )
RETURN
    CALCULATE (
        &#x5B;Sales Amount],
        KEEPFILTERS (
            TREATAS ( NewCustomers, Customer&#x5B;CustomerKey] )
        )
    )
</pre>
<p>AI being AI, the result is not always the same. However, most of the time it produces a similar algorithm. Despite not being super-duper optimized, this algorithm works fine, and it is not a bad solution for a small/medium data model.</p>
<p>As humans, we quickly notice that most of the code is duplicated. Both measures compute the <em>NewCustomers</em> variable; the first one just counts its rows, whereas the second uses the variable as a table filter in CALCULATE. It is the perfect scenario where designing a user-defined function (UDF) would have been a great solution.</p>
<p>However, because of the final step, creating the function is not entirely obvious. A good DAX developer would solve the scenario immediately. What about AI? Let us test it:</p>
<blockquote><p>Both measures have a very similar logic, apart from the last step. Can you create a function that embeds most of the logic of new customers, so to produce simpler measures and put the DAX code at a common factor in the function?</p></blockquote>
<p>AI decides to create a function for the <em>NewCustomers</em> variable only and, AI being AI, it decides to make the function as <a href="https://www.sqlbi.com/articles/model-dependent-and-model-independent-user-defined-functions-in-dax/">model-independent</a> as possible, by passing the required columns as arguments. This is definitely a very good idea, but it also increases the number of parameters to call the function, unless we create a proxy:</p>
<div class="dax-code-title">Function</div>
<pre class="brush: dax; title: ; snippet: Function; notranslate">
NewCustomersSet = (
    customerKey : COLUMNREF,
    orderDate : COLUMNREF,
    dateColumn : COLUMNREF
) =&gt;
    VAR FirstVisibleDate = MIN ( dateColumn )
    VAR LastVisibleDate = MAX ( dateColumn )
    VAR CustomersInCurrentPeriod = VALUES ( customerKey )
    VAR NewCustomers =
        FILTER (
            CustomersInCurrentPeriod,
            VAR CurrentCustomerKey = customerKey
            VAR FirstPurchaseDate =
                CALCULATE (
                    MIN ( orderDate ),
                    REMOVEFILTERS (),
                    TREATAS ( { CurrentCustomerKey }, customerKey )
                )
            RETURN
                FirstPurchaseDate &gt;= FirstVisibleDate
                    &amp;&amp; FirstPurchaseDate &lt;= LastVisibleDate
        )
    RETURN
        NewCustomers
</pre>
<p>Indeed, despite the function being very generic, the measures are still quite complicated:</p>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
# New Customers =
COUNTROWS (
    NewCustomersSet (
        Sales&#x5B;CustomerKey],
        Sales&#x5B;Order Date],
        &#039;Date&#039;&#x5B;Date]
    )
)
</pre>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
Sales New Customers =
VAR NewCustomers =
    NewCustomersSet (
        Sales&#x5B;CustomerKey],
        Sales&#x5B;Order Date],
        &#039;Date&#039;&#x5B;Date]
    )
RETURN
    CALCULATE (
        &#x5B;Sales Amount],
        KEEPFILTERS (
            TREATAS ( NewCustomers, Customer&#x5B;CustomerKey] )
        )
    )
</pre>
<p>We want to go one step further by simplifying the measures at the cost of generating a model-dependent UDF:</p>
<blockquote><p>I don't want the function to use so many arguments. Create one function that accepts as its only parameter the formula to compute, and then returns the formula computed for only the new customers.</p></blockquote>
<p>Now that our intentions are clearer, AI produces the desired code:</p>
<div class="dax-code-title">Function</div>
<pre class="brush: dax; title: ; snippet: Function; notranslate">
NewCustomers = (
    expression : SCALAR EXPR
) =&gt;
    VAR FirstVisibleDate = MIN ( &#039;Date&#039;&#x5B;Date] )
    VAR LastVisibleDate = MAX ( &#039;Date&#039;&#x5B;Date] )
    VAR CustomersInCurrentPeriod = VALUES ( Sales&#x5B;CustomerKey] )
    VAR NewCustomersSet =
        FILTER (
            CustomersInCurrentPeriod,
            VAR CurrentCustomerKey = Sales&#x5B;CustomerKey]
            VAR FirstPurchaseDate =
                CALCULATE (
                    MIN ( Sales&#x5B;Order Date] ),
                    REMOVEFILTERS (),
                    TREATAS ( { CurrentCustomerKey }, Customer&#x5B;CustomerKey] )
                )
            RETURN
                FirstPurchaseDate &gt;= FirstVisibleDate
                    &amp;&amp; FirstPurchaseDate &lt;= LastVisibleDate
        )
    RETURN
        CALCULATE (
            expression,
            KEEPFILTERS (
                TREATAS ( NewCustomersSet, Customer&#x5B;CustomerKey] )
            )
        )
</pre>
<p>And the two measures are straightforward, as desired:</p>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
# New Customers = 
NewCustomers ( &#x5B;# Customers] )
</pre>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
Sales New Customers = 
NewCustomers ( &#x5B;Sales Amount] )
</pre>
<p>There are several non-trivial aspects to note here. First, when asked to refactor the function, AI actually changed the algorithm for <em>NewCustomers</em>, moving from COUNTROWS to CALCULATE with a filter. This is not an easy step at all. Moreover, when we requested simpler code, AI decided to reuse the <em># Customers</em> measure rather than keeping the old COUNTROWS.</p>
<h2>Conclusions</h2>
<p>AI is a great tool to author DAX code. Clearly, the code needs to be validated thoroughly before putting it in production. When using AI, a good practice is to ask exactly for what you want, so the code can be generated in a single pass and be good, straight out of the box.</p>
<p>User-defined functions are a great tool in Power BI to centralize code. When authoring code with AI, never forget to search for opportunities to use UDFs, as AI does not always use them. However, a human who can read and understand DAX can produce good results with minimal effort by using functions.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/articles/creating-dax-functions-with-ai-to-remove-duplicated-code/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		      <dc:creator><![CDATA[Alberto Ferrari]]></dc:creator>
	</item>
		<item>
		<title>Restoring alternating rows in Power BI matrices with Fluent 2</title>
		<link>https://www.sqlbi.com/blog/marco/2026/08/10/restoring-alternating-rows-in-power-bi-matrices-with-fluent-2/</link>
					<comments>https://www.sqlbi.com/blog/marco/2026/08/10/restoring-alternating-rows-in-power-bi-matrices-with-fluent-2/#respond</comments>
		
		<dc:creator><![CDATA[Marco Russo]]></dc:creator>
		<pubDate>Mon, 10 Aug 2026 07:22:49 +0000</pubDate>
				<category><![CDATA[Data visualization]]></category>
		<category><![CDATA[DAX]]></category>
		<category><![CDATA[Power BI]]></category>
		<guid isPermaLink="false">https://www.sqlbi.com/?post_type=blogpost&#038;p=902151</guid>

					<description><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/matrixdifferences.png" class="webfeedsFeaturedVisual" /></figure>Microsoft recently introduced a new default visual design style for Power BI named Fluent 2. I personally disagree with a few choices in the new defaults, including the removal of alternating row colors from Table and Matrix visuals. However, the&#8230;]]></description>
										<content:encoded><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/matrixdifferences.png" class="webfeedsFeaturedVisual" /></figure><p>Microsoft recently introduced a new default visual design style for Power BI named Fluent 2. I personally disagree with a few choices in the new defaults, including the removal of alternating row colors from Table and Matrix visuals.</p>
<p>However, the feature is still available. You can select a Matrix and change its Style property from the Format pane, under Visual, Style presets. Here is the difference between the two styles: <code>None</code> is the setting you get when you create a new matrix with Fluent 2, which is on the right in the following screenshot. On the left is how the matrix looks once we set the Style to <code>Default</code>.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/matrixdifferences.png" alt="" width="800" /></p>
<p><img decoding="async" class="size-full wp-image-902155 alignright" src="https://www.sqlbi.com/wp-content/uploads/matrixstyle.png" alt="" width="157" /></p>
<p data-pm-slice="1 1 []">Therefore, fixing one Matrix is straightforward: change its style from <code>None</code> to <code>Default</code>.</p>
<p data-pm-slice="1 1 []">Because it took me almost half an hour to fix this properly using modern LLMs, I hope this post will help others (humans and agents) facing the same issue in the future!</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/blog/marco/2026/08/10/restoring-alternating-rows-in-power-bi-matrices-with-fluent-2/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Analyzing the performance impact of visual calculations</title>
		<link>https://www.sqlbi.com/tv/analyzing-the-performance-impact-of-visual-calculations/</link>
					<comments>https://www.sqlbi.com/tv/analyzing-the-performance-impact-of-visual-calculations/#respond</comments>
		
		<dc:creator><![CDATA[Alberto Ferrari]]></dc:creator>
		<pubDate>Tue, 28 Jul 2026 10:00:00 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<guid isPermaLink="false">http://www.sqlbi.com/?post_type=video&#038;p=900884</guid>

					<description><![CDATA[<figure><img src="https://i.ytimg.com/vi/CrQZwxv9OiA/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure>Visual calculations can either improve or degrade the performance of a report. Let's see how we ensure visual calculations have a positive impact on performance.]]></description>
										<content:encoded><![CDATA[<figure><img src="https://i.ytimg.com/vi/CrQZwxv9OiA/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure><p>Visual calculations can either improve or degrade the performance of a report. Let's see how we ensure visual calculations have a positive impact on performance.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/tv/analyzing-the-performance-impact-of-visual-calculations/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Analyzing the performance impact of visual calculations</title>
		<link>https://www.sqlbi.com/articles/analyzing-the-performance-impact-of-visual-calculations/</link>
					<comments>https://www.sqlbi.com/articles/analyzing-the-performance-impact-of-visual-calculations/#respond</comments>
		
		<dc:creator><![CDATA[Alberto Ferrari]]></dc:creator>
		<pubDate>Mon, 27 Jul 2026 19:30:54 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<category><![CDATA[Power BI]]></category>
		<category><![CDATA[Optimization]]></category>
		<category><![CDATA[Visual calculations]]></category>
		<guid isPermaLink="false">https://www.sqlbi.com/?p=901273</guid>

					<description><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/C0298-3.png" class="webfeedsFeaturedVisual" /></figure>Visual calculations can either improve or degrade the performance of a report. In this article, we outline how we ensure visual calculations have a positive impact on performance. The goal of visual calculations is to simplify some reports and calculations,&#8230;]]></description>
										<content:encoded><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/C0298-3.png" class="webfeedsFeaturedVisual" /></figure><p>Visual calculations can either improve or degrade the performance of a report. In this article, we outline how we ensure visual calculations have a positive impact on performance.<br />
<span id="more-901273"></span></p>
<p>The goal of visual calculations is to simplify some reports and calculations, rather than to optimize performance. However, it is common sense that – in some scenarios – visual calculations can bring some benefit from the performance point of view.</p>
<p>The main idea is that a report may precompute some values and then, to further elaborate on them, it may use the content of the virtual table rather than recompute the values multiple times.</p>
<p>Imagine we want to compute a report containing the distinct count of customers by year, as well as the growth in percentage between the current year and the previous years.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0298-1.png" width="600" /></p>
<p>The measure required is straightforward:</p>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
YOY % =
VAR CY = &#x5B;# Customers]
VAR PY =
    CALCULATE ( &#x5B;# Customers], SAMEPERIODLASTYEAR ( &#039;Date&#039;&#x5B;Date] ) )
VAR Result =
    DIVIDE ( CY - PY, PY )
RETURN
    Result
</pre>
<p>We chose DISTINCTCOUNT on purpose: we wanted a measure that is non-aggregatable and heavy in terms of computational requirements. When executed on the version of Contoso with 23M rows in <em>Sales</em>, the report requires around 13 seconds to execute, and this is not surprising at all. Before looking at the details in the server timings, let us reason as to why the measure is expected to be slow.</p>
<p>First of all, not only is DISTINCTCOUNT a heavy operation to perform, but it is also non-additive – as such, it requires multiple scans of the fact table to compute. Secondly, time intelligence calculations require complex reasoning that is difficult to optimize. The DAX engine first computes the distinct count of customers by year. Then for each year, it determines the previous year; and to compute the previous year's distinct count, it then runs another query against the database. In other words, each cell requires its own query to retrieve the value of the previous year.</p>
<p>Indeed, when looking at the details, we see confirmation of our reasoning: there are many storage engine queries, each computing the distinct count for one year.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0298-2.png" width="800" /></p>
<p>As humans, the first consideration that comes to mind is quite simple: why compute the value of the measure on a single year when the engine has already computed the value of the measure grouped by year? The value is already there; just use it without needing to recalculate it, right? True, but this is a very human behavior: we see the yearly value because the matrix is sliced by year; therefore, we cut corners and use the previously-computed value. DAX is more generic; it needs to work no matter what we slice by, which is why it uses a slower, yet more generic algorithm.</p>
<p>However, at the end of the day, we still feel that something can be improved. If the value is already there, there should be a way to use it with no further calculation. Indeed, a visual calculation does exactly this. When using visual calculations, the engine first computes the source query (that is, a query that retrieves all the values computed by model measures), and then the next step of calculation happens on the virtual table, with no further need to query the data model.</p>
<p>The same algorithm, with a visual calculation, is the following:</p>
<div class="dax-code-title">Visual Calculation</div>
<pre class="brush: dax; title: ; snippet: Visual Calculation; notranslate">
YOY % =
VAR CY = &#x5B;# Customers]
VAR PY =
    PREVIOUS ( &#x5B;# Customers], COLUMNS )
VAR Result =
    DIVIDE ( CY - PY, PY )
RETURN
    Result
</pre>
<p>By using PREVIOUS, we are asking DAX to retrieve the value of <em># Customers</em> in the previous column, with no need to invoke another query on the database.</p>
<p>The version using the visual calculation is significantly faster.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0298-3.png" width="800" /></p>
<p>6 seconds rather than the previous 13 seconds. Not only is it faster, but by analyzing the storage engine queries, we observe a much more reasonable behavior. The engine retrieves the base measure by the different aggregation levels – it requires subtotals, which is why there are multiple storage engine queries – and then the remaining part of the calculation is all in the formula engine. Because the virtual table is rather small, the cost to the formula engine is quite low.</p>
<p>If we had used <em>Sales Amount,</em> an additive and lighter measure, rather than a distinct count, the result would be similar, but the timings would be so quick – with the query running in around 100 milliseconds – that we would have to use a much larger database to get a feeling for the behavior. Distinct count is used only to make the scenario clearer and to avoid optimizations specific to additive measures.</p>
<p>So far, we have seen one side of the coin: visual calculations improve performance when there is a small number of basic heavy calculations on top of which we need to perform further elaboration. The good news is that this happens pretty frequently, so giving visual calculation a try is a good step in building reports. However, there is another side of the coin to investigate: is it possible that visual calculations slow down performance? Unfortunately, the answer is yes.</p>
<p>When a visual calculation is added to a visual, the engine adds the VISUAL SHAPE structure to the source table so that it can navigate in the hierarchies defined by the visual (ROWS and COLUMNS). Adding VISUAL SHAPE forces the table to undergo a densification process.</p>
<p>Densification adds to the source table all the combinations of values that may be present, but that were skipped as part of the optimizations of SUMMARIZECOLUMNS. We describe the densification process in more detail in the SQLBI+ whitepaper <a href="https://www.sqlbi.com/whitepapers/understanding-visual-calculations-in-dax/">Understanding Visual Calculations in DAX</a>, and we demonstrate it further in the <a href="https://www.sqlbi.com/learn/understanding-visual-calculations-in-dax/">corresponding SQLBI+ video course</a>.</p>
<p>For the sake of this article, it suffices to think of densification as a step that adds rows to the virtual table to represent all combinations of values across rows and columns. In our example, there are 11 brands and 10 years; therefore, the source table can contain a maximum of 110 rows (not including subtotals, which we ignore to simplify the scenario). All 110 cells contain a value; therefore, SUMMARIZECOLUMNS returns all of them. However, if there were no purchases for one of the brands within one of the years, the corresponding cell in the matrix would be blank, and the original virtual table returned by SUMMARIZECOLUMNS would not even include that row, because it would have a useless BLANK value. It is worth remembering that SUMMARIZECOLUMNS does not return rows where all the measures evaluate to BLANK. During the densification process, DAX adds that row back to accommodate any visual calculation results that may be non-blank.</p>
<p>In other words, SUMMARIZECOLUMNS has a specific optimization to reduce the number of rows returned to avoid processing empty rows. However, in visual calculations, the engine needs to recreate those rows, albeit temporarily, to guarantee a correct evaluation of the visual calculations themselves. If the virtual table is very large, the densification process can take a long time, thereby degrading the report’s performance significantly.</p>
<p>To demonstrate this, we use <em>Sales Amount</em> and the <em>YOY %</em> of the sales amount (if we were to use DISTINCTCOUNT, the timings would just be too large to make the tests viable), and we slice by <em>Store[Name]</em>, <em>Product[Name]</em>, and <em>Date[Year]</em>.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0298-4.png" width="800" /></p>
<p>There are 67 stores, 2,517 products, and 10 years. The full source table is now much larger: 1,686,390 rows in total. Because there are so many cells to compute, the report with the measure is much slower than the one we used earlier, and the storage engine queries also take much longer to execute.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0298-5.png" width="800" /></p>
<p>The report using the measure is running in 15.6 seconds, with a significant amount of time spent in the formula engine processing SAMEPERIODLASTYEAR across many combinations of values.</p>
<p>In the server timings, it is worth noting line 2, where 1,318,524 rows were processed. That is the original SUMMARIZECOLUMNS. Line 8 computes 16,458,828 rows and aggregates sales by date, so to compute the SAMEPERIODLASTYEAR in the formula engine.</p>
<p>Despite looking like a bad result, it is not. Not only is it not bad, but it is also the result of a combination of optimizations that aim to reduce the number of rows processed by both Power BI and SUMMARIZECOLUMNS.</p>
<p>The effect of these optimizations vanishes when using a visual calculation. When using visual calculations, the DAX engine must materialize the full result of SUMMARIZECOLUMNS and then extend it during the densification step to handle missing values. On large virtual tables, this takes a lot of time.</p>
<p>These are the server timings of the visual calculation version of the same report.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0298-6.png" width="800" /></p>
<p>The version with visual calculations is now four times slower than the version based on the measure. There are no more multiple queries to the storage engine (the engine only needs to retrieve the most granular data for the sales amount; everything else is computed from this raw dataset), but the densification step takes a long time, resulting in poor performance.</p>
<p>With different database sizes, the numbers are different. With the small database that you can download with this article, the difference looks even larger: it goes from around 40 milliseconds for the measure-based report to more than 400 milliseconds for the visual calculation. The reason is that the storage engine becomes irrelevant, which somewhat distorts the results.</p>
<h2>Conclusions</h2>
<p>Visual calculations are a powerful tool. When it comes to making some calculations easier, they are just great. However, do not assume they are always faster than measure-based reports, as they compute values only once. We performed several other tests, and our conclusion is that what matters most is the size of the virtual table. With small virtual tables, visual calculations are great. As soon as the virtual table grows, performance can be at serious risk. Be mindful that measure-based reports are also optimized to compute only what is visible in the matrix. Therefore, if some levels are not expanded, they are not computed. With visual calculations, it does not matter whether the matrix is fully expanded or not; the engine must always produce the full densified virtual table at the leaf level.</p>
<p>As always, when it comes to performance, you should perform extensive testing. Your model, your measures, and your report may all have different characteristics from our demo model. Hence, your mileage may vary widely.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/articles/analyzing-the-performance-impact-of-visual-calculations/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		      <dc:creator><![CDATA[Marco Russo]]></dc:creator>
	</item>
		<item>
		<title>Dynamic formatting by hierarchy level with ISINSCOPE and ISATLEVEL</title>
		<link>https://www.sqlbi.com/tv/dynamic-formatting-by-hierarchy-level-with-isinscope-and-isatlevel/</link>
					<comments>https://www.sqlbi.com/tv/dynamic-formatting-by-hierarchy-level-with-isinscope-and-isatlevel/#respond</comments>
		
		<dc:creator><![CDATA[Marco Russo]]></dc:creator>
		<pubDate>Tue, 14 Jul 2026 10:00:00 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<guid isPermaLink="false">https://www.sqlbi.com/?post_type=video&#038;p=900859</guid>

					<description><![CDATA[<figure><img src="https://i.ytimg.com/vi/l73ccrZJLO8/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure>How to apply a different formatting rule at each level of a hierarchy using ISINSCOPE in a measure or ISATLEVEL in a visual calculation.]]></description>
										<content:encoded><![CDATA[<figure><img src="https://i.ytimg.com/vi/l73ccrZJLO8/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure><p>How to apply a different formatting rule at each level of a hierarchy using ISINSCOPE in a measure or ISATLEVEL in a visual calculation.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/tv/dynamic-formatting-by-hierarchy-level-with-isinscope-and-isatlevel/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Dynamic formatting by hierarchy level with ISINSCOPE and ISATLEVEL</title>
		<link>https://www.sqlbi.com/articles/dynamic-formatting-by-hierarchy-level-with-isinscope-and-isatlevel/</link>
					<comments>https://www.sqlbi.com/articles/dynamic-formatting-by-hierarchy-level-with-isinscope-and-isatlevel/#respond</comments>
		
		<dc:creator><![CDATA[Marco Russo]]></dc:creator>
		<pubDate>Mon, 13 Jul 2026 20:00:42 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<category><![CDATA[Power BI]]></category>
		<category><![CDATA[Filter Context]]></category>
		<category><![CDATA[Visual calculations]]></category>
		<guid isPermaLink="false">https://www.sqlbi.com/?p=898866</guid>

					<description><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/image1-134.png" class="webfeedsFeaturedVisual" /></figure>This article describes how to apply different formatting rules at each level of a hierarchy (one rule at the year level, another at the quarter level, another at the month level) using ISINSCOPE in a measure or ISATLEVEL in a&#8230;]]></description>
										<content:encoded><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/image1-134.png" class="webfeedsFeaturedVisual" /></figure><p>This article describes how to apply different formatting rules at each level of a hierarchy (one rule at the year level, another at the quarter level, another at the month level) using ISINSCOPE in a measure or ISATLEVEL in a visual calculation.<br />
<span id="more-898866"></span></p>
<p>A challenging requirement in Power BI reports is that of applying different formatting rules based on the level of aggregation. At the year level, the background shade may reflect each year's share of the grand total. At the quarter level, a status color may indicate whether the quarter is above or below the average. At the month level, the color may flag exceptional values, like months that contribute more than a defined threshold to their year. Each level has its own logic; what the conditional expression of the measure needs to know is which level the current cell belongs to.</p>
<p>Two DAX functions address this challenge: ISINSCOPE and ISATLEVEL. They look similar, but they live in different places. ISINSCOPE inspects the group-by columns of the query and is used within a measure that becomes part of the semantic model. ISATLEVEL inspects the visual layout and is used within a visual calculation that lives at the report layer. The choice between the two is about where the per-level logic should live, and not about which one detects the level correctly (both do).</p>
<p>In this article, we describe how to use both approaches to drive per-level formatting rules. We start with a matrix visual that applies a different background color rule at each level of a Year-Quarter-Month hierarchy. We then move to a report with a <a href="https://okviz.com/synoptic-panel/">Synoptic Panel</a> visual, where the principle is the same, but the hierarchy is different.</p>
<p>If you are new to ISINSCOPE and want to understand how it differs from HASONEVALUE, please look at the article, <a href="https://www.sqlbi.com/articles/distinguishing-hasonevalue-from-isinscope/">Distinguishing HASONEVALUE from ISINSCOPE</a>.</p>
<h2>The scenario: a different rule at each level</h2>
<p>We start with a matrix that displays <em>Sales Amount</em> across the <em>Year</em>, <em>Quarter</em>, and <em>Month</em> levels of the <em>Calendar</em> hierarchy. Our goal is to drive the cell background color with three independent rules. At the year level, the shade reflects each year's share of the grand total: darker shades for years that contribute more, lighter shades for years that contribute less. At the quarter level, the cell turns green when the quarter's value is at or above the average quarter, and pink when it is below. At the month level, the cell is highlighted in gold only when that month contributes more than 15% towards that year's total, marking it as an exception.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image1-134.png" width="200" /></p>
<p>This treatment is not achievable with a single static rule applied to every cell, nor with a fixed color per level. Each level requires a calculation of its own. The expression must first detect the current level, then run the rule associated with that level. The expression can be either a measure in the semantic model or a visual calculation in the visual itself.</p>
<h2>Using ISINSCOPE in a measure</h2>
<p>ISINSCOPE returns TRUE when the specified column is currently used as a group-by column in the query. In a matrix, this corresponds to the column being either the row of the current cell or a column above it in the hierarchy. We use ISINSCOPE to dispatch each cell to the rule that applies to its level.</p>
<p>We define a measure that returns a color name, with one branch per level:</p>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
Level Color = 
SWITCH (
    TRUE,

    -- Month level: highlight months exceeding 15% of their year
    ISINSCOPE ( &#039;Date&#039;&#x5B;Year Month] ) || ISINSCOPE ( &#039;Date&#039;&#x5B;Year Month Short] ),
        VAR MonthValue = &#x5B;Sales Amount]
        VAR YearTotal =
            CALCULATE (
                &#x5B;Sales Amount],
                REMOVEFILTERS ( &#039;Date&#039; ),
                VALUES ( &#039;Date&#039;&#x5B;Year] )
            )
        VAR Share = DIVIDE ( MonthValue, YearTotal )
        RETURN
            IF ( Share &gt; 0.15, &quot;Gold&quot;, BLANK () ),

    -- Quarter level: green if at or above the average quarter, pink if below
    ISINSCOPE ( &#039;Date&#039;&#x5B;Year Quarter] ),
        VAR QuarterValue = &#x5B;Sales Amount]
        VAR AverageQuarter =
            CALCULATE ( 
                AVERAGEX (
                    VALUES ( &#039;Date&#039;&#x5B;Year Quarter] ),
                    &#x5B;Sales Amount]
                ),
                REMOVEFILTERS ( &#039;Date&#039; )
            )
        RETURN
            IF ( QuarterValue &gt;= AverageQuarter, &quot;LightGreen&quot;, &quot;LightPink&quot; ),

    -- Year level: shade by share of grand total
    ISINSCOPE ( &#039;Date&#039;&#x5B;Year] ),
        VAR YearValue = &#x5B;Sales Amount]
        VAR GrandTotal =
            CALCULATE ( &#x5B;Sales Amount], REMOVEFILTERS ( &#039;Date&#039; ) )
        VAR Share = DIVIDE ( YearValue, GrandTotal )
        RETURN
            SWITCH (
                TRUE,
                Share &gt; 0.40, &quot;SteelBlue&quot;, 
                Share &gt; 0.25, &quot;CornflowerBlue&quot;, 
                Share &gt; 0.15, &quot;SkyBlue&quot;, 
                &quot;LightBlue&quot; 
            )
)
</pre>
<p>The order of the conditions inside the outer SWITCH is important. When the current cell in the report is at the month level, ISINSCOPE returns TRUE for <em>Year Month Short</em>, <em>Year</em> <em>Quarter</em>, and <em>Year</em>, because all three columns are in scope. We test from the most specific level to the most general, so the first match identifies the actual current level and runs the rule that belongs to it. The code checks two columns for the month level (<em>Year Month</em> and <em>Year Month Short</em>) because the model has two versions: the full month name and the short 3-letter name, which is the one used in the previous screenshot.</p>
<p>Each branch is self-contained. The month branch uses the REMOVEFILTER / VALUES pattern to obtain the year total (read <a href="https://www.sqlbi.com/articles/using-allexcept-versus-all-and-values/">Using ALLEXCEPT versus ALL and VALUES</a> for more details about the pattern); the quarter branch averages <em>Sales Amount</em> across all quarters in the model; the year branch divides the year value by the grand total. The functions used within each branch are standard DAX and available in any measure of the semantic model.</p>
<p>The logic implemented at the quarter and year levels compares values against the model totals, ignoring slicers that could limit the date range shown in the visual. If the comparison should consider only the time period displayed in the visual, we could use ALLSELECTED instead of REMOVEFILTERS in the expressions at the quarter and year levels.</p>
<p>The measure is now part of the semantic model and is available to any report that uses it. The advantage is reusability across reports; the cost is that only someone with semantic model authoring rights can create it.</p>
<h2>Using ISATLEVEL in a visual calculation</h2>
<p>ISATLEVEL serves the same dispatching purpose, but it uses a different mechanism. ISATLEVEL indicates whether a column is visible at the current level of the visual. While ISINSCOPE inspects the group-by columns of the query, ISATLEVEL inspects the visual layout (technically, it is the VISUAL SHAPE of the query). ISATLEVEL is designed for visual calculations, which are defined in the report layer of a specific visual and do not require any change to the semantic model.</p>
<p>The structure of the expression is the same SWITCH dispatch on the current level, this time using ISATLEVEL:</p>
<div class="dax-code-title">Visual calculation</div>
<pre class="brush: dax; title: ; snippet: Visual calculation; notranslate">
Visual Level Color = 
SWITCH (
    TRUE,

    -- Month level: highlight months exceeding 15% of their year
    ISATLEVEL ( &#x5B;Year-Quarter-Month Month] ),
        VAR MonthValue = &#x5B;Sales Amount]
        VAR YearTotal = COLLAPSE ( &#x5B;Sales Amount], &#x5B;Year-Quarter-Month Quarter] )
        VAR Share = DIVIDE ( MonthValue, YearTotal )
        RETURN
            IF ( Share &gt; 0.15, &quot;Gold&quot;, BLANK () ),

    -- Quarter level: green if at or above the average quarter, pink if below
    ISATLEVEL ( &#x5B;Year-Quarter-Month Quarter] ),
        VAR QuarterValue = &#x5B;Sales Amount]
        VAR AverageQuarter =
            CALCULATE ( 
                AVERAGEX (
                    ROWS,
                    &#x5B;Sales Amount]
                )
            )
        RETURN
            IF ( QuarterValue &gt;= AverageQuarter, &quot;LightGreen&quot;, &quot;LightPink&quot; ),

    -- Year level: shade by share of grand total
    ISATLEVEL ( &#x5B;Year-Quarter-Month Year] ),
        VAR YearValue = &#x5B;Sales Amount]
        VAR GrandTotal =
            COLLAPSEALL ( &#x5B;Sales Amount], ROWS )
        VAR Share = DIVIDE ( YearValue, GrandTotal )
        RETURN
            SWITCH (
                TRUE,
                Share &gt; 0.40, &quot;SteelBlue&quot;, 
                Share &gt; 0.25, &quot;CornflowerBlue&quot;, 
                Share &gt; 0.15, &quot;SkyBlue&quot;, 
                &quot;LightBlue&quot; 
            )
)
</pre>
<p>The reference syntax <em>[Year-Quarter-Month Month]</em> corresponds to the column as it appears in the matrix visual. The exact name depends on how the hierarchy is configured in the visual. Keep in mind that ISATLEVEL takes a visual reference, not a model column path.</p>
<p>Inside each branch, the per-level arithmetic is the same as in the measure version, but the building blocks are visual-aware. In a visual calculation, we obtain the year total of a month row by collapsing the visual up to the year level with COLLAPSE; we obtain the grand total by collapsing all the way up with COLLAPSEALL. The arithmetic is identical, but it is expressed in terms of the visual matrix rather than the model.</p>
<p>The result matches the version driven by the ISINSCOPE measure. This is not a coincidence: in a standard hierarchical matrix, the visual shape mirrors the group-by columns of the query, so the dispatch reaches the same branch, and the per-level arithmetic produces the same value. The semantic model, however, is unchanged. The entire logic lives in the report and can be modified by a report developer who does not have authoring rights over the semantic model.</p>
<p>Another difference worth mentioning is that the results are identical because we did not filter out any dates outside the matrix visual. If we did that, for example, by filtering only a few years, the results would differ because the visual calculation only considers the visible periods, whereas the measure we implemented always compares the displayed values against the full model. While for the visual calculation we have no choice, for the measure we could have implemented a calculation that is local to the visual by using ALLSELECTED instead of REMOVEFILTERS in the quarter and year levels, as we mentioned in the previous section.</p>
<h2>A Synoptic Panel demo</h2>
<p>The matrix is the simplest scenario because the hierarchy is a familiar calendar. To show that the same principle applies in a completely different context, we move to the <a href="https://okviz.com/synoptic-panel/">Synoptic Panel</a> custom visual, which has been created by <a href="https://okviz.com/">OKVIZ</a>, a sister company of SQLBI. Synoptic Panel displays measures over an image, associating values with regions drawn on the image. The hierarchy involved is not a calendar hierarchy, but the different levels that group tickets sold in a large venue (we simulated The Sphere in Las Vegas for this example). The individual seats are grouped by sector, and the sectors are grouped by category.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image2-130-scaled.png" width="1530" /></p>
<p>The business logic required is the following:</p>
<ul>
<li>Category: gradient according to occupation % for selected events</li>
<li>Sector: gradient according to occupation % for selected events</li>
<li>Seat: full color if there is at least one ticket sold for the selected events</li>
</ul>
<p>The <em>% Occupation</em> measure is assigned to the Custom Color property for the areas. However, the behavior of that measure depends on the level displayed in the visual, which is detected by using ISINSCOPE:</p>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
% Occupation = 
VAR AverageTicketEvent = DIVIDE ( &#x5B;# Tickets], &#x5B;# Events] )
RETURN SWITCH (
    TRUE,
    ISINSCOPE ( Seats&#x5B;Seat] ), 
        (AverageTicketEvent &gt; 0) * 1,
    ISINSCOPE ( Seats&#x5B;Sector] ) || ISINSCOPE ( Seats&#x5B;Category] ),
        DIVIDE ( AverageTicketEvent, &#x5B;Tot seats] ),
    BLANK()
) 
</pre>
<p>The expression follows the same pattern as the one we used in the matrix earlier: a SWITCH dispatch with one branch per level, and a different calculation inside each branch. In this case, we used the same algorithm for two levels, <em>Sector</em> and <em>Category</em>.</p>
<p>The same algorithm can be implemented as a visual calculation; in this case, the <em># Tickets</em> and <em># Events</em> measures must be included as hidden measures in the visual to be available as columns in the visual calculation:</p>
<div class="dax-code-title">Visual calculation</div>
<pre class="brush: dax; title: ; snippet: Visual calculation; notranslate">
Occupation Visual Calc = 
VAR AverageTicketEvent = DIVIDE ( &#x5B;# Tickets], &#x5B;# Events] )
RETURN SWITCH (
    TRUE,
    ISATLEVEL ( &#x5B;Seat] ), 
        (AverageTicketEvent &gt; 0) * 1,
    ISATLEVEL ( &#x5B;Sector] ) || ISATLEVEL ( &#x5B;Category] ),
        DIVIDE ( AverageTicketEvent, &#x5B;Tot seats] ),
    BLANK()
) 
</pre>
<p>The difference is in where the logic lives and not in the result. The measure-based approach adds an artifact to the semantic model. The visual calculation approach keeps the logic confined to the visual, even though we must include in the visual the measures that provide the information to implement the algorithm (<em># Tickets</em>, <em># Events</em>, and <em>Tot seats</em>).</p>
<h2>Choosing between the two approaches</h2>
<p>The choice between ISINSCOPE in a measure and ISATLEVEL in a visual calculation is about where the per-level logic should live, not a question of correctness.</p>
<p>A measure with ISINSCOPE is the natural choice when we own the semantic model, and we want the level-detection logic to be reusable across multiple reports and visuals. The measure becomes part of the shared model and can be referenced anywhere. Moreover, ISINSCOPE is the mandatory choice if the format logic needs data not represented in the visual, because a visual calculation cannot access other data in the model.</p>
<p>A visual calculation with ISATLEVEL is the natural choice when we cannot or do not want to modify the semantic model. This is a common situation for a report developer who is consuming a shared dataset or who is building a report on top of a Power BI semantic model published by another team. In these cases, the developer does not have the rights to add measures to the model. An alternative could be to create a composite model solely to define additional measures to support formatting; however, this approach also requires the rights to create and publish a new semantic model, which could be another limiting factor. A visual calculation keeps the logic local to the report and does not require additional permissions.</p>
<p>Surprisingly, even when the developer owns the semantic model and has all necessary rights, a visual calculation may still be preferable for purely presentation-related logic. A per-level coloring expression is closer to the visual than to the data. Keeping it in the visual calculation avoids cluttering the semantic model with measures that have no analytical meaning beyond a specific visual.</p>
<p>Finally, from a performance standpoint, a visual calculation is usually preferable because it operates on the smaller set of rows used to populate the visual, whereas a measure could require additional internal queries, thus resulting in slower reports.</p>
<h2>Conclusions</h2>
<p>Dynamic formatting by hierarchy level becomes useful when each level has its own rule: share-of-total shading at one level, status comparison at another, exception highlight at a third. The pattern is always the same: a SWITCH detects the current level and runs the appropriate rule; however, detection can occur at two different architectural layers of the report.</p>
<p>ISINSCOPE is used in model measures and inspects the group-by columns of the query. ISATLEVEL inspects the visual layout and is used in visual calculations that live at the report layer. Both detect the level correctly in a matrix and in a Synoptic Panel; both can produce the same visible result.</p>
<p>For a model developer building a reusable artifact, ISINSCOPE is the natural option in a measure. For a report developer working on a shared dataset without the ability to modify the model, ISATLEVEL in a visual calculation is the practical choice. The choice between the two is about where the per-level logic should live.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/articles/dynamic-formatting-by-hierarchy-level-with-isinscope-and-isatlevel/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		      <dc:creator><![CDATA[Alberto Ferrari]]></dc:creator>
	</item>
		<item>
		<title>Generative AI guidelines at SQLBI (2026 update)</title>
		<link>https://www.sqlbi.com/blog/marco/2026/07/08/generative-ai-guidelines-at-sqlbi-2026-update/</link>
					<comments>https://www.sqlbi.com/blog/marco/2026/07/08/generative-ai-guidelines-at-sqlbi-2026-update/#respond</comments>
		
		<dc:creator><![CDATA[Marco Russo]]></dc:creator>
		<pubDate>Wed, 08 Jul 2026 09:30:41 +0000</pubDate>
				<guid isPermaLink="false">https://www.sqlbi.com/?post_type=blogpost&#038;p=900369</guid>

					<description><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/sqlbi-logo-video.png" class="webfeedsFeaturedVisual" /></figure>3 years ago, we wrote our “Generative AI guidelines at SQLBI”. We think it’s time to update them. Over the past 3 years, the world has changed: it is simply no longer possible to avoid using generative AI services for&#8230;]]></description>
										<content:encoded><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/sqlbi-logo-video.png" class="webfeedsFeaturedVisual" /></figure><p>3 years ago, we wrote our “<a href="https://www.sqlbi.com/blog/marco/2023/05/20/generative-ai-guidelines-at-sqlbi/">Generative AI guidelines at SQLBI</a>”.</p>
<p>We think it’s time to update them. Over the past 3 years, the world has changed: it is simply no longer possible to avoid using generative AI services for a multitude of purposes. Therefore, we want to update our guidelines to clarify how we use these tools.</p>
<p>First of all, our two simple concepts did not change:</p>
<ul>
<li>We look forward to using AI to improve productivity: our productivity and the productivity of our readers.</li>
<li>Whenever we publish content generated by AI engines, we will always make that clear to our readers.</li>
</ul>
<p>While these principles are still valid, I want to update the considerations I wrote three years ago.</p>
<p>I want to start with our current adoption in production. <strong>We produce video courses about DAX and Data Modeling for the international market.</strong> Thanks to AI, we improved the quality of subtitles in the original language (English) and their derived translations, which now benefit from higher quality. There is still a human review at the end of the loop, but we know that the quality of our video courses has definitely improved.</p>
<p><strong>We can use AI for DAX analysis and rarely in DAX coding, and we perform a human review before publishing.</strong> While this is not the case for the articles and books we publish, we have seen improvements in many models for simple, recurring tasks that generate DAX code, and we have started using these tools for testing, evaluation, use case preparation, and demos. We see, almost daily, that knowing DAX allows users to write more structured prompts that prevent LLMs from going down the wrong path. For now, knowing DAX is still an advantage even if you do not write it directly.</p>
<p><strong>We use AI in other stages of the development of content and semantic models.</strong> We do not create articles with AI. However, we use AI across different parts of the production process, primarily to improve the quality of the final result rather than just to increase our productivity. Providing tools to help AI agents be more accurate and efficient is an area we are exploring.</p>
<p><strong>We will improve the consumption experience on our websites for users and AI agents.</strong> We did not integrate AI services into our websites as we intended to three years ago. It seems more productive to look at how to collaborate with AI agents. In a world where AI agents do not pay for training, this access might not remain free. However, we are far from understanding what constitutes a sustainable economic model for advanced content.</p>
<p><strong>We will always be transparent when using AI-generated technical content.</strong> Compared to three years ago, I added “technical” to the previous sentence. We use AI to generate the comics in the <a href="https://www.sqlbi.com/newsletter/">SQLBI newsletter</a>, and a full disclaimer seems excessive for a comic named “AI BI Blunders”. We want to keep our freedom to have fun, and the risk that someone takes things too seriously is a price we are willing to pay to put a smile on your face (and definitely ours).</p>
<p>Rereading my post, I’ve managed to keep it shorter than the one I wrote three years ago. So, let me add one short pro tip: use AI to improve your productivity, but don’t become a victim of AI. If you stop thinking, you become “disposable”. Your added value is in applying the 5 to 10% of corrections needed on the AI output. In the case of DAX, probably more than 5 to 10%, especially if you also need efficient code.</p>
<p>Thank you for your time reading it!</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/blog/marco/2026/07/08/generative-ai-guidelines-at-sqlbi-2026-update/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Using REMOVEFILTERS in DAX user defined functions</title>
		<link>https://www.sqlbi.com/tv/using-removefilters-in-dax-user-defined-functions/</link>
					<comments>https://www.sqlbi.com/tv/using-removefilters-in-dax-user-defined-functions/#respond</comments>
		
		<dc:creator><![CDATA[Marco Russo]]></dc:creator>
		<pubDate>Tue, 30 Jun 2026 10:00:00 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<guid isPermaLink="false">https://www.sqlbi.com/?post_type=video&#038;p=899183</guid>

					<description><![CDATA[<figure><img src="https://i.ytimg.com/vi/cQNMLWVeLlo/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure>How to implement a DAX function that removes filter-keep column filters from a calendar, using REMOVEFILTERS as the return value of the function.]]></description>
										<content:encoded><![CDATA[<figure><img src="https://i.ytimg.com/vi/cQNMLWVeLlo/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure><p>How to implement a DAX function that removes filter-keep column filters from a calendar, using REMOVEFILTERS as the return value of the function.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/tv/using-removefilters-in-dax-user-defined-functions/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Using REMOVEFILTERS in DAX user-defined functions</title>
		<link>https://www.sqlbi.com/articles/using-removefilters-in-dax-user-defined-functions/</link>
					<comments>https://www.sqlbi.com/articles/using-removefilters-in-dax-user-defined-functions/#respond</comments>
		
		<dc:creator><![CDATA[Alberto Ferrari]]></dc:creator>
		<pubDate>Mon, 29 Jun 2026 20:00:16 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<category><![CDATA[UDF]]></category>
		<category><![CDATA[User-defined functions]]></category>
		<guid isPermaLink="false">https://www.sqlbi.com/?p=898872</guid>

					<description><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/RemoveFilterKeep.png" class="webfeedsFeaturedVisual" /></figure>In this article, we implement a function that removes filter-keep column filters from a calendar, using REMOVEFILTERS as the return value of the function. A DAX user-defined function, also known as a UDF, is expected to return a scalar or&#8230;]]></description>
										<content:encoded><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/RemoveFilterKeep.png" class="webfeedsFeaturedVisual" /></figure><p>In this article, we implement a function that removes filter-keep column filters from a calendar, using REMOVEFILTERS as the return value of the function.<br />
<span id="more-898872"></span></p>
<p>A DAX user-defined function, also known as a UDF, is expected to return a scalar or a table. However, because functions are fundamentally macro-expansion of DAX code, it is possible to return CALCULATE modifiers if the function is to be called only as a filter argument of CALCULATE.</p>
<p>To show a practical example of when the feature proves to be useful, we debug a measure that fails because some calendar filters are not being removed correctly. Fixing the measure elegantly requires creating a function that removes filters rather than returning a value.</p>
<h2>Introducing the scenario</h2>
<p>We wrote a measure that computes the running total for the last three months, using basic time intelligence functions and calendar-based time intelligence:</p>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
Measure 3 Months = 
VAR RefDate = MAX ( &#039;Date&#039;&#x5B;Date] )
RETURN
    CALCULATE (
        &#x5B;Sales Amount],
        DATESINPERIOD ( &#039;Gregorian&#039;, RefDate, -3, MONTH, ENDALIGNED )
    )
</pre>
<p>Please note that we used ENDALIGNED in DATESINPERIOD to ensure the calculation aligns the time period with its end. If you are not familiar with the behavior of ENDALIGNED, you should read <a href="https://www.sqlbi.com/articles/understanding-dateadd-parameters-with-calendar-based-time-intelligence/">Understanding DATEADD parameters with calendar-based time intelligence</a>. A thorough understanding of the particular behavior of ENDALIGNED is key in order to fully appreciate the problem to fix, so we strongly recommend checking out that article before or after reading this one.</p>
<p>To verify that the measure computes the correct value, we also authored a visual calculation that computes the same value, with the visual calculation technique:</p>
<div class="dax-code-title">Visual calculation</div>
<pre class="brush: dax; title: ; snippet: Visual calculation; notranslate">
Visual 3 Months = 
    CALCULATE ( 
        SUM ( &#x5B;Sales Amount] ), 
        RANGE ( -2, TRUE, ROWS ) 
    )
</pre>
<p>It is worth noting that we had to use -2 in RANGE rather than -3, because the current row is included in the range.</p>
<p>The two measures produce different results at the quarter and year levels because of the different techniques they use.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image1-135.png" width="550" /></p>
<p>However, we are mainly interested in the measure, and we use the visual calculation only for debugging purposes. If you want to better understand how RANGE and visual calculations work, make sure to check out this mini-course in SQLBI+: <a href="https://www.sqlbi.com/learn/understanding-visual-calculations-in-dax/">Understanding visual calculations in DAX</a>. From now on, we will focus only on the month level.</p>
<h2>Spotting the glitch in the measure</h2>
<p>Right now, all the numbers look correct. However, because there may be many rows to check, a simple visual calculation helps in focusing on the presence of errors:</p>
<div class="dax-code-title">Visual calculation</div>
<pre class="brush: dax; title: ; snippet: Visual calculation; notranslate">
Test = IF ( &#x5B;Measure 3 Months] - &#x5B;Visual 3 Months] &lt;&gt; 0, &quot;Error&quot; )
</pre>
<p>The calendar table includes, among the many columns, one column indicating the weekday. One of the requirements is that the measure should work if users decide to focus on specific weekdays. In technical terms, we call such columns filter-keep columns, that is, columns whose filter needs to be maintained when the filter on the <em>Date</em> table is changed. You can find more information about filter-keep columns here: <a href="https://www.sqlbi.com/articles/introducing-calendar-based-time-intelligence-in-dax/">Introducing calendar-based time intelligence in DAX</a>. Luckily, the calendar-based time-intelligence functions treat filter-keep columns in a sweet and safe way. Unfortunately, as we will discover, our measure does not. To demonstrate this, we add a slicer for the weekday and the test column to the report, focusing on Wednesday only.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image2-131.png" width="800" /></p>
<p>You can see that several months show an error: the visual calculation does not compute the same value as the measure. We will spare you the math: the visual calculation works smoothly, whereas the measure fails to compute the correct result.</p>
<p>In the video, we outline the full debugging process to explain how to find the issue. Here, we go straight to the solution.</p>
<p>When the measure is computing the reference date, it uses this expression:</p>
<pre class="brush: dax; title: ; notranslate">
VAR RefDate = MAX ( &#039;Date&#039;&#x5B;Date] )
</pre>
<p>MAX is being computed in the current filter context, which includes the weekday. Therefore, it finds the last Wednesday in the month. For some months, this will be the end of the month. For some others, it will be very close to the end of the month, while for several other months it will be a few days before the end of the month. Because of this, it will happen pretty frequently that the value of <em>RefDate</em> is not close enough to the end of the month to trigger the behavior of ENDALIGNED. As a consequence, the dates returned by DATESINPERIOD will include periods from subsequent months, thus producing an incorrect result.</p>
<p>Therefore, we need to ensure that the reference date ignores the weekday filter.</p>
<h2>Fixing the bug</h2>
<p>To fix the problem, we could add REMOVEFILTERS on the <em>Date[Weekday]</em> column (as well as weekday number) because the sort-by-column feature is being used. While focusing on the columns we want to remove the filter from, we may also notice that the table includes not only the weekday, but also its short version (Mon, Tue, and so on). We need to remove the filters from these columns as well to provide more flexibility for our users.</p>
<p>In general, we need to remove filters from any column that is not already included in the calendar (either as a level or as a time-related column) and that we want to consider as a filter-keep column. The list is known today, but it may grow later, when the semantic model is further developed.</p>
<p>Therefore, we decided to consolidate the list of columns into a function that removes the filter from any filter-keep column in the specific calendar.</p>
<p>The thing is: we need to remove filters, not return a table. When we think about a function, we think about a DAX expression that has a return value. In our case, the function needs to perform an action (removing the filter) rather than returning a table. However, because functions will be expanded inside the code, we can make a function “return” REMOVEFILTERS, thereby leveraging the fact that its code will be replaced when the function is being invoked:</p>
<div class="dax-code-title">Function</div>
<pre class="brush: dax; title: ; snippet: Function; notranslate">
Gregorian.RemoveFilterKeepColumns = () =&gt; 
REMOVEFILTERS ( 
    &#039;Date&#039;&#x5B;Day of Week], 
    &#039;Date&#039;&#x5B;Day of Week Number], 
    &#039;Date&#039;&#x5B;Day of Week Short] 
)
</pre>
<p>The function can work only when it is being used as a CALCULATE argument, as we do in the measure:</p>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
Measure 3 Months = 
VAR RefDate = 
    CALCULATE ( 
        MAX ( &#039;Date&#039;&#x5B;Date] ),
        Gregorian.RemoveFilterKeepColumns()
    )
RETURN
    CALCULATE (
        &#x5B;Sales Amount],
        DATESINPERIOD ( &#039;Gregorian&#039;, RefDate, -3, MONTH, ENDALIGNED )
    )
</pre>
<p>Because the function body will be expanded in the code, at execution time, this is the actual measure being executed:</p>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
Measure 3 Months = 
VAR RefDate = 
    CALCULATE ( 
        MAX ( &#039;Date&#039;&#x5B;Date] ),
        REMOVEFILTERS ( 
            &#039;Date&#039;&#x5B;Day of Week], 
            &#039;Date&#039;&#x5B;Day of Week Number], 
            &#039;Date&#039;&#x5B;Day of Week Short] 
        )
    )
RETURN
    CALCULATE (
        &#x5B;Sales Amount],
        DATESINPERIOD ( &#039;Gregorian&#039;, RefDate, -3, MONTH, ENDALIGNED )
    )
</pre>
<p>The function will not work if called differently, because its result is not a table; it is a CALCULATE modifier. However, as long as developers call the function from inside CALCULATE, it works just fine.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image3-116.png" width="800" /></p>
<p>Now that the measure has been verified and debugged, we can remove the visual calculation and proceed with further development of the model.</p>
<p>An alternative, a very valid alternative, is to use an EXPR parameter and embed the CALCULATE call inside the function:</p>
<div class="dax-code-title">Function</div>
<pre class="brush: dax; title: ; snippet: Function; notranslate">
Gregorian.ComputeRemovingFilterKeepColumns = ( formulaExpr : EXPR ) =&gt; 
CALCULATE ( 
    formulaExpr,
    REMOVEFILTERS ( 
        &#039;Date&#039;&#x5B;Day of Week], 
        &#039;Date&#039;&#x5B;Day of Week Number], 
        &#039;Date&#039;&#x5B;Day of Week Short] 
    )
)
</pre>
<h2>Conclusions</h2>
<p>Functions use macro-expansion in DAX. This opens up the possibility of using code in functions that would not work as standalone code, but will work when executed in the proper environment. Specifically, in this article, we outlined how you can make a function “return” REMOVEFILTERS if the function is being used in CALCULATE only.</p>
<p>As a bonus takeaway from the article, we outlined a specific behavior of filter-keep columns in calendar-based time intelligence by debugging a measure that is incorrect when filters are applied through slicers.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/articles/using-removefilters-in-dax-user-defined-functions/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		      <dc:creator><![CDATA[Marco Russo]]></dc:creator>
	</item>
		<item>
		<title>Optional parameters in DAX user defined functions</title>
		<link>https://www.sqlbi.com/tv/optional-parameters-in-dax-user-defined-functions/</link>
					<comments>https://www.sqlbi.com/tv/optional-parameters-in-dax-user-defined-functions/#respond</comments>
		
		<dc:creator><![CDATA[Marco Russo]]></dc:creator>
		<pubDate>Tue, 16 Jun 2026 10:00:00 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<guid isPermaLink="false">https://www.sqlbi.com/?post_type=video&#038;p=899182</guid>

					<description><![CDATA[<figure><img src="https://i.ytimg.com/vi/tPT-bOAnR3g/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure>How to define optional parameters in DAX user-defined functions and set default values for parameters not specified by the caller.]]></description>
										<content:encoded><![CDATA[<figure><img src="https://i.ytimg.com/vi/tPT-bOAnR3g/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure><p>How to define optional parameters in DAX user-defined functions and set default values for parameters not specified by the caller.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/tv/optional-parameters-in-dax-user-defined-functions/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Optional parameters in DAX user-defined functions</title>
		<link>https://www.sqlbi.com/articles/optional-parameters-in-dax-user-defined-functions/</link>
					<comments>https://www.sqlbi.com/articles/optional-parameters-in-dax-user-defined-functions/#respond</comments>
		
		<dc:creator><![CDATA[Marco Russo]]></dc:creator>
		<pubDate>Mon, 15 Jun 2026 20:00:50 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<category><![CDATA[UDF]]></category>
		<category><![CDATA[User-defined functions]]></category>
		<guid isPermaLink="false">https://www.sqlbi.com/?p=899210</guid>

					<description><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/C0297-default-udf.png" class="webfeedsFeaturedVisual" /></figure>This article describes how to define optional parameters in DAX user-defined functions and set default values for parameters not specified by the caller. When Microsoft announced that DAX User-defined functions (UDFs) are generally available (GA), another new feature was also&#8230;]]></description>
										<content:encoded><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/C0297-default-udf.png" class="webfeedsFeaturedVisual" /></figure><p>This article describes how to define optional parameters in DAX user-defined functions and set default values for parameters not specified by the caller.<br />
<span id="more-899210"></span></p>
<p>When Microsoft announced that DAX User-defined functions (UDFs) are generally available (GA), another new feature was also announced: it is now possible to define optional parameters in a function and assign them default values.</p>
<p>A parameter is optional when the caller can leave it out. In that case, the function still needs a value to work with, so it falls back to a default. DAX provides that default through an expression written directly in the function signature, next to the parameter it belongs to. This is the mechanism we describe in this article.</p>
<p>If you are new to DAX user-defined functions and you want to learn more, please take a look at <a href="https://www.sqlbi.com/dax-user-defined-functions-udf/">our other articles about DAX user-defined functions</a> before reading any further. Here, we assume you already know how to define a function and call it from a measure or a calculated column. We deliberately use simple functions in the first part of the article, so that the syntax remains the focus; a more practical scenario follows later.</p>
<h2>The syntax of optional parameters</h2>
<p>When we declare a parameter, we can add a default expression after the parameter name, followed by its optional type hints, using an equal sign. The general form of a function is the following:</p>
<pre class="brush: dax; title: ; notranslate">
&lt;FunctionName&gt; =
    ( &lt;ParameterName&gt; &#x5B; : &lt;Type&gt; &lt;Subtype&gt; &lt;PassingMode&gt; ] &#x5B; = &lt;DefaultExpression&gt; ], ... )
    =&gt; &lt;FunctionBody&gt;
</pre>
<p>The only addition compared to a function with mandatory parameters is the default expression, <em>= &lt;DefaultExpression&gt;</em>. A parameter with a default expression is optional; one without is mandatory.</p>
<p>Consider the following function, which increments a number. The first parameter, <em>x</em>, is mandatory. The second parameter, <em>y</em>, is the increment amount and is optional. If it is not specified, the function adds 1:</p>
<div class="dax-code-title">query</div>
<pre class="brush: dax; title: ; snippet: query; notranslate">
DEFINE
    FUNCTION Increment = ( x : NUMERIC, y : NUMERIC = 1 ) =&gt; x + y
EVALUATE
{
    Increment ( 3 ),        -- Returns 4, the default for y is 1
    Increment ( 10, 20 )    -- Returns 30, y is specified to be 20
}
</pre>
<p>The first call provides only <em>x</em>. Because y is omitted, DAX evaluates its default expression, 1, and uses the result as the value of <em>y</em>; the function returns 3 + 1. The second call provides both arguments, so the function returns 10 + 20.</p>
<p>When the caller omits an argument, DAX evaluates the corresponding default expression and uses its result as the value of that parameter. The default expression respects the type hints of the parameter and can call other functions, both built-in functions and user-defined functions, but it cannot reference other parameters of the same function.</p>
<h2>Omitting the trailing parameters</h2>
<p>A function can have more than one optional parameter. The next function extends <em>Increment</em> with a third parameter, <em>limit</em>, which caps the result. Both <em>y</em> and <em>limit</em> are optional. By default, the function increments by 1 and caps the result at 10:</p>
<div class="dax-code-title">query</div>
<pre class="brush: dax; title: ; snippet: query; notranslate">
DEFINE
    FUNCTION IncrementLimit =
        (
            x : NUMERIC,
            y : NUMERIC = 1,
            limit : NUMERIC = 10
        ) =&gt;
            MIN ( x + y, limit )

EVALUATE
{
    IncrementLimit ( 1 ),        -- Returns 2, the default for y is 1 and for limit it is 10
    IncrementLimit ( 5, 4 ),     -- Returns 9, y is 4 and the default for limit is 10
    IncrementLimit ( 5, 25, 20 ) -- Returns 20, y is 25 and limit is 20
}
</pre>
<p>The first call provides only <em>x</em>: <em>y</em> defaults to 1 and <em>limit</em> defaults to 10, so the result is <em>MIN ( 1 + 1, 10 )</em>, which is 2. The second call provides <em>x</em> and <em>y</em> but omits <em>limit</em>, so the result is <em>MIN ( 5 + 4, 10 )</em>, which is 9. The third call provides all three arguments, so the result is <em>MIN ( 5 + 25, 20 )</em>, which is 20.</p>
<p>When the parameters you skip are the last ones, you do not need to write anything in their place. You stop the list of arguments earlier, and DAX uses the default expression for every parameter you did not provide. In other words, you only need the separating commas up to the last argument you actually provide; everything after that can be left out.</p>
<h2>Skipping a parameter in the function call</h2>
<p>The previous calls always omit parameters from the end of the list. You can also skip a parameter in the middle, while still passing an argument in a later position. To do this, you leave the position empty. You write the comma, but no value before it:</p>
<div class="dax-code-title">query</div>
<pre class="brush: dax; title: ; snippet: query; notranslate">
DEFINE
    FUNCTION IncrementLimit =
        (
            x : NUMERIC,
            y : NUMERIC = 1,
            limit : NUMERIC = 10
        ) =&gt;
            MIN ( x + y, limit )

EVALUATE
{
    IncrementLimit ( 5, 25, 20 ), -- Returns 20, y is 25 and limit is 20
    IncrementLimit ( 5, , 20 )    -- Returns 6, the default for y is 1 and limit is 20
}
</pre>
<p>In the second call, the empty position before the comma tells DAX to use the default expression of <em>y</em>. The value 20 is assigned to <em>limit</em>. The result is therefore <em>MIN ( 5 + 1, 20 )</em>, which is 6.</p>
<p>This syntax is useful when a function has several optional parameters, and you want to set only one of the later ones. The empty position is not pleasant to read, but it is valid; here, leaving the gap is a choice of the caller, not something the function definition imposes.</p>
<h2>Optional parameters should come last</h2>
<p>DAX does not require optional parameters to be the last ones in the signature. You can declare a mandatory parameter after an optional one, as in the following function, where <em>limit</em> is mandatory but follows the optional <em>y</em>:</p>
<div class="dax-code-title">query</div>
<pre class="brush: dax; title: ; snippet: query; notranslate">
DEFINE
    FUNCTION IncrementBadPractice =
        (
            x : NUMERIC,
            y : NUMERIC = 1,
            limit : NUMERIC
        ) =&gt;
            MIN ( x + y, limit )

EVALUATE
{
    IncrementBadPractice ( 5, , 20 ) -- Returns 6, the default for y is 1 and limit is 20
}
</pre>
<p>The result is the same as before, 6. The problem is the <em>IncrementBadPractice</em> function call, not the result. Because <em>limit</em> is mandatory, the caller must always provide it. However, limit comes after the optional <em>y</em>, so the only way to provide limit while keeping the default of <em>y</em> is to write the empty position: <em>IncrementBadPractice ( 5, , 20 )</em>. Here the gap is not a choice; the function forces the caller to write it.</p>
<p>For this reason, we suggest the <strong>best practice</strong>: When you make a parameter optional, make all the following parameters optional as well. The <em>IncrementLimit</em> function follows this rule: once <em>y</em> is optional, <em>limit</em> is optional too. The <em>IncrementBadPractice</em> function breaks it, and the cost is awkward code at every call site.</p>
<h2>Detecting missing parameters</h2>
<p>So far, every default has been a fixed value. Sometimes there is no natural fixed default, and you want the function to behave differently depending on whether the caller provided the argument at all. You can obtain this behavior by using BLANK as the default expression and then testing the parameter with ISBLANK inside the function body.</p>
<p>For example, the following function divides two numbers. The third parameter, <em>roundingDigits</em>, controls the number of decimal places. When the caller omits it, the function returns the full-precision result. When the caller provides it, the function rounds the result to the requested number of digits:</p>
<div class="dax-code-title">query</div>
<pre class="brush: dax; title: ; snippet: query; notranslate">
DEFINE
    FUNCTION RoundDivision =
        (
            x : NUMERIC,
            y : NUMERIC,
            roundingDigits = BLANK ()
        ) =&gt;
            VAR Result = DIVIDE ( x, y )
            RETURN
                IF (
                    ISBLANK ( roundingDigits ),
                    Result,
                    ROUND ( Result, roundingDigits )
                )

EVALUATE
{
    FORMAT ( RoundDivision ( 2, 3 ), &quot;0.#########&quot; ),    -- Returns 0.666666667, no rounding
    FORMAT ( RoundDivision ( 2, 3, 0 ), &quot;0.#########&quot; ), -- Returns 1, round to 0 digits
    FORMAT ( RoundDivision ( 2, 3, 1 ), &quot;0.#########&quot; ), -- Returns 0.7, round to 1 digit
    FORMAT ( RoundDivision ( 2, 3, 2 ), &quot;0.#########&quot; ), -- Returns 0.67, round to 2 digits
    FORMAT ( RoundDivision ( 2, 3, 3 ), &quot;0.#########&quot; )  -- Returns 0.667, round to 3 digits
}
</pre>
<p>The first call omits <em>roundingDigits</em>. Its default expression, BLANK, becomes the value of the parameter, so ISBLANK returns TRUE and the function returns the unrounded result. The other calls provide the number of digits, so ISBLANK returns FALSE and the function rounds the result accordingly: zero digits give 1, one digit gives 0.7, two digits give 0.67, and three digits give 0.667.</p>
<p>This pattern is useful when the choice is between doing something and doing nothing. By using BLANK as the default value, we represent the absence of a value, which the function then interprets.</p>
<p>Be mindful of one limitation: the function cannot distinguish an omitted argument from an argument that is explicitly blank. If the caller writes <em>RoundDivision ( 2, 3, BLANK () )</em>, ISBLANK returns TRUE exactly as it does for an omitted argument. This is rarely a problem in practice, but it is worth keeping in mind when blank is a legitimate value for the parameter.</p>
<h2>Conclusions</h2>
<p>Optional parameters make a user-defined function easier to call in the common case, while still allowing full control when needed. You define an optional parameter by giving it a default expression in the signature. You skip it at call time by omitting the trailing arguments, or by leaving an empty position when you want to keep the default of one parameter while setting a later one.</p>
<p>The default expression is evaluated only when the caller omits the argument; it respects the type hints of the parameter, and it can call other functions. The default values are visible next to the parameters they belong to, which keeps the signature self-documenting.</p>
<p>Be mindful of the order of the parameters. DAX lets you place a mandatory parameter after an optional one, but this should be avoided: every parameter after the first optional one should be optional as well. Otherwise, the caller is forced to write empty positions just to reach a mandatory argument. Follow the rule, and your functions will be both correct and pleasant to call.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/articles/optional-parameters-in-dax-user-defined-functions/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		      <dc:creator><![CDATA[Alberto Ferrari]]></dc:creator>
	</item>
		<item>
		<title>ALL vs ALLSELECTED vs ALLEXCEPT vs REMOVEFILTERS</title>
		<link>https://www.sqlbi.com/tv/all-vs-allselected-vs-allexcept-vs-removefilters/</link>
					<comments>https://www.sqlbi.com/tv/all-vs-allselected-vs-allexcept-vs-removefilters/#respond</comments>
		
		<dc:creator><![CDATA[Alberto Ferrari]]></dc:creator>
		<pubDate>Tue, 02 Jun 2026 10:00:00 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<guid isPermaLink="false">http://www.sqlbi.com/?post_type=video&#038;p=898388</guid>

					<description><![CDATA[<figure><img src="https://i.ytimg.com/vi/N5P6ac1VhSo/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure>Learn the differences between similar but different DAX functions that remove filters from the filter context.]]></description>
										<content:encoded><![CDATA[<figure><img src="https://i.ytimg.com/vi/N5P6ac1VhSo/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure><p>Learn the differences between similar but different DAX functions that remove filters from the filter context.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/tv/all-vs-allselected-vs-allexcept-vs-removefilters/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>ALL vs ALLSELECTED vs ALLEXCEPT vs REMOVEFILTERS</title>
		<link>https://www.sqlbi.com/articles/all-vs-allselected-vs-allexcept-vs-removefilters/</link>
					<comments>https://www.sqlbi.com/articles/all-vs-allselected-vs-allexcept-vs-removefilters/#respond</comments>
		
		<dc:creator><![CDATA[Alberto Ferrari]]></dc:creator>
		<pubDate>Mon, 01 Jun 2026 20:00:59 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<category><![CDATA[Filter Context]]></category>
		<category><![CDATA[Filter Context Manipulation]]></category>
		<guid isPermaLink="false">https://www.sqlbi.com/?p=898609</guid>

					<description><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/image5-93.png" class="webfeedsFeaturedVisual" /></figure>DAX offers many functions to remove filters from the filter context. In this article, we analyze the differences among the most commonly-used functions. Computing values in DAX is all about understanding how to manipulate the filter context to obtain the&#8230;]]></description>
										<content:encoded><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/image5-93.png" class="webfeedsFeaturedVisual" /></figure><p>DAX offers many functions to remove filters from the filter context. In this article, we analyze the differences among the most commonly-used functions.<br />
<span id="more-898609"></span></p>
<p>Computing values in DAX is all about understanding how to manipulate the filter context to obtain the desired output. DAX offers a wide variety of functions to manipulate the filter context, including a rich set designed to remove filters. Among the many, four are used the most: ALL, ALLSELECTED, ALLEXCEPT, and REMOVEFILTERS. Choosing the right one can be tough.</p>
<p>In this article, we do not want to dive into too many details; the goal is to let our readers understand when to use which function. Whenever needed, we provide links to deepen your knowledge about specific topics. Make sure to read the additional content if you want to know more about some specific behaviors.</p>
<h2>Table functions or CALCULATE modifiers?</h2>
<p>You probably have noticed that we introduced the goal of the functions as functions to remove filters. However, ALL, ALLSELECTED, and ALLEXCEPT are generally described as table functions; they return a table rather than remove a filter. Unfortunately, this adds confusion to the narrative. These functions are, at the same time, filter removal functions and table functions. Deciding when to use them, and in which of their dual form, is not a simple task.</p>
<p>If you want to deepen your knowledge about the difference between ALL-prefixed functions being used as CALCULATE modifiers or as table functions, you should read the following article: <a href="https://www.sqlbi.com/articles/managing-all-functions-in-dax-all-allselected-allnoblankrow-allexcept/">Managing “all” functions in DAX: ALL, ALLSELECTED, ALLNOBLANKROW, ALLEXCEPT</a>.</p>
<p>As concerns this article, the important fact is that any function whose name starts with ALL can be used either as a CALCULATE modifier or as a table function. When used as CALCULATE modifiers, these functions do not return a table; instead, they simply remove filters from the filter context. Over time, this dual behavior of ALL* functions created some confusion. This is why, in August 2019, Microsoft introduced the new REMOVEFILTERS function. REMOVEFILTERS is just an alias for ALL, and it can only be used as a CALCULATE modifier. There is no difference between REMOVEFILTERS and ALL when used in CALCULATE. Therefore, these two CALCULATE expressions produce the very same result:</p>
<pre class="brush: dax; title: ; notranslate">
CALCULATE ( &#x5B;Sales Amount], ALL ( Product&#x5B;Category] ) )

CALCULATE ( &#x5B;Sales Amount], REMOVEFILTERS ( Product&#x5B;Category] ) )
</pre>
<p>REMOVEFILTERS is the only alias that exists for the set of ALL* functions. As you may easily imagine, an alias for ALLSELECTED would be REMOVEFILTERSSELECTED, not a cute alias at all! It would only add confusion to an already complex topic.</p>
<p>The good news is that this simple consideration reduces the number of differences to learn, from four to three: there is no need to distinguish between REMOVEFILTERS and ALL. We use ALL when we need the function to return a table; we can choose between REMOVEFILTERS and ALL when we want a CALCULATE modifier. In our opinion, REMOVEFILTERS provides a better idea of the modifier goal.</p>
<h2>To remove or to ignore filters, that is the question</h2>
<p>ALL, ALLEXCEPT, and ALLSELECTED perform a slightly different operation depending on how we use them. When used as table functions, they <strong>ignore</strong> certain filters in the filter context and return a table computed without them. When used as CALCULATE modifiers, these functions instruct CALCULATE to modify the filter context by removing some filters. The difference is subtle.</p>
<p>In the following code snippet, we use ALL to ignore the filter context and return all the products, despite the filter context filtering only red products:</p>
<pre class="brush: dax; title: ; notranslate">
CALCULATE (
    COUNTROWS ( 
        ALL ( Product )
    ),
    Product&#x5B;Color] = &quot;Red&quot;
)
</pre>
<p>ALL is used as a table function; it returns all products, regardless of the filter. An alternative, more verbose and less understandable way of expressing the same code is the following:</p>
<pre class="brush: dax; title: ; notranslate">
CALCULATE (
    CALCULATE ( 
        COUNTROWS ( Product ),
        ALL ( Product )
    ),
    Product&#x5B;Color] = &quot;Red&quot;
)
</pre>
<p>In this example, ALL (we could have used REMOVEFILTERS) instructs CALCULATE to remove any filter from the <em>Product</em> table. Therefore, when <em>Product</em> is evaluated in COUNTROWS, the filter context is different: any filter on <em>Product</em> is removed.</p>
<p>The difference is not very relevant in most scenarios. However, when learning DAX, it is important to understand the subtle difference between removing and ignoring, as it greatly helps clarify the filter context in specific areas of your formula.</p>
<h2>Choosing when to use what: the short answer</h2>
<p>Before diving into more detailed descriptions, here is how you choose when to use what:</p>
<ul>
<li>Use REMOVEFILTERS when the intent is simply to clear filters in CALCULATE</li>
<li>Use ALL when a table expression is needed, or when using legacy patterns that rely on the dual nature of ALL.</li>
<li>Use ALLSELECTED when computing visual totals that keep outer selections.</li>
<li>Use ALLEXCEPT when the goal is to preserve one grouping grain and remove the rest; however, you should prefer the REMOVEFILTERS/VALUES combination, instead of ALLEXCEPT.</li>
</ul>
<p>You can use this short set of rules as a reference. In the remainder of the article, we provide a short explanation of these rules, with pointers to additional content for a more complete description.</p>
<h2>When to use ALL and REMOVEFILTERS</h2>
<p>ALL is useful whenever one needs to ignore filters present in the filter context. For example, the following measure computes the sales amount of all the brands, regardless of any filter existing on the <em>Product[Brand]</em> column:</p>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
All Brands Sales = 
CALCULATE ( 
    &#x5B;Sales Amount], 
    REMOVEFILTERS ( &#039;Product&#039;&#x5B;Brand] )     -- You can use ALL, with no differences
)
</pre>
<p>When used in a matrix that slices by <em>Product[Brand]</em>, this measure always produces the total.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image1-133.png" width="367" /></p>
<p>If the matrix is not sliced by <em>Product[Brand]</em>, then REMOVEFILTERS has no effect, because there is no filter on <em>Product[Brand]</em> to remove.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image2-129.png" width="430" /></p>
<p>REMOVEFILTERS can be used with a column, as in the example, or with a table as an argument. When used with a table, it removes (or ignores!) filters on any table column. Indeed, the <em>All Products Sales</em> measure that uses REMOVEFILTERS on <em>Product</em>, produces the grand total even when slicing by <em>Product[Category]</em>:</p>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
All Products Sales = 
CALCULATE ( 
    &#x5B;Sales Amount], 
    REMOVEFILTERS ( &#039;Product&#039; )     -- You can use ALL, with no differences
)
</pre>
<p>Here is the result, with the two measures side-by-side.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image3-115.png" width="555" /></p>
<p>A third, less frequently-used version of REMOVEFILTERS takes no argument. One can use REMOVEFILTERS() or ALL () to remove any filter from any table in the entire model.</p>
<p>In short, we use REMOVEFILTERS (or ALL) when we want to explicitly remove (or ignore) filters from columns in the model. The goal is almost always to obtain the grand total of a matrix (or any visual).</p>
<p>One scenario where we must use ALL rather than REMOVEFILTERS is when we need a table and not a filter modifier in CALCULATE: in that case, REMOVEFILTERS is not allowed. For example, the following code does not work, because REMOVEFILTERS is not a table function:</p>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
Sum All Products = SUMX ( REMOVEFILTERS ( Product ), &#x5B;Sales Amount] )
</pre>
<p>Indeed, SUMX requires a table to iterate, and REMOVEFILTERS is not a table function. The code runs correctly when we use ALL:</p>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
Sum All Products = SUMX ( ALL ( Product ), &#x5B;Sales Amount] )
</pre>
<p>The other functions described in this article do not have this distinction, so we can use them as both modifiers and table functions.</p>
<h2>When to use ALLSELECTED</h2>
<p>Sometimes REMOVEFILTERS (or ALL) is overkill. REMOVEFILTERS ignores all filters in the filter context, including not only the filters from the current visual but also those from other visuals. Look what happens with the matrix if we add a slicer that filters certain brands.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image4-109.png" width="577" /></p>
<p>The slicer is filtering five brands. However, the measure still reports the grand total of 4,373,105.53 because REMOVEFILTERS is removing any filters on the <em>Product[Brand]</em> column, regardless of which visual created the filter. In the example, there are two filters. One is created by the slicer and one is created by the current visual; both filters operate on the <em>Product[Brand]</em> column. REMOVEFILTERS is ignoring both.</p>
<p>The requirement to remove the filter from the current visual while keeping filters on other visuals is very common, and it is often referred to as “visual totals”. You can see that the total shown in the matrix right now has no visual explanation. We know it is the grand total of all products, but a user looking at the report may be confused. On the other hand, a visual total produces a total that is strongly connected to the numbers already present in the visual.</p>
<p>The DAX function used to achieve visual totals is ALLSELECTED. ALLSELECTED ignores filters from the current visual, but it maintains filters from other visuals:</p>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
Allselected Brands Sales = 
CALCULATE ( 
    &#x5B;Sales Amount], 
    ALLSELECTED ( &#039;Product&#039;&#x5B;Brand] )
)
</pre>
<p>Looking at the result, you can appreciate that the total considers the filter on the five brands selected with the slicer, but it ignores the filter on the brand on the current row of the matrix.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image5-93.png" width="749" /></p>
<p>ALLSELECTED is a very commonly-used function in DAX. That said, it is also very dangerous if you do not follow best practices. You can read about the best practices of ALLSELECTED in the article, <a href="https://www.sqlbi.com/articles/allselected-best-practices/">ALLSELECTED best practices</a>. For the bravest among our readers, a complete explanation of how ALLSELECTED works with shadow filter contexts can be found here: <a href="https://www.sqlbi.com/articles/the-definitive-guide-to-allselected/">The definitive guide to ALLSELECTED</a>. This latter article is not for the faint of heart; we strongly recommend following the best practices, as this makes it unnecessary to read the most complex topics, while still living a happy life as a DAX developer.</p>
<h2>When to use ALLEXCEPT</h2>
<p>ALLEXCEPT is a variation of REMOVEFILTERS (or ALL). It produces the same effect as REMOVEFILTERS, except for some columns. It is a very tempting function, because it is short and sweet, but it hides some level of complexity. ALLEXCEPT removes (or it ignores) filters from any column of a table, except the ones specifically provided as further arguments:</p>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
AllExcept Brand Sales = 
CALCULATE (
    &#x5B;Sales Amount],
    ALLEXCEPT ( &#039;Product&#039;, &#039;Product&#039;&#x5B;Brand] )
)
</pre>
<p>In this example, the measure ignores all filters on the <em>Product</em> table, except those on the <em>Product[Brand]</em> column. It is very useful when we need to compute subtotals in a matrix, like in the following report.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image6-84.png" width="496" /></p>
<p>As you can see, <em>AllExcept Brand Sales</em> produces the subtotal at the <em>Product[Brand]</em> level. It produces the subtotal by removing all filters from the <em>Product</em> table, except for the <em>Product[Brand]</em> column. By using ALLEXCEPT, one can use any column from <em>Product</em> as the second level of the matrix, while still producing the subtotal as the measure result. It is mostly used when there is a need to compute the ratio of the current selection against the brand total.</p>
<p>Despite ALLEXCEPT being tempting, we suggest that our readers use the REMOVEFILTERS/VALUES combination to obtain the same result:</p>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
AllValues Brand Sales = 
CALCULATE (
    &#x5B;Sales Amount],
    REMOVEFILTERS ( &#039;Product&#039; ),
    VALUES ( &#039;Product&#039;&#x5B;Brand] )
)
</pre>
<p>This last measure produces the same result as the one using ALLEXCEPT, but it works smoothly even when no filter is explicitly applied to the <em>Product[Brand]</em> column. If you want to understand more about why this is relevant, you can read the following article: <a href="https://www.sqlbi.com/articles/using-allexcept-versus-all-and-values/">Using ALLEXCEPT versus ALL and VALUES</a>.</p>
<h2>Conclusions</h2>
<p>Choosing which function to use to remove filters from the filter context is a simple topic for DAX experts. However, if you are unsure about which function to use, then you are in very good hands. Many DAX developers are still unsure about when to use what.</p>
<p>Let us conclude with the set of rules again:</p>
<ul>
<li>Use REMOVEFILTERS when the intent is simply to clear filters in CALCULATE.</li>
<li>Use ALL when a table expression is needed, or when using legacy patterns that rely on its dual nature.</li>
<li>Use ALLSELECTED when computing visual totals that keep outer selections.</li>
<li>Use ALLEXCEPT when the goal is to preserve one grouping grain and remove the rest; however, you should prefer the REMOVEFILTERS/VALUES combination over ALLEXCEPT.</li>
</ul>
<p>After you have read the article, and properly digested the rationale behind each rule, this set should guide you in choosing the right function for your task.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/articles/all-vs-allselected-vs-allexcept-vs-removefilters/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		      <dc:creator><![CDATA[Marco Russo]]></dc:creator>
	</item>
		<item>
		<title>Introducing user-aware calculated columns in Power BI</title>
		<link>https://www.sqlbi.com/tv/introducing-user-aware-calculated-columns-in-power-bi/</link>
					<comments>https://www.sqlbi.com/tv/introducing-user-aware-calculated-columns-in-power-bi/#respond</comments>
		
		<dc:creator><![CDATA[Marco Russo]]></dc:creator>
		<pubDate>Tue, 19 May 2026 10:00:00 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<guid isPermaLink="false">http://www.sqlbi.com/?post_type=video&#038;p=897677</guid>

					<description><![CDATA[<figure><img src="https://i.ytimg.com/vi/dTyZMHR8ZDo/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure>User-aware calculated columns are not materialized: we can use them as virtual calculated columns for localization and for custom security scenarios. This feature is available through the new Expression Context property of calculated columns in Power BI.]]></description>
										<content:encoded><![CDATA[<figure><img src="https://i.ytimg.com/vi/dTyZMHR8ZDo/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure><p>User-aware calculated columns are not materialized: we can use them as virtual calculated columns for localization and for custom security scenarios. This feature is available through the new Expression Context property of calculated columns in Power BI.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/tv/introducing-user-aware-calculated-columns-in-power-bi/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Introducing user-aware calculated columns in Power BI</title>
		<link>https://www.sqlbi.com/articles/introducing-user-aware-calculated-columns-in-power-bi/</link>
					<comments>https://www.sqlbi.com/articles/introducing-user-aware-calculated-columns-in-power-bi/#respond</comments>
		
		<dc:creator><![CDATA[Marco Russo]]></dc:creator>
		<pubDate>Mon, 18 May 2026 20:00:24 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<category><![CDATA[Power BI]]></category>
		<guid isPermaLink="false">https://www.sqlbi.com/?p=898094</guid>

					<description><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/image1-132.png" class="webfeedsFeaturedVisual" /></figure>This article describes the new Expression Context property of calculated columns in Power BI, explaining how user-aware calculated columns work, why they are not materialized, and how to use them as virtual calculated columns for localization and custom security scenarios.&#8230;]]></description>
										<content:encoded><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/image1-132.png" class="webfeedsFeaturedVisual" /></figure><p>This article describes the new Expression Context property of calculated columns in Power BI, explaining how user-aware calculated columns work, why they are not materialized, and how to use them as virtual calculated columns for localization and custom security scenarios.<br />
<span id="more-898094"></span></p>
<p>A calculated column is computed when the table is refreshed and stored in the model (in Import mode), just like any other column, so its value does not depend on the user who is connected. The introduction of user-aware calculated columns in Power BI changes this picture because we can define a calculated column that is evaluated at query time and depends on the user running the query. This behavior can be obtained by setting the Expression Context property of a calculated column to User Context.</p>
<blockquote><p>
<strong>NOTE</strong>: You may find the term “user-context-aware” in articles and documentation from other sources. At SQLBI, we felt that “user-aware” is simpler and less ambiguous as to the scope of this feature. The focus is really on user awareness.
</p></blockquote>
<p>This feature might seem to be a small addition intended to support localization scenarios. However, the implications go beyond localization: any calculated column with a simple expression can become a <em>virtual calculated column</em>: a column that exists in the model but is not stored in memory. Indeed, a consequence of user-aware calculated columns is that they do not materialize the columns, even in Import mode. The ability to manage unmaterialized calculated columns is a feature required to support calculated columns in Direct Lake over OneLake; this topic is not discussed in this article.</p>
<p>We start by introducing the Expression Context property, which enables user-aware calculated columns. We then present three main use cases for user-aware calculated columns: localization based on user culture, row-level calculations stored as virtual columns, and securing sensitive columns without relying on object-level security (OLS). In the second part of the article, we provide more information about the internals of this feature if you are interested in knowing more about the implications of materialization, the DAX functions that make a column user-aware, and the limitations of user-aware calculated columns.</p>
<h2>The Expression Context property</h2>
<p>When we create a calculated column in Power BI, we can now choose the <strong>Expression Context</strong> for the column. The property has two values: Standard and User Context. <strong>Standard</strong> is the default and represents the historical behavior: the column is computed at process time, the result is stored in the model, and the expression cannot use user-aware DAX functions like USERCULTURE.</p>
<p><strong>User Context</strong> is the new option. A column with Expression Context set to User Context is called a user-aware calculated column. The expression is evaluated at query time, in an empty filter context, with access to the model restricted by active security roles. Within this evaluation, the engine recognizes the user-aware DAX functions and returns values that depend on the current user.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image1-132.png" width="400" /></p>
<p>The semantics of a user-aware calculated column are otherwise identical to those of a standard calculated column. The expression is evaluated for each row of the table, with a row context active on the table itself. Relationships behave as expected, RELATED and RELATEDTABLE work, and calculations on the row are performed as usual. The result of the expression does not depend on the report or on any visual: the value of a user-aware column is the same in every visual that displays it, given that the user is the same.</p>
<p>In other words, user-aware calculated columns have the same row-by-row semantics as a standard calculated column, with three differences:</p>
<ol>
<li>The calculated column is executed within the security context of the current user.</li>
<li>The result of the calculated column can depend on the user identity if its DAX expression includes user-aware functions.</li>
<li>The column is not materialized.</li>
</ol>
<p><em>When a User Context column does not use any user-aware function and does not access rows from other tables, it returns the same value for every user. The only difference from a Standard calculated column is that it is not materialized. We call these columns</em> <strong>virtual calculated columns</strong>: <em>columns that exist in the model and are available to filters, slicers, and visuals, but are not stored in memory</em>.</p>
<h2>Use cases for user-aware calculated columns</h2>
<p>We identified three main use cases for user-aware calculated columns; we expect more patterns to emerge in the future.</p>
<h3>Localization with user-aware calculated columns</h3>
<p>Localization is the main use case that motivated the design of user-aware calculated columns. The scenario is straightforward: we want columns whose values depend on the language of the user running the report. For example, consider a <em>Date</em> table that localizes month and day-of-week names. A natural choice is the same formula we would use as a part of a <em>Date</em> calculated table, just with the addition of the USERCULTURE parameter:</p>
<pre class="brush: dax; title: ; notranslate">
Month = 
FORMAT ( 
    &#039;Date&#039;&#x5B;Date], 
    &quot;mmmm&quot;, 
    USERCULTURE() 
)
</pre>
<p>However, when a calculated column must be computed at query time, we want to reduce its dependence on cardinality to control the overall execution cost. For example, if a column represents a month, it is better to depend only on a column that has the same cardinality (<em>Month Number</em>) instead of depending on a column with more unique values that would return the same result (<em>Date</em>):</p>
<div class="dax-code-title">Calculated column in Date table</div>
<pre class="brush: dax; title: ; snippet: Calculated column; table: Date; notranslate">
Month = 
FORMAT ( 
    DATE ( 2020, &#039;Date&#039;&#x5B;Month Number], 1 ), 
    &quot;mmmm&quot;, 
    USERCULTURE() 
)
</pre>
<div class="dax-code-title">Calculated column in Date table</div>
<pre class="brush: dax; title: ; snippet: Calculated column; table: Date; notranslate">
Month Short = 
FORMAT ( 
    DATE ( 2020, &#039;Date&#039;&#x5B;Month Number], 1 ), 
    &quot;mmm&quot;, 
    USERCULTURE() 
)
</pre>
<p>For these expressions to work, we set Expression Context to User Context in both calculated columns, <em>Month</em> and <em>Month Short</em>. With the columns configured as user-aware, USERCULTURE returns the culture of the current user, and the FORMAT function returns the month name in the appropriate language. A German user sees Januar, an Italian user sees Gennaio, and a French user sees Janvier.</p>
<p>Similarly, we create two columns to display the day of the week that depend on <em>Date[Day of Week Number]</em>:</p>
<div class="dax-code-title">Calculated column in Date table</div>
<pre class="brush: dax; title: ; snippet: Calculated column; table: Date; notranslate">
Day of Week = 
FORMAT ( 
    DATE ( 2020, 1, 4 + &#039;Date&#039;&#x5B;Day of Week Number] ), 
    &quot;dddd&quot;, 
    USERCULTURE() 
)
</pre>
<div class="dax-code-title">Calculated column in Date table</div>
<pre class="brush: dax; title: ; snippet: Calculated column; table: Date; notranslate">
Day of Week Short = 
FORMAT ( 
    DATE ( 2020, 1, 4 + &#039;Date&#039;&#x5B;Day of Week Number] ), 
    &quot;ddd&quot;, 
    USERCULTURE() 
)
</pre>
<p>These columns change the names displayed in a report depending on the user. In the following example, the same report is shown side by side, for two users with different languages; column and measure names are displayed without translation (e.g., <em>Year</em>, <em>Day of Week</em>, and <em>Sales Amount</em>).</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image2-128.png" width="600" /></p>
<p>In this article, we discuss a new feature for translating the content of the model, not its metadata – such as column and measure names. Those are covered in the existing documentation. If you are new to localization in Power BI semantic models and you want to learn more about metadata and report translations, please take a look at <a href="https://learn.microsoft.com/en-us/power-bi/guidance/multiple-language-translation">Plan Translation for Multiple-Language Reports in Power BI</a> in the Microsoft documentation.</p>
<p>However, the previous report shows another important design challenge for the semantic model: if we want to make sure that the selection applied to a report shown in a certain language will be preserved when the same report is shown in another language, we cannot apply a filter or a selection directly on a user-aware column. To prevent Power BI from doing that, we should use the <strong>Group By Columns </strong>property, instructing our <em>Month</em> and <em>Day of Week</em> columns to use <em>Month Number</em> and <em>Day of Week Number</em>, respectively, not only for the sort order but also to identify the unique values of the columns. This way, the slicer will store the filter as a selection of numeric values from <em>Date[Day of Week Number]</em> rather than a list of translated strings that would not exist in other languages. We can edit the Group By Columns property in TMDL view or in Tabular Editor, and we provide more information about this property in the <a href="https://www.sqlbi.com/articles/understanding-group-by-columns-in-power-bi/">Understanding Group By Columns in Power BI</a> article on SQLBI.</p>
<p>For example, the following screenshot shows the TMDL view definition of <em>Day of Week</em>, but we can generalize the rule for any user-aware column we want to use for translations:</p>
<ul>
<li>Use the USERCULTURE function in the DAX expression of the calculated column,</li>
<li>Set Expression Context to User Context,</li>
<li>Apply the proper Sort by Column property if required,</li>
<li>Assign the proper Group by Columns setting to identify the column(s) to use to identify the selection without relying on a translated, user-aware column.</li>
</ul>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image3-114.png" width="581" /></p>
<h3>Virtual calculated columns for row-level calculations</h3>
<p>As we mentioned earlier, virtual calculated columns enable redundant columns without storage and processing costs being incurred.</p>
<p>For example, instead of importing <em>Sales[Line Amount]</em> we often compute it by using <em>Sales[Quantity] * Sales[Net Price]</em> to keep the model consistent and efficient, as in this measure:</p>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
Sales Amount = 
SUMX ( 
    Sales, 
    Sales&#x5B;Quantity] * Sales&#x5B;Net Price] 
)
</pre>
<p>This works well for aggregations, but the measure becomes the only way to access the calculation, and there is no <em>Sales[Line Amount]</em> field to drag into a slicer or use in the filter pane. User-defined functions solve the centralization problem, but they must still be invoked from other DAX expressions, and a user cannot apply a filter on <em>Sales[Line Amount]</em> through them.</p>
<p>User-aware calculated columns offer a new option. The classic <em>Line Amount</em> expression in a <em>Sales</em> table can be written as a virtual calculated column:</p>
<div class="dax-code-title">Calculated column in Sales table</div>
<pre class="brush: dax; title: ; snippet: Calculated column; table: Sales; notranslate">
Line Amount = Sales&#x5B;Quantity] * Sales&#x5B;Net Price]
</pre>
<p>In a Standard calculated column, this expression produces a high-cardinality column. The values are computed at process time, stored in memory, and compressed by VertiPaq with limited efficiency precisely because of the high cardinality. However, the calculation itself is trivial and could be performed at query time at a negligible cost.</p>
<p>When Expression Context is set to User Context, the same column becomes virtual. The expression is evaluated at query time when a measure or visual references the column. There is no memory cost, no processing cost, and the logic remains centralized in the model where it belongs. We can still write filters and measures that reference <em>Sales[Line Amount]</em> without the cost of a redundant high-cardinality column.</p>
<p>The potential higher cost at query time is limited when we have simple expressions like this one. The reason is that the formula engine can push the calculation to the storage engine when the expression involves only basic operators on columns of the same table. In this case, the storage engine performs the multiplication during the column scan, with no need for the formula engine to iterate row by row. For tables with fewer than 100 million rows, the cost difference compared to reading a materialized column is typically not relevant; we will revisit this with concrete benchmarks once the feature reaches general availability.</p>
<p>The story is different when the expression cannot be pushed to the storage engine, as is the case with complex DAX measures with iterators. Whenever the expression triggers a callback to the formula engine for each row, the cost can grow significantly. As a rule, simple arithmetic on columns of the same table is pushed down efficiently, whereas expressions involving table functions, complex IF branches, or user-aware functions usually require formula engine intervention.</p>
<p>In short, virtual calculated columns work best for row-level expressions that the storage engine can compute. This way, we obtain the centralization advantages of a column in the model (the logic lives in one place, and the column is available to filters, slicers, and visuals) without paying the cost of redundant high-cardinality columns. We will revisit the full guidance in the Conclusions.</p>
<p><strong><em>Important</em></strong><em>: Virtual calculated columns can have a significant impact on how we design optimized semantic models. Be mindful, however, that this feature is in preview. As such, we will publish more guidelines once it is consolidated.</em></p>
<h3>Securing sensitive columns with user-aware calculated columns</h3>
<p>The presence of sensitive columns that must be hidden from certain users is typically addressed by object-level security (OLS), which removes the column from the model entirely for those users. The problem with OLS is that any Power BI report referencing the hidden column becomes invalid for restricted users: the visual fails with an error, because the column does not exist for them. Therefore, report designers have to maintain separate report pages, or even separate reports, for each role, which quickly becomes impractical.</p>
<p>The user-aware calculated columns offer a workaround for this limitation by using row-level security (RLS). The goal is to hide sensitive information from some users while keeping the rest of the table fully accessible. The technique shown here trades column-level invisibility for <em>content</em>-level invisibility: the column is still present in the model and in the field list, the report continues to render correctly, but the values are blank for users without permission. The same report works for both admin and restricted users without any role-specific layout. The trade-off is that restricted users can see that restricted columns exist, but they cannot see their values.</p>
<p>The canonical example is a <em>Salary</em> column in an <em>Employee</em> table, but the Contoso sample we use does not include one, so we demonstrate the same pattern with an <em>Income Bracket</em> column in the <em>Customer</em> table. The technique applies equally to any other case in which a single column contains information that only certain users should be privy to.</p>
<p>We focus on three tables in the model: <em>Sales</em>, <em>Customer</em>, and <em>CustomerIncome</em>. <em>CustomerIncome</em> is hidden from the report and stores <em>Bracket Number</em> and <em>Income Bracket</em> for every customer.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image4-108.png" width="800" /></p>
<p>RLS on <em>CustomerIncome</em> is defined for the "No Income Bracket" role, with a filter that returns FALSE for every row in the table. That makes the entire <em>CustomerIncome</em> table invisible to members of that role; an admin role with no filter sees all rows.</p>
<p>As the diagram view shows, <strong>there is no relationship between Customer and CustomerIncome</strong>. Quite surprisingly, if we created a relationship from <em>CustomerIncome</em> to <em>Customer</em>, the RLS filter that returns FALSE on <em>CustomerIncome</em> would propagate through the relationship to <em>Customer</em> and then to <em>Sales</em>. Restricted users would then see no customers and no sales at all: the report would become empty rather than partially redacted. The design relies on the filter remaining confined to the LOOKUPVALUE expression inside the calculated column. Keeping the two tables disconnected is what makes that possible, and for the same reason, we must avoid any relationship that could let the <em>CustomerIncome</em> filter reach the rest of the model.</p>
<p>We then copy the sensitive columns into <em>Customer</em> using two calculated columns, where we set the Expression Context to User Context:</p>
<div class="dax-code-title">Calculated column in Customer table</div>
<pre class="brush: dax; title: ; snippet: Calculated column; table: Customer; notranslate">
Income Bracket = 
LOOKUPVALUE ( 
    CustomerIncome&#x5B;Income Bracket], 
    CustomerIncome&#x5B;CustomerKey], Customer&#x5B;CustomerKey] 
)
</pre>
<div class="dax-code-title">Calculated column in Customer table</div>
<pre class="brush: dax; title: ; snippet: Calculated column; table: Customer; notranslate">
Income Bracket Number = 
LOOKUPVALUE ( 
    CustomerIncome&#x5B;Bracket Number], 
    CustomerIncome&#x5B;CustomerKey], Customer&#x5B;CustomerKey] 
)
</pre>
<p>The key behavior is that LOOKUPVALUE returns BLANK whenever the matching row in <em>CustomerIncome</em> is filtered out by the active security role. Users in "No Income Bracket" therefore see a blank value in <em>Customer[Income Bracket]</em> for every customer, while admins see the real bracket. This is the matrix with <em>Sales Amount</em> by <em>Income Bracket</em> and <em>Continent</em> visible to admin users, who see every bracket and every region populated correctly.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image5-92.png" width="600" /></p>
<p>Users in the "No Income Bracket" role do not see any names in the <em>Income Bracket</em> column. Note below the <em>Now viewing as: No Income Bracket</em> banner at the top: the column still exists, the report still runs, but <em>Income Bracket</em> collapses to a single blank row. All the other <em>Customer</em> columns (<em>Address</em>, <em>Age</em>, <em>City</em>, <em>Country</em>, and so on) remain fully accessible because no filter is propagated onto <em>Customer</em>.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image6-83.png" width="500" /></p>
<p>It is important to note that the cost of LOOKUPVALUE is paid for each row of the <em>Customer</em> table where the column is evaluated, at every query. For a small <em>Customer</em> table, this is negligible; for a large one, it can become noticeable. Be mindful of this when applying the pattern to high-cardinality tables.</p>
<h2>Materialization of calculated columns</h2>
<p>A Standard calculated column in Import mode is materialized: the engine evaluates the expression for each row during model processing and stores the result in the column, exactly like any other imported column. From the storage engine point of view, there is no difference between a materialized calculated column and an imported column. Both are queried at the storage engine level with the same speed and behavior – except for a potentially lower compression rate for the calculated column.</p>
<p>A user-aware calculated column is not materialized. The column does not occupy memory and does not exist in the storage engine. When a query references the column, the storage engine evaluates the expression at query time.</p>
<p>It is important to note that materialization is not a property that we can control directly. Materialization is the consequence of the combination of two factors: the Expression Context property and the storage mode of the table. In Import mode, a Standard column is materialized, and a User Context column is not. In DirectQuery mode, calculated columns have always been unmaterialized: the engine translates the expression into a SQL query and computes the values at query time. With DirectQuery, the User Context property does not change the materialization, since the column was already unmaterialized.</p>
<p>The following table shows the combinations of table storage mode and supported Expression Context settings.</p>
<table width="780">
<thead>
<tr>
<td width="400">Storage mode</td>
<td width="220">Standard (default)</td>
<td width="160">User Context</td>
</tr>
</thead>
<tbody>
<tr>
<td width="400">Import</td>
<td width="220">Materialized</td>
<td width="160">Unmaterialized</td>
</tr>
<tr>
<td width="400">Direct Lake on OneLake</td>
<td width="220">Unmaterialized</td>
<td width="160">Unmaterialized</td>
</tr>
<tr>
<td width="400">Direct Lake on SQL</td>
<td width="220">N/A</td>
<td width="160">N/A</td>
</tr>
<tr>
<td width="400">DirectQuery</td>
<td width="220">Unmaterialized</td>
<td width="160">Unmaterialized</td>
</tr>
<tr>
<td width="400">Dual</td>
<td width="220">Materialized (Import), unmaterialized (DirectQuery)</td>
<td width="160">Unmaterialized</td>
</tr>
<tr>
<td width="400">DirectQuery on Power BI semantic models</td>
<td width="220">Unmaterialized</td>
<td width="160">N/A</td>
</tr>
</tbody>
</table>
<p>Reading the table is straightforward once we accept that materialization is derived rather than chosen: we pick Standard or User Context, we pick the storage mode of the table, and the engine determines whether the column is materialized in memory. Direct Lake on OneLake is the storage mode where the calculated column is always unmaterialized, regardless of the Expression Context property. Import is the only storage mode in which a calculated column is materialized; the User Context option also makes the column unmaterialized in Import mode.</p>
<h2>User-aware expressions and calculated columns</h2>
<p>A user-aware DAX expression is one that depends on the user running the query, affecting which DAX functions can be used and the security perimeter for accessing data.</p>
<p>The set of user-aware DAX functions includes USERCULTURE, USERPRINCIPALNAME, USEROBJECTID, USERNAME, and CUSTOMDATA. An expression is user-aware when it calls one of these functions directly, or when it references another expression that does so indirectly, like a measure or a calculated column that internally uses USERPRINCIPALNAME.</p>
<p>These functions return values that are known only when a user runs a query. Therefore, they cannot be used in a Standard calculated column because there is no user at process time. The engine raises an error when a Standard calculated column attempts to use a user-aware function. Be mindful that User Context is what allows the column to use these functions. We explicitly choose User Context as the Expression Context, and only then can the expression invoke the functions listed above.</p>
<p>A user-aware calculated column has access only to the rows visible to the user through the corresponding security roles. If a DAX expression aggregates rows from a table or attempts to access other rows or tables in the model, the access is limited to the security perimeter defined by the active security roles for the current user. This is an important difference in the semantics of a calculated column: for example, certain classification techniques (e.g. best products) may require the use of Standard calculated columns that are not user-aware; implementing <a href="https://www.sqlbi.com/articles/implement-non-visual-totals-with-power-bi-security-roles/">Non Visual Totals</a> in a semantic model requires calculated tables that access the entire model regardless of the security roles – although, at the time of writing, we do not have an Expression Context property for calculated tables.</p>
<p>It is important to note that user-aware columns can also contain expressions that do not use any user-aware functions and do not access any other rows of the model, whether in the same table or in other tables. In that case, the column is just a virtual calculated column.</p>
<h2>Limitations of user-aware calculated columns</h2>
<p>User-aware calculated columns have four important limitations:</p>
<ul>
<li>They <strong>cannot be used in relationships</strong>. A relationship in Import mode creates a model-level structure that cannot depend on the user.</li>
<li>They <strong>cannot be referenced</strong> (directly or indirectly) <strong>in standard calculated columns</strong>. Because a Standard calculated column must not depend on the user context, any direct or indirect dependency is not allowed. The model prevents us from creating such conditions, and will raise an error if we try to save a model that violates this.</li>
<li>They <strong>cannot be referenced</strong> (directly or indirectly) <strong>in calculated tables</strong>. A calculated table cannot be user-aware. Therefore, like standard calculated columns, calculated tables must not depend on the user context, directly or indirectly. The model raises an error if we try to save a model that violates that.</li>
<li>They <strong>cannot be referenced</strong> (directly or indirectly) <strong>in row-level security (RLS)</strong>. The row-level security expressions must be evaluated to determine which rows are visible in the user-aware space. Therefore, they cannot depend on user-aware columns. In this case as well, the model raises an error if we try to save a model that violates this condition.</li>
</ul>
<p>The relationship limitation warrants a more detailed explanation because it affects certain modeling techniques. A relationship is a structural property of the model in Import mode that is defined during processing. The engine relies on relationships to optimize queries and to propagate filters between tables. To do so efficiently, the engine builds internal data structures that require for the column to be materialized. A user-aware column does not exist in storage, so there is no column for the relationship to use.</p>
<p>This limitation has a significant practical consequence. User-aware columns cannot be used to build <em>calculated relationships</em>: relationships built on columns that are themselves the result of a calculation. Common examples include a column that combines two existing columns to form a composite key (see COMBINEVALUES) or a column that retrieves a price range based on a value in the table (as in <a href="https://www.daxpatterns.com/static-segmentation/">Static segmentation</a>, shown in the following picture). These columns must remain Standard calculated columns and pay the cost of materialization, because they exist for the sole purpose of feeding a relationship.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image7-71.png" width="550" /></p>
<h2>Conclusions</h2>
<p>Recapping, user-aware calculated columns introduce a new dimension to a feature (calculated columns) that has existed since the very first version of DAX. The Expression Context property determines whether a column is evaluated in the model context or the user context; only in the latter case can the expression use user-aware DAX functions.</p>
<p>The most evident use case is localization, but the implications are broader. Virtual calculated columns occupy a useful middle ground between columns imported from the source and measures defined in the model: they expose a column to the user interface without paying the cost of additional storage, while keeping the calculation centralized in a single place. Custom security and personalization scenarios can also be implemented with this new feature.</p>
<p>There are limitations to be aware of. User-aware columns trade structural participation in the model (relationships, calculated tables, RLS, and dependencies from standard calculated columns) for the flexibility of being evaluated at query time; the cost can become noticeable for high-cardinality tables.</p>
<p>The rule is simple: use user-aware columns when a column depends on the user, or when the expression is simple enough that the storage engine can evaluate it at query time at minimal cost. Use standard calculated columns when materialization is needed for relationships, when the expression is complex, or when query performance is critical. As is often the case, understanding when each option applies is what makes the difference between a model that scales well and one that does not.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/articles/introducing-user-aware-calculated-columns-in-power-bi/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		      <dc:creator><![CDATA[Alberto Ferrari]]></dc:creator>
	</item>
		<item>
		<title>Filtering measures through slicers</title>
		<link>https://www.sqlbi.com/tv/filtering-measures-through-slicers/</link>
					<comments>https://www.sqlbi.com/tv/filtering-measures-through-slicers/#respond</comments>
		
		<dc:creator><![CDATA[Alberto Ferrari]]></dc:creator>
		<pubDate>Tue, 05 May 2026 10:00:00 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<guid isPermaLink="false">http://www.sqlbi.com/?post_type=video&#038;p=895452</guid>

					<description><![CDATA[<figure><img src="https://i.ytimg.com/vi/E0KNojT3hwA/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure>A slicer cannot filter a measure: let's analyze this common request by explaining how to use a slicer to filter a measure, after discussing the real meaning of using a measure with a slicer.]]></description>
										<content:encoded><![CDATA[<figure><img src="https://i.ytimg.com/vi/E0KNojT3hwA/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure><p>A slicer cannot filter a measure: let's analyze this common request by explaining how to use a slicer to filter a measure, after discussing the real meaning of using a measure with a slicer.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/tv/filtering-measures-through-slicers/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Filtering measures through slicers</title>
		<link>https://www.sqlbi.com/articles/filtering-measures-through-slicers/</link>
					<comments>https://www.sqlbi.com/articles/filtering-measures-through-slicers/#respond</comments>
		
		<dc:creator><![CDATA[Alberto Ferrari]]></dc:creator>
		<pubDate>Mon, 04 May 2026 20:00:33 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<category><![CDATA[Power BI]]></category>
		<guid isPermaLink="false">https://www.sqlbi.com/?p=896499</guid>

					<description><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/image1-131.png" class="webfeedsFeaturedVisual" /></figure>A slicer cannot filter a measure. In this article, we analyze this common request by explaining how to use a slicer to filter a measure, after discussing the real meaning of using a measure with a slicer. A very common&#8230;]]></description>
										<content:encoded><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/image1-131.png" class="webfeedsFeaturedVisual" /></figure><p>A slicer cannot filter a measure. In this article, we analyze this common request by explaining how to use a slicer to filter a measure, after discussing the real meaning of using a measure with a slicer.<br />
<span id="more-896499"></span></p>
<p>A very common request by Power BI newbies is, “How can I use a slicer to filter a measure rather than a regular model column?” The most common answer to this question is, “You cannot filter a measure through a slicer”. The answer is entirely correct because there is no such thing as “filtering a measure”. However, elaborating on the why gives us a good way to explain not only what is wrong with the question, but also how to further reason about the requirements needed to obtain a working solution.</p>
<h2>Interpreting the question</h2>
<p>Let us pretend the question is “I want to filter the sales amount greater than 100,000 USD.” The question is incomplete. One may want to filter products with sales greater than 100,000, or customers, or stores, or any other table/column. Indeed, a filter is always applied to a column, not to a measure. A measure cannot be used as a filter unless we define the granularity of the filter, that is, the column we will use to evaluate the measure in a given context.</p>
<p>Let us add some background information, as the misunderstanding stems from the fact that you <strong>can</strong> filter a visual with a measure in the Power BI user interface. Indeed, you can use a measure as a filter in a matrix, as in the following example, where the matrix on the right is filtered by the <em>Sales Amount</em> measure in the filter pane.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image1-131.png" width="560" style="border:0" /></p>
<p>However, the filter is not on the measure itself. The matrix filters brands with <em>Sales Amount</em> greater than 100,000. The granularity of the filter is provided automatically by the matrix, which groups data by brand. Indeed, changing the column we slice by in the matrix, changes the result.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image3-113.png" width="599" style="border:0" /></p>
<p>The total of the unfiltered matrix is the same as above, whereas the total of the filtered matrix is different, because the filter has a different granularity.</p>
<p>In other words, a measure can <strong>be</strong> a filter if you define its granularity. Filtering <em>Sales Amount</em> greater than 100,000 USD is nonsense. On the other hand, filtering customers (or products) with sales greater than 100,000 USD makes a lot of sense.</p>
<h2>Implementing a slicer</h2>
<p>If one wants to use a slicer to filter a measure, they can do so as long as they define the granularity of the filter. The slicer can be conveniently used to adjust the filter parameters; in our example, the <em>Sales Amount</em> value that should be used as the minimum.</p>
<p>As an example, one may want to produce a report like the following.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image4-107.png" width="604" style="border:0" /></p>
<p>The slicer filters the <em>Sales Amount</em> measure, but it clearly states the granularity at which the filter is applied: products. Therefore, selecting an item in the slicer restricts the calculation to products with sales exceeding the specified limit.</p>
<p>There are multiple ways to produce such a report; a very convenient one is to rely on a function to embed the filtering logic and on a calculation group to serve as the user interface. Each calculation item in the calculation group invokes the function passing the correct parameters.</p>
<p>First, the function:</p>
<div class="dax-code-title">Function</div>
<pre class="brush: dax; title: ; snippet: Function; notranslate">
Local.FilterProductsBasedOnMeasure = 
(
    resultExpression : EXPR,
    filterMeasure: MEASUREREF,
    filterLimit: SCALAR
) =&gt;
    CALCULATE (
        resultExpression,
        KEEPFILTERS (
            FILTER ( Product, filterMeasure &gt; filterLimit )
        )
    )
</pre>
<p>The function accepts three parameters: the result to produce, the measure to use as a filter, and the filter limit. The granularity of the filter is specified inside the function body, as the function filters <em>Product</em>, providing the product as the filter granularity.</p>
<p>Once the function is defined, one calculation group can contain all the items that produce the filtering, by invoking the function passing SELECTEDMEASURE as the value to compute, <em>Sales Amount</em> as the measure to use as a filter, and the limit as the third argument:</p>
<div class="dax-code-title">Calculation item in Filter Product Sales table</div>
<pre class="brush: dax; title: ; snippet: Calculation item; table: Filter Product Sales; notranslate">
Products selling more than 100 USD = 
Local.FilterProductsBasedOnMeasure ( SELECTEDMEASURE ( ), &#x5B;Sales Amount], 100 )
</pre>
<div class="dax-code-title">Calculation item in Filter Product Sales table</div>
<pre class="brush: dax; title: ; snippet: Calculation item; table: Filter Product Sales; notranslate">
Products selling more than 1,000 USD = 
Local.FilterProductsBasedOnMeasure ( SELECTEDMEASURE ( ), &#x5B;Sales Amount], 1000 )
</pre>
<div class="dax-code-title">Calculation item in Filter Product Sales table</div>
<pre class="brush: dax; title: ; snippet: Calculation item; table: Filter Product Sales; notranslate">
Products selling more than 10,000 USD = 
Local.FilterProductsBasedOnMeasure ( SELECTEDMEASURE ( ), &#x5B;Sales Amount], 10000 )
</pre>
<div class="dax-code-title">Calculation item in Filter Product Sales table</div>
<pre class="brush: dax; title: ; snippet: Calculation item; table: Filter Product Sales; notranslate">
Products selling more than 100,000 USD = 
Local.FilterProductsBasedOnMeasure ( SELECTEDMEASURE ( ), &#x5B;Sales Amount], 100000 )
</pre>
<h2>Creating a more flexible solution</h2>
<p>Depending on the user’s needs, the granularity of the filter can be passed as an additional parameter. This way, the calculation items can change not only the limit but also the granularity (or the measure) to use as a filter. Despite its simplicity, this pattern can easily be extended to accommodate rather complex user needs.</p>
<p>For example, we can create two slicers: one to define the granularity, and one to define the measure boundaries, like in the following example, where we first create a new disconnected table to let users select the filter granularity:</p>
<div class="dax-code-title">Calculated table</div>
<pre class="brush: dax; title: ; snippet: Calculated table; notranslate">
TableToFilter = 
    SELECTCOLUMNS ( 
       { &quot;Product&quot;, &quot;Customer&quot;, &quot;Store&quot; },
       &quot;Table to filter&quot;, &#x5B;Value]
    )
</pre>
<p>A new function reads the content of the selected item in the slicer to direct the filter to the proper granularity:</p>
<div class="dax-code-title">Function</div>
<pre class="brush: dax; title: ; snippet: Function; notranslate">
Local.FilterTableBasedOnMeasure = (
        resultExpression : EXPR,
        filterMeasure : MEASUREREF,
        filterLimit : SCALAR
    ) =&gt;
    VAR TableToFilter =
        SELECTEDVALUE ( TableToFilter&#x5B;Table to filter] )
    VAR Result =
        SWITCH (
            TableToFilter,
            &quot;Product&quot;,
                CALCULATE (
                    resultExpression,
                    FILTER (
                        Product,
                        filterMeasure &gt; filterLimit
                    )
                ),
            &quot;Customer&quot;,
                CALCULATE (
                    resultExpression,
                    FILTER (
                        Customer,
                        filterMeasure &gt; filterLimit
                    )
                ),
            &quot;Store&quot;,
                CALCULATE (
                    resultExpression,
                    FILTER (
                        Store,
                        filterMeasure &gt; filterLimit
                    )
                )
        )
    RETURN
        Result
</pre>
<p>Finally, a new calculation group lets users choose the amount only, without defining the granularity – which is selected through the disconnected table:</p>
<div class="dax-code-title">Calculation item in Filter Sales Amount table</div>
<pre class="brush: dax; title: ; snippet: Calculation item; table: Filter Sales Amount; notranslate">
    Sales amount more than 100 USD = 
    Local.FilterTableBasedOnMeasure ( SELECTEDMEASURE (), &#x5B;Sales Amount], 100 )
</pre>
<div class="dax-code-title">Calculation item in Filter Sales Amount table</div>
<pre class="brush: dax; title: ; snippet: Calculation item; table: Filter Sales Amount; notranslate">
    Sales amount more than 1000 USD = 
    Local.FilterTableBasedOnMeasure ( SELECTEDMEASURE (), &#x5B;Sales Amount], 1000 )
</pre>
<div class="dax-code-title">Calculation item in Filter Sales Amount table</div>
<pre class="brush: dax; title: ; snippet: Calculation item; table: Filter Sales Amount; notranslate">
    Sales amount more than 10,000 USD = 
    Local.FilterTableBasedOnMeasure ( SELECTEDMEASURE (), &#x5B;Sales Amount], 10000 )
</pre>
<div class="dax-code-title">Calculation item in Filter Sales Amount table</div>
<pre class="brush: dax; title: ; snippet: Calculation item; table: Filter Sales Amount; notranslate">
    Sales amount more than 100,000 USD = 
    Local.FilterTableBasedOnMeasure ( SELECTEDMEASURE (), &#x5B;Sales Amount], 100000 )
</pre>
<div class="dax-code-title">Calculation item in Filter Sales Amount table</div>
<pre class="brush: dax; title: ; snippet: Calculation item; table: Filter Sales Amount; notranslate">
    Sales amount more than 1,000,000 USD = 
    Local.FilterTableBasedOnMeasure ( SELECTEDMEASURE (), &#x5B;Sales Amount], 1000000 )
</pre>
<p>The result is a report where users can choose both the limit value and the granularity.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image5-91.png" width="602" style="border:0" /></p>
<h2>Conclusions</h2>
<p>Sometimes, answering a newbie’s question with the most concise answer is the best option; sometimes it is not. A simple (wrong) requirement, like the one analyzed in this article, may require further investigation to explain why it is wrong and, ultimately, yield interesting solutions.</p>
<p>The key takeaway is that a slicer does not (and cannot) filter a measure directly; instead, it lets users tune the parameters of a filter that is applied to a business entity at a specific granularity (such as <em>Product</em>, <em>Customer</em>, or <em>Store</em>). Once you make that granularity explicit, you can deliver the “filter a measure” experience in a correct, flexible way: You can encapsulate the logic in a reusable function and expose choices through calculation groups.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/articles/filtering-measures-through-slicers/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		      <dc:creator><![CDATA[Marco Russo]]></dc:creator>
	</item>
		<item>
		<title>Parameter types in DAX user defined functions UDF</title>
		<link>https://www.sqlbi.com/tv/parameter-types-in-dax-user-defined-functions-udf/</link>
					<comments>https://www.sqlbi.com/tv/parameter-types-in-dax-user-defined-functions-udf/#respond</comments>
		
		<dc:creator><![CDATA[Marco Russo]]></dc:creator>
		<pubDate>Tue, 21 Apr 2026 10:00:00 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<guid isPermaLink="false">http://www.sqlbi.com/?post_type=video&#038;p=896557</guid>

					<description><![CDATA[<figure><img src="https://i.ytimg.com/vi/q29LycVpcko/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure>Learn how to specify the parameter types in DAX user-defined functions using MEASUREREF, COLUMNREF, TABLEREF, and CALENDARREF.]]></description>
										<content:encoded><![CDATA[<figure><img src="https://i.ytimg.com/vi/q29LycVpcko/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure><p>Learn how to specify the parameter types in DAX user-defined functions using  MEASUREREF, COLUMNREF, TABLEREF, and CALENDARREF.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/tv/parameter-types-in-dax-user-defined-functions-udf/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Understanding parameter types in DAX user-defined functions (UDF)</title>
		<link>https://www.sqlbi.com/articles/understanding-parameter-types-in-dax-user-defined-functions-udf/</link>
					<comments>https://www.sqlbi.com/articles/understanding-parameter-types-in-dax-user-defined-functions-udf/#respond</comments>
		
		<dc:creator><![CDATA[Marco Russo]]></dc:creator>
		<pubDate>Mon, 20 Apr 2026 20:00:43 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<category><![CDATA[UDF]]></category>
		<category><![CDATA[User-defined functions]]></category>
		<guid isPermaLink="false">https://www.sqlbi.com/?p=895583</guid>

					<description><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/parameter-types.png" class="webfeedsFeaturedVisual" /></figure>This article describes the parameter types available in DAX user-defined functions, focusing on the specialized reference types MEASUREREF, COLUMNREF, TABLEREF, and CALENDARREF. In a previous article, Introducing user-defined functions in DAX, we described the syntax for creating user-defined functions, including&#8230;]]></description>
										<content:encoded><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/parameter-types.png" class="webfeedsFeaturedVisual" /></figure><p>This article describes the parameter types available in DAX user-defined functions, focusing on the specialized reference types MEASUREREF, COLUMNREF, TABLEREF, and CALENDARREF.<br />
<span id="more-895583"></span></p>
<p>In a previous article, <a href="https://www.sqlbi.com/articles/introducing-user-defined-functions-in-dax/">Introducing user-defined functions in DAX</a>, we described the syntax for creating user-defined functions, including the two passing modes (VAL and EXPR) and the fundamental parameter types SCALAR and TABLE. In this article, we build on that foundation and focus on the complete type system, with particular attention to the reference types introduced in March 2026 that provide better documentation, stronger validation, and improved IntelliSense support.</p>
<p>Before diving into the new types, let us briefly recap the full picture of parameter types and passing modes available in DAX user-defined functions.</p>
<h2>Parameter types and passing modes</h2>
<p>Each parameter of a user-defined function has two properties: a type, which describes what kind of value the parameter accepts, and a passing mode, which describes how the value is transferred from the caller to the body of the function. The following table summarizes all the valid combinations.</p>
<div style="display: flex;gap: 80px">
<table width="310">
<tbody>
<tr>
<td width="110"><strong>Type</strong></td>
<td width="200"><strong>Passing mode</strong></td>
</tr>
<tr>
<td>ANYVAL</td>
<td>VAL / EXPR</td>
</tr>
<tr>
<td><strong>SCALAR (*)</strong></td>
<td>VAL / EXPR</td>
</tr>
<tr>
<td>TABLE</td>
<td>VAL / EXPR</td>
</tr>
<tr>
<td>ANYREF</td>
<td>EXPR</td>
</tr>
<tr>
<td>MEASUREREF</td>
<td>EXPR</td>
</tr>
<tr>
<td>COLUMNREF</td>
<td>EXPR</td>
</tr>
<tr>
<td>TABLEREF</td>
<td>EXPR</td>
</tr>
<tr>
<td>CALENDARREF</td>
<td>EXPR</td>
</tr>
</tbody>
</table>
<table width="109">
<tbody>
<tr>
<td width="109"><strong>SCALAR (*) Subtype</strong></td>
</tr>
<tr>
<td>VARIANT</td>
</tr>
<tr>
<td>INT64</td>
</tr>
<tr>
<td>DECIMAL</td>
</tr>
<tr>
<td>DOUBLE</td>
</tr>
<tr>
<td>STRING</td>
</tr>
<tr>
<td>DATETIME</td>
</tr>
<tr>
<td>BOOLEAN</td>
</tr>
<tr>
<td>NUMERIC</td>
</tr>
</tbody>
</table>
</div>
<p>SCALAR and TABLE are the two types that work with both VAL and EXPR. When no passing mode is specified, the default is VAL for both. ANYVAL is an abstract type for SCALAR and TABLE. Despite the name, it does not exactly restrict the passing mode. You could use ANYVAL as a shortcut for VAL for any data type; we discourage using ANYVAL with EXPR because of the confusion it could generate. All the remaining types (ending with “REF”) force the EXPR passing mode. The passing mode keyword can be omitted for these types because only EXPR is valid.</p>
<p>The types in the lower part of the Type/Passing Mode table (MEASUREREF, COLUMNREF, TABLEREF, and CALENDARREF) are specializations of ANYREF. They share the same passing mode, but they restrict the kind of expression the caller can provide. These are the types we focus on in the rest of this article.</p>
<h3>ANYREF and its limitations</h3>
<p>ANYREF declares a parameter that accepts any expression and is always passed as an expression. It is the most permissive reference type: the function accepts whatever expression the caller provides: a measure reference, a column reference, a table reference, or an arbitrary DAX calculation. The expression provided is substituted into the body of the function wherever the parameter appears. It is important to highlight that a DAX formula is accepted by ANYREF as a valid argument: ANYREF should not be interpreted as “a reference to any existing object” but rather “a reference to any expression”. Writing ANYREF implies EXPR: writing ANYREF with or without EXPR has the same meaning and produces the same effects.</p>
<p>This flexibility comes at a cost. Because ANYREF accepts anything, the function author cannot make assumptions about the nature of the expression. Is it a measure that triggers a context transition? Is it a simple column reference? Is it an arbitrary calculation? With ANYREF, the answer could be any of these. The function code must therefore be <a href="https://jumpcloud.com/it-index/what-is-defensive-coding">defensive</a>: whether the expression may or may not trigger a context transition, the function author should use an explicit CALCULATE to ensure consistent behavior if a context transition is needed – something that would not be necessary if the parameter passed were a measure reference.</p>
<p>The lack of specificity also affects the caller’s experience. IntelliSense and other development tools cannot provide meaningful guidance when the parameter accepts just any expression. The developer who calls the function must rely on documentation, or on reading the function body, to understand what is expected.</p>
<p>When a parameter is declared as ANYREF, <a href="https://docs.sqlbi.com/dax-style/dax-naming-conventions#parameters">we recommend</a> using the suffix <em>Expr</em> in the parameter name: for example, <em>amountExpr</em> or <em>targetExpr</em>.</p>
<p>For example, here is a model-dependent function that filters customers whose purchase amount (provided as ANYREF) is greater than a minimum value (<em>lowerAmount</em>):</p>
<div class="dax-code-title">Function</div>
<pre class="brush: dax; highlight: [2]; title: ; snippet: Function; notranslate">
Local.TopCustomersAnyRefA = ( 
    amountExpr : ANYREF, 
    lowerAmount : DOUBLE 
) =&gt;
    FILTER (
        Customer, 
        amountExpr &gt; lowerAmount
    )
</pre>
<p>The <em>Local.TopCustomersAnyRefA</em> function can be used in three different versions of the <em>AnyRef A</em> measure: as a measure reference, as an expression, and as an expression embedded in CALCULATE, respectively. The expression used is the same as that defined in the <em>Total Quantity</em> measure:</p>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
Total Quantity = 
SUM ( Sales&#x5B;Quantity] )
</pre>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
AnyRef-A 1 = 
CALCULATE ( 
    &#x5B;Sales Amount],
    Local.TopCustomersAnyRefA ( &#x5B;Total Quantity], 20 )
)
</pre>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
AnyRef-A 2 = 
CALCULATE ( 
    &#x5B;Sales Amount],
    Local.TopCustomersAnyRefA ( SUM ( Sales&#x5B;Quantity] ), 20 )
)
</pre>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
AnyRef-A 3 = 
CALCULATE ( 
    &#x5B;Sales Amount],
    Local.TopCustomersAnyRefA ( CALCULATE ( SUM ( Sales&#x5B;Quantity] ) ), 20 )
)
</pre>
<p>The second version of <em>AnyRef A</em>, which has an expression not embedded in CALCULATE, returns the same values as <em>Sales Amount</em> because the <em>Local.TopCustomerAnyRefA</em> function returns all customers: the result of the <em>amountExpr</em> argument is evaluated without filtering the iterated customer, since the context transition is missing.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image1-130.png" width="600" /></p>
<p>We can fix the function by embedding the <em>amountExpr</em> parameter in a CALCULATE, which is redundant but harmless when the argument is a measure reference. However, this would prevent using a column reference if the developer wanted to provide a <em>Customer</em> column as the argument of <em>amountExpr</em>. Not that it would have been a good idea, but ANYREF does not impose any restrictions on the argument to use:</p>
<div class="dax-code-title">Function</div>
<pre class="brush: dax; title: ; snippet: Function; notranslate">
Local.TopCustomersAnyRefB = ( 
    amountExpr : ANYREF, 
    lowerAmount : DOUBLE 
) =&gt;
    FILTER (
        Customer, 
        CALCULATE ( amountExpr ) &gt; lowerAmount
    )
</pre>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
AnyRef-B 2 = 
CALCULATE ( 
    &#x5B;Sales Amount],
    Local.TopCustomersAnyRefB ( SUM ( Sales&#x5B;Quantity] ), 20 )
)
</pre>
<p>This way, all the versions of the measure AnyRef-B return the same value.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image2-126.png" width="600" /></p>
<h3>MEASUREREF</h3>
<p>A MEASUREREF parameter accepts only a reference to a measure defined in the semantic model. The caller must provide the name of an existing measure; arbitrary DAX expressions are not accepted.</p>
<p>This restriction has an important semantic implication. A measure reference always triggers a context transition when evaluated in a row context. When we declare a parameter as MEASUREREF, we inform the reader of the function code that a context transition will occur wherever this parameter is used within an iterator. This makes the code easier to think about because the parameter’s behavior is predictable.</p>
<p>With ANYREF, the function author should wrap the parameter in an explicit CALCULATE to guarantee context transition, because the caller might provide an expression that does not trigger it on its own, as we illustrated with the previous examples for ANYREF. With MEASUREREF, CALCULATE is redundant for this purpose, though it causes no harm. The constraint imposed by the MEASUREREF type guarantees the behavior.</p>
<p>When a parameter is declared as MEASUREREF, <a href="https://docs.sqlbi.com/dax-style/dax-naming-conventions#parameters">we recommend</a> using the suffix <em>Measure</em> in the parameter name: for example, <em>salesMeasure</em> or <em>targetMeasure</em>.</p>
<p>For this example, we created a version of the function we used in the previous ANYREF example, this time specifying MEASUREREF as the type of the <em>amountMeasure</em> parameter:</p>
<div class="dax-code-title">Function</div>
<pre class="brush: dax; title: ; snippet: Function; notranslate">
Local.TopCustomersMeasureRef = ( 
    amountMeasure : MEASUREREF, 
    lowerAmount : DOUBLE 
) =&gt;
    FILTER (
        Customer, 
        amountMeasure &gt; lowerAmount
    )
</pre>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
MeasureRef 1 = 
CALCULATE ( 
    &#x5B;Sales Amount],
    Local.TopCustomersMeasureRef ( &#x5B;Total Quantity], 20 )
)
</pre>
<p>There is only one version of the measure we can use: the one that provides <em>Total Quantity</em> as an argument.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image3-112.png" width="389" /></p>
<p>Indeed, trying to provide an expression as the argument for <em>amountMeasure</em> generates a syntax error:</p>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
MeasureRef 2 = 
CALCULATE ( 
    &#x5B;Sales Amount],
    Local.TopCustomersMeasureRef ( SUM ( Sales&#x5B;Quantity] ), 20 )
)
</pre>
<p>The declaration of <em>MeasureRef 2</em> would return the following error: <em>An invalid argument type was passed into parameter ‘amountMeasure’ of the user-defined function. Expected ‘MEASUREREF’ but got ‘SCALAR’.</em></p>
<p>The error message mentions SCALAR because the expression <em>SUM ( Sales[Quantity] )</em> could be evaluated before executing the <em>Local.TopCustomersMeasureRef</em> function, and its result would be a scalar in that case. However, the important part is that the expected argument should have been MEASUREREF, and it is not.</p>
<h3>COLUMNREF</h3>
<p>A COLUMNREF parameter accepts only a reference to a column defined in a table in the semantic model. The caller must provide a qualified column reference, such as <em>Sales[Unit Price]</em> or <em>Product[Unit Price]</em>; arbitrary expressions are not accepted.</p>
<p>COLUMNREF is particularly useful when writing model-independent functions. Instead of hardcoding column names in the function body (which would create a dependency on the model structure), we declare the columns as COLUMNREF parameters and let the caller specify which columns to use. This design makes the function portable across models with different table and column names.</p>
<p>COLUMNREF parameters work well in combination with two DAX functions designed for inspecting reference parameters, TABLEOF and NAMEOF:</p>
<ul>
<li><strong>TABLEOF</strong> retrieves the table where a given column is defined: if the caller passes <em>Sales[Unit Price]</em> as the <em>priceColumn</em> parameter, then <em>TABLEOF ( priceColumn )</em> returns the <em>Sales</em> This combination allows us to reduce the number of parameters in the function signature. Instead of asking the caller for both a table and a column from that table, we can ask for only the column and from there, derive the table by using TABLEOF.</li>
<li><strong>NAMEOF</strong> returns the name of a column reference as a string, which can be useful for dynamic operations that require the column name in text form.</li>
</ul>
<p>When a parameter is declared as COLUMNREF, <a href="https://docs.sqlbi.com/dax-style/dax-naming-conventions#parameters">we recommend</a> using the suffix <em>Column</em> in the parameter name: for example, <em>priceColumn</em> or <em>dateColumn</em>.</p>
<p>We start with an educational example, <em>SumProduct</em>, which just multiplies two columns row-by-row and sums the result:</p>
<div class="dax-code-title">Function</div>
<pre class="brush: dax; title: ; snippet: Function; notranslate">
SumProduct = ( 
    quantityColumn: COLUMNREF, 
    priceColumn: COLUMNREF 
) =&gt;
    SUMX (
        TABLEOF ( quantityColumn ),
        quantityColumn * priceColumn
    )
</pre>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
Total Cost = 
SumProduct ( Sales&#x5B;Quantity], Sales&#x5B;Unit Cost] )
</pre>
<p>The result of the <em>Total Cost</em> measure computed this way is identical to <em>SUMX ( Sales, Sales[Quantity] * Sales[Unit Cost] )</em>. However, this first example is meant to be merely educational to show that by using TABLEOF, it is possible to obtain the table from a column reference parameter without an additional parameter for the table reference.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image4-106.png" width="338" /></p>
<p>However, this simple example already shows an important limitation: the formula inside the function assumes that the two columns belong to the same table. If this condition is not true, the error could be misleading. For example, the following measure generates a syntax error and is not valid:</p>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
Total Cost Mismatch = 
SumProduct ( Sales&#x5B;Quantity], &#039;Product&#039;&#x5B;Unit Cost] )
</pre>
<p>The syntax error is: <em>A single value for column ‘Unit Cost’ in table ‘Product’ cannot be determined.</em> This error is not very clear because it is generated by the SUMX function used in <em>SumProduct</em> when referencing columns from two different tables. Unfortunately, the current version of UDFs in preview comes with limitations in what we are going to describe now, but validating the parameters is something we want to introduce in this article. Ideally, we would like to customize the error message by validating that the two arguments belong to the same table. We achieve this by using the following code:</p>
<div class="dax-code-title">Function</div>
<pre class="brush: dax; title: ; snippet: Function; notranslate">
SumProductSafe = ( 
    quantityColumn: COLUMNREF, 
    priceColumn: COLUMNREF 
) =&gt;
    IF (
        NAMEOF ( TABLEOF ( quantityColumn ) ) == NAMEOF ( TABLEOF ( priceColumn ) ),
        SUMX (
            TABLEOF ( quantityColumn ),
            quantityColumn * priceColumn
        ),
        ERROR ( &quot;All the column references must belong to the same table&quot; )
    )
</pre>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
Total Cost Safe Mismatch = 
SumProduct ( Sales&#x5B;Quantity], &#039;Product&#039;&#x5B;Unit Cost] )
</pre>
<p>In this case, the error message should be: <em>All the column references must belong to the same table.</em> Unfortunately, the current implementation does not support this kind of validation before execution. We hope that Microsoft will support such validation before the user-defined functions are generally available, by using the syntax in this example, or equivalent.</p>
<p>The result of the <em>Total Cost</em> measure computed by <em>SumProduct</em> is identical to <em>SUMX ( Sales, Sales[Quantity] * Sales[Unit Cost] )</em>. However, this first example is meant to be merely educational.</p>
<p>For a more meaningful example, consider a scenario in which a <em>PriceRange</em>-disconnected table in the model defines price ranges (a more complete coverage of this scenario is available in the DAX Pattern, <a href="https://www.daxpatterns.com/static-segmentation/">Static segmentation</a>).</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image5-90.png" width="303" /></p>
<p>We can create a model-independent function that retrieves the segment corresponding to a specified value. In order to be model-independent, the function exposes all the model dependencies as parameters, which in this case are all column references:</p>
<div class="dax-code-title">Function</div>
<pre class="brush: dax; title: ; snippet: Function; notranslate">
RangeLookupUnchecked = (
    search        : SCALAR VAL,
    minColumn     : COLUMNREF,
    maxColumn     : COLUMNREF,
    targetColumn  : COLUMNREF
) =&gt;
    SELECTCOLUMNS (
        FILTER ( 
            TABLEOF ( minColumn ),
            minColumn &lt;= search &amp;&amp; maxColumn &gt; search 
        ),
        &quot;@Result&quot;, targetColumn
    )
</pre>
<p>The <em>RangeLookupUnchecked</em> function does not validate that the three columns belong to the same table. An error in the arguments provided to the function might be difficult to interpret. Therefore, we would like to create a safer version of the function that verifies that all the column references do belong to the same table, and returns a specific error otherwise:</p>
<div class="dax-code-title">Function</div>
<pre class="brush: dax; title: ; snippet: Function; notranslate">
RangeLookup = (
    search        : SCALAR VAL,
    minColumn     : COLUMNREF,
    maxColumn     : COLUMNREF,
    targetColumn  : COLUMNREF
) =&gt;
    IF (
        NAMEOF ( TABLEOF ( minColumn ) ) == NAMEOF ( TABLEOF ( maxColumn ) )
            &amp;&amp; NAMEOF ( TABLEOF ( minColumn ) ) == NAMEOF ( TABLEOF ( targetColumn ) ),
        SELECTCOLUMNS (
            FILTER ( 
                TABLEOF ( minColumn ),
                minColumn &lt;= search &amp;&amp; maxColumn &gt; search 
            ),
            &quot;@Result&quot;, targetColumn
        ),
        ERROR ( &quot;All the column references must belong to the same table&quot; )
    )
</pre>
<p>Currently, the syntax error from an invalid column reference occurs before the code that generates the customized error, but we hope to make this check possible in the future. We could also define a version of the function for the <a href="https://www.daxpatterns.com/dynamic-segmentation/">dynamic segmentation</a> pattern, which returns a table and can be used as a CALCULATE filter in a measure (with the same disclaimer for the validation code that might not be executed as we would like in the current preview of UDFs):</p>
<div class="dax-code-title">Function</div>
<pre class="brush: dax; title: ; snippet: Function; notranslate">
ValuesInSegment = (
    filterColumn  : COLUMNREF,
    minColumn     : COLUMNREF,
    maxColumn     : COLUMNREF,
    targetColumn  : COLUMNREF
) =&gt;
    GENERATE (
        TABLEOF ( targetColumn ),
        FILTER ( 
            VALUES ( filterColumn ),
            IF (
                NAMEOF ( TABLEOF ( minColumn ) ) == NAMEOF ( TABLEOF ( maxColumn ) )
                    &amp;&amp; NAMEOF ( TABLEOF ( minColumn ) ) == NAMEOF ( TABLEOF ( targetColumn ) 
),
                minColumn &lt;= filterColumn &amp;&amp; maxColumn &gt; filterColumn,
                ERROR ( &quot;minColumn, maxColumn, and targetColumn arguments must belong to the same table&quot; )
            )
        )
    )
</pre>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
Segmented Sales = 
CALCULATE ( 
    &#x5B;Sales Amount],
    ValuesInSegment (
        Sales&#x5B;Net Price],
        PriceRange&#x5B;Min Price], PriceRange&#x5B;Max Price], PriceRange&#x5B;Segment]
    )
)
</pre>
<p>The result of <em>Segmented Sales</em> filters <em>Sales Amount</em> only for the segment grouped in the visual, whereas the original <em>Sales Amount</em> measure ignores that filter because it comes from a disconnected table.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image6-82.png" width="299" /></p>
<h3>TABLEREF</h3>
<p>A TABLEREF parameter accepts only a reference to a table defined in the semantic model. The caller must provide the name of an existing table; table expressions such as FILTER or SELECTCOLUMNS are not accepted.</p>
<p>This type is useful when the function needs to operate on a model table and must guarantee that the provided argument is an actual table from the model, not a derived or filtered table expression. By us constraining the parameter to a table reference, the function can rely on the table having the full set of columns and relationships defined in the model.</p>
<p>When a parameter is declared as TABLEREF, <a href="https://docs.sqlbi.com/dax-style/dax-naming-conventions#parameters">we recommend</a> using the suffix <em>Table</em> in the parameter name: for example, <em>salesTable</em> or <em>customerTable</em>.</p>
<p>Using TABLEREF is probably uncommon because TABLE EXPR is more flexible and does not impose a restriction on the table that should be evaluated inside the function. However, we may want to ensure that the table is a model table so we can use functions like ISFILTERED and ISCROSSFILTERED using a valid table argument. For example, the <em>HasRelationships</em> function returns TRUE if the <em>sourceTable</em> filters <em>targetTable</em> in the current filter context, meaning that there are one or more relationships connecting the two tables and propagating the filter context:</p>
<div class="dax-code-title">Function</div>
<pre class="brush: dax; title: ; snippet: Function; notranslate">
HasRelationships = (
    targetTable : TABLEREF,
    sourceTable : TABLEREF
) =&gt; 
    CALCULATE (
        ISCROSSFILTERED ( targetTable ),
        sourceTable,
        REMOVEFILTERS ()
    )
</pre>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
Product filter Sales = 
HasRelationships ( 
    Sales,
    &#039;Product&#039;
)
</pre>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
PriceRange filter Sales = 
HasRelationships ( 
    Sales,
    PriceRange
)
</pre>
<p>The <em>Product filter Sales</em> and <em>PriceRange filter Sales</em> measures show how to use the <em>HasRelationships</em> function.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image7-70.png" width="423" /></p>
<p>The example is merely educational. We suggest using TABLEOF whenever possible to reduce the number of parameters, and considering TABLE EXPR instead of TABLEREF to give more flexibility to the developers using a function.</p>
<h3>CALENDARREF</h3>
<p>A CALENDARREF parameter accepts only a reference to a calendar defined in the semantic model. CALENDARREF is designed for calendar-based time intelligence functions.</p>
<p>When a parameter is declared as CALENDARREF, <a href="https://docs.sqlbi.com/dax-style/dax-naming-conventions#parameters">we recommend</a> using the suffix <em>Calendar</em> in the parameter name — for example, <em>dateCalendar</em>.</p>
<p>As an example, we create a <em>DatesPYTD</em> function that applies a previous year-to-date transformation by combining DATESYTD and SAMEPERIODLASTYEAR:</p>
<div class="dax-code-title">Function</div>
<pre class="brush: dax; title: ; snippet: Function; notranslate">
DatesPYTD = ( targetCalendar : CALENDARREF ) =&gt;
    CALCULATETABLE (
        DATESYTD ( targetCalendar ),
        SAMEPERIODLASTYEAR ( targetCalendar )
    )
</pre>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
YTD Sales = 
CALCULATE ( 
    &#x5B;Sales Amount],
    DATESYTD ( &#039;Gregorian&#039; )
)
</pre>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
PYTD Sales = 
CALCULATE (
    &#x5B;Sales Amount],
    DatesPYTD ( &#039;Gregorian&#039; )
)
</pre>
<p>The result of <em>PYTD Sales</em> is like <em>YTD Sales,</em> shifted by one year.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/image8-58.png" width="448" /></p>
<h2>Why use specific reference types instead of ANYREF</h2>
<p>The new reference types are specializations of ANYREF: they share the same passing mode (EXPR), but they restrict the accepted expressions. A natural question is, “why should we bother with the restriction when ANYREF already works?”. There are two primary reasons.</p>
<p>The first reason is validation. When we use a specific reference type, the engine and IntelliSense can enforce constraints at the point of the function call. If a developer mistakenly passes a column reference to a MEASUREREF parameter, the error is reported immediately with a clear message. If a developer passes a FILTER expression to a TABLEREF parameter, the engine rejects it before the function body executes. With ANYREF, these mistakes would produce confusing errors deep inside the function body or, worse, incorrect results without any error at all.</p>
<p>The second reason is documentation. A function signature is the first thing a developer reads when deciding whether and how to use a function. A parameter declared as MEASUREREF immediately communicates that the function expects a measure, that context transition will occur, and that arbitrary expressions are not accepted. A parameter declared as COLUMNREF communicates that the caller must provide a column from a model table. A parameter declared as ANYREF communicates none of these things; the developer must read the function body to understand what is expected, even though adopting a consistent <a href="https://docs.sqlbi.com/dax-style/dax-naming-conventions#parameters">naming convention for the parameters</a> helps clarify that.</p>
<p>These two reasons reinforce each other. Better documentation reduces the likelihood of mistakes, and stronger validation catches the mistakes that still occur. Together, they make functions easier to use, easier to maintain, and safer to share across models and libraries.</p>
<h2>Conclusions</h2>
<p>The parameter type system in DAX user-defined functions (UDFs) provides a spectrum from the most permissive type (ANYREF) to the most restrictive (MEASUREREF, COLUMNREF, TABLEREF, and CALENDARREF), which are specializations of ANYREF that restrict the accepted expressions to specific categories.</p>
<p>The rule is simple: use the most specific parameter type that satisfies your function’s requirements. If the function expects a measure, use MEASUREREF. If it expects a column, use COLUMNREF. If it expects a model table reference, use TABLEREF. If it expects a calendar, use CALENDARREF. Reserve ANYREF for those cases where the function genuinely needs to be able to accept any kind of expression. The more specific the type, the clearer the intent of the function, the stronger the validation, and the more helpful the development tools become for the developers who use your functions.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/articles/understanding-parameter-types-in-dax-user-defined-functions-udf/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		      <dc:creator><![CDATA[Alberto Ferrari]]></dc:creator>
	</item>
	</channel>
</rss>
