Tableau Migration Calculated Fields DAX

Tableau Calculated Fields to DAX: Function Mapping and Conversion Reference

July 27, 2026

Most Tableau calculated fields convert to DAX by substituting function names. Two things break a naive rewrite: DAX has no row context inside a measure, and several DAX functions share a name with a Tableau function while doing something else entirely. This page maps the function library, then covers the traps that do not raise an error.

Tableau to DAX function mapping

Logic and conditionals

TableauDAXNote
IF … THEN … ELSEIF … ENDIF() or SWITCH(TRUE(), …)SWITCH(TRUE(), …) is far more readable past two branches
IIF(test, then, else, [unknown])IF(test, then, else)DAX has no fourth "unknown" argument — handle blanks explicitly
CASE [x] WHEN … THEN … ENDSWITCH([x], …)Direct equivalent
AND / OR / NOT&& / || / NOTAND() and OR() exist in DAX but take exactly two arguments
ISNULL([x])ISBLANK([x])A DAX blank is not a SQL null — it behaves as zero in arithmetic
IFNULL(a, b)COALESCE(a, b)Direct equivalent
ZN([x])COALESCE([x], 0)In ratios, prefer DIVIDE(a, b, 0) — see below

Aggregation

TableauDAXNote
SUM, MIN, MAX, MEDIANSame namesDirect equivalents
AVG([x])AVERAGE([x])Renamed
COUNTD([x])DISTINCTCOUNT([x])Renamed
COUNT([x])COUNT([x]) or COUNTA([x])DAX COUNT handles numbers, dates and text but not TRUE/FALSE — use COUNTA for boolean columns
ATTR([x])SELECTEDVALUE([x])Returns blank instead of * when values are not unique
STDEV / VARSTDEV.S / VAR.SVAR is a reserved keyword in DAX — it declares a variable
SUM([a]) / SUM([b])DIVIDE(SUM([a]), SUM([b]), 0)Third argument replaces the divide-by-zero result

Dates

TableauDAXNote
TODAY() / NOW()TODAY() / NOW()Direct equivalents
DATEDIFF('day', [a], [b])DATEDIFF([a], [b], DAY)Argument order differs; the DAX interval is an unquoted keyword
DATEADD('month', 3, [d])EDATE([d], 3)DAX DATEADD is a different function — see below
DATEPART('year', [d])YEAR([d])Also QUARTER, MONTH, DAY, HOUR
DATETRUNC('month', [d])DATE(YEAR([d]), MONTH([d]), 1)Row-level form; STARTOFMONTH is time intelligence
DATENAME('month', [d])FORMAT([d], "MMMM")Returns text — sort it with a numeric month column
MAKEDATE(y, m, d)DATE(y, m, d)Renamed
DATEPARSE(fmt, str)No DAX equivalent; parse in Power Query

Strings and type conversion

TableauDAXNote
LEFT, RIGHT, LEN, UPPER, LOWER, TRIMSame namesDirect equivalents
MID(s, start)MID(s, start, num_chars)DAX requires the third argument; pass LEN(s) to reach the end
CONTAINS(s, sub)CONTAINSSTRING(s, sub)DAX version is case-insensitive; CONTAINSSTRINGEXACT is not
REPLACE(s, sub, new)SUBSTITUTE(s, sub, new)DAX REPLACE is positional: REPLACE(s, start, length, new)
FIND(s, sub)SEARCH(sub, s, 1, 0)Argument order reverses; SEARCH is case-insensitive, while DAX's own FIND is case-sensitive
REGEXP_EXTRACT, REGEXP_MATCH, REGEXP_REPLACEDAX has no regular expressions; move the logic to Power Query
INT([x])TRUNC([x])Not INT() — see below
STR([x]) / FLOAT([x])FORMAT([x], "General Number") / VALUE([x])CONVERT([x], STRING) also works

Measure or calculated column?

Tableau splits calculated fields into row-level and aggregate. Power BI splits them into calculated columns and measures, and the two splits do not line up. Mapping them wrongly is the most common cause of a converted field that returns a plausible but incorrect number.

A calculated column is evaluated once per row on refresh and has row context, so [Price] * [Quantity] works exactly as it did in Tableau. A measure is evaluated at query time against a filter context and has no row context at all — the same expression fails, because there is no current row to read.

Tableau calculated fieldPower BI equivalentWhy
Row-level, used as a dimension, filter, or slicerPower Query column (preferred) or calculated columnComputed on refresh, compresses well, filterable
Row-level, only ever aggregated in the viewMeasure with an iterator, e.g. SUMX(Sales, Sales[Price] * Sales[Qty])Avoids materialising a column you never filter on
Aggregate (contains SUM, AVG, COUNTD)MeasureMust respond to slicers and to the visual's grain

The default should be a measure. Calculated columns cost memory on every refresh and cannot respond to a slicer, so reach for one only when the value has to be filterable or appear on an axis.

Traps that silently change results

1. DATEADD is not DATEADD. Both languages have a function with this name and they are unrelated. Tableau's is row-level date arithmetic. The DAX DATEADD is a time-intelligence function that returns a table of shifted dates and errors when the dates in context are not contiguous. For row-level month arithmetic use EDATE; keep DATEADD for shifting a measure across periods.

2. DATEDIFF reverses its arguments. Tableau takes the interval first, DATEDIFF('day', [Start], [End]); DAX takes it last, DATEDIFF([Start], [End], DAY). A converted formula that compiles is not proof of correctness — swapping the two date arguments returns the same magnitude with the opposite sign.

3. INT rounds down instead of truncating. Tableau's INT() truncates toward zero, so INT(-9.7) returns -9. The DAX INT() rounds down: Microsoft's own remark is that TRUNC(-4.3) returns -4 but INT(-4.3) returns -5. The two agree on every positive number, which is why this survives testing and surfaces in production on refunds, adjustments, and variances. Use TRUNC().

A fourth issue is not a trap so much as a habit: DAX blanks. A blank behaves as zero in arithmetic, and a measure that returns blank makes the row disappear from a visual entirely. Tableau's ZN() wrapper has no automatic counterpart, so ratios need explicit handling — which is what the third argument of DIVIDE is for.

What Antares does with calculated fields

Antares reads every calculated field in a workbook before any DAX is written, resolves what each formula depends on, and separates the mechanical conversions from the ones that need a person. Formulas with structural translation blockers (LOD expressions, table calculations, RAWSQL_ passthrough, and R/Python SCRIPT_ functions) are reported as manual actions with the recommended approach rather than guessed at, because a silently wrong rewrite costs more to find in UAT than a flagged one costs to write.

Run the free Analyzer to see the calculated-field breakdown for your own workbooks before committing to an estimate.

Related reading: Tableau LOD expressions to DAX for FIXED, INCLUDE, and EXCLUDE, and Tableau table calculations to DAX for RUNNING_SUM, WINDOW_AVG, and RANK. Primary sources: Tableau's function reference and Microsoft's DAX function reference.

← Back to Complete Migration Guide

Related Migration Resources

Frequently asked questions

Is there a direct Tableau to DAX function mapping?

For most of the library, yes. AVG becomes AVERAGE, COUNTD becomes DISTINCTCOUNT, IFNULL becomes COALESCE, ATTR becomes SELECTEDVALUE, ZN(x) becomes COALESCE(x, 0). The exceptions are DATEADD, DATEDIFF, INT and REPLACE, which exist in both languages with different meanings or argument orders.

Why does DAX DATEADD not work like Tableau DATEADD?

They are unrelated functions with the same name. Tableau DATEADD is row-level date arithmetic; DAX DATEADD is a time-intelligence function that returns a table of shifted dates and errors when the dates in the current context are not contiguous. For row-level month arithmetic in DAX use EDATE(date, months) instead.

Should a Tableau calculated field become a DAX measure or a calculated column?

A measure by default. Use a calculated column only when the value has to be filterable, appear on an axis, or feed a slicer — Tableau row-level fields used as dimensions. Row-level fields that are only ever aggregated are better as a measure with an iterator such as SUMX.

Why does INT() give a different answer after conversion?

Tableau INT truncates toward zero, so INT(-9.7) returns -9. DAX INT rounds down: Microsoft documents that TRUNC(-4.3) returns -4 while INT(-4.3) returns -5. The two agree on every positive number, which is why this survives testing and appears in production on refunds and variances. Use TRUNC().

Let's talk migration.

Get your free Migration Readiness Score, or talk to our team about end-to-end delivery.