Table of Contents
- DAX Through a Data Engineer’s Lens
- DAX vs SQL — Thinking Differently
- Measures vs Calculated Columns — The Critical Distinction
- Row Context vs Filter Context — The Core Concept
- Essential Aggregation Functions
- CALCULATE — The Most Important DAX Function
- ALL — Removing Filters
- FILTER — Custom Filter Tables
- RELATED and RELATEDTABLE — Navigating Relationships
- Iterator Functions — Row-by-Row Calculations
- The Date Table — Foundation of Time Intelligence
- Time Intelligence Functions
- Variables — Clean and Efficient DAX
- Practical Patterns Data Engineers Use
- Common Mistakes
- Interview Questions
- Wrapping Up
In the previous post, we covered every chart type and interactive feature. The visuals are the output — but DAX is the engine that powers the numbers behind every chart. DAX (Data Analysis Expressions) is Power BI’s formula language for creating measures, calculated columns, and custom tables. This post covers DAX from a data engineer’s perspective: enough to build semantic models, understand what analysts write, debug performance issues, and answer interview questions confidently.
Analogy — DAX is a smart calculator, not a query language. SQL says “give me these rows from these tables where this condition is true.” DAX says “calculate this value in this context.” SQL processes rows and returns rows. DAX processes a filter context and returns a single value (for measures) or a column of values (for calculated columns). The biggest mental shift from SQL to DAX: you do not write WHERE clauses. Instead, the filter context (slicers, visuals, rows in a matrix) determines which data the formula sees. CALCULATE is the function that lets you override that context.
DAX Through a Data Engineer’s Lens
What data engineers need from DAX:
MUST understand:
- Measures vs calculated columns (when to use each)
- Filter context (why the same measure shows different numbers in different visuals)
- CALCULATE (modifying filter context)
- Basic aggregations (SUM, COUNT, AVERAGE, MIN, MAX)
- Time intelligence (YTD, prior year, month-over-month)
- RELATED (navigating star schema relationships)
GOOD to understand:
- Iterator functions (SUMX, AVERAGEX)
- ALL (removing filters for percentage-of-total)
- Variables (VAR/RETURN pattern)
- FILTER (custom filtering)
USUALLY left to BI developers:
- Complex DAX patterns (many-to-many, role-playing dimensions)
- Advanced time intelligence (fiscal calendars, custom periods)
- Calculation groups
- Query-scoped measuresDAX vs SQL — Thinking Differently
SQL approach (what you know):
SELECT
Region,
SUM(Amount) AS TotalRevenue,
COUNT(*) AS OrderCount,
SUM(Amount) / (SELECT SUM(Amount) FROM Orders) AS PctOfTotal
FROM Orders
WHERE OrderDate >= '2026-01-01'
GROUP BY Region
ORDER BY TotalRevenue DESC;
DAX approach (how Power BI thinks):
Total Revenue = SUM(Orders[Amount])
Order Count = COUNTROWS(Orders)
Pct of Total = DIVIDE(SUM(Orders[Amount]), CALCULATE(SUM(Orders[Amount]), ALL(Orders)))
The WHERE and GROUP BY are NOT in the formula.
They come from the VISUAL:
- A bar chart with Region on the axis = GROUP BY Region
- A date slicer set to 2026 = WHERE OrderDate >= '2026-01-01'
- The measure adapts to whatever context it is placed in
Key differences:
SQL: query returns ROWS
DAX: measure returns a SINGLE VALUE (calculated in context)
SQL: you specify the filter (WHERE clause)
DAX: the visual provides the filter (filter context)
SQL: you specify the grouping (GROUP BY)
DAX: the visual provides the grouping (rows/columns in matrix, axis in chart)Measures vs Calculated Columns — The Critical Distinction
This is the single most important DAX concept. Getting it wrong causes performance problems, wrong results, and confused analysts.
Measures:
- Calculated at QUERY TIME (when a user views a visual)
- Respond to filter context (change based on slicers, visual axes)
- NOT stored in the model (computed on the fly)
- Use for: aggregations, KPIs, ratios, time intelligence
- Example: Total Revenue = SUM(Orders[Amount])
In a bar chart by Region → shows revenue PER region
In a card with no filter → shows TOTAL revenue
Same formula, different results based on context
Calculated Columns:
- Calculated at REFRESH TIME (when data is imported)
- Evaluated row by row (each row gets a value)
- STORED in the model (consumes memory)
- Use for: categorization, sorting, filtering that cannot be a measure
- Example: Price Category = IF(Orders[Amount] > 100, "High", "Low")
Every row gets "High" or "Low" -- stored in the table
Does NOT change based on slicers (it is a fixed column)
The rule:
If it is an aggregation (SUM, COUNT, AVERAGE) → MEASURE
If it is a row-level classification or lookup → CALCULATED COLUMN
When in doubt → MEASURE (measures are more efficient)Common examples:
MEASURE (correct):
Total Revenue = SUM(Orders[Amount])
Order Count = COUNTROWS(Orders)
Avg Order Value = AVERAGE(Orders[Amount])
Revenue YTD = TOTALYTD(SUM(Orders[Amount]), 'Date'[Date])
CALCULATED COLUMN (correct):
Price Bucket = IF(Products[Price] > 100, "Premium", "Standard")
Full Name = Customers[FirstName] & " " & Customers[LastName]
Age Group = SWITCH(TRUE(),
Customers[Age] < 25, "18-24",
Customers[Age] < 35, "25-34",
Customers[Age] < 50, "35-49",
"50+"
)
WRONG (calculated column for aggregation):
Total Revenue = SUM(Orders[Amount]) ← as a calculated column
This calculates the grand total for EVERY row (same number repeated).
Wastes memory and gives the wrong result in visuals.Row Context vs Filter Context — The Core Concept
Analogy — A spreadsheet vs a pivot table. Row context is like a spreadsheet formula in cell C2: it knows it is in row 2 and can reference A2 and B2. Filter context is like a pivot table: when you place “Region” on rows and “Revenue” on values, the pivot table automatically calculates revenue for each region. The formula does not know about regions — the pivot table’s filter context handles it.
Row Context:
- Exists inside CALCULATED COLUMNS and ITERATOR functions (SUMX, FILTER)
- The formula "knows" which ROW it is currently evaluating
- Each row is evaluated independently
- Like a FOR loop iterating over rows
Example (calculated column):
Profit = Orders[Revenue] - Orders[Cost]
Row 1: Profit = 100 - 60 = 40
Row 2: Profit = 250 - 180 = 70
Each row uses ITS OWN values
Filter Context:
- Exists in MEASURES when placed in visuals
- Determined by: slicers, visual axes, page filters, report filters
- The formula does NOT "know" about rows -- it sees aggregated data
- Like a GROUP BY that the visual provides
Example (measure in a matrix):
Total Revenue = SUM(Orders[Amount])
Matrix rows: Region | Total Revenue
Ontario | $450,000 ← filter context: Region = Ontario
Quebec | $320,000 ← filter context: Region = Quebec
Alberta | $180,000 ← filter context: Region = Alberta
Grand Total| $950,000 ← filter context: no Region filter
The same measure formula produces different results because
the FILTER CONTEXT is different in each row of the matrix.Essential Aggregation Functions
Basic aggregations:
SUM(table[column]) -- Sum of all values in the column
AVERAGE(table[column]) -- Average of all values
MIN(table[column]) -- Minimum value
MAX(table[column]) -- Maximum value
COUNT(table[column]) -- Count of non-blank values
COUNTROWS(table) -- Count of rows in the table
DISTINCTCOUNT(table[column]) -- Count of unique values
DIVIDE(numerator, denominator, alternateResult) -- Safe division (no divide-by-zero error)
Examples:
Total Revenue = SUM(Orders[Amount])
Avg Order Value = AVERAGE(Orders[Amount])
Unique Customers = DISTINCTCOUNT(Orders[CustomerID])
Order Count = COUNTROWS(Orders)
Revenue per Customer = DIVIDE(SUM(Orders[Amount]), DISTINCTCOUNT(Orders[CustomerID]), 0)CALCULATE — The Most Important DAX Function
CALCULATE evaluates an expression in a modified filter context. It is the only function that lets you change what the measure sees. If DAX had only one function, it would be CALCULATE.
Analogy — CALCULATE is noise-canceling headphones. The filter context is the noise in the room (all the slicers, visual axes, page filters). CALCULATE lets you put on headphones that block certain noises (remove filters with ALL) or add new noises (add new filters). Without CALCULATE, you hear whatever the room provides. With CALCULATE, you control what you hear.
Syntax:
CALCULATE(expression, filter1, filter2, ...)
expression: the calculation to perform (SUM, COUNT, etc.)
filters: modify the filter context (add, remove, or replace filters)
Examples:
-- Revenue only for Ontario (regardless of slicer selection)
Ontario Revenue = CALCULATE(SUM(Orders[Amount]), Orders[Region] = "Ontario")
-- Revenue for all regions (ignore region slicer)
Total Revenue All Regions = CALCULATE(SUM(Orders[Amount]), ALL(Orders[Region]))
-- Revenue for high-value orders only
High Value Revenue = CALCULATE(SUM(Orders[Amount]), Orders[Amount] > 1000)
-- Revenue for Ontario AND high-value (multiple filters = AND logic)
Ontario High Value = CALCULATE(
SUM(Orders[Amount]),
Orders[Region] = "Ontario",
Orders[Amount] > 1000
)ALL — Removing Filters
ALL removes filters from a table or column. It is essential for calculating percentages of total and comparisons that should not be affected by slicers.
Three forms:
ALL(table) -- Remove ALL filters from the entire table
ALL(table[column]) -- Remove filter from ONE column only
ALL(table[col1], table[col2]) -- Remove filters from specific columns
Examples:
-- Percentage of total revenue (ignore all Order filters)
Pct of Total = DIVIDE(
SUM(Orders[Amount]),
CALCULATE(SUM(Orders[Amount]), ALL(Orders))
)
In a bar chart by Region:
Ontario: $450K / $950K = 47.4%
Quebec: $320K / $950K = 33.7%
Alberta: $180K / $950K = 18.9%
The numerator respects the Region filter (Ontario only).
The denominator uses ALL(Orders) to ignore the Region filter (grand total).
-- Percentage of category total (remove only Category filter)
Pct of Category = DIVIDE(
SUM(Orders[Amount]),
CALCULATE(SUM(Orders[Amount]), ALL(Products[Category]))
)
-- ALLEXCEPT: remove all filters EXCEPT specified columns
Revenue Keep Region = CALCULATE(SUM(Orders[Amount]), ALLEXCEPT(Orders, Orders[Region]))FILTER — Custom Filter Tables
FILTER returns a table with rows that meet a condition. Used inside CALCULATE for complex filtering.
Syntax:
FILTER(table, condition)
Examples:
-- Count of orders over $500 (simple filter in CALCULATE)
Big Orders = CALCULATE(COUNTROWS(Orders), Orders[Amount] > 500)
-- Same using FILTER (more flexible for complex conditions)
Big Orders = CALCULATE(
COUNTROWS(Orders),
FILTER(Orders, Orders[Amount] > 500 AND Orders[Status] = "Completed")
)
-- Revenue from top customers (customers with > 10 orders)
Top Customer Revenue = CALCULATE(
SUM(Orders[Amount]),
FILTER(
VALUES(Orders[CustomerID]),
CALCULATE(COUNTROWS(Orders)) > 10
)
)RELATED and RELATEDTABLE — Navigating Relationships
RELATED pulls a value from a related table (like a SQL JOIN). RELATEDTABLE returns the matching rows from the “many” side.
RELATED (many-to-one direction):
Use in a calculated column on the MANY side to get a value from the ONE side.
-- In the Orders table, get the customer's region
Customer Region = RELATED(Customers[Region])
-- In Orders, get the product category
Product Category = RELATED(Products[Category])
Equivalent SQL: SELECT o.*, c.Region FROM Orders o JOIN Customers c ON ...
RELATEDTABLE (one-to-many direction):
Use in a calculated column on the ONE side to count/aggregate from the MANY side.
-- In the Customers table, count their orders
Order Count = COUNTROWS(RELATEDTABLE(Orders))
-- In Products, sum revenue
Product Revenue = SUMX(RELATEDTABLE(Orders), Orders[Amount])Iterator Functions — Row-by-Row Calculations
Iterator functions (ending in X) loop through a table row by row, evaluate an expression for each row, and then aggregate the results.
SUMX: Sum of a row-level expression
-- Revenue = Quantity * Unit Price (calculated per row, then summed)
Total Revenue = SUMX(Orders, Orders[Quantity] * Orders[UnitPrice])
SQL equivalent: SELECT SUM(Quantity * UnitPrice) FROM Orders
AVERAGEX: Average of a row-level expression
Avg Revenue per Order = AVERAGEX(Orders, Orders[Quantity] * Orders[UnitPrice])
COUNTX: Count rows where an expression is not blank
Orders with Discount = COUNTX(Orders, IF(Orders[Discount] > 0, 1, BLANK()))
MAXX / MINX: Max/Min of an expression
Largest Order = MAXX(Orders, Orders[Quantity] * Orders[UnitPrice])
RANKX: Rank items
Customer Rank = RANKX(ALL(Customers), [Total Revenue],, DESC, Dense)
When to use iterators vs simple aggregations:
SUM(Orders[Amount]) → when the column already contains the value
SUMX(Orders, Qty * Price) → when you need to CALCULATE the value per row firstThe Date Table — Foundation of Time Intelligence
Time intelligence functions require a proper Date table. Without one, YTD, prior year, and month-over-month calculations silently return wrong results.
Date table requirements:
1. Continuous dates: every date from earliest to latest, no gaps
2. One row per day (not per transaction)
3. Marked as a Date table in Power BI (Modeling → Mark as Date Table)
4. Related to your fact tables via a date column
Date table columns (recommended):
Date | 2026-01-01, 2026-01-02, ...
Year | 2026
Quarter | Q1, Q2, Q3, Q4
Month | January, February, ...
MonthNumber | 1, 2, 3, ... 12 (for sorting)
Day | 1, 2, 3, ... 31
DayOfWeek | Monday, Tuesday, ...
WeekNumber | 1, 2, ... 52
FiscalYear | FY2026 (if fiscal year differs from calendar)
FiscalQuarter| FQ1, FQ2, ... (if needed)
IsWeekday | TRUE/FALSE
IsHoliday | TRUE/FALSE
Creating a Date table in DAX:
DateTable = CALENDAR(DATE(2020, 1, 1), DATE(2027, 12, 31))
Then add calculated columns:
Year = YEAR('DateTable'[Date])
Month = FORMAT('DateTable'[Date], "MMMM")
MonthNumber = MONTH('DateTable'[Date])
Quarter = "Q" & QUARTER('DateTable'[Date])Time Intelligence Functions
Time intelligence functions shift or accumulate values across time periods. They all require a proper Date table.
Year-to-Date (YTD):
Revenue YTD = TOTALYTD(SUM(Orders[Amount]), 'Date'[Date])
January: $100K (Jan only)
February: $230K (Jan + Feb)
March: $380K (Jan + Feb + Mar)
Accumulates from January 1 to the current date in each row
Month-to-Date (MTD):
Revenue MTD = TOTALMTD(SUM(Orders[Amount]), 'Date'[Date])
Quarter-to-Date (QTD):
Revenue QTD = TOTALQTD(SUM(Orders[Amount]), 'Date'[Date])
Same Period Last Year:
Revenue Last Year = CALCULATE(SUM(Orders[Amount]), SAMEPERIODLASTYEAR('Date'[Date]))
Compares current period to the same period one year ago
In a visual showing March 2026 → returns March 2025 revenue
Year-over-Year Growth:
YoY Growth =
VAR CurrentYear = SUM(Orders[Amount])
VAR LastYear = CALCULATE(SUM(Orders[Amount]), SAMEPERIODLASTYEAR('Date'[Date]))
RETURN DIVIDE(CurrentYear - LastYear, LastYear, 0)
Previous Month:
Revenue Prev Month = CALCULATE(SUM(Orders[Amount]), DATEADD('Date'[Date], -1, MONTH))
Month-over-Month Growth:
MoM Growth =
VAR CurrentMonth = SUM(Orders[Amount])
VAR PrevMonth = CALCULATE(SUM(Orders[Amount]), DATEADD('Date'[Date], -1, MONTH))
RETURN DIVIDE(CurrentMonth - PrevMonth, PrevMonth, 0)
Rolling 3 Months:
Rolling 3M Revenue = CALCULATE(
SUM(Orders[Amount]),
DATESINPERIOD('Date'[Date], MAX('Date'[Date]), -3, MONTH)
)Variables — Clean and Efficient DAX
Variables (VAR/RETURN) make DAX readable and efficient. A variable is calculated once and reused — improving performance when the same expression appears multiple times.
Without variables (hard to read, calculated twice):
YoY Growth = DIVIDE(
SUM(Orders[Amount]) - CALCULATE(SUM(Orders[Amount]), SAMEPERIODLASTYEAR('Date'[Date])),
CALCULATE(SUM(Orders[Amount]), SAMEPERIODLASTYEAR('Date'[Date])),
0
)
With variables (clear, calculated once):
YoY Growth =
VAR CurrentRevenue = SUM(Orders[Amount])
VAR LastYearRevenue = CALCULATE(SUM(Orders[Amount]), SAMEPERIODLASTYEAR('Date'[Date]))
VAR Growth = CurrentRevenue - LastYearRevenue
RETURN DIVIDE(Growth, LastYearRevenue, 0)
Rules:
- VAR is evaluated ONCE (even if referenced multiple times)
- RETURN is required (the final expression to return)
- Variables cannot be modified after assignment (immutable)
- Use descriptive names (CurrentRevenue, not x)Practical Patterns Data Engineers Use
Percentage of Total:
Pct of Total = DIVIDE(SUM(Orders[Amount]), CALCULATE(SUM(Orders[Amount]), ALL(Orders)))
Running Total:
Running Total = CALCULATE(
SUM(Orders[Amount]),
FILTER(ALL('Date'), 'Date'[Date] <= MAX('Date'[Date]))
)
New vs Returning Customers:
New Customers = CALCULATE(
DISTINCTCOUNT(Orders[CustomerID]),
FILTER(
VALUES(Orders[CustomerID]),
CALCULATE(MIN(Orders[OrderDate])) = MAX('Date'[Date])
)
)
Revenue Contribution Rank:
Revenue Rank = RANKX(ALL(Products), [Total Revenue],, DESC, Dense)
Moving Average (30-day):
MA 30 = AVERAGEX(
DATESINPERIOD('Date'[Date], MAX('Date'[Date]), -30, DAY),
[Total Revenue]
)
Conditional Count:
High Value Orders = CALCULATE(COUNTROWS(Orders), Orders[Amount] > 1000)
IF with Measures:
Performance Status =
VAR Revenue = [Total Revenue]
VAR Target = [Revenue Target]
RETURN IF(Revenue >= Target, "On Track", "Behind")Common Mistakes
Using SUM in a calculated column for aggregation. Writing
Total Revenue = SUM(Orders[Amount])as a calculated column puts the grand total in every row. Aggregations should always be measures, not calculated columns. Measures respond to filter context; calculated columns do not.Not building a proper Date table. Time intelligence functions silently return wrong results without a continuous Date table marked as a Date table in Power BI. If your Date column has gaps (weekends missing, holidays skipped), YTD and SAMEPERIODLASTYEAR calculations break without any error message.
Confusing filter context with row context. A measure placed in a matrix shows different values per row because of filter context, not because it iterates over rows. If you need row-level calculations inside a measure, use iterator functions (SUMX, FILTER) which create row context explicitly.
Writing CALCULATE without understanding context override.
CALCULATE(SUM(Orders[Amount]), Products[Category] = "Electronics")replaces the existing Category filter. If a slicer is set to “Furniture,” this measure still shows Electronics revenue — it overrides the slicer. Use KEEPFILTERS to add a filter without removing the existing one.Not using DIVIDE for safe division.
[Revenue] / [Order Count]crashes with a division-by-zero error when Order Count is 0. Always useDIVIDE([Revenue], [Order Count], 0)which returns the alternate result (0) when the denominator is zero or blank.Creating too many calculated columns. Each calculated column is stored in memory. A model with 50 calculated columns on a 10-million-row fact table consumes significant memory. Convert to measures wherever possible. Use calculated columns only when the value is needed for sorting, filtering, or slicing.
Not using variables in complex measures. A measure that references the same sub-expression three times calculates it three times. Using VAR calculates it once. Variables also make the formula readable and debuggable — you can hover over each VAR in DAX Studio to see its value.
Ignoring the RELATED function for star schema navigation. Data engineers build star schemas with fact-dimension relationships. In Power BI, use RELATED to pull dimension attributes into fact table contexts. Without RELATED, analysts write complex LOOKUPVALUE formulas that are slower and harder to maintain.
Interview Questions
Q: What is the difference between a measure and a calculated column in DAX? A: A measure is calculated at query time based on the current filter context — it dynamically responds to slicers, visual axes, and page filters. It is not stored in the model. A calculated column is calculated at refresh time, evaluated row by row, and stored in the model consuming memory. Use measures for aggregations (SUM, COUNT, AVERAGE) and dynamic calculations. Use calculated columns for row-level categorizations (price buckets, full names) and values needed for sorting or slicing. When in doubt, use a measure.
Q: What is filter context and how does it affect measures? A: Filter context is the set of filters active when a DAX measure is evaluated. It comes from slicers, visual axes (rows/columns in a matrix), page-level filters, and report-level filters. The same measure Total Revenue = SUM(Orders[Amount]) shows different values depending on the filter context: in a card with no filters it shows the grand total, in a matrix row for Ontario it shows Ontario revenue only. CALCULATE is the function that lets you modify this filter context — adding, removing, or replacing filters.
Q: What does CALCULATE do and why is it the most important DAX function? A: CALCULATE evaluates an expression in a modified filter context. It is the only function that lets you override the filters provided by the visual. Without CALCULATE, measures always respond to whatever filters are active. With CALCULATE, you can add filters (show only Ontario), remove filters (ALL to show grand total regardless of slicers), or replace filters. Time intelligence functions like SAMEPERIODLASTYEAR work because they use CALCULATE internally to shift the date filter to the prior year period.
Q: How do you calculate percentage of total in DAX? A: Use DIVIDE with ALL: Pct of Total = DIVIDE(SUM(Orders[Amount]), CALCULATE(SUM(Orders[Amount]), ALL(Orders))). The numerator respects the current filter context (e.g., Ontario revenue). The denominator uses ALL(Orders) to remove all filters from the Orders table, giving the grand total. The result is the percentage of the filtered value relative to the unfiltered total. Use ALL(table[column]) instead of ALL(table) to remove filters from a specific column only.
Q: What is a Date table and why is it required for time intelligence? A: A Date table is a dimension table with one row per calendar day, covering the full date range of your data with no gaps. It must be marked as a Date table in Power BI. Time intelligence functions (TOTALYTD, SAMEPERIODLASTYEAR, DATEADD) rely on this table to shift or accumulate values across time periods. Without a proper Date table, these functions silently return incorrect results. The Date table should include columns for Year, Quarter, Month, MonthNumber (for sorting), and optionally fiscal year/quarter.
Q: What is the difference between SUM and SUMX? A: SUM aggregates a single column directly: SUM(Orders[Amount]) adds up all values in the Amount column. SUMX is an iterator that evaluates a row-level expression for each row and then sums the results: SUMX(Orders, Orders[Quantity] * Orders[UnitPrice]) calculates Quantity times UnitPrice per row, then sums all results. Use SUM when the column already contains the value you want to aggregate. Use SUMX when you need to calculate the value per row before aggregating.
Q: How would you calculate year-over-year growth in DAX? A: Use SAMEPERIODLASTYEAR with variables for clarity: YoY Growth = VAR CurrentRevenue = SUM(Orders[Amount]) VAR LastYearRevenue = CALCULATE(SUM(Orders[Amount]), SAMEPERIODLASTYEAR('Date'[Date])) RETURN DIVIDE(CurrentRevenue - LastYearRevenue, LastYearRevenue, 0). This measure shows the current period revenue, calculates the same period last year using SAMEPERIODLASTYEAR (which shifts the date filter back one year), and divides the difference by last year’s value. The DIVIDE function handles cases where last year’s revenue is zero.
Wrapping Up
DAX is not SQL — it does not process rows with WHERE clauses and GROUP BYs. It calculates values in a filter context that the visual provides. Measures respond to context dynamically. Calculated columns are fixed at refresh time. CALCULATE overrides the context. ALL removes filters. Time intelligence functions shift dates. And a proper Date table makes it all work.
For data engineers, the key insight is this: the quality of your star schema directly determines how simple or complex the DAX needs to be. A clean star schema with well-designed dimension tables means simple DAX measures. A messy, denormalized table structure means analysts write complex, slow DAX to compensate. Build the schema right, and the DAX writes itself.
In the next post, we will cover Power BI data modeling — star schema design, relationships, cardinality, role-playing dimensions, and building models that data engineers are proud of.
Related posts: – Power BI Architecture – Power BI Visualizations – SQL Window Functions – Star Schema and Normalization – Fabric Power BI Direct Lake