Tableau Table Calculations to DAX: Running Totals, Ranks, and Window Functions
July 27, 2026
Tableau table calculations run on the result set after aggregation, using the layout of the view to define direction and scope. DAX has no result set to walk, so every conversion rebuilds "Compute Using" as an explicit filter or window definition. The classic approach wraps CALCULATE around a FILTER. Newer DAX window functions (OFFSET, WINDOW, ROWNUMBER) express the same logic more directly where your Power BI version supports them.
Table calculation to DAX conversion table
| Tableau | DAX equivalent | Approach |
|---|---|---|
RUNNING_SUM(SUM([Sales])) | CALCULATE(SUM(Sales[Amount]), FILTER(ALLSELECTED('Date'[Date]), 'Date'[Date] <= MAX('Date'[Date]))) | Cumulative filter |
WINDOW_AVG(SUM([Sales])) | AVERAGEX(ALLSELECTED('Date'[Month]), CALCULATE(SUM(Sales[Amount]))) | Iterate the partition |
WINDOW_AVG(SUM([Sales]), -2, 0) | AVERAGEX(WINDOW(-2, REL, 0, REL, …), CALCULATE(SUM(Sales[Amount]))) | Window function |
TOTAL(SUM([Sales])) | CALCULATE(SUM(Sales[Amount]), ALLSELECTED()) | Whole-partition total |
RANK(SUM([Sales])) | RANKX(ALLSELECTED(Product[Product]), CALCULATE(SUM(Sales[Amount]))) | Ranking |
INDEX() | ROWNUMBER(ALLSELECTED(…), ORDERBY(…)) | Positional |
SIZE() | COUNTROWS(ALLSELECTED(Product[Product])) | Positional |
LOOKUP(SUM([Sales]), -1) | CALCULATE(SUM(Sales[Amount]), OFFSET(-1, ALLSELECTED('Date'[Month]), ORDERBY('Date'[Month]))) | Offset |
PERCENT_OF_TOTAL(SUM([Sales])) | DIVIDE(SUM(Sales[Amount]), CALCULATE(SUM(Sales[Amount]), ALLSELECTED(Product[Product]))) | Ratio to partition |
PREVIOUS_VALUE() | — | Recursive; no DAX equivalent |
Running totals and moving averages
A running total is a cumulative filter: sum everything up to and including the current row's position on the axis. Two details decide whether it matches Tableau. Use ALLSELECTED rather than ALL, so the running total stays inside the user's slicer selection the way Tableau's default addressing does. And read the current position with MAX('Date'[Date]) — that idiom stands in for "the current row" when there is no row context. If the Tableau calculation restarts on a dimension ("Restarting every Year"), add that column back as a filter argument so it survives ALLSELECTED.
Moving averages need a bounded window rather than an open-ended one. The WINDOW function takes relative or absolute boundaries plus an explicit sort, so WINDOW(-2, REL, 0, REL, …) is the direct equivalent of Tableau's -2, 0 offsets. Without window functions, the equivalent is AVERAGEX over DATESINPERIOD, which requires a date table. With no offsets at all, the window is the whole partition and the conversion collapses to an iterator over ALLSELECTED — the same shape covers WINDOW_SUM, WINDOW_MIN, WINDOW_MAX, and WINDOW_MEDIAN.
Ranks and positions
Tableau's five ranking functions differ mainly in how they treat ties, and RANKX covers most of them through its optional ties argument. Note that DAX has its own functions called RANK and INDEX; neither is the equivalent of the Tableau function with the same name. Both languages rank from the highest value down by default, and Skip ties are the RANKX default, so the plain form already matches Tableau's RANK. Watch the direction on RANK_PERCENTILE, though: Tableau puts the lowest value at 0 and the highest at 1, which is the reverse of a descending rank.
| Tableau | Ties produce | DAX |
|---|---|---|
RANK | 1, 2, 2, 4 | RANKX(…) — Skip is the default |
RANK_DENSE | 1, 2, 2, 3 | RANKX(table, expr, , , "Dense") — ties is the fifth argument |
RANK_UNIQUE | 1, 2, 3, 4 | ROWNUMBER(…, ORDERBY(…)) |
RANK_MODIFIED | 1, 3, 3, 4 | No direct form — derive from RANKX and the tie count |
RANK_PERCENTILE | 0–1 scale, lowest value is 0 | DIVIDE(COUNTROWS(ALLSELECTED(…)) - RANKX(…), COUNTROWS(ALLSELECTED(…)) - 1) |
INDEX() returns a position in the addressing order rather than a rank by value, so its equivalent is ROWNUMBER, not RANKX. From there, SIZE() is COUNTROWS(ALLSELECTED(…)), FIRST() is 1 - ROWNUMBER(…), and LAST() is COUNTROWS(ALLSELECTED(…)) - ROWNUMBER(…).
LOOKUP and PREVIOUS_VALUE
LOOKUP fetches a value from another position in the partition, and OFFSET is the direct translation. When the offset is genuinely a time shift and a proper date table exists, time intelligence is clearer and faster — PREVIOUSMONTH or DATEADD. The difference matters when periods are missing from the data: OFFSET steps to the previous row that exists, time intelligence steps to the previous calendar month whether or not it has data.
PREVIOUS_VALUE() has no DAX equivalent. It is recursive by nature: each row reads the calculation's own output from the row before it, and a DAX measure cannot reference its own prior result. Every occurrence needs restructuring, usually into a running aggregate or a Power Query step that materialises the sequence at load time. These are the most expensive items in a table-calculation inventory and should be scoped separately.
Compute Using: addressing and partitioning in DAX
The Compute Using dialog is the whole configuration of a table calculation, and it is not stored in the formula text. Two calculations with identical text return different numbers if their addressing differs, which is why reading formulas alone is not enough to plan this work — you have to open the workbook.
| Tableau concept | What it controls | DAX equivalent |
|---|---|---|
| Addressing (Compute Using) | The direction the calculation moves | ORDERBY() |
| Partitioning (Restarting every) | Where the calculation resets | PARTITIONBY() |
| Table (across) | Walk columns, reset per row | ORDERBY on the column field, PARTITIONBY on the row field |
| Table (down) | Walk rows, reset per column | The reverse pairing |
| Specific dimensions | An explicit list, with a sort order | ORDERBY plus PARTITIONBY naming those columns |
| Pane versus Table scope | Whether an outer dimension resets it | Add the outer column to PARTITIONBY, or leave it out |
What Antares does with table calculations
Table calculations are not auto-converted — they are reported as manual DAX work, alongside LOD expressions and the other structural blockers. What the Analyzer does is inventory them precisely: each one is identified by kind, so a running total is not counted the same as a recursive calculation. It also detects Quick Table Calcs, which never appear as function names in the formula text and are therefore missed by any count based on searching formulas.
That distinction is what makes an estimate defensible. A workbook with thirty running totals is a day of mechanical work; a workbook with three PREVIOUS_VALUE calculations is a modelling problem. Both look identical in a raw formula count. Run the free Analyzer to get the breakdown for your own estate.
Related reading: Tableau LOD expressions to DAX and Tableau calculated fields to DAX. Primary sources: Tableau's table calculation function reference and Microsoft's DAX function reference.
← Back to Complete Migration Guide
Related Migration Resources
Frequently asked questions
What is the DAX equivalent of RUNNING_SUM?
A cumulative filter: CALCULATE(SUM(Sales[Amount]), FILTER(ALLSELECTED('Date'[Date]), 'Date'[Date] <= MAX('Date'[Date]))). ALLSELECTED keeps the running total inside the user's slicer selection, matching Tableau's default addressing, and MAX('Date'[Date]) stands in for the current row's position.
What is the DAX equivalent of Tableau's Compute Using setting?
ORDERBY and PARTITIONBY. Addressing — the direction a calculation moves — becomes ORDERBY; partitioning, or Restarting every, becomes PARTITIONBY. Compute Using is not stored in the formula text, so two identical Tableau formulas can return different numbers and must be inspected in the workbook.
Can PREVIOUS_VALUE be converted to DAX?
No. PREVIOUS_VALUE is recursive — each row reads the calculation's own output from the previous row — and a DAX measure cannot reference its own prior result. Every occurrence needs restructuring, usually into a running aggregate or a Power Query step that materialises the sequence at load time.
Does Antares convert Tableau table calculations automatically?
No — they are reported as manual DAX work. What the Analyzer does is inventory them by kind, so a running total is not counted the same as a recursive calculation, and it detects Quick Table Calcs, which never appear as function names in the formula text and are missed by any count based on searching formulas.