Power BI Lesson 34 – Common DAX Functions | Dataplexa
DAX · Lesson 34

Common DAX Functions

A working Power BI model uses a surprisingly small set of DAX functions repeatedly — perhaps thirty functions cover ninety percent of real-world report requirements. This lesson is a complete practical reference for those functions: every aggregation, the essential text and date functions, the logical patterns used daily, and the information functions that make defensive measures possible. Every function is shown with a concrete example and the gotcha that catches beginners.

The Working Dataset

Orders table — used for all examples in this lesson
OrderID Revenue Quantity Cost Status Region OrderDate
10011,2004800DeliveredNorth2024-01-05
10028500600ProcessingSouth2024-01-18
10034302290LateNorth2024-02-03
100495160LateSouth2024-02-14
10051,5406980ShippedWest2024-02-20

Aggregation Functions

Function What it does Example → Result (no filter)
SUM(column) Adds all values in a numeric column. Ignores BLANK rows. SUM(Orders[Revenue]) → 4,115
AVERAGE(column) Mean of all non-blank values. Denominator = count of non-blank rows. AVERAGE(Orders[Revenue]) → 823
MIN(column) Smallest value. Works on numbers, text (alphabetical), and dates. MIN(Orders[Revenue]) → 95
MAX(column) Largest value. Works on numbers, text, and dates. MAX(Orders[Revenue]) → 1,540
COUNT(column) Counts non-blank numeric values only. Skips text and blank. COUNT(Orders[Revenue]) → 5
COUNTA(column) Counts non-blank values of any type — text, number, date. COUNTA(Orders[Status]) → 5
COUNTROWS(table) Counts all rows including those with blank values. Takes a table, not a column. COUNTROWS(Orders) → 5
COUNTBLANK(column) Counts blank and empty-string cells. Useful for data quality checks. COUNTBLANK(Orders[Region]) → 0
DISTINCTCOUNT(column) Counts unique non-blank values. The go-to for customer count, SKU count, etc. DISTINCTCOUNT(Orders[Region]) → 3
DIVIDE(num, den, alt) Safe division. Returns alt (default BLANK) when denominator is 0 or BLANK. Always use instead of /. DIVIDE(SUM([Rev]),COUNTROWS(Orders)) → 823
// Five measures every model starts with
Total Revenue      = SUM(Orders[Revenue])
Order Count        = COUNTROWS(Orders)
Avg Order Value    = DIVIDE([Total Revenue], [Order Count])
Unique Customers   = DISTINCTCOUNT(Orders[CustomerID])
Total Cost         = SUM(Orders[Cost])
Gross Margin       = [Total Revenue] - [Total Cost]
Gross Margin %     = DIVIDE([Gross Margin], [Total Revenue])

// COUNT vs COUNTA vs COUNTROWS — the common confusion
-- COUNT(Orders[Revenue])    counts numeric non-blank Revenue values
-- COUNTA(Orders[Status])    counts non-blank Status values (text)
-- COUNTROWS(Orders)         counts all rows regardless of blanks
-- Use COUNTROWS for "how many orders?" — it is the most reliable

Logical Functions

// IF(condition, result_if_true, result_if_false)
High Value Flag = IF([Revenue] >= 1000, "High", "Standard")

// IF with BLANK() — return nothing when condition not met
Late Flag = IF([Status] = "Late", "⚠ Late", BLANK())
// BLANK() keeps the cell empty in visuals — cleaner than ""

// Nested IF — use SWITCH(TRUE()) for 3+ branches instead
Tier = IF([Revenue]>=1500,"Platinum",IF([Revenue]>=1000,"Gold",
         IF([Revenue]>=500,"Silver","Bronze")))

// SWITCH(expression, val1, result1, val2, result2, ..., default)
Status Label =
    SWITCH([Status],
        "Delivered",  "✓ Complete",
        "Shipped",    "↗ Transit",
        "Processing", "⏳ Pending",
        "Late",       "⚠ Overdue",
        "? Unknown"
    )

// SWITCH(TRUE(), ...) for range conditions
Tier =
    SWITCH(TRUE(),
        [Revenue] >= 1500, "Platinum",
        [Revenue] >= 1000, "Gold",
        [Revenue] >= 500,  "Silver",
        "Bronze"
    )

// IFERROR(expression, value_if_error)
Safe Calc = IFERROR([Revenue] / [Quantity], BLANK())
// Catches any error in the expression — use sparingly
// DIVIDE() is better for division; IFERROR hides ALL errors

// AND() / OR() — function form (equivalent to && and ||)
Both Conditions = AND([Revenue] > 500, [Status] = "Delivered")
Either Condition = OR([Status] = "Late", [Status] = "Processing")
Logical function results — all five orders
OrderID Revenue HighValue LateFlag Tier StatusLabel
10011,200High(blank)Gold✓ Complete
1002850Standard(blank)Silver⏳ Pending
1003430Standard⚠ LateBronze⚠ Overdue
100495Standard⚠ LateBronze⚠ Overdue
10051,540High(blank)Platinum↗ Transit

Information Functions

Information functions test the type or state of a value and return TRUE or FALSE. They are the building blocks of defensive measures — measures that handle missing data, unexpected blanks, and type mismatches without showing errors in visuals.

// ISBLANK(value) — TRUE only when value is BLANK
//   Note: ISBLANK returns FALSE for 0 and "" (empty string)
//   Use for: conditional display, null-safe arithmetic
Has Revenue    = NOT(ISBLANK([Revenue]))
Safe Revenue   = IF(ISBLANK(SUM(Orders[Revenue])), BLANK(), SUM(Orders[Revenue]))

// ISERROR(expression) — TRUE when expression produces any error
Safe Margin % = IF(ISERROR(DIVIDE([Margin],[Revenue])), BLANK(),
                   DIVIDE([Margin],[Revenue]))
// Prefer DIVIDE() with fallback over ISERROR — ISERROR catches too broadly

// ISNUMBER(value) — TRUE when value is numeric
// ISTEXT(value) — TRUE when value is text
// ISLOGICAL(value) — TRUE when value is TRUE or FALSE
Type Check =
    SWITCH(TRUE(),
        ISNUMBER([Revenue]),  "Number",
        ISTEXT([Revenue]),    "Text",
        ISBLANK([Revenue]),   "Blank",
        "Other"
    )

// HASONEVALUE(column) — TRUE when exactly one value is visible
//   in the current filter context for that column
//   Essential for measures that only make sense for a single selection
Selected Region Label =
    IF(
        HASONEVALUE(Orders[Region]),
        "Region: " & SELECTEDVALUE(Orders[Region]),
        "Multiple regions selected"
    )

// SELECTEDVALUE(column, alternateResult)
//   Returns the single selected value if exactly one is selected
//   Returns alternateResult otherwise
Current Year =
    SELECTEDVALUE(Calendar[Year], "All Years")
// Slicer has 2024 selected → "2024"
// No slicer selection → "All Years"
// Multiple years selected → "All Years"

Text Functions

// Concatenation
Full Name     = Customers[FirstName] & " " & Customers[LastName]
Order Label   = "Order #" & Orders[OrderID]

// CONCATENATEX — concatenate values across rows (with separator)
// Useful in a measure to list items for the current filter context
Selected Regions =
    CONCATENATEX(
        VALUES(Orders[Region]),   // distinct regions in context
        Orders[Region],           // value to concatenate
        ", "                      // separator
    )
// No filter: "North, South, West"
// Slicer = North: "North"

// LEFT / RIGHT / MID
Category    = LEFT(Orders[ProductCode], 4)      // "ELEC"
SKU         = RIGHT(Orders[ProductCode], 4)     // "1042"
MiddlePart  = MID(Orders[ProductCode], 3, 2)   // characters 3-4

// LEN — character count
Code Length = LEN(Orders[ProductCode])          // 9 for "ELEC-1042"

// UPPER / LOWER / PROPER
Normalised  = UPPER(Customers[Status])          // "DELIVERED"
Email       = LOWER(Customers[Email])           // lowercase all
Name        = PROPER(Customers[FullName])       // Title Case

// TRIM — removes leading and trailing spaces
Clean Name  = TRIM(Customers[FullName])

// SUBSTITUTE — replace text within a string
No Dash     = SUBSTITUTE(Orders[ProductCode], "-", "")  // "ELEC1042"
Redacted    = SUBSTITUTE(Customers[Email],
                  LEFT(Customers[Email],
                       FIND("@", Customers[Email]) - 1),
                  "***")                        // "***@domain.com"

// FIND vs SEARCH
// FIND: case-sensitive, returns position, errors if not found
// SEARCH: case-insensitive, returns position, errors if not found
At Position    = FIND("@", Customers[Email])    // position of "@"
Contains_UK    = NOT(ISERROR(SEARCH("uk", Orders[Region])))

// FORMAT — convert number or date to formatted text string
Revenue Str    = FORMAT([Total Revenue], "$#,##0.00")   // "$4,115.00"
Date Label     = FORMAT(MAX(Orders[OrderDate]), "MMM YYYY")  // "Feb 2024"
Pct Label      = FORMAT([Gross Margin %], "0.0%")           // "46.5%"
Text function results — applied to Orders dataset
CONCATENATEX regions
"North, South, West"
FORMAT Revenue
"$4,115.00"
FORMAT Margin %
"46.5%" (1630 margin / 4115 revenue)
FORMAT max OrderDate
"Feb 2024"
SELECTEDVALUE Region (one selected)
"Region: North"
SELECTEDVALUE Region (none/multiple)
"Multiple regions selected"

Date Functions (Without Time Intelligence)

These date functions work directly on date columns without requiring a Calendar table relationship. They are used in calculated columns and in measures where you need to extract, compare, or calculate date values at the row level or within a filtered context.

// Extraction — pull components from a date
Order Year    = YEAR(Orders[OrderDate])           // 2024
Order Month   = MONTH(Orders[OrderDate])          // 1, 2, ...12
Order Day     = DAY(Orders[OrderDate])            // 1..31
Order Quarter = QUARTER(Orders[OrderDate])        // 1..4
Order WeekNum = WEEKNUM(Orders[OrderDate])        // 1..53
Order Weekday = WEEKDAY(Orders[OrderDate], 2)     // 1=Mon..7=Sun (mode 2)

// TODAY() and NOW()
Days Since Order =
    DATEDIFF(Orders[OrderDate], TODAY(), DAY)
// Returns number of days between OrderDate and today
// Recalculates every time the report is opened

// DATEDIFF(start, end, interval)
// Intervals: SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER, YEAR
Days To Deliver =
    DATEDIFF(Orders[OrderDate], Orders[DeliveryDate], DAY)

Months Since =
    DATEDIFF(Orders[OrderDate], TODAY(), MONTH)

// DATE(year, month, day) — construct a date from parts
First of Month =
    DATE(YEAR(Orders[OrderDate]), MONTH(Orders[OrderDate]), 1)

// EOMONTH(date, months_offset) — end of month
// 0 = end of same month, -1 = end of previous month, 1 = end of next
End of Order Month  = EOMONTH(Orders[OrderDate], 0)
End of Prior Month  = EOMONTH(Orders[OrderDate], -1)
End of Next Month   = EOMONTH(Orders[OrderDate], 1)

// DATEVALUE(text) — parse a text string as a date
Parsed Date = DATEVALUE("2024-01-15")   // returns a date type

// Compare dates
Is This Year =
    IF(YEAR(Orders[OrderDate]) = YEAR(TODAY()), "Current Year", "Prior")

Math Functions

// Rounding
Rounded Rev   = ROUND([Total Revenue], 2)      // standard rounding
Floor Rev     = ROUNDDOWN([Total Revenue], 0)  // always toward zero
Ceiling Rev   = ROUNDUP([Total Revenue], 0)    // always away from zero
Int Rev       = INT([Total Revenue])           // floor to integer
Truncated     = TRUNC([Total Revenue], 1)      // truncate, no rounding

// Absolute value and sign
Abs Variance  = ABS([Revenue] - [Budget])
Direction     = SIGN([Revenue] - [Budget])     // -1, 0, or 1

// Power and roots
Squared       = POWER([Value], 2)
Sq Root       = SQRT([Value])

// MOD and QUOTIENT
Remainder     = MOD([OrderID], 10)             // last digit of ID
Batches       = QUOTIENT([OrderID], 100)       // batch number

// Statistical
Std Dev       = STDEV.P(Orders[Revenue])       // population std dev
Variance      = VAR.P(Orders[Revenue])         // population variance

// RANKX(table, expression, value, order, ties)
-- Measure: rank current product by revenue within context
Revenue Rank =
    RANKX(
        ALLSELECTED(Products),   // rank across all selected products
        [Total Revenue],          // rank by this measure
        ,                         // value: blank = use current context
        DESC,                     // largest revenue = rank 1
        DENSE                     // no gaps in rank sequence
    )

The Gotchas — One Per Function Category

Aggregation
gotcha
AVERAGE ignores blank, but COUNTROWS does not. If 3 of 5 revenue rows are blank, AVERAGE divides by 2 (non-blank rows). COUNTROWS / COUNT gives different denominators. Be explicit about which denominator you want. DIVIDE(SUM([Rev]), COUNTROWS(Orders)) includes blank rows in the denominator.
Logical
gotcha
IFERROR masks all errors, including ones you want to see. If you write IFERROR([measure], 0) and your measure has a bug that returns an error, the IFERROR silently returns 0 and you never find the bug. Use DIVIDE() for division safety, and leave other errors visible during development.
Text
gotcha
FORMAT() returns text, not a number. FORMAT([Revenue], "$#,##0") returns the string "$4,115" — you cannot do arithmetic on it. Use FORMAT only for display labels in card visuals, tooltips, and concatenated strings. Apply number formatting via column or measure format settings for visuals that need to sort or calculate.
Date
gotcha
TODAY() recalculates on every report open, not on refresh. A measure using TODAY() will show different values to users who open the report on different days — even if the underlying data has not refreshed. This is usually correct behaviour, but if "today" should mean "as of last refresh," use MAX(Orders[OrderDate]) instead.
RANKX
gotcha
RANKX with ALL() vs ALLSELECTED(). ALL(Products) ranks across every product regardless of slicers. ALLSELECTED(Products) ranks within the current slicer selection. Almost always use ALLSELECTED for user-facing rank columns so the rank reflects what the user filtered to.

Teacher's Note: The most reliable way to learn DAX functions is to build every new measure in a table visual that shows the result for each row in the current context — not just a card that shows one grand total. A card showing $4,115 tells you nothing about whether the measure is working correctly. A table showing North: $1,630, South: $945, West: $1,540, Total: $4,115 tells you whether the filter context is being applied correctly, whether the denominator of a percentage is what you intended, and whether any region is unexpectedly blank. Always test in a table first, then move to the final visual type.

Practice

Practice 1 of 3

To count the number of rows in the Orders table regardless of blank values in any column — the most reliable "how many orders?" measure — you use ___(Orders).

Practice 2 of 3

The function ___(column, alternateResult) returns the single selected value from a column when exactly one value is visible in the current filter context, and returns the alternate result when no value or multiple values are selected — making it ideal for dynamic title labels in card visuals.

Practice 3 of 3

The FORMAT() function is useful for creating display labels like "$4,115.00" or "Feb 2024" — but its output is ___ type, which means you cannot perform arithmetic on it or sort a visual column numerically.

Lesson Quiz

Quiz 1 of 3

Your Orders table has 500 rows, but 50 rows have a blank Revenue value. What does each of these measures return?
A = AVERAGE(Orders[Revenue])
B = DIVIDE(SUM(Orders[Revenue]), COUNTROWS(Orders))

Quiz 2 of 3

A card visual uses the measure Latest Order = FORMAT(MAX(Orders[OrderDate]), "DD MMM YYYY"). A colleague tries to use this measure in a date slicer and gets an error. Why?

Quiz 3 of 3

You wrap a complex measure in IFERROR to avoid showing errors: Safe Measure = IFERROR([Complex Measure], 0). Three months later the report shows zeros for the West region that should be showing revenue. What likely happened and what is the better approach?

Next up — Lesson 35 covers Row Context and Filter Context in full depth, explaining exactly how DAX decides which rows each expression sees, how context transitions work, and why the same column reference produces different results depending on where the formula is evaluated.