Excelgoodies logo +44 (0)20 3769 3689

LEARN THIS HANDS ON

Microsoft Excel Reporting

. Live Online FILLING FAST
View all upcoming batches
Modern Excel Formula Patterns for 2026: Stop Using OFFSET and Volatile Hacks

Modern Excel Formula Patterns for 2026: Stop Using OFFSET and Volatile Hacks

If you're still leaning on OFFSET, legacy array formulas, and volatile tricks, you're leaving performance and maintainability on the table. This article shows how to replace those patterns with LET, LAMBDA, and Dynamic Arrays, using concrete patterns you can drop into production models and report templates. If you want to turn these ideas into end‑to‑end reporting workflows, pair them with solid Excel reporting foundations.


The Old Patterns We’re Replacing

Before jumping into modern patterns, it helps to name the usual suspects you probably have in your models today:

  • OFFSET-based ranges
    • Rolling ranges: =SUM(OFFSET(B2,0,0,12,1))
    • Dynamic named ranges: =OFFSET(Sheet1!$A$1,0,0,COUNTA(Sheet1!$A:$A),1)
  • Legacy CSE (Ctrl+Shift+Enter) array formulas
    • Conditional sums before SUMIFS
    • Multi-condition lookups like INDEX/MATCH arrays
  • Volatile hacks
    • INDIRECT to build references from text
    • NOW, TODAY, RAND, RANDBETWEEN used as cheap recalc triggers

These patterns work, but they:

  • Recalculate more often than necessary (volatile)
  • Are hard to audit and explain
  • Break easily when structures change

The goal: move to non-volatile, readable, reusable formulas built on:

  • Dynamic arrays (SPILL behavior)
  • LET for named sub-expressions
  • LAMBDA for reusable custom functions

Pattern 1: Dynamic Ranges Without OFFSET

Old: OFFSET-based Dynamic Named Range

A classic dynamic named range for a growing list:

=OFFSET(Sheet1!$A$2,0,0,COUNTA(Sheet1!$A:$A)-1,1)

Issues:

  • OFFSET is volatile
  • Hard to understand at a glance

New: Dynamic Arrays + INDEX

Use INDEX (non-volatile) with full-column references and SEQUENCE where needed.

Example: Last 12 months of sales from a growing column

Old:

=SUM(OFFSET(B2,COUNTA(B:B)-12,0,12,1))

New:

=LET(
    data, B:B,
    n, COUNTA(data),
    last12, INDEX(data, SEQUENCE(12, 1, n-11)),
    SUM(last12)
)

Why this is better:

  • No volatile OFFSET
  • All logic is named: data, n, last12
  • Easy to adjust the window size (change 12 in one place)

New: Dynamic Named Range with TAKE

If you’re on the latest Excel build with TAKE, this gets even cleaner:

=LET(
    data, B2:INDEX(B:B, COUNTA(B:B)+1),
    TAKE(data, -12)
)

Use this as the Refers to: formula in Name Manager. Anywhere you previously used MyLast12Months, the range now updates automatically without volatility.


Pattern 2: Replacing CSE Array Formulas with Dynamic Arrays

Old: Multi-Condition Lookup with CSE

Legacy pattern:

{=INDEX($D$2:$D$1000,
  MATCH(1,
    ($A$2:$A$1000=H2)*($B$2:$B$1000=H3),
  0)
)}

Problems:

  • Requires Ctrl+Shift+Enter
  • Hard to maintain and explain

New: FILTER + INDEX (or TAKE)

Dynamic arrays make this pattern trivial.

=LET(
    id, H2,
    region, H3,
    rows, FILTER($D$2:$D$1000, ($A$2:$A$1000=id)*($B$2:$B$1000=region)),
    INDEX(rows, 1)
)

Or, if you’re fine with a spilled result (all matches):

=FILTER($D$2:$D$1000, ($A$2:$A$1000=H2)*($B$2:$B$1000=H3))

Benefits:

  • No CSE
  • Works across the grid, spilling results
  • Easy to extend to more conditions

New: Dynamic Array Conditional Aggregations

Old conditional sum with CSE:

{=SUM(($A$2:$A$1000=H2)*($B$2:$B$1000=H3)*$C$2:$C$1000)}

Modern alternative using SUMIFS where possible:

=SUMIFS($C$2:$C$1000, $A$2:$A$1000, H2, $B$2:$B$1000, H3)

If the logic is more complex (e.g., OR conditions), use LET + arrays:

=LET(
    regionOK, ($A$2:$A$1000="North") + ($A$2:$A$1000="East"),
    productOK, ($B$2:$B$1000="Widget"),
    values, $C$2:$C$1000,
    SUM(FILTER(values, (regionOK>0)*productOK))
)

No CSE, and the boolean logic is explicit.


Pattern 3: Killing INDIRECT for Dynamic Sheet/Range References

INDIRECT is useful but volatile and fragile (breaks on sheet renames). Often you can avoid it.

Scenario: Pick a Table by Name

Old pattern:

=SUM(INDIRECT(A1 & "[Amount]"))

Where A1 contains a table name like Sales2025.

Modern pattern: store actual references, not text.

  1. Create a small mapping table:
    • Column A: label (e.g., 2025, 2026)
    • Column B: direct reference (e.g., =Sales2025[Amount], =Sales2026[Amount])
  2. Use XLOOKUP to return the range directly.
=LET(
    year, H2,
    rng, XLOOKUP(year, Map[Year], Map[Range]),
    SUM(rng)
)

No INDIRECT, no volatility, and sheet renames are handled by Excel’s reference engine.

Scenario: Dynamic Column Selection Without INDIRECT

Old pattern:

=SUM(INDIRECT("Table1[" & H2 & "]"))

New pattern: CHOOSECOLS with MATCH.

=LET(
    colName, H2,
    tbl, Table1,
    colIndex, MATCH(colName, DROP(Table1[#Headers],,0), 0),
    SUM(CHOOSECOLS(tbl, colIndex))
)

You’re still dynamic, but the formula is non-volatile and strongly typed against the table.


Pattern 4: Using LET to De-Duplicate Logic

LET isn’t just syntactic sugar. It:

  • Reduces repeated calculations
  • Makes formulas debuggable
  • Documents intent inline

Example: Complex Margin Calculation

Old, repeated logic:

=(Revenue - Cost - IF(Type="Promo", PromoCost, 0))/Revenue

Copied across thousands of rows with small variations.

New pattern with LET:

=LET(
    revenue,  [@Revenue],
    cost,     [@Cost],
    promo,    [@PromoCost],
    type,     [@Type],
    adjCost,  cost + IF(type="Promo", promo, 0),
    margin,   (revenue - adjCost) / revenue,
    margin
)

Benefits:

  • Names match business concepts
  • Easier to test: temporarily return adjCost or revenue to debug
  • No repeated [@Revenue] or [@Cost] expressions

Example: Reusing a Filtered Subset

Old:

=SUM(FILTER(Sales[Amount], Sales[Region]="North")) /
 SUM(FILTER(Sales[Amount], Sales[Region]<>"North"))

The same filter logic is repeated.

New:

=LET(
    amt, Sales[Amount],
    region, Sales[Region],
    north, FILTER(amt, region="North"),
    other, FILTER(amt, region<>"North"),
    SUM(north) / SUM(other)
)

Cleaner, and potentially faster on large ranges.


Pattern 5: LAMBDA as Your In-Workbook Function Library

LAMBDA lets you define custom functions without VBA, store them in Name Manager, and reuse them across the workbook.

Example: Rolling N-Period Average (Replacing OFFSET)

Old pattern with OFFSET:

=AVERAGE(OFFSET(B2,COUNT(B:B)-N,0,N,1))

New LAMBDA function:

  1. Define a name RollingAverage in Name Manager.
  2. Use this formula:
=LAMBDA(range, periods,
    LET(
        n, ROWS(range),
        lastN, DROP(range, n-periods),
        AVERAGE(lastN)
    )
)

Now call it like a native function:

=RollingAverage(B2:B1000, 12)

No volatility, and the logic is centralised.

Example: Multi-Condition Lookup as a Reusable Function

Define a generic function FirstMatch:

=LAMBDA(criteriaRange1, criteriaValue1,
        criteriaRange2, criteriaValue2,
        returnRange,
    LET(
        mask, (criteriaRange1=criteriaValue1) * (criteriaRange2=criteriaValue2),
        result, FILTER(returnRange, mask),
        INDEX(result, 1)
    )
)

Usage:

=FirstMatch(A:A, H2, B:B, H3, D:D)

You’ve effectively built a custom INDEX/MATCH variant without any VBA.

Tips for LAMBDA in Production Models

  • Keep parameter lists short (3–7 arguments)
  • Use descriptive names (SalesByRegion, NormalizeRange, RollingAverage)
  • Document usage in a comment next to the first call

Pattern 6: Reducing Volatility in Date and Random Logic

Some volatile functions are unavoidable (NOW, TODAY), but you can control their blast radius.

Centralise Volatile Inputs

Instead of sprinkling TODAY() everywhere:

  1. Put =TODAY() in a single cell, e.g., Config!B2.
  2. Use LET to reference it:
=LET(
    today, Config!$B$2,
    dueDate, [@DueDate],
    MAX(0, dueDate - today)
)

This:

  • Makes it obvious what “today” means in the model
  • Lets you override Config!B2 for testing

Replace RAND/RANDBETWEEN Where Possible

For sampling or random IDs, consider:

  • Generating once and storing values
  • Using deterministic sequences (SEQUENCE) combined with hashing logic

Example: pseudo-random but stable ID per row:

=LET(
    key, [@CustomerID] & [@OrderDate],
    hash, SUMPRODUCT(CODE(MID(key, SEQUENCE(LEN(key)), 1))),
    TEXT(hash, "000000")
)

Not cryptographic, but stable and non-volatile.


Pattern 7: Migration Strategy for Legacy Workbooks

You don’t need to rewrite everything at once. A pragmatic approach:

  1. Identify hotspots
    • Use Formulas > Error Checking > Circular References to spot complex areas
    • Search for OFFSET(, INDIRECT(, and { (CSE) in formulas
  2. Wrap first, then refactor
    • Use LET to name existing logic before changing it
    • Example: wrap an OFFSET formula in LET and then swap out the OFFSET for INDEX/TAKE
  3. Promote repeated patterns to LAMBDA
    • If you see the same 4–5 line LET block 10 times, it’s a candidate
  4. Lock in tests
    • Add a small test sheet comparing old vs new formulas side by side
    • Use =IF(ABS(old-new)<0.00001,"OK","CHECK") for numeric comparisons

This keeps risk manageable while modernising the workbook.


One Concrete Next Step

Pick one of your existing workbooks that uses OFFSET or INDIRECT heavily and:

  1. Search for OFFSET(.
  2. Replace a single OFFSET-based dynamic range with a LET + dynamic array pattern from this article.
  3. Add a short comment explaining the new pattern.

Do this once, properly, and you’ll have a reusable template for refactoring the rest of your models away from volatile hacks and into modern, robust Excel.

Excel Formulas

New

Next Batches Now Live

Power BIPower BI
SQLSQL
Power AppsPower Apps
Power AutomatePower Automate
Microsoft FabricMicrosoft Fabrics
AzureAzure Data Engineering
Explore Dates & Reserve Your Spot → Reserve Your Spot →