Most DAX tutorials explain EARLIER with a toy example and move on. This lesson gives you the full mental model: how row context nesting actually works, why EARLIER exists, and how to use it to build production-grade running totals, conditional rankings, and partitioned cumulative logic in calculated columns. Includes performance analysis and the modern variable-based refactoring pattern.

Picture this: you've inherited a Power BI report for a regional sales team. The business wants a leaderboard that shows each sales rep's cumulative revenue as of each transaction date, a conditional rank that only counts reps who have exceeded their quota, and a running total of days worked. You know measures can handle some of this, but the business needs these values materialized in a table — because a downstream Power Automate flow reads the columns directly. Measures won't cut it here.
This is the moment you meet EARLIER. Or, if you've already met it and walked away confused, this is where it finally clicks.
EARLIER is one of the most misunderstood functions in DAX — not because it's complicated in isolation, but because understanding it requires holding two levels of evaluation in your head simultaneously. When you add nested contexts and complex aggregation logic on top, most tutorials tap out. This lesson doesn't. We're going to work through the full mental model, build genuinely useful calculated columns, and learn exactly when EARLIER is the right tool (and when it isn't).
What you'll learn:
EARLIER exists to navigate between levelsEARLIER-based patterns to use variables for readability and performanceYou should be comfortable with the following before continuing:
SUMX and FILTER create a row context as they traverse a table. If you need a refresher, the lesson on Row Context vs Filter Context: The Mental Model Every DAX User Needs is essential reading.SUMX, COUNTX, and FILTER do. We'll be using them heavily.Before we write a single line of code, you need to understand why EARLIER exists. This isn't academic — without this mental model, you'll write formulas that feel right but produce garbage.
When DAX evaluates a calculated column, it automatically establishes a row context: a pointer to the current row being evaluated. Every column reference in your formula implicitly reads from that current row.
Now, here's where it gets interesting. When you use an iterator inside a calculated column — say, SUMX(Sales, Sales[Revenue]) — DAX creates a second, inner row context for the iteration. At this point, you have two row contexts active simultaneously:
Inside the inner context, any plain column reference like Sales[Revenue] resolves to the inner row — the one the iterator is currently touching. But what if you need to compare the inner row's value against the outer row's value? You need a way to reach up through the context stack and grab the outer value.
That's exactly what EARLIER(column) does. It steps back one level in the row context stack and returns the value of column from the previous (outer) context.
EARLIEST(column) goes all the way to the outermost context, but in practice you'll almost never need it. Most real-world logic requires exactly one step back, which is what EARLIER provides.
Key insight:
EARLIERdoesn't mean "the row from an earlier time." It means "the value of this column in the row context that existed one level higher in the evaluation stack." The name refers to context depth, not time sequence.
Let's make this concrete with a scenario.
Throughout this lesson, we'll use a realistic sales transaction table called SalesTransactions with the following columns:
| Column | Type | Description |
|---|---|---|
TransactionID |
Integer | Unique transaction identifier |
RepID |
Text | Sales representative identifier |
TransactionDate |
Date | Date of the transaction |
Revenue |
Decimal | Revenue from the transaction |
Quota |
Decimal | Monthly quota for the rep at time of transaction |
Region |
Text | Geographic region |
Here's a sample of the data:
| TransactionID | RepID | TransactionDate | Revenue | Quota | Region |
|---|---|---|---|---|---|
| 1001 | REP-A | 2024-01-05 | 4,200 | 10,000 | East |
| 1002 | REP-B | 2024-01-07 | 8,900 | 10,000 | West |
| 1003 | REP-A | 2024-01-12 | 6,100 | 10,000 | East |
| 1004 | REP-C | 2024-01-15 | 3,400 | 10,000 | East |
| 1005 | REP-A | 2024-01-20 | 5,800 | 10,000 | East |
| 1006 | REP-B | 2024-01-22 | 2,300 | 10,000 | West |
We'll be adding calculated columns to this table throughout the lesson.
The running total — or cumulative sum — is the canonical EARLIER use case, and understanding it deeply unlocks every other pattern.
Here's the business requirement: for each transaction row, show the total revenue that Rep A has accumulated from the beginning of the dataset up to and including the current transaction date.
Actually, let's make it rep-agnostic: for each row, show the cumulative revenue earned by that row's rep across all transactions on or before that row's transaction date.
This is a self-referential calculation — each row needs to look at other rows in the same table to compute its value.
Here's the formula:
Revenue Running Total =
CALCULATE(
SUM(SalesTransactions[Revenue]),
FILTER(
SalesTransactions,
SalesTransactions[RepID] = EARLIER(SalesTransactions[RepID])
&& SalesTransactions[TransactionDate] <= EARLIER(SalesTransactions[TransactionDate])
)
)
Let's walk through what happens when DAX evaluates this for row 1003 (REP-A, 2024-01-12, $6,100):
RepID = "REP-A", TransactionDate = 2024-01-12FILTER begins iterating over every row in SalesTransactions, creating an inner row contextSalesTransactions[RepID] → reads from the inner row context (e.g., "REP-B")EARLIER(SalesTransactions[RepID]) → reads from the outer row context ("REP-A")SalesTransactions[TransactionDate] → reads from the inner row context (e.g., 2024-01-07)EARLIER(SalesTransactions[TransactionDate]) → reads from the outer row context (2024-01-12)SUM adds up the Revenue from those filtered rows: rows 1001 and 1003, totaling $10,300The result for row 1003 is $10,300.
Warning: This pattern has a real performance cost. For a table with N rows, DAX performs N filter evaluations, each of which scans the full table. That's O(N²) complexity. On a 500-row lookup table it's instant. On a 10-million-row transaction table it will bring your Power BI model to its knees. We'll discuss mitigation strategies later in this lesson.
To really burn the mental model in, let's trace through the full evaluation for row 1005 (REP-A, 2024-01-20, $5,800) step by step.
Outer row context (calculated column evaluation):
SalesTransactions[RepID] = "REP-A"SalesTransactions[TransactionDate] = 2024-01-20SalesTransactions[Revenue] = 5,800FILTER begins iterating. Inner row context, first iteration (row 1001):
SalesTransactions[RepID] (inner) = "REP-A"EARLIER(SalesTransactions[RepID]) (outer) = "REP-A"SalesTransactions[TransactionDate] (inner) = 2024-01-05EARLIER(SalesTransactions[TransactionDate]) (outer) = 2024-01-20Inner row context, second iteration (row 1002):
Inner row context, third iteration (row 1003):
Inner row context, fifth iteration (row 1005 — the current row itself):
<= includes the current row)Final SUM: 4,200 + 6,100 + 5,800 = $16,100
This is the accumulated revenue for REP-A through January 20th: correct.
Tip: Whether you use
<=or<determines whether the running total includes or excludes the current row. Most business use cases (balance forward, cumulative revenue) want<=. If you want "revenue before this transaction," use<. Get this right before you ship the report — it's a subtle difference that changes the semantics of the entire column.
Now let's tackle something trickier: ranking rows based on a condition that itself depends on comparing rows within the table.
Business requirement: Rank each sales rep by total revenue, but only count reps who have at least one transaction exceeding their quarterly quota. Reps below quota get a NULL rank. This is a column, not a measure, because it needs to be available in a flat export.
First, let's think about what we need per row:
This is a compound EARLIER problem. Let's build it piece by piece.
Step 1: Total revenue per rep (as a column, for reference)
Rep Total Revenue =
CALCULATE(
SUM(SalesTransactions[Revenue]),
FILTER(
SalesTransactions,
SalesTransactions[RepID] = EARLIER(SalesTransactions[RepID])
)
)
Step 2: Quota achievement flag
Quota Achieved =
IF(
CALCULATE(
MAXX(SalesTransactions, SalesTransactions[Revenue]),
FILTER(
SalesTransactions,
SalesTransactions[RepID] = EARLIER(SalesTransactions[RepID])
)
) > SalesTransactions[Quota],
TRUE,
FALSE
)
This returns TRUE if the rep on this row has at least one transaction exceeding the quota amount.
Step 3: Conditional rank by total revenue, quota achievers only
Quota Achiever Rank =
IF(
SalesTransactions[Quota Achieved] = TRUE,
COUNTROWS(
FILTER(
SalesTransactions,
CALCULATE(
SUM(SalesTransactions[Revenue]),
FILTER(
SalesTransactions,
SalesTransactions[RepID] = EARLIER(EARLIER(SalesTransactions[RepID]))
)
) >
CALCULATE(
SUM(SalesTransactions[Revenue]),
FILTER(
SalesTransactions,
SalesTransactions[RepID] = EARLIER(SalesTransactions[RepID])
)
)
&& SalesTransactions[Quota Achieved] = TRUE
)
) + 1,
BLANK()
)
This formula is doing a lot. Let's trace through it:
The outer IF guards on the Quota Achieved flag — no rank unless you're a quota achiever. Then COUNTROWS(FILTER(...)) counts how many other quota-achieving reps have a higher total revenue than the current row's rep. Adding 1 converts a zero-based count into a 1-based rank (a rep with no one above them gets rank 1).
Notice EARLIER(EARLIER(SalesTransactions[RepID])) — this is the double EARLIER pattern. Here's why we need it:
FILTER(SalesTransactions, ...) creates an inner row context (middle: iterating all rows)CALCULATE, there's another FILTER(SalesTransactions, ...) creating a third, innermost row context (inner-inner: iterating all rows again)From inside the deepest FILTER, you need to reach two levels up to get the current row's RepID. That's EARLIER(EARLIER(...)).
Warning: Double EARLIER is a signal that your logic might be better expressed using variables. As we'll discuss shortly,
VARcan capture the outer context values before the iteration begins, making the formula both more readable and less error-prone. The double EARLIER is valid DAX, but it's a cognitive hazard — use it sparingly and always with a comment explaining the context levels.
For production use, here's the same rank formula refactored with variables:
Quota Achiever Rank V2 =
VAR CurrentRep = SalesTransactions[RepID]
VAR CurrentRepTotalRevenue = SalesTransactions[Rep Total Revenue]
VAR CurrentRepQualifies = SalesTransactions[Quota Achieved]
RETURN
IF(
CurrentRepQualifies = TRUE,
COUNTROWS(
FILTER(
SalesTransactions,
CALCULATE(
SUM(SalesTransactions[Revenue]),
FILTER(
SalesTransactions,
SalesTransactions[RepID] = EARLIER(SalesTransactions[RepID])
)
) > CurrentRepTotalRevenue
&& SalesTransactions[Quota Achieved] = TRUE
)
) + 1,
BLANK()
)
By capturing CurrentRepTotalRevenue in a variable at the outer context, we've eliminated the need for the inner CALCULATE/FILTER block entirely. The variable's value is baked in before iteration begins, so inside the FILTER, we just compare against a scalar — no EARLIER needed for that part.
Key insight: Variables in DAX are evaluated in the context where they're defined, not where they're used. When you define
VAR CurrentRep = SalesTransactions[RepID]at the top of a calculated column formula, that variable captures the outer row context's value. Inside any subsequent iterator, referencingCurrentRepalways returns the outer row's RepID — functionally identical toEARLIER(SalesTransactions[RepID]), but far more readable. For deeper patterns on this, see DAX Variables in Practice: Using VAR and RETURN to Simplify Complex Measures.
Real-world data is messy. Running total and ranking formulas break in predictable ways when you encounter ties (two transactions on the same date), gaps (a rep with no transactions in a date range), and date skew (transactions loaded out of chronological order). Let's work through each.
In our dataset, if REP-A had two transactions on 2024-01-12 (rows 1003 and 1003B), the running total should include both by the time we reach the later of those rows. Since we use <= on the date, both rows would include both same-day transactions — which is correct for a "cumulative through date" interpretation.
But what if the business wants the running total to be sequential by TransactionID, not by date? In other words, row 1003 should only include revenue up to and including TransactionID 1003, even if 1003B has the same date.
Revenue Running Total by ID =
CALCULATE(
SUM(SalesTransactions[Revenue]),
FILTER(
SalesTransactions,
SalesTransactions[RepID] = EARLIER(SalesTransactions[RepID])
&& SalesTransactions[TransactionID] <= EARLIER(SalesTransactions[TransactionID])
)
)
Switching from date-based to ID-based comparison eliminates the tie problem entirely, assuming your IDs are assigned sequentially. This is often the better choice for transaction-level running totals.
Gaps are less of a DAX problem and more of a data modeling problem. If a rep has no transactions in February, your running total column simply won't have any rows for that period — which is fine, because there's nothing to accumulate. The running total as of January's last transaction remains correct when February rows start appearing.
Where gaps do cause problems is when you join the SalesTransactions table to a date table to produce a continuous timeline. Those "empty" rows generated by the join won't have running total columns because they don't exist in SalesTransactions. This is a case where a measure becomes the right tool — measures can evaluate over any filter context, including date ranges with no underlying transactions.
Note: The fundamental tension in Power BI is that calculated columns can't respond to slicer context, but measures can't be materialized in export-friendly columns. When you need both — a materialized value that also responds to context — you're often looking at a Power Query solution (pre-computing in M), not a DAX calculated column. Know your escape hatches.
Another common cumulative requirement: how many distinct reps have made at least one transaction up to and including this row's date? This is a cumulative distinct count.
Cumulative Distinct Reps =
CALCULATE(
DISTINCTCOUNT(SalesTransactions[RepID]),
FILTER(
SalesTransactions,
SalesTransactions[TransactionDate] <= EARLIER(SalesTransactions[TransactionDate])
)
)
This is clean and doesn't require nested EARLIER because we're not filtering by the current row's rep — we're just capping by date across all reps.
Let's push further. Imagine the business wants a column that computes, for each East region transaction, the cumulative revenue from East region transactions only, but resets to zero at the start of each month.
This is a partitioned running total — partitioned by region and month.
East Revenue MTD Running Total =
IF(
SalesTransactions[Region] = "East",
CALCULATE(
SUM(SalesTransactions[Revenue]),
FILTER(
SalesTransactions,
SalesTransactions[Region] = "East"
&& SalesTransactions[RepID] = EARLIER(SalesTransactions[RepID])
&& YEAR(SalesTransactions[TransactionDate]) = YEAR(EARLIER(SalesTransactions[TransactionDate]))
&& MONTH(SalesTransactions[TransactionDate]) = MONTH(EARLIER(SalesTransactions[TransactionDate]))
&& SalesTransactions[TransactionDate] <= EARLIER(SalesTransactions[TransactionDate])
)
),
BLANK()
)
The key additions:
SalesTransactions[Region] = "East" filters to East transactions only inside the FILTER (hardcoded here for clarity; you could also use = EARLIER(SalesTransactions[Region]) for a dynamic version that partitions by whatever the current row's region is)YEAR(...) = YEAR(EARLIER(...)) and MONTH(...) = MONTH(EARLIER(...)) ensure the running total resets every monthFor the dynamic version that works across all regions:
Revenue MTD Running Total by Region =
VAR CurrentRep = SalesTransactions[RepID]
VAR CurrentRegion = SalesTransactions[Region]
VAR CurrentDate = SalesTransactions[TransactionDate]
VAR CurrentYear = YEAR(CurrentDate)
VAR CurrentMonth = MONTH(CurrentDate)
RETURN
CALCULATE(
SUM(SalesTransactions[Revenue]),
FILTER(
SalesTransactions,
SalesTransactions[RepID] = CurrentRep
&& SalesTransactions[Region] = CurrentRegion
&& YEAR(SalesTransactions[TransactionDate]) = CurrentYear
&& MONTH(SalesTransactions[TransactionDate]) = CurrentMonth
&& SalesTransactions[TransactionDate] <= CurrentDate
)
)
This version uses variables exclusively — no EARLIER in sight. It's exactly equivalent in behavior, but vastly more readable. Any DAX developer picking this up cold will understand it immediately.
This pattern integrates naturally with time intelligence work. If you're building these partitioned running totals as part of a broader date-aware model, the concepts in Time Intelligence in DAX: YTD, MTD, Previous Period, and Rolling Averages will give you additional context on calendar-based partitioning strategies.
DAX supports EARLIER with an optional depth parameter: EARLIER(column, 2) goes two levels up instead of one (equivalent to EARLIER(EARLIER(column))). This is useful when you genuinely have three nested iterators, but it's rare and it's a sign that your model might need rethinking.
If you find yourself writing EARLIER(column, 3), stop. You almost certainly need to restructure the logic using variables, helper columns, or a different approach entirely.
EARLIER doesn't work in measures. Measures don't have a persistent row context at the point of calculation — they operate in filter context. If you try to use EARLIER in a measure, you'll get an error. If you're seeing ranking or running total requirements that originally seemed like column problems, read through DAX Ranking Patterns in Practice: RANKX, TOPN, and Dense vs. Sparse Rankings Across Dynamic Filter Contexts for the measure-based equivalents.
When you place CALCULATE inside a FILTER, DAX performs a context transition for each row the iterator visits. This can produce unexpected results if you're not careful.
Consider:
-- CAREFUL: This might not do what you expect
Bad Running Total =
CALCULATE(
SUM(SalesTransactions[Revenue]),
FILTER(
ALL(SalesTransactions), -- Note the ALL() here
SalesTransactions[TransactionDate] <= EARLIER(SalesTransactions[TransactionDate])
)
)
By using ALL(SalesTransactions) instead of SalesTransactions, you explicitly ignore any existing filter context on the table. Whether this is right or wrong depends entirely on your intent. In a calculated column context, there's usually no active filter context, so the distinction is moot — but in a measure or within a more complex expression, this matters significantly.
Tip: When writing
FILTERinside aCALCULATE, always ask yourself: "Do I want to filter within the current filter context, or should I start fresh?" If you want to start fresh, useALL(TableName). If you want to respect existing filters, use the table name directly. Getting this wrong produces numbers that look plausible but are subtly incorrect — the worst kind of bug.
Let's be direct about the performance reality of EARLIER-based patterns.
Every running total or conditional ranking using EARLIER requires the storage engine to perform a scan of the table for each row being computed. For a table with 1,000 rows, that's 1,000 scans of 1,000 rows each = 1,000,000 row evaluations. For 100,000 rows, it's 10 billion. These numbers are not theoretical — they're the reason your calculated column takes 45 minutes to refresh on a large table.
Mitigation option 1: Power Query pre-computation
Move the running total logic to Power Query's M language, which has a more efficient execution model for sequential operations. Use List.Generate or Table.AddIndexColumn combined with custom aggregations. The result is the same materialized column, but computed in O(N log N) or better.
Mitigation option 2: Use measures instead of columns
For interactive reporting, DAX Iterators Explained: How SUMX, AVERAGEX, and MAXX Evaluate Row by Row to Solve Problems SUM Cannot covers patterns where the engine can use columnar compression and cache results more efficiently than a full table scan per row.
Mitigation option 3: Segment your data
If you must use EARLIER-based columns on large tables, partition the computation. Instead of one table with 10 million rows, could you compute the column on monthly summary tables (one row per rep per day) and then join those summaries back? Reducing the table size from 10M to 50K rows reduces the scan count by a factor of 40,000.
Mitigation option 4: Profile before optimizing
Don't assume your formula is slow. Use DAX Studio's server timings to measure the actual SE (Storage Engine) and FE (Formula Engine) time. Small tables with aggressive filter conditions may perform acceptably in practice even with O(N²) theoretical complexity. The lesson on Performance Tuning DAX: Optimize Slow Measures with DAX Studio covers the profiling workflow in detail.
Key insight: The VertiPaq storage engine compresses data columnar-style and can often execute bulk predicates very fast. A filter on a low-cardinality column like
Region = "East"might scan 10 million rows in milliseconds because the engine operates on compressed column segments. High-cardinality filters likeTransactionDate <= someDateare harder to compress and hit performance more severely. The composition of your EARLIER conditions matters as much as the volume of data.
A question that comes up constantly in the DAX community: "Should I even use EARLIER anymore, or should I always use variables?"
The honest answer is: for new code, prefer variables almost always. Here's why:
| Dimension | EARLIER | Variables |
|---|---|---|
| Readability | Requires understanding context stack | Reads like a variable assignment in any language |
| Debuggability | Hard to inspect intermediate values | Variables can be returned individually for debugging |
| Nesting depth | Gets unwieldy at 2+ levels | No nesting needed; just define more variables |
| Performance | Equivalent in most cases | Can be faster when the same value is used multiple times |
| Compatibility | Supported in all DAX environments | Supported since SSAS 2016 and all Power BI versions |
The primary case where you might encounter EARLIER and genuinely need it (rather than choosing it) is when you're working with someone else's existing formulas, or when you're using a DAX pattern from older documentation. Understanding EARLIER is essential for reading and maintaining that code even if you write variables yourself.
There's also one scenario where variables can't replace EARLIER: when the value you need from the outer context changes on every row and that variability is itself part of the logic. In practice, this is the same as the normal EARLIER use case — and variables handle it just as well by capturing the column reference at definition time.
Work through these progressively harder problems using the SalesTransactions table described earlier. Start each from a blank calculated column.
Problem 1 — Basic Running Total:
Create a column RevenueCumulativeAllReps that shows, for each row, the total revenue across all reps from the earliest transaction date through the current row's transaction date. (No rep filter — accumulate everything.)
Hint: You only need one EARLIER reference (for the date), no rep condition.
Problem 2 — Rank Within Region:
Create a column RegionRevenueRank that ranks each rep within their region by total revenue. Two reps in the East region should be ranked 1 and 2 independently of reps in the West region.
Hint: You need EARLIER (or variables) for both RepID and Region. The rank counts reps in the same region with higher total revenue.
Problem 3 — Conditional Running Count:
Create a column HighValueTransactionCount that shows, for each row, how many prior transactions for the same rep had a Revenue value greater than 5,000. Include only transactions strictly before the current row's date.
Hint: Use COUNTROWS(FILTER(...)) with conditions on RepID, date, and Revenue threshold.
Problem 4 — Refactor Using Variables:
Take your solution to Problem 2 and rewrite it entirely using VAR declarations, eliminating all EARLIER references.
Problem 5 — Reset Running Total:
Create a column RevenueRunningTotalByMonth that computes a running total of revenue per rep, resetting to zero at the start of each calendar month. A rep's running total in February should not carry over revenue from January.
Verification: For REP-A in January (using our sample data), the three rows should show values of 4,200, 10,300, and 16,100 respectively. In a second month, REP-A's first transaction would start from that month's first revenue value, not 16,100.
-- This will error
Bad Column = EARLIER(SalesTransactions[Revenue]) * 2
EARLIER requires two active row contexts — the inner one created by an iterator and the outer one it's reaching back to. If you write EARLIER at the top level of a calculated column with no enclosing iterator, there's no inner context, and DAX throws an error. You can't use EARLIER outside a loop.
Fix: You're probably not inside an iterator when you think you are. Wrap the expression in a FILTER or iterator that actually needs the outer value comparison.
-- This gives you point-in-time, not cumulative
Wrong Running Total =
CALCULATE(
SUM(SalesTransactions[Revenue]),
FILTER(
SalesTransactions,
SalesTransactions[TransactionDate] = EARLIER(SalesTransactions[TransactionDate])
)
)
Using = instead of <= gives you the total for the current date only, not the cumulative total through the current date. Always double-check your comparison operator against the business definition.
-- Cumulates all reps together, not per rep
Wrong Per-Rep Total =
CALCULATE(
SUM(SalesTransactions[Revenue]),
FILTER(
SalesTransactions,
SalesTransactions[TransactionDate] <= EARLIER(SalesTransactions[TransactionDate])
)
)
This gives REP-A's row a running total that includes REP-B and REP-C's revenue as well. Always include the partition key condition (RepID = EARLIER(RepID)) unless you genuinely want cross-rep accumulation.
If your ranking column returns 0 for non-qualifying rows instead of BLANK(), those rows will appear in rank-based visuals as rank 0. Use BLANK() explicitly — it suppresses the row from many visuals and communicates clearly that the rank is undefined, not zero.
EARLIER does not return the value from the row above in sorted order. It returns the value from the outer row context. There is no built-in DAX function that gives you the "previous row" in a sequence. If you need the previous row's value, you need to compute it using a filter on TransactionID or date that returns exactly the preceding row — typically using MAXX(FILTER(...), ...) with a date or ID strictly less than the current one.
Previous Transaction Revenue =
VAR CurrentID = SalesTransactions[TransactionID]
VAR CurrentRep = SalesTransactions[RepID]
RETURN
MAXX(
FILTER(
SalesTransactions,
SalesTransactions[RepID] = CurrentRep
&& SalesTransactions[TransactionID] < CurrentID
),
SalesTransactions[Revenue]
)
This returns the revenue from the highest TransactionID that is still less than the current row's ID — effectively the immediately preceding transaction for the same rep.
Tip: For lag/lead patterns (prior row values), the approach above using MAXX or MINX on a filtered table is the standard DAX technique. Dynamic segmentation and pattern detection built on this approach are explored in Dynamic Segmentation and Grouping with DAX: Build Flexible Customer Analytics.
You've now worked through the full EARLIER mental model and its practical applications. Let's consolidate what you've learned:
EARLIER navigates the row context stack. When an iterator creates an inner row context inside an existing outer row context, EARLIER(column) retrieves the column's value from the outer context. It's a context-level reference, not a time-based one.
Running totals use FILTER with EARLIER for both partition and date conditions. The pattern SalesTransactions[RepID] = EARLIER(SalesTransactions[RepID]) && SalesTransactions[TransactionDate] <= EARLIER(SalesTransactions[TransactionDate]) is the workhorse of cumulative column logic.
Conditional rankings add a COUNTROWS(FILTER(...)) wrapper that counts how many rows exceed the current row's metric value, then adds 1. BLANK() gates non-qualifying rows.
Double EARLIER navigates two levels up. It's valid but signals complexity — prefer refactoring to variables.
Variables are EARLIER's modern replacement for new code. They're more readable, easier to debug, and semantically identical in behavior.
Performance degrades quadratically with table size for EARLIER-based patterns. Profile first, optimize by reducing table size, moving logic to Power Query, or restructuring as measures.
Where to go next:
The ability to control row context precisely — to know exactly which row's value you're reading at every point in a formula — is the skill that separates competent DAX users from genuinely expert ones. EARLIER is the window into that precision.