Bidirectional relationships: when they silently corrupt totals
Why 'both directions' cross-filtering on a dimension breaks counts and totals, how to see it in a report, and what to use instead.
6 min read · updated 2026-09-21
What it is
A relationship normally filters one way: the dimension filters the fact. Setting cross-filter direction to Both lets the fact filter the dimension too. It looks harmless in Model view and it is the first thing people reach for when a slicer 'does not filter the other slicer'.
The problem is not the slicer. It is every measure that now sees a dimension whose rows have been reduced by another fact table, or by a second dimension through the fact.
How it breaks
Take DimCustomer filtered both ways from a Sales fact and a Budget fact. A card showing COUNTROWS(DimCustomer) no longer counts customers: it counts customers with sales in the current selection. Filter a product and the customer count changes even though the customer table did not.
Worse, two facts filtering the same dimension both ways create ambiguity. The engine picks a path, the path can change when you add a table, and the total on a matrix stops matching the sum of its rows. These bugs survive testing because every individual row looks plausible.
Bidirectional filters also make every query slower: the engine has to propagate filters through the fact table, which is the largest table in the model.
What to use instead
For slicer sync ('only show products that this customer bought'), keep the relationship single-direction and put a measure on the slicer's visual-level filter: `Products bought = COUNTROWS(Sales)` with the filter 'is not blank'. That gives the same behaviour without touching the model.
For a specific measure that genuinely needs the reverse path, use CROSSFILTER inside CALCULATE. The change is scoped to that measure, documented in its DAX, and invisible to the rest of the model.
For many-to-many between two dimensions, build a bridge table and filter it single-direction from both sides, or move the attribute onto the fact.
Customers with sales = CALCULATE ( COUNTROWS ( DimCustomer ), CROSSFILTER ( Sales[CustomerKey], DimCustomer[CustomerKey], BOTH ) )
What the audit checks
The static audit flags every relationship set to Both, with a higher severity when the relationship touches a fact table or when two bidirectional paths reach the same dimension. The AI review then reads the measures that depend on those tables and tells you which totals are actually affected.