When the metric's meaning changes under you
The most dangerous performance fix in business intelligence is the one that also, quietly, changes what the numbers mean — because nobody is looking for that, and the dashboard doesn't announce it.
A faster dashboard is supposed to be an unambiguous win. Sometimes it is. But the most dangerous performance fix in business intelligence is the one that also, quietly, changes what the numbers mean - because nobody is looking for that, and the dashboard doesn't announce it.
A 1000x win that moved every number
The model was a relationship-intelligence system - a passive CRM built from communication metadata - and its client-detail pages were painfully slow. The measures behind them ran off a pairwise table recording every internal-person-to-external-contact pairing per interaction, reached over DirectQuery through a composite model, with a pile of calculated columns doing per-row classification the source tables didn't provide. Repointing those measures onto an imported fact table - one row per interaction per client, with the classification flags (is email, is inbound, has key client) precomputed as real columns - collapsed every measure into a plain sum or row count over something the in-memory engine chews through instantly. My own note at the time, verbatim: "ok this is loading 1000x faster." The second half of that sentence was the discovery that a few stragglers were still pointed at the old tables - and, once everything moved, that every card total on the page had dropped.
The instinct is to treat a number that changed as a number that broke. This one didn't break. It started answering a different question.
The drop was correct
Do the arithmetic on one email. Two people from our side, three contacts on the client side: the pairwise table records that single email up to six times - every internal-by-external combination. Cards built by counting those rows answer "how many person-to-person touches involved this client." The new fact records the same email once for the client, so the same cards now answer "how many interactions happened with this client." Both are defensible metrics. They are not the same metric, and on a busy thread they differ by a factor of six.
-- Before: one row per (internal person × client contact) per interaction
Total Interactions = COUNTROWS ( interaction_pairs )
-- After: one row per interaction per client
Total Interactions = COUNTROWS ( engagement_fact )
Both compile. Both render. Both are "right." They don't measure the same thing.
Dimensional modeling has had a name for this trap for decades. A many-to-many association like participants-per-interaction is what Kimball handles with a multivalued-dimension bridge table, and the literature's standing warning is exactly this failure: group facts through a multivalued bridge and you over-count unless a weighting factor allocates each row's share. Our pairwise table was a bridge being used as a fact, with no weights - every count query paid the full multiplicity. The performance rewrite fixed the grain, and fixing the grain moved the number. The volume didn't fall because data was lost; it fell because the definition moved from "was on the thread" to "the interaction itself."
One residue survives even in the new grain, and it's worth documenting rather than discovering: an interaction touching two key clients still lands once per client, so per-client slices are exact but the all-clients grand total counts it twice. That's a modeling decision - and the difference between a modeling decision and a bug is whether anyone said it out loud.
The danger isn't the change. It's shipping the speed-up and letting every stakeholder assume the definition held.
A measure has two properties that change independently: how fast it computes, and what it means. Optimize the first, and check whether you moved the second.
There is a class of speed-up that genuinely doesn't move meaning, and it's useful to know its shape: Power BI's user-defined aggregations cache pre-aggregated data behind the model and transparently redirect queries to it - the aggregation table is hidden, and consumers only ever address the detail table. Same answers, faster. That's the bar a "pure performance fix" has to clear. The moment a rewrite changes attribution rules - pair-grain to interaction-grain, every-participant to owner - it has left that category and become a redefinition wearing a performance fix's clothes.
Hidden filter context travels with the old source
Repointing a measure also detaches it from context the old source was silently supplying - and those breaks are quiet too. Three surfaced here, in escalating subtlety.
The thirty-day window wasn't where everyone thought it was. The cards were windowed not by the measures but by a page filter on the old table's period-type column - the pairwise table carried a period_type = '30day' attribute and the page filtered on it. The repointed measures fell entirely outside that filter's scope: no error, no warning, cards silently showing all-time totals. The fix was to re-home the window where it belongs - a relative-date page filter on the Date dimension - so it survives any future fact swap too.
Two visuals started repeating one number. They grouped by a participants dimension that had a relationship to the old fact and none to the new one. Filters stopped propagating, and every person's row showed the identical total. This is documented Power BI behavior, not an exotic bug - group by a table that can't filter the fact table and, in the docs' own words, you get "a column of values that are all the same". The stopgap was honest parallel measures - Pair Volume, Pair Email Count - holding the old pairwise DAX so those visuals keep their original semantics until they're redesigned, under names that admit what they count.
And the visual that didn't break still changed meaning. The per-person table kept working because the new fact reaches the internal-users dimension through an owner key - but that means each interaction now credits its single owning user, where the old grain credited every internal participant. Per-person volume shifted from "was on it" to "owns it." Nothing on the canvas flags that; a leaderboard just quietly reorders itself.
When you re-point a measure, audit every filter and every relationship the old source quietly provided. Each one is a regression waiting to happen with no error to announce it.
Reconcile before stakeholders do
The check that catches all of this before the meeting does is boring and takes an afternoon:
- Run old and new side by side for a handful of entities and one time window, as paired DAX queries. Every gap should be explainable by the definition change, in the predicted direction and rough magnitude - the two-on-three email predicts up to 6:1 on pair-counted cards, and residuals beyond that are actual bugs.
- Check filter-flag parity. Old exclusion columns rarely map one-to-one onto new ones - here the old
is_deletedfilter becamehas_blocked_interaction, same intent, different name and default. A measure that silently stops excluding blocked rows is another meaning change hiding inside the migration. - Sum the slices against the total. Per-client values that add up to more than the grand total quantify the multi-client attribution exactly - that's the number to put in the documentation, not to stumble over later.
Make the semantic change a deliverable
The fix isn't technical, it's communicative. Before the new dashboard reaches anyone, the meaning change has to be stated as loudly as the performance win - like a migration note: the old definition, the new definition, the expected direction and rough size of the shift ("cards will drop; the old number counted a two-on-three email six times"), which visuals changed meaning, and which are definitionally identical. This is the argument the semantic-layer movement makes at the tooling level - one governed definition, changed in one reviewed place instead of drifting per-dashboard - but no tool substitutes for the sentence that tells a stakeholder the number moved on purpose. A KPI whose meaning shifts without anyone saying so is exactly how trust in an entire report dies - one "wait, why is this different from last week" at a time.
The short version
Repointing a measure changes speed and can change meaning. Do the multiplicity arithmetic on one concrete event to see which question each grain answers, audit the filter and relationship context the old source silently supplied, reconcile old against new before anyone else can, and keep honestly-named parallel measures where the old semantics are still wanted. Never let a definition shift ride along quietly inside a performance fix.