<?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>Thu, 22 Oct 2026 07:31:03 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.3</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>Install DAX libraries into Power BI Desktop by asking your AI assistant</title>
		<link>https://www.sqlbi.com/blog/marco/2026/10/22/install-dax-libraries-into-power-bi-desktop-by-asking-your-ai-assistant/</link>
					<comments>https://www.sqlbi.com/blog/marco/2026/10/22/install-dax-libraries-into-power-bi-desktop-by-asking-your-ai-assistant/#respond</comments>
		
		<dc:creator><![CDATA[Marco Russo]]></dc:creator>
		<pubDate>Thu, 22 Oct 2026 07:30:36 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[DAX]]></category>
		<category><![CDATA[Power BI]]></category>
		<guid isPermaLink="false">https://www.sqlbi.com/?post_type=blogpost&#038;p=907384</guid>

					<description><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/daxlib-2.png" class="webfeedsFeaturedVisual" /></figure>At the end of 2025, we launched DAX Lib, a public repository of DAX user-defined functions. So far, importing a DAX library has required a few manual steps, like clicking Add to Power BI on daxlib.org and then copying and&#8230;]]></description>
										<content:encoded><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/daxlib-2.png" class="webfeedsFeaturedVisual" /></figure><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/daxlib-1.png" alt="" width="238" height="198" />At the end of 2025, we launched <a href="https://daxlib.org/">DAX Lib</a>, a public repository of DAX user-defined functions. So far, importing a DAX library has required a few manual steps, like clicking Add to Power BI on daxlib.org and then copying and pasting the DAX code into the TMDL View of your model, or using the dedicated tool in Tabular Editor 3. Both work well, but if you already use an AI assistant next to Power BI Desktop, we can now delegate this task to it. The DAX Lib MCP server version 0.1.0 is available now.</p>
<h2>MCP server</h2>
<p>The DAX Lib MCP server connects your assistant to the DAX Lib catalog and to the Power BI Desktop models on your computer. It should work with any MCP client that supports local (stdio) servers, including Claude, Codex, GitHub Copilot in VS Code, Grok Code, Cursor, and many others. If it does not work for you, please <a href="https://github.com/daxlib/daxlib-mcp/issues">let us know</a>.</p>
<h2>What you can do with it</h2>
<p>You can make requests like these to your AI assistant:</p>
<ul>
<li>search DAX Lib for packages ("Search DAX Lib for packages that generate SVG images");</li>
<li>look at the details, the versions, and the README of a package ("Show the README of DaxLib.SVG");</li>
<li>list the Power BI Desktop models open on your machine;</li>
<li>install the functions of a package into a local model ("Install DaxLib.SVG into the Sales Report model").</li>
</ul>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/daxlib-mcp-01.png" width="800" /><br />
The MCP server downloads the package and writes the functions into the model directly, so you get the original code (including format and comments) and not a version retyped by a language model.</p>
<p>The installation adds the user-defined functions of the package to the semantic model. If a function with the same name already exists, it is replaced, without changing anything else in the model. The MCP server does not save your file after you import a package; save it in Power BI Desktop to keep the changes.<br />
<img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/daxlib-mcp-02.png" width="800" /></p>
<h2>You stay in control</h2>
<p>You choose the package, the version, and the target model. The assistant follows your instructions and doesn’t choose a package for you. The install tool is marked as a tool that changes data, so most MCP clients ask for your approval before they run it. Read the request before you approve it.</p>
<p>About privacy: the MCP server has no telemetry and does not collect usage data. It only invokes the DAX Lib API to search the catalog and to download package details, READMEs, and packages. The server is open source under the MIT license, so you can verify all these statements in the code on GitHub: <a href="https://github.com/daxlib/daxlib-mcp">daxlib/daxlib-mcp</a>.</p>
<h2>Limits you should know</h2>
<p>The DAX Lib MCP server has a few limits:</p>
<ul>
<li>It runs on Windows x64 only. No macOS plans at this time; if you need it, open a GitHub issue.</li>
<li>It installs only into models open in Power BI Desktop on the same computer.</li>
<li>The model needs compatibility level 1702 or higher, required for user-defined functions. The DAX Lib MCP server doesn't change it for you, but the <a href="https://learn.microsoft.com/en-us/power-bi/developer/mcp/power-bi-authoring-mcp">Power BI Authoring MCP server</a> (if installed) can raise it.</li>
<li>It does not remove or update existing functions. To update a package, just install the new version.</li>
<li>If you do not specify a version of a package, you get the latest published one, which can be a prerelease.</li>
</ul>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/daxlib-mcp-03.png" width="800" /></p>
<h2>How to start</h2>
<p>You need Windows x64, Node.js 18 or later, and an MCP client. Follow the <a href="https://docs.daxlib.org/user-guide/mcp/install">installation instructions</a> and start looking for function libraries that can help your models and your reports.</p>
<p>We suggest using the Power BI Authoring MCP server to move from "is there a library for this?" to "write the measure" in the same conversation by using both MCP servers.</p>
<p>DAX Lib is a community project, and SQLBI supports it by developing and maintaining its infrastructure and tools. The MCP server is one more of those tools.</p>
<p>There are no more excuses to include user-defined functions in your semantic models: try DAX Lib with your assistant and <a href="https://github.com/daxlib/daxlib-mcp/issues">write your feedback</a> if you have suggestions or find bugs.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/blog/marco/2026/10/22/install-dax-libraries-into-power-bi-desktop-by-asking-your-ai-assistant/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Continuous axis for any Date table in Power BI</title>
		<link>https://www.sqlbi.com/tv/continuous-axis-for-any-date-table-in-power-bi/</link>
					<comments>https://www.sqlbi.com/tv/continuous-axis-for-any-date-table-in-power-bi/#respond</comments>
		
		<dc:creator><![CDATA[Marco Russo]]></dc:creator>
		<pubDate>Tue, 20 Oct 2026 10:00:00 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<guid isPermaLink="false">https://www.sqlbi.com/?post_type=video&#038;p=906780</guid>

					<description><![CDATA[<figure><img src="https://i.ytimg.com/vi/W_mQVl5KQoM/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure>How to get a continuous X-axis at the month level in Power BI with any Date table, like the one we get using the auto date/time feature.]]></description>
										<content:encoded><![CDATA[<figure><img src="https://i.ytimg.com/vi/W_mQVl5KQoM/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure><p>How to get a continuous X-axis at the month level in Power BI with any Date table, like the one we get using the auto date/time feature.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/tv/continuous-axis-for-any-date-table-in-power-bi/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Continuous axis for any Date table in Power BI</title>
		<link>https://www.sqlbi.com/articles/continuous-axis-for-any-date-table-in-power-bi/</link>
					<comments>https://www.sqlbi.com/articles/continuous-axis-for-any-date-table-in-power-bi/#respond</comments>
		
		<dc:creator><![CDATA[Marco Russo]]></dc:creator>
		<pubDate>Mon, 19 Oct 2026 20:00:42 +0000</pubDate>
				<category><![CDATA[Power BI]]></category>
		<category><![CDATA[semantic model]]></category>
		<category><![CDATA[Tabular]]></category>
		<guid isPermaLink="false">https://www.sqlbi.com/?p=906737</guid>

					<description><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/C0310-11.png" class="webfeedsFeaturedVisual" /></figure>Power BI enables the continuous X-axis on the auto date/time hierarchy even at the month level. We can get the same behavior on any Date table by setting the right metadata in TMDL. In a previous article, Creating a simpler&#8230;]]></description>
										<content:encoded><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/C0310-11.png" class="webfeedsFeaturedVisual" /></figure><p>Power BI enables the continuous X-axis on the auto date/time hierarchy even at the month level. We can get the same behavior on any Date table by setting the right metadata in TMDL.<br />
<span id="more-906737"></span></p>
<p>In a previous article, <a href="https://www.sqlbi.com/articles/creating-a-simpler-and-chart-friendly-date-table-in-power-bi/">Creating a simpler and chart-friendly Date table in Power BI</a>, we showed you how to create a <em>Date</em> table you can use in a line chart with a continuous X-axis instead of a categorical one, even when the granularity is the month rather than the day. That technique stores every level as a DateTime column and relies on custom format strings to display the month name.</p>
<p>The auto date/time feature in Power BI provides the same user experience using a different technique. Its hidden date table uses text columns for quarters and months, sorted by a numeric column. However, the line chart still offers a continuous X-axis when you drill down to the month level. We keep saying that auto date/time is not a best practice, but this behavior is useful. Can we obtain it with our own <em>Date</em> table? Yes, we can.</p>
<p>If you have a <em>Date</em> table in your model (as you should!), this article explains how to enable the continuous X-axis in your report when the user groups dates by year, quarter, month, or week.</p>
<h2>How auto date/time navigation works</h2>
<p>The table created by auto date/time exposes a single hierarchy with four levels: <em>Year</em>, <em>Quarter</em>, <em>Month</em>, and <em>Day</em>. You cannot customize these levels, and the user does not see any other column in that table. Internally, <em>Quarter</em> and <em>Month</em> are text columns sorted by a hidden numeric column.</p>
<p>If we place the <em>Date</em> hierarchy from the auto date/time column on the X-axis of a line chart and drill down to the <em>Month</em> level, the X-axis type is still Continuous, whereas when we use the <em>Year Month</em> column of the classic <em>Date</em> table, the X-axis is Categorical.<br />
<img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0310-01.png" width="800" /><br />
The Type property of the line chart is usually available only when you can switch between Ordinal and Categorical, as in the auto date/time example. However, it is hidden when Power BI forces the X-axis to Categorical, as in the previous line chart for the classic <em>Date</em> table.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0310-02.png" width="200" /><br />
At first glance, this looks like an exception that Power BI makes for its own tables. But in reality, the line chart offers the continuous axis because the hidden table carries specific metadata, and that metadata tells the visual how to treat each level of the hierarchy. If we apply the same metadata to our <em>Date</em> table, we get the same behavior.</p>
<p>This is the TMDL definition of the hidden table generated by auto date/time, stripped of annotations, formats, lineage tags, and summarize attributes:</p>
<pre class="brush: plain; highlight: [7]; title: ; notranslate">
table LocalDateTable_eee2517b-a62f-4b3d-a512-4aa237fa2329
    isHidden
    showAsVariationsOnly
    column Date
        dataType: dateTime
        isHidden
        dataCategory: PaddedDateTableDates
        sourceColumn: &#x5B;Date]

    column Year = YEAR(&#x5B;Date])
        dataType: int64
        isHidden
        dataCategory: Years

    column MonthNo = MONTH(&#x5B;Date])
        dataType: int64
        isHidden
        dataCategory: MonthOfYear

    column Month = FORMAT(&#x5B;Date], &quot;MMMM&quot;)
        dataType: string
        isHidden
        dataCategory: Months
        sortByColumn: MonthNo

    column QuarterNo = INT((&#x5B;MonthNo] + 2) / 3)
        dataType: int64
        isHidden
        dataCategory: QuarterOfYear

    column Quarter = &quot;Qtr &quot; &amp;amp; &#x5B;QuarterNo]
        dataType: string
        isHidden
        dataCategory: Quarters
        sortByColumn: QuarterNo

    column Day = DAY(&#x5B;Date])
        dataType: int64
        isHidden
        dataCategory: DayOfMonth
        summarizeBy: none

    hierarchy &#039;Date Hierarchy&#039;
        level Year
            column: Year
        level Quarter
            column: Quarter
        level Month
            column: Month
        level Day
            column: Day
</pre>
<p>Note the dataCategory property on the column: only those on <em>Date</em> and <em>Year</em> matter. Indeed, the dataCategory for <em>Quarter</em> is Quarters when it should be QuarterOfYear, but these intermediate levels between <em>Year</em> and <em>Date</em> are ignored by Power BI.</p>
<h2>The metadata needed</h2>
<p>The line chart provides the continuous axis on a hierarchy when all these conditions are met:</p>
<ol>
<li>The columns come from a <strong>user hierarchy</strong>. Placing the individual columns from a table on the X-axis is not enough; you must use hierarchy levels.</li>
<li>The <strong>lowest level </strong>of the hierarchy is a date column, with dataCategory set to PaddedDateTableDates.</li>
<li>The <strong>top level </strong>of the hierarchy has dataCategory set to Years.</li>
<li>The visual includes only<strong> levels </strong>of the hierarchy starting from the top level marked as Years.</li>
</ol>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0310-04.svg" width="800" /><br />
That is it. You do not need anything else, but a few practical examples can help!</p>
<p>The date-related values accepted by dataCategory on a column are: Years, Quarters, QuarterOfYear, Months, MonthOfYear, Weeks, WeekOfYear, WeekOfMonth, Days, DayOfYear, DayOfMonth, DayOfWeek, Hours, Minutes, Seconds, Date, PaddedDateTableDates. Power BI Desktop does not show any of them in the Data category dropdown, which only lists geography, URL, image, and barcode categories. You need the TMDL view in Power BI Desktop, Tabular Editor, or the Power BI Modeling MCP server described later.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0310-05.png" width="300" /></p>
<h2>Configuring the hierarchies</h2>
<p>In the <em>Date</em> table we created in our sample model, we already have columns like <em>Year</em>, <em>Year Month</em> sorted by <em>Year Month Number</em>, and so on. This is a partial definition to highlight the properties to set in the TMDL view of Power BI Desktop, or in Tabular Editor.</p>
<pre class="brush: plain; highlight: [8,13,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39]; title: ; notranslate">
table Date
    dataCategory: Time

    column Date
        dataType: dateTime
        isKey
        formatString: mm/dd/yyyy
        dataCategory: PaddedDateTableDates
        sourceColumn: Date

    column Year
        dataType: int64
        dataCategory: Years
        sourceColumn: Year

    column &#039;Year Quarter&#039;
        dataType: string
        isHidden
        sourceColumn: Year Quarter
        sortByColumn: &#039;Year Quarter Number&#039;
&#x5B;...]

    hierarchy Year-Quarter-Month
        level Year
            column: Year
        level Quarter
            column: &#039;Year Quarter&#039;
        level Month
            column: &#039;Year Month Short&#039;
        level Date
            column: Date

    hierarchy Year-Month
        level Year
            column: Year
        level &#039;Year Month&#039;
            column: &#039;Year Month&#039;
        level Date
            column: Date
</pre>
<p>Three things to highlight:</p>
<ul>
<li>The table is marked as a date table. This sets dataCategory: Time on the table and isKey on the <em>Date</em> Only the latter (isKey) is required to activate the Continuous axis; the dataCategory property of the table is ignored.</li>
<li>The <em>Date[Date]</em> column has dataCategory: PaddedDateTableDates<strong>.</strong> This is an undocumented but required property to activate the Continuous axis.</li>
<li>The hierarchies start at <em>Year</em> and end at <em>Date</em>. The intermediate levels can be any column with a date-related category.</li>
</ul>
<p>In the following screenshot, you can see the continuous chart used at the quarter and month level of two different hierarchies.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0310-06.png" width="800" /></p>
<h2>Considerations about visualizations</h2>
<p>The line chart tooltip shows the <em>Year</em> and <em>Month</em> level of the hierarchy, even when the month-level column is <em>Year Month</em>, which already includes the year. For example, in the <em>Year-Month</em> hierarchy, the tooltip displays 2024 twice: once at the year level and again in the month description.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0310-07.png" width="300" /></p>
<p>This is more visible when we use the <em>Year-Quarter-Month</em> hierarchy: the drill-down to the <em>Month</em> level shows the name in three levels in the tooltip: <em>Year</em>, <em>Quarter</em>, and <em>Month</em>.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0310-08.png" width="300" /></p>
<p>This is mainly a cosmetic issue, but it is worth noting. To improve the tooltip user experience, you can use the <em>Quarter</em> and <em>Month</em> columns in the <em>Date</em> table to create a hierarchy that shows non-redundant names in the tooltip. For example, this is the tooltip of the <em>Chart-Friendly</em> hierarchy included in the sample file.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0310-09.png" width="300" /></p>
<p>The metadata does not change the behavior of visuals that do not have a continuous axis type, such as the clustered column chart. The hierarchy works as usual there and in any other visual, including slicers and filters.</p>
<h2>Automating the change with the Power BI Modeling MCP server</h2>
<p>If you use an LLM with the Power BI Authoring MCP server connected to Power BI Desktop, you can describe the configuration instead of writing the TMDL by hand. The server exposes operations on columns and user hierarchies, including the dataCategory property. You can find an updated description of the AI clients available for this task here: <a href="https://docs.sqlbi.com/ai/tools/">AI tools for Power BI Desktop</a>.</p>
<p>Here is a prompt that produces the configuration of the first example:</p>
<blockquote><p>Connect to the semantic model open in Power BI Desktop.<br />
In the table 'Date', set the dataCategory property of these columns without changing any other property:<br />
- Date: PaddedDateTableDates<br />
- Year: Years<br />
Make sure the Date column is key.<br />
Create a user hierarchy named 'Calendar' in 'Date' with these levels in this order:<br />
Year (column Year), Quarter (column 'Quarter'), Month (column 'Month'), Date (column Date).<br />
Do not create, delete, or rename any other object. When done, export the TMDL of the 'Date' table so I can review the result.</p></blockquote>
<p>The LLM completes the action requested smoothly.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0310-10.png" width="800" /></p>
<p>In Power BI Desktop, we have the new <em>Calendar</em> hierarchy that displays the expected tooltip by selecting a point on the <em>Month</em> level on the Continuous X-axis.</p>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0310-11.png" width="800" /></p>
<h2>Conclusions</h2>
<p>The continuous X-axis for a date hierarchy on a line chart is not a privilege of the auto date/time table. It depends on a few dataCategory values (the most important one being PaddedDateTableDates on the date column) and a user hierarchy that starts with the year and ends with the date. With these properties set through TMDL, Tabular Editor, or an MCP-driven LLM, your <em>Date</em> table offers the same navigation without the downsides of auto date/time.</p>
<p>We covered another approach in a previous article that does not require metadata which is not visible in the Power BI Desktop UI. However, in some cases you may want more control over the name of each level, or other clients you use may not fully support date columns with a custom format for hierarchy levels. The solution in this article may feel more “natural” to users with past experience navigating auto date/time tables.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/articles/continuous-axis-for-any-date-table-in-power-bi/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		      <dc:creator><![CDATA[Alberto Ferrari]]></dc:creator>
	</item>
		<item>
		<title>Understanding DAX contracts when using AI</title>
		<link>https://www.sqlbi.com/tv/understanding-dax-contracts-when-using-ai/</link>
					<comments>https://www.sqlbi.com/tv/understanding-dax-contracts-when-using-ai/#respond</comments>
		
		<dc:creator><![CDATA[Alberto Ferrari]]></dc:creator>
		<pubDate>Tue, 06 Oct 2026 10:00:00 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<guid isPermaLink="false">https://www.sqlbi.com/?post_type=video&#038;p=904162</guid>

					<description><![CDATA[<figure><img src="https://i.ytimg.com/vi/QzqSthdmXDw/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure>Defining the right contract is a key activity for a DAX developer using AI. Here we introduce the concept of a contract through simple examples.]]></description>
										<content:encoded><![CDATA[<figure><img src="https://i.ytimg.com/vi/QzqSthdmXDw/maxresdefault.jpg" class="webfeedsFeaturedVisual" /></figure><p>Defining the right contract is a key activity for a DAX developer using AI. Here we introduce the concept of a contract through simple examples. </p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/tv/understanding-dax-contracts-when-using-ai/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Understanding DAX contracts when using AI</title>
		<link>https://www.sqlbi.com/articles/understanding-dax-contracts-when-using-ai/</link>
					<comments>https://www.sqlbi.com/articles/understanding-dax-contracts-when-using-ai/#respond</comments>
		
		<dc:creator><![CDATA[Marco Russo]]></dc:creator>
		<pubDate>Mon, 05 Oct 2026 20:00:41 +0000</pubDate>
				<category><![CDATA[DAX]]></category>
		<category><![CDATA[AI]]></category>
		<guid isPermaLink="false">https://www.sqlbi.com/?p=904082</guid>

					<description><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/C0309-01-contract-before-dax.png" class="webfeedsFeaturedVisual" /></figure>Defining the right contract is a key activity for a DAX developer using AI. In this article, we introduce the concept of a contract through simple examples. Contract. As a DAX developer, you will use this term frequently; indeed, when&#8230;]]></description>
										<content:encoded><![CDATA[<figure><img src="https://www.sqlbi.com/wp-content/uploads/C0309-01-contract-before-dax.png" class="webfeedsFeaturedVisual" /></figure><p>Defining the right contract is a key activity for a DAX developer using AI. In this article, we introduce the concept of a contract through simple examples.<br />
<span id="more-904082"></span></p>
<p><strong><em>Contract</em></strong>. As a DAX developer, you will use this term frequently; indeed, when developing code with an AI assistant, defining the contract is the most important task you need to complete to obtain a useful semantic model.</p>
<p>First things first: what is a contract? In simple terms, it is a text that identifies the rules at work in a specific semantic model. It is not just an algorithm definition: a contract can define multiple measures and multiple algorithms at the same time. However, it is not even a vague description of the model in terms of entities and relationships. It sits somewhere between the two: documentation that defines how to perform calculations in a specific model.</p>
<p>To describe what a contract is, let me indulge for a bit in the evolution of what happened in the world of Business Intelligence over the years. We promise it will be just a few paragraphs, but we feel it is important to properly introduce the contract and put it in the correct perspective.</p>
<h2>How business logic is hidden in code</h2>
<p><img decoding="async" style="float: right;margin: 0 0 0 0;border: none !important" src="https://www.sqlbi.com/wp-content/uploads/C0309-icon-01-users-and-developers.svg" alt="" width="150" height="150" />Once upon a time, there were users and developers. Users wanted reports, but they did not know how to build the models and calculations. Developers could build models and calculations, but they did not know the business details or the specific requirements for calculating over user data. Developers needed precise instructions to write the calculations, and this led to an insane number of endless meetings between users and developers, with the (mostly vain) goal of clearly defining how to compute measures. Developers kept bringing up edge cases where the calculation defined so far would fail, and users got bored by all the details developers were asking for. Eventually it worked, but the friction was real.</p>
<p>Sometimes, young developers (including ourselves!) believed that writing proper documentation upfront would help solve the problem. Therefore, developers wrote insanely complex documentation, and users read it, mostly five minutes before the meeting, because it was just complex and considered unnecessary. Eventually, models were in place, and the real documentation on how the model computed values ended up hidden in DAX (or MDX, at the time) and SQL code. Any update, as you may well imagine, turned into a nightmare.</p>
<p><img decoding="async" style="float: left;margin: 0 0 0 0;border: none !important" src="https://www.sqlbi.com/wp-content/uploads/C0309-icon-02-logic-hidden-in-code.svg" alt="" width="150" height="150" />With the advent of self-service BI, many users became developers. They no longer needed any documentation because they know the business; instead, they now write Power Query transformations, calculated columns, and complex DAX code to make the semantic model work. Again, the business logic remains hidden in many measures and transformations that often contain serious levels of complexity. The model works for a while but, after a couple of months, nobody knows exactly how it computes the numbers.</p>
<p>The outcome is the same: the business logic remains hidden; the only difference is that the number of models grew immensely.</p>
<p>This scenario is not unique to Business Intelligence: most IT businesses work the same way. Documentation is boring to write, boring to read, mostly non-existent, and, when it exists, it is never up to date. We are pretty sure you know a few Excel files that nobody can understand or modify, but that are crucial to your (or your customer's) business.</p>
<h2>AI changes the role of documentation</h2>
<p>Now that AI assistants are a thing, the scenario is likely to change again, and not for the better. Users will remain users; they no longer need to become expert developers. The AI agent covers the role of developers by writing DAX code, Power Query, or SQL transformations based on the users’ prompts. The number of reports will grow again, and knowledge of how they work will be hidden even further.</p>
<p><img loading="lazy" decoding="async" style="float: right;margin: 0 0 0 0;border: none !important" src="https://www.sqlbi.com/wp-content/uploads/C0309-icon-03-ai-and-contract.svg" alt="" width="150" height="150" />However, there is some good news. Developers hate writing documentation. Users hate reading documentation, not to mention writing it. But… You know who loves both writing and reading documentation? Your AI agent. An agent is not bored by documentation details, and it is also great at taking partial information and correcting the documentation on the fly, keeping information about how a model computes values up to date. An agent is also very good at finding small inconsistencies in your specifications and providing clear examples of when and how they manifest. Finally, agents are extremely good at guiding users through writing concise documentation and verifying that all the code in your semantic model follows the rules defined in the documentation.</p>
<p>The documentation the agent creates is the contract. A contract does not contain anything obvious (like “The customer table contains customers”); it speaks the language of the business, and it defines clear rules to compute values over your data. Those rules need not be defined up-front through meetings. They can be derived by trial and error, and they can be adjusted at any time: every time you discover a behavior that you did not consider in the beginning, you change the measure by changing the contract and asking AI to review the DAX code.</p>
<p>When you write DAX with an AI assistant, the measure is not the most important part of the work. The contract is.<br />
<img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0309-01-contract-before-dax.svg" width="800" /></p>
<h2>Defining a customer ranking contract</h2>
<p>Let me show you a very practical example of how to change your mindset when working with AI. Imagine you want to create a measure to compute the ranking of a customer. The most obvious prompt is something like: “Create a measure to compute the ranking of a customer”, right? Wrong. This is not the right way to approach creating a measure.</p>
<p>Before anything else, you start with the contract, and you use AI to define it through a simple dialogue:</p>
<blockquote><p>I want to compute a customer ranking based on Sales Amount. What questions do we need to answer before we can proceed?</p></blockquote>
<p>The agent answers by asking the most relevant questions you need to answer before even approaching the measure. For example, ChatGPT Astra produces these questions:</p>
<table>
<tbody>
<tr>
<td width="582">Before writing the DAX measure, we need to define what “customer rank” means:</p>
<ol>
<li><strong>Who competes in the ranking?</strong> All customers, only customers selected by slicers, or customers within the same country, region, or segment?</li>
<li><strong>Which filters affect Sales Amount?</strong> Should the ranking reflect the selected dates, products, and other report filters? Should any filters be ignored?</li>
<li><strong>Which direction?</strong> Should the customer with the highest Sales Amount rank <strong>1</strong>?</li>
<li><strong>How should ties work?</strong> For equal sales, use <strong>1, 2, 2, 3</strong> (dense) or <strong>1, 2, 2, 4</strong> (skip)? Or break ties using a unique customer key?</li>
<li><strong>Which customers qualify?</strong> Include or exclude customers with blank sales, zero sales, or negative sales?</li>
<li><strong>What identifies a customer?</strong> Rank individual customer keys, even when names repeat, or group customers by displayed name?</li>
<li><strong>Where will the measure appear?</strong> A customer table, a matrix with subtotals, a card for one selected customer, or a visual filtered to the top customers? What should totals display?</li>
</ol>
<p>These answers define the measure’s contract: the results it must produce as filters and visual context change.</td>
</tr>
</tbody>
</table>
<p>Answering all these questions defines the contract. That is why your next prompt should define the contract, not the measure. We are not going to elaborate on the type of ranking that we want to create. The definition is in the contract, despite our personal way of ranking being quite exotic:</p>
<blockquote><p>Save this contract for the ranking of customers:<br />
- A customer competes with all the customers that purchased at the time of the analysis. For example, if the report shows March 2026 and the first sale is on January 1st 2026, the customer cohort includes all customers who purchased between Jan 1<sup>st</sup> and March 31<sup>st</sup>.<br />
- Customers with no sales are automatically excluded from the ranking because of the previous rule<br />
- All filters need to be considered for the ranking, except for any filter on customers.<br />
- The customer with the highest sales is ranked 1. Ties need to be removed by adding the customer key to the ranking.<br />
- Customers are identified by Customer[CustomerKey]. However, on the matrix, users will use the customer name. In case of ambiguity (multiple customers visible), show a blank.</p></blockquote>
<p>You may think the contract is complete. It is not; unanswered questions remain because we asked the agent to consider the competitor cohort as the list of all customers who purchased anything up to now. This leads to further questions, and the agent highlights them:</p>
<table>
<tbody>
<tr>
<td width="582">Saved in [customer-ranking-contract.md].</p>
<p>Also recorded three unresolved details: selected-period versus cumulative sales for ordering, customer-key tie-break direction, and treatment of historical buyers with no sales in the selected period.</td>
</tr>
</tbody>
</table>
<p>Therefore, the next prompt solves the three open issues:</p>
<blockquote><p>Modify the contract for the ranking of customers, and add these rules:<br />
- use the cumulative sales to produce the ranking<br />
- customer key is added in ascending order to the rank order<br />
- historical buyers with no sales in the selected period participate in the ranking</p></blockquote>
<p>&nbsp;</p>
<p>Please note: so far, we asked it to save the contract, not just use it for the measure. The agent will save its own version of the contract, whose wording is likely to differ from mine and be organized differently. It is always a good idea to show the contract once it has been digested by the agent:</p>
<blockquote><p>Show me the contract used to rank customers</p></blockquote>
<p>In our experience, AI-written contracts are clearer and less ambiguous than what we provided, and you can see that in the answer.</p>
<table>
<tbody>
<tr>
<td width="582"><strong>Customer ranking contract</strong></p>
<p><strong>Customer cohort</strong></p>
<ul>
<li>A customer competes with all customers who have purchased from the first-ever sale through the end of the period being analyzed.</li>
<li>Example: if the first-ever sale is January 1, 2026, and the report shows March 2026, the cohort includes customers who purchased between January 1 and March 31, 2026.</li>
<li>Customers with no sales in this historical window are excluded by this cohort rule.</li>
<li>Historical buyers with no sales in the selected period participate in the ranking.</li>
</ul>
<p><strong>Filter behavior</strong></p>
<ul>
<li>Consider all filters for the ranking except filters on customers.</li>
<li>Apply the historical window described above when determining cohort membership and computing cumulative Sales Amount.</li>
</ul>
<p><strong>Ranking and ties</strong></p>
<ul>
<li>Rank customers by cumulative Sales Amount from the first-ever sale through the end of the period being analyzed, with the highest amount ranked 1.</li>
<li>Eliminate ties by using Customer[CustomerKey] ascending as a secondary ranking criterion. When cumulative Sales Amount is equal, the smaller customer key ranks first.</li>
</ul>
<p><strong>Customer identity and display</strong></p>
<ul>
<li>Identify customers by Customer[CustomerKey].</li>
<li>Users will display customer names in the matrix.</li>
<li>Return blank when the current context contains multiple customers, including when a displayed name corresponds to multiple customer keys.</li>
</ul>
</td>
</tr>
</tbody>
</table>
<p>Now that the contract is well defined, we have obtained several benefits:</p>
<ul>
<li>We have clear, concise documentation of the measure’s behavior. This behavior is easy to understand without even looking at the DAX code, and it can be shared with both AI and humans to discuss the algorithm;</li>
<li>AI has a precise set of instructions to write the DAX code for the measure;</li>
<li>If we must modify this measure in the future, we do not have to review the code; we can speed up development by reading and modifying the contract, then ask AI to modify the measure to adhere to the new contract.</li>
</ul>
<p><img decoding="async" src="https://www.sqlbi.com/wp-content/uploads/C0309-02-evolve-the-contract.svg" width="800" /></p>
<p>The last prompt is what we thought should have been the first:</p>
<blockquote><p>Create a measure to compute the ranking of a customer</p></blockquote>
<p>To this last prompt, we obtain the desired measure, which is not trivial:</p>
<div class="dax-code-title">Measure in Sales table</div>
<pre class="brush: dax; title: ; snippet: Measure; table: Sales; notranslate">
Customer Rank = 
VAR HasOneCustomer = HASONEVALUE ( Customer&#x5B;CustomerKey] )
VAR CurrentCustomerKey = SELECTEDVALUE ( Customer&#x5B;CustomerKey] )
VAR LastVisibleDate = MAX ( &#039;Date&#039;&#x5B;Date] )
VAR CustomersWithSales =
    CALCULATETABLE (
        ADDCOLUMNS (
            SUMMARIZE ( Sales, Customer&#x5B;CustomerKey] ),
            &quot;@CumulativeSales&quot;, &#x5B;Sales Amount]
        ),
        REMOVEFILTERS ( Customer ),
        FILTER (
            ALL ( &#039;Date&#039; ),
            &#039;Date&#039;&#x5B;Date] &amp;lt;= LastVisibleDate
        )
    )
VAR CurrentCustomer =
    FILTER (
        CustomersWithSales,
        Customer&#x5B;CustomerKey] = CurrentCustomerKey
    )
VAR CurrentSales = MAXX ( CurrentCustomer, &#x5B;@CumulativeSales] )
VAR Result =
    IF (
        HasOneCustomer &amp;amp;&amp;amp; COUNTROWS ( CurrentCustomer ) = 1,
        1
            + COUNTROWS (
                FILTER (
                    CustomersWithSales,
                    &#x5B;@CumulativeSales] &amp;gt; CurrentSales
                        || (
                            &#x5B;@CumulativeSales] = CurrentSales
                                &amp;amp;&amp;amp; Customer&#x5B;CustomerKey] &amp;lt; CurrentCustomerKey
                        )
                )
            )
    )
RETURN
    Result
</pre>
<p>The measure is far from perfect; it can probably be written more efficiently using RANK, and, most importantly, it should be carefully validated before going into production. However, these tasks are outside the scope of this article. Here we only wanted to show the process of defining the contract.</p>
<p>You can see the measure at work in the following matrix: Karen Westbrook, for example, ranked first in 2023 but moved to third in 2024 because new customers appeared who purchased more than she did.</p>
<p><img decoding="async" style="border: none" src="https://www.sqlbi.com/wp-content/uploads/C0309-screenshot.png" width="360" /></p>
<p>You can easily see how much easier it is to read the contract than the measure.</p>
<h2>Conclusions</h2>
<p>When developing semantic models, contracts are key to creating calculations successfully. Although it is tempting to do so, you should refrain from creating any measure, no matter how simple, without first creating a contract.</p>
<p>Contracts can be very simple or very complex, but they offer a compact way to store and share details about your semantic model, how values are computed, and any peculiarities in your calculations.</p>
<p>Start getting used to it. Contracts are the new first-class citizens in the BI world; they bring many benefits and, to our knowledge so far, no drawbacks.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.sqlbi.com/articles/understanding-dax-contracts-when-using-ai/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		      <dc:creator><![CDATA[Alberto Ferrari]]></dc:creator>
	</item>
		<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 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-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 loading="lazy" 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 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-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>
	</channel>
</rss>
