
You've built a data model with a date table. Smart move. You've connected it to your fact table on OrderDate. Also smart. But now your stakeholders want a report that shows sales by ship date side by side with sales by order date — and suddenly your elegant model has a problem. You can only have one active relationship between two tables, and you've already used it. Welcome to one of the most common and most misunderstood challenges in Power BI: working with multiple relationships between the same tables and controlling which direction filters flow.
USERELATIONSHIP and CROSSFILTER are the two DAX functions that solve these problems — but most practitioners use them only halfway. They know how to swap a relationship from inactive to active. What they miss is the deeper mechanic: how filter propagation actually works, why some models need bidirectional filtering, and when forcing it with CROSSFILTER is the right call versus a dangerous one. By the end of this lesson, you'll understand both functions at a level that lets you design and debug complex models with confidence.
Here's what we're building toward: a realistic sales analytics model with a Calendar table connected to a FactSales table through both OrderDate and ShipDate, a many-to-many relationship resolved through a bridge table, and several measures that switch between relationship contexts on the fly. No toy data, no artificial scenarios — just the kind of model you'll actually build on the job.
What you'll learn:
USERELATIONSHIP to activate an alternate relationship inside a specific measure calculationCROSSFILTER to enable, disable, or reverse filter direction within a measureCALCULATE to build measures that handle role-playing dimensions, many-to-many bridge patterns, and complex filter scenariosThis lesson assumes you're comfortable with:
CALCULATE expressions and understanding filter context modificationSUMX, AVERAGEX)If the phrase "CALCULATE replaces the filter context" doesn't immediately make sense to you, spend time with the DAX filter context fundamentals first, then come back here.
Before you can master USERELATIONSHIP and CROSSFILTER, you need a clear mental model of what a relationship actually is inside the VertiPaq engine.
When you create a relationship between two tables, Power BI stores it as a directed link: a one side (the lookup table) and a many side (the fact table). This relationship has two properties that matter here:
Active vs. Inactive — Only one relationship between any two tables can be "active" at a time. The active relationship is the one the engine uses automatically when a filter propagates from one table to another. Inactive relationships exist in the model but are ignored unless you explicitly activate them in a DAX measure.
Cross-filter direction — A relationship can filter in one direction (from the one side to the many side) or in both directions. Single-direction filtering is the default and the safer choice. Bidirectional filtering makes filters flow both ways across the relationship.
The reason only one active relationship is permitted between two tables is disambiguation. If both relationships were active simultaneously, the engine would have two different paths to propagate a filter, and the results would be ambiguous or would require the engine to pick one arbitrarily. The model designer — you — must be explicit about intent.
Think of it this way: the active relationship is the highway. Inactive relationships are service roads. They exist, they're maintained, but traffic doesn't flow on them unless you deliberately redirect it.
Let's build the scenario we'll work with throughout this lesson. Imagine you work for a distribution company. Your FactSales table has the following key columns:
SalesOrderIDCustomerIDProductIDOrderDateShipDateDeliveryDateSalesAmountQuantityYour DimCalendar table has one row per date with columns like Date, Year, Quarter, Month, MonthName, WeekNumber.
Your DimProduct table has ProductID, ProductName, Category, SubCategory.
Your DimCustomer table has CustomerID, CustomerName, Region, Segment.
In your model view, you've created these relationships:
DimCalendar[Date] → FactSales[OrderDate] — Active, single direction (calendar filters sales)DimCalendar[Date] → FactSales[ShipDate] — Inactive, single directionDimCalendar[Date] → FactSales[DeliveryDate] — Inactive, single directionDimProduct[ProductID] → FactSales[ProductID] — Active, single directionDimCustomer[CustomerID] → FactSales[CustomerID] — Active, single directionThis is the classic role-playing dimension pattern. The DimCalendar table plays three different roles: order date calendar, ship date calendar, and delivery date calendar. Your active relationship handles order date. Now let's write measures that use the other two.
The syntax of USERELATIONSHIP is straightforward:
USERELATIONSHIP(<column1>, <column2>)
Both columns must be the two ends of an existing relationship in your model. That relationship can be active or inactive — USERELATIONSHIP works by overriding whatever the current active relationship is between those two tables for the duration of the CALCULATE expression it lives inside.
Here's your first measure:
Sales by Ship Date =
CALCULATE(
SUM(FactSales[SalesAmount]),
USERELATIONSHIP(DimCalendar[Date], FactSales[ShipDate])
)
Drop this into a matrix visual with DimCalendar[Month] on rows. The measure now aggregates SalesAmount by matching each calendar date to ShipDate in the fact table instead of OrderDate. The active relationship is temporarily suspended — DimCalendar stops filtering through OrderDate and filters through ShipDate instead.
Let's add the delivery date version:
Sales by Delivery Date =
CALCULATE(
SUM(FactSales[SalesAmount]),
USERELATIONSHIP(DimCalendar[Date], FactSales[DeliveryDate])
)
And for completeness, the baseline measure that uses the active relationship implicitly:
Sales by Order Date =
SUM(FactSales[SalesAmount])
You can now place all three measures in a single table visual alongside DimCalendar[Year] and DimCalendar[Month] and see the same calendar dimension playing three different analytical roles simultaneously. This is the power of the pattern.
Important:
USERELATIONSHIPonly works insideCALCULATE. It's a filter modifier, not a standalone function. If you try to use it outside ofCALCULATE, you'll get an error. This is by design — it modifies the filter context, and filter context only exists in the context ofCALCULATE.
This is where most practitioners get confused, so let's be precise.
When you write:
Sales by Ship Date =
CALCULATE(
SUM(FactSales[SalesAmount]),
USERELATIONSHIP(DimCalendar[Date], FactSales[ShipDate])
)
...the engine does the following:
DimCalendar and FactSales (the OrderDate one)DimCalendar and FactSales[ShipDate]SUM(FactSales[SalesAmount]) with the ShipDate relationship activeCALCULATE exitsThis means any filter coming from DimCalendar — whether from a slicer, a row in a matrix, or a filter argument — will propagate through the ShipDate relationship during the evaluation of this measure. The OrderDate relationship is completely deactivated for this scope.
What does this mean practically? If you have a date slicer on your report page set to filter DimCalendar[Year] = 2024, then:
Sales by Order Date returns sales where OrderDate falls in 2024Sales by Ship Date returns sales where ShipDate falls in 2024Sales by Delivery Date returns sales where DeliveryDate falls in 2024All from the same slicer, all from the same calendar dimension. That's elegant data modeling.
Once you understand the mechanics, you can build more sophisticated measures. Here's one that calculates the fulfillment gap — the difference in sales volume between when orders were placed versus when they shipped:
Fulfillment Gap =
VAR OrderedSales = SUM(FactSales[SalesAmount])
VAR ShippedSales =
CALCULATE(
SUM(FactSales[SalesAmount]),
USERELATIONSHIP(DimCalendar[Date], FactSales[ShipDate])
)
RETURN
OrderedSales - ShippedSales
This measure, placed in a monthly matrix, will show positive values in months where more was ordered than shipped (backlog building) and negative values where more was shipped than ordered (clearing backlog). You get this insight entirely from one fact table and one calendar dimension, driven by the inactive relationship pattern.
Here's another useful pattern — average days to ship, calculated using both date roles simultaneously:
Avg Days to Ship =
AVERAGEX(
FactSales,
FactSales[ShipDate] - FactSales[OrderDate]
)
This one doesn't need USERELATIONSHIP because it's iterating over the fact table directly and computing a row-level difference. Know when you need these functions and when you don't. USERELATIONSHIP is for when you need a dimension to filter through an alternate relationship — not for row-level date arithmetic.
CROSSFILTER is a different animal. Where USERELATIONSHIP switches which relationship is active, CROSSFILTER controls how a specific relationship propagates filters — and it can even disable a relationship entirely.
The syntax:
CROSSFILTER(<column1>, <column2>, <direction>)
The <direction> argument accepts one of three values:
| Value | Meaning |
|---|---|
NONE |
Disables the relationship for this calculation — no filter propagates |
ONEWAY |
Filter flows only from the one side to the many side (standard behavior) |
BOTH |
Filter flows in both directions (bidirectional) |
Like USERELATIONSHIP, CROSSFILTER is a CALCULATE modifier and must live inside a CALCULATE call.
Let's look at a scenario where CROSSFILTER solves a real problem.
Your model has a DimProduct table and a DimCustomer table. You want to understand which customers have purchased products from which categories. In marketing analytics, this kind of analysis requires traversing from DimCustomer through FactSales to DimProduct.
With single-direction relationships (the default), filters flow:
DimCustomer into FactSales (the active relationship flows this way)DimProduct into FactSales (same)But what if you want to know: "How many distinct customers bought products in the Electronics category?" You need DimProduct to filter FactSales, and then FactSales to filter back to DimCustomer. With standard single-direction filtering, this works because both relationships flow into FactSales. This example doesn't actually need bidirectional filtering.
The case where CROSSFILTER BOTH genuinely helps is when you need the fact table to drive a filter back into a dimension — which is a more exotic pattern. Let's construct it properly.
Suppose you have a ProductBundles bridge table:
BundleIDProductIDAnd a DimBundle table:
BundleIDBundleNameThe relationships are:
DimBundle[BundleID] → ProductBundles[BundleID] — Active, single directionDimProduct[ProductID] → ProductBundles[ProductID] — Active, single directionThis is a many-to-many relationship between bundles and products through a bridge. If you want a measure that counts products in a selected bundle, the standard setup works because filters flow from DimBundle through the bridge to DimProduct.
But if you want to know: "For this selected product, which bundles contain it?" — you need filters to flow from DimProduct through ProductBundles back to DimBundle. That's the reverse direction, and that's where CROSSFILTER helps:
Bundles Containing Product =
CALCULATE(
DISTINCTCOUNT(ProductBundles[BundleID]),
CROSSFILTER(DimProduct[ProductID], ProductBundles[ProductID], BOTH)
)
Warning: Bidirectional filtering introduces ambiguity risk. If your model has multiple paths between two tables, enabling
BOTHon all relationships can create circular filter paths that produce incorrect results. Power BI will sometimes warn you about this, but not always. UseCROSSFILTER BOTHsurgically — inside specific measures where you need it — rather than enabling bidirectional filtering at the relationship level in the model settings.
The NONE option is underused and underappreciated. It completely breaks a relationship for the duration of a CALCULATE expression, meaning no filter from that relationship propagates at all.
When would you need this? Consider a percent-of-total calculation. Normally you'd use ALL() or ALLSELECTED() to remove filters. But in certain models — particularly those with complex relationship webs — you might want to remove the filtering effect of a specific relationship without touching other filter context. CROSSFILTER NONE gives you that surgical precision.
Sales Ignoring Category Filter =
CALCULATE(
SUM(FactSales[SalesAmount]),
CROSSFILTER(DimProduct[ProductID], FactSales[ProductID], NONE)
)
This measure returns total sales as if the DimProduct to FactSales relationship doesn't exist. Any category, subcategory, or product filter from the DimProduct dimension is completely ignored. This can be useful for computing denominators in ratio calculations when you want to compare a segment's sales to the total regardless of what's selected in a product filter.
It's a powerful tool — use it intentionally.
These functions can coexist inside a single CALCULATE call. The key rule is that they operate on different relationships — you can't apply both to the same pair of columns simultaneously.
Here's a realistic scenario: you want to calculate the percentage of revenue that shipped in the same calendar month it was ordered, filtering only through the ship date relationship and ignoring the standard order date relationship:
Same Month Ship Rate =
VAR TotalShipped =
CALCULATE(
SUM(FactSales[SalesAmount]),
USERELATIONSHIP(DimCalendar[Date], FactSales[ShipDate])
)
VAR SameMonthShipped =
CALCULATE(
SUMX(
FactSales,
IF(
FORMAT(FactSales[OrderDate], "YYYY-MM") = FORMAT(FactSales[ShipDate], "YYYY-MM"),
FactSales[SalesAmount],
0
)
),
USERELATIONSHIP(DimCalendar[Date], FactSales[ShipDate])
)
RETURN
DIVIDE(SameMonthShipped, TotalShipped, 0)
This measure shows, for each month on the calendar (filtered by ship date), what percentage of shipped revenue was also ordered in that same month. The calendar slicer filters the ship date. The row-level calculation checks whether the order date matches. Two different date fields, one calendar dimension, one clean measure.
USERELATIONSHIP and CROSSFILTER aren't free. Every time you use them, you're asking the VertiPaq engine to reconfigure the relationship graph for that query. Here's what to keep in mind:
Inactive relationships on large fact tables: When you activate an inactive relationship via USERELATIONSHIP, the engine must scan the alternate join column in the fact table. If your ShipDate column is poorly compressed — for example, if it has many nulls or high cardinality — this can be slow. Ensure your date columns are stored as integer keys (YYYYMMDD format) rather than datetime values for best compression and scan performance.
CROSSFILTER BOTH on large tables: Bidirectional cross-filtering essentially doubles the filter propagation work. For models with millions of rows, enabling BOTH — even inside a measure — can cause query times to spike. Always benchmark your measures with realistic data volumes.
Multiple USERELATIONSHIP calls in a single CALCULATE: Each modifier adds overhead. If you're finding yourself stacking three or four USERELATIONSHIP or CROSSFILTER calls in one expression, step back and reconsider your model design. Sometimes an explicit bridge table or a calculated column in the fact table is a cleaner solution.
Tip: Use DAX Studio's query plan output to see how the engine handles relationship switching. Look for
RelLogOpoperators in the physical query plan — they indicate relationship activation. If you see them appearing in unexpected places, you may have relationship ambiguity issues.
The functions in this lesson are powerful, but they're not always the right answer. Here's a decision framework:
Use inactive relationships + USERELATIONSHIP when:
Use CROSSFILTER when:
Consider an alternative when:
USERELATIONSHIP in more than 80% of your measures — it may be worth making that relationship active and the current active one inactiveBuild the following in a Power BI Desktop file with the schema described in this lesson. You can generate sample data using Power Query's Table.FromRows with 1,000+ rows.
Exercise tasks:
Task 1 — Build the role-playing calendar model:
Create DimCalendar, FactSales with OrderDate, ShipDate, and DeliveryDate. Set up the three relationships with OrderDate as active. Verify in the model view that the inactive relationships show as dashed lines.
Task 2 — Write the three date-role measures:
Write Sales by Order Date, Sales by Ship Date, and Sales by Delivery Date. Place them all in a matrix with DimCalendar[MonthName] on rows. Confirm that the three measures show different values — if they're all identical, check that your sample data actually has variation in the three date fields.
Task 3 — Build the Fulfillment Gap measure:
Write the Fulfillment Gap measure from earlier in this lesson. Create a line chart with months on the X axis and the gap measure on the Y axis. What does a large positive value in a given month tell you about that month's operations?
Task 4 — Experiment with CROSSFILTER NONE:
Write a measure called Sales Ignoring Product Filter using CROSSFILTER NONE on the DimProduct to FactSales relationship. Add it to a table alongside Sales by Order Date and a DimProduct[Category] filter. Verify that the Sales Ignoring Product Filter measure returns the same value regardless of which category is selected. Then add ALL(DimProduct) as an alternative and verify you get the same result. Understand the difference: ALL removes the filter from the column; CROSSFILTER NONE removes the relationship entirely.
Task 5 — Debug a broken measure:
Create a measure that intentionally references USERELATIONSHIP outside of CALCULATE:
Broken Measure = USERELATIONSHIP(DimCalendar[Date], FactSales[ShipDate])
Read the error message. Then fix it by wrapping it in a proper CALCULATE expression.
Mistake 1: Using USERELATIONSHIP with a non-existent relationship
If you write USERELATIONSHIP(DimCalendar[Date], FactSales[ShipDate]) but haven't actually created a relationship between those two columns in the model, you'll get an error. USERELATIONSHIP can only activate relationships that already exist in the model — active or inactive. Go to model view, confirm the relationship is there (even as a dashed inactive line), and then try again.
Mistake 2: Expecting USERELATIONSHIP to work without DimCalendar in the filter context
If no filter from DimCalendar is reaching your measure — no date slicer, no date on the visual axis — then swapping which relationship is active makes no difference. You'll get the same total either way. This confuses people into thinking their measure is broken when it's actually working correctly. Add a date dimension to the visual and you'll see the effect.
Mistake 3: Enabling BOTH direction at the model level without understanding ambiguity
Setting a relationship to bidirectional in the model properties dialog affects all measures, not just the ones you intend. In models with multiple paths between tables (which most non-trivial models have), this can create incorrect totals and unpredictable filter propagation. Use CROSSFILTER BOTH inside specific measures instead of enabling it globally.
Mistake 4: Stacking USERELATIONSHIP and CROSSFILTER on the same relationship
You cannot use both USERELATIONSHIP and CROSSFILTER to modify the same relationship pair within a single CALCULATE expression. One will override the other, and you'll get unexpected behavior. Apply each to a different relationship within the expression.
Mistake 5: Forgetting that USERELATIONSHIP suspends the active relationship
If your visual has a filter from DimCalendar applied (say, a year slicer), and your measure uses USERELATIONSHIP to activate the ship date relationship, the active order date relationship is suspended. If your measure tries to reference FactSales[OrderDate] in a way that depends on the calendar filtering through order date, that filtering is gone. Design your measures to be aware of which relationship is active at which point in the expression.
Debugging tip: Use a simple test measure
When you suspect a relationship issue, write a stripped-down test measure:
DEBUG Ship Date Row Count =
CALCULATE(
COUNTROWS(FactSales),
USERELATIONSHIP(DimCalendar[Date], FactSales[ShipDate])
)
Put this in a matrix with months from DimCalendar. If it returns the same value for every month, your ship date data doesn't vary across months, or the relationship column has nulls, or the relationship isn't set up correctly. Isolate the problem with COUNTROWS before layering in complex aggregations.
You now understand USERELATIONSHIP and CROSSFILTER at a depth that most Power BI practitioners never reach. Let's consolidate what you've built:
USERELATIONSHIP activates an inactive relationship for the scope of a CALCULATE expression, suspending the currently active relationship between those same tables. It's the backbone of the role-playing dimension pattern and the right tool whenever one dimension table needs to connect to multiple fact table columns.
CROSSFILTER controls the direction of filter propagation for any relationship in the model — during a specific calculation. BOTH enables bidirectional filtering surgically. NONE suppresses a relationship entirely. ONEWAY restores single-direction behavior if you need to explicitly reset after another modifier.
Neither function should be used carelessly. Both have performance implications. Both can produce subtle, hard-to-debug errors when model relationships are ambiguous or incorrectly configured.
The right answer to many relationship problems is better model design — inactive relationships, bridge tables, and careful consideration of which relationship should be active by default. These DAX functions are precision tools for the cases where model design alone isn't enough.
Where to go from here:
CROSSFILTER BOTH necessary and learn when a bridge table is a cleaner solution.The measures you build with these functions will separate good Power BI models from great ones. Use the hands-on exercise, get your fingers into the functions, and bring these patterns into your next real project.
Learning Path: DAX Mastery