Dynamic period comparisons, variable-driven flexibility, and production-grade patterns for enterprise Qlik Sense dashboards.
Hardcoding years in Set Analysis is the most common mistake in production dashboards. Every comparison - YTD, LY, QTD, Rolling 12M - must self-adjust as time moves forward.
// Breaks every Jan 1st
Sum({<Year={2024}>} Sales)
// Needs manual update every year
Sum({<Year={2024},Month={<=3}>}
Sales)
// Always current year
Sum({<Year={$(=Max(Year))}>} Sales)
// Always current year, Q1-present
Sum({$<Year={$(=Max(Year))},
Month={$(=Month(Today()))}>}
Sales)
Returns all data from January 1st of the current year through today's date:
// Pattern 1 - using Year + Month modifiers
Sum({$<Year={$(=Max({1}Year))},
Month={"<=$(=Month(Today()))"}>} Sales)
// Pattern 2 - using Date range with YearStart()
Sum({$<Date={">=$(=YearStart(Today()))<$(=Today())"}>} Sales)
// Pattern 3 - most readable (recommended)
Sum({1<Date={$(=Chr(34)&YearStart(Today())&Chr(34)
&"<"&Chr(34)&Today()&Chr(34)}>} Sales)
{1} bypasses all selections, so the maximum year returned is always the global maximum in the data model - not filtered by current user selections. Use this when the "current year" anchor must be independent of what the user has selected.
Same period as YTD but one year earlier. The key is subtracting 1 from the dynamic year:
// LY - same months as current YTD, prior year
Sum({$<Year={$(=Max({1}Year)-1)},
Month={"<=$(=Month(Today()))"}>} Sales)
// YoY Growth % - complete KPI expression
Num(
(Sum({$} Sales) / Sum({$<Year={$(=Max({1}Year)-1)}>} Sales)) - 1,
'+#,##0.0%;-#,##0.0%;0.0%'
)
// Full year LY (not YTD-aligned)
Sum({1<Year={$(=Max({1}Year)-1)}>} Sales)
Current quarter only, dynamically computed from today's date:
// QTD - current quarter using QuarterStart()
Sum({$<Date={">=$(=QuarterStart(Today()))"}>} Sales)
// Quarter number modifier - cleaner for month-based models
Sum({$<Year={$(=Year(Today()))},
Quarter={$(=Ceil(Month(Today())/3))}>} Sales)
// LQT - Last Quarter Total
Sum({1<Date={">=$(=QuarterStart(AddMonths(Today(),-3)))
<$(=QuarterStart(Today()))"}>} Sales)
The most powerful trend indicator - a true rolling window that moves with every passing month:
// Rolling 12M - last 365 days from today
Sum({$<Date={">=$(=AddMonths(Today(),-12))"}>} Sales)
// Rolling 12M - month-aligned (cleaner for YoY comparison)
Sum({$<Date={">=$(=MonthStart(AddMonths(Today(),-12)))
<$(=MonthStart(Today()))"}>} Sales)
// R12M vs Prior R12M
R12M_Current:
Sum({$<Date={">=$(=AddMonths(Today(),-12))"}>} Sales)
R12M_Prior:
Sum({$<Date={">=$(=AddMonths(Today(),-24))
<$(=AddMonths(Today(),-12))"}>} Sales)
LET vMaxDate = Date(Max(SaleDate))) rather than computing Today() in every expression. This ensures temporal consistency across all KPIs and allows you to test with historical data.
When the same filter logic appears in 15 expressions, changing one condition means 15 edits and 15 opportunities for bugs. Indirect Set Analysis solves this with variables as single sources of truth.
The simplest form: a variable holds a filter value used across multiple expressions.
// In the load script or Variables panel:
LET vActiveStatus = 'Active';
LET vKeyAccount = 'Key Account';
LET vCurrentYear = Year(Today());
// Usage - clean and consistent across all objects
Sum({$<Status={'$(vActiveStatus)'}>} Revenue)
Count({$<CustomerTier={'$(vKeyAccount)'},Status={'$(vActiveStatus)'}>} DISTINCT CustomerID)
Sum({$<Year={'$(vCurrentYear)'}>} Revenue)
The variable holds the entire set modifier. This enables true single-point-of-change architecture:
// Variable contains the complete set modifier
LET vSetStrategic = "<CustomerTier={'Strategic'},Status={'Active'}>";
LET vSetYTD = "<Year={'$(=Max({1}Year))'},Month={'<=$(=Month(Today()))'}>";
// These two expressions are now DRY - change the variable, update everywhere
Sum({$$(vSetStrategic)} Revenue)
Count({$$(vSetStrategic)} DISTINCT CustomerID)
Sum({$$(vSetYTD)} Revenue)
Avg({$$(vSetYTD)} TicketSize)
$(vMySet) without extra braces. The expansion inserts the literal string <Field={'Value'}> directly into the expression. Adding {} around the variable is a common mistake that produces syntax errors.
The most powerful indirect pattern: a variable acts as a switch between two different data contexts. Critical rule: the If() must be at the expression level, not inside the aggregation.
// This does NOT toggle the Set context
// It filters rows AFTER aggregation
Sum(If(vMode=1,
{<Status={'Active'}>} Rev,
{<Status={'All'}>} Rev))
// This properly toggles the Set Analysis
If(vMode=1,
Sum({<Status={'Active'}>} Revenue),
Sum({<Status={'Churned'}>} Revenue)
)
// vDisplayMode is set by a button or input box (0 or 1)
// Button Action: Set variable vDisplayMode to '1' (or toggle)
// KPI expression with toggle
If(vDisplayMode = 0,
// Mode 0: Current Year Performance
Sum({$<Year={$(=Max({1}Year))}>} Revenue),
// Mode 1: Prior Year Performance
Sum({$<Year={$(=Max({1}Year)-1)}>} Revenue)
)
// Dynamic label for the KPI title
If(vDisplayMode = 0,
'Revenue ' & Max({1}Year),
'Revenue ' & (Max({1}Year)-1)
)
// Variables set by user input or app logic
LET vRegionFilter = 'North America';
LET vChannelFilter = 'Online';
LET vProductFilter = '*'; // wildcard = all
// Single expression, parametrized via variables
Sum({$<
Region = {'$(vRegionFilter)'},
Channel = {'$(vChannelFilter)'},
Product = {'$(vProductFilter)'}
>} Revenue)
Nesting brings Set Analysis to its full power: P() and E() element functions can be composed with operators and modifiers, enabling cohort detection, cross-field filtering, and multi-dimensional analysis that would be impossible with simple modifiers.
Returns the set of field values that exist in the qualifying records of a set expression. Used to bring associated values into a modifier.
// Customers who bought in 2024
CustomerID = P({<Year={2024}>} CustomerID)
Returns values that are excluded from the qualifying set. The logical complement of P(). Essential for "never bought" or churned customer patterns.
// Customers who never bought in 2024
CustomerID = E({<Year={2024}>} CustomerID)
P() inside P() - each level further restricts the cohort. Read inside out:
// Level 1: Customers who purchased Product A
// Level 2: From those, who also purchased Product B
// Count of those cross-buy customers in current selections
Count(
{$<CustomerID =
P({$<Product={'B'}>}
P({$<Product={'A'}>} CustomerID)
)
>}
DISTINCT CustomerID
)
// 3-level: Customers in Strategic tier, who bought in Q4,
// and who also bought in Q1 of the following year (loyalty signal)
Count(
{$<CustomerID =
P({$<Quarter={1},Year={$(=Max({1}Year))}>}
P({$<Quarter={4},Year={$(=Max({1}Year)-1)}>}
P({$<Tier={'Strategic'}>} CustomerID)
)
)
>}
DISTINCT CustomerID
)
// Customers who bought A OR B (Union)
Count({$<CustomerID =
P({$<Product={'A'}>} CustomerID)
+
P({$<Product={'B'}>} CustomerID)
>} DISTINCT CustomerID)
// Customers who bought A AND B (Intersection)
Count({$<CustomerID =
P({$<Product={'A'}>} CustomerID)
*
P({$<Product={'B'}>} CustomerID)
>} DISTINCT CustomerID)
// Customers who bought A but NOT B (Difference)
Count({$<CustomerID =
P({$<Product={'A'}>} CustomerID)
-
P({$<Product={'B'}>} CustomerID)
>} DISTINCT CustomerID)
TOTAL bypasses chart dimensions but does not bypass Set Analysis. Combining them enables percentage-of-total calculations with full filter awareness:
// % share of each product within current selections
// (TOTAL still respects Set Analysis filters)
Sum({$} Revenue) /
Sum(TOTAL {$} Revenue)
// % of YTD sales per region, out of full YTD total
Sum({$<Year={$(=Max({1}Year))}>} Revenue) /
Sum(TOTAL {$<Year={$(=Max({1}Year))}>} Revenue)
// % vs unfiltered total (ignores ALL selections)
Sum({$} Revenue) /
Sum(TOTAL {1} Revenue)
TOTAL <DimA> computes the total while keeping DimA in scope and removing all other dimensions. This lets you calculate "% of dimension A subtotal" rather than grand total. Example: Sum(TOTAL <Region> {$} Revenue) returns the regional total regardless of other chart dimensions.
P() does not have to return the same field it filters on. This enables cross-field cohort bridges:
// Products purchased by customers in 'Strategic' tier
// (bridge: Customer filter โ Product set)
Sum({$<ProductID =
P({$<CustomerTier={'Strategic'}>} ProductID)
>} Revenue)
// Orders from cities where we have physical stores
Sum({$<CityID =
P({1<HasPhysicalStore={'Yes'}>} CityID)
>} OnlineRevenue)
// Sales reps who hit quota last year
Count({$<SalesRepID =
P({1<Year={$(=Max({1}Year)-1)},
QuotaAchieved={'Yes'}>} SalesRepID)
>} DISTINCT OpportunityID)
Four battle-tested patterns you can drop into any Qlik Sense project. Each one combines the techniques from both parts of this series.
// === KPI: Current Revenue ===
Sum({$} Revenue)
// === KPI: YoY % Variation (with color-coded formatting) ===
Num(
(Sum({$} Revenue) /
Sum({$<Year={$(=Max({1}Year)-1)}>} Revenue)) - 1,
'+#,##0.0%;-#,##0.0%;0.0%'
)
// === Background color expression (green = positive, red = negative) ===
If(
(Sum({$} Revenue) / Sum({$<Year={$(=Max({1}Year)-1)}>} Revenue)) - 1 > 0,
RGB(34,197,94), // Green
RGB(239,68,68) // Red
)
// === Trend icon ===
If(
(Sum({$} Revenue) / Sum({$<Year={$(=Max({1}Year)-1)}>} Revenue)) - 1 > 0,
'โฒ',
'โผ'
)
// === Active customers: purchased in last 90 days ===
Count(
{$<CustomerID =
P({1<OrderDate={">=$(=Today()-90)"}>}
CustomerID)
>}
DISTINCT CustomerID
)
// === Churned: bought last year but NOT in last 90 days ===
Count(
{$<CustomerID =
P({1<Year={$(=Max({1}Year)-1)}>} CustomerID)
-
P({1<OrderDate={">=$(=Today()-90)"}>} CustomerID)
>}
DISTINCT CustomerID
)
// === Churn rate ===
Num(
Count({$<CustomerID =
P({1<Year={$(=Max({1}Year)-1)}>} CustomerID)
-
P({1<OrderDate={">=$(=Today()-90)"}>} CustomerID)
>} DISTINCT CustomerID)
/
Count({$<CustomerID =
P({1<Year={$(=Max({1}Year)-1)}>} CustomerID)
>} DISTINCT CustomerID),
'#,##0.0%'
)
// === Running cumulative % share ===
// (sort the chart by Revenue descending)
RangeSum(Above(Sum({$} Revenue), 0, RowNo())) /
Sum(TOTAL {$} Revenue)
// === Flag products in top 80% ===
If(
RangeSum(Above(Sum({$} Revenue), 0, RowNo())) /
Sum(TOTAL {$} Revenue) <= 0.8,
'Top 80%',
'Long Tail'
)
// === Revenue from top 80% products ===
Sum({$<ProductID =
P({$<ParetoCategory={'Top 80%'}>} ProductID)
>} Revenue)
// Note: ParetoCategory must be a field in the data model,
// or use a calculated dimension approach in a straight table
// === Actual (from selections) ===
Sum({$<RecordType={'Actual'}>} Amount)
// === Budget (locked - never filtered by user) ===
Sum({1<RecordType={'Budget'},
Year={$(=Max({1}Year))}>} Amount)
// === Attainment % with null guard ===
If(
Sum({1<RecordType={'Budget'}>} Amount) > 0,
Num(
Sum({$<RecordType={'Actual'}>} Amount) /
Sum({1<RecordType={'Budget'}>} Amount),
'#,##0.0%'
),
'No Budget'
)
// === Gap (Actual minus Budget) ===
Sum({$<RecordType={'Actual'}>} Amount)
-
Sum({1<RecordType={'Budget'},
Year={$(=Max({1}Year))}>} Amount)
Set Analysis expressions fail silently - instead of errors, they return 0 or NULL. Here is a systematic approach to isolate exactly where an expression breaks down.
Replace the aggregation with Count() and verify the record count is what you expect before worrying about the measure.
// Replace this...
Sum({$set} Revenue)
// ...with this first
Count({$set} CustomerID)
Add a text box showing the raw expansion result. If the value is wrong, the set cannot work correctly.
// Text box expression
'Max Year: '
& Max({1}Year)
'Today: '
& Today()
'vMyVar: '
& $(vMyVar)
Start with the simplest valid set and add modifiers one at a time. The step that breaks it is the bug.
Count({1} OrderID)
Count({$} OrderID)
Count({$<Region={'US'}>} OrderID)
Count({$<Region={'US'},Year={2024}>} OrderID)
| Mistake | Symptom | Fix |
|---|---|---|
| Hardcoded year in modifier | Correct today, wrong next year | Use {$(=Max({1}Year))} |
| Missing quotes around expression | Returns 0 or ignored | Field={">=$(=Min(Date))"} - curly + double quotes |
| Set 1 when set $ was intended | Ignores all user selections | Check whether you need {1} or {$} as identifier |
| If() inside aggregation for toggle | Toggle appears to work but uses wrong set context | Move If() outside - wrap the entire aggregation |
| P() on a field with no association | Always returns empty or full set | Verify the data model - fields must be in related tables |
| Variable with quotes inside modifier | Syntax error or wrong filter value | Test the raw expansion: 'vMyVar = ' & vMyVar |
Forgetting {1} inside P() for global cohort |
Cohort changes with user selections | P({1<...>} Field) for a fixed reference cohort |
| TOTAL without Set Analysis on % expressions | Denominator changes unexpectedly with selections | Add explicit set to TOTAL: Sum(TOTAL {$} Revenue) |
{1}, {$}, {$_1}, named bookmarks=), expression-derived, and wildcards$(#var) for locale-safe numbers