Le relazioni come motore del filter context
In un semantic model di Power BI le relazioni non servono solo a “collegare le tabelle”: determinano come il filter context si propaga dalle dimension table verso le fact table quando l’utente interagisce con un visual. Capire cardinalità e direzione del filtro significa capire perché una misura restituisce il numero giusto — o uno sbagliato in modo silenzioso.
Il modello di riferimento è lo star schema: dimension table (Date, Product, Customer) collegate a una o più fact table (Sales). La relazione tipica va dal lato one (la dimension, con chiave univoca) al lato many (la fact, con valori ripetuti).
Cardinalità: quale scegliere e perché
- One-to-many (1:*) — la cardinalità corretta nel 95% dei casi. Il lato one è la dimension con chiave univoca, il lato many è la fact. È la base dello star schema e ciò che il motore VertiPaq ottimizza meglio.
- One-to-one (1:1) — solo quando due tabelle descrivono la stessa entità con chiavi univoche su entrambi i lati (es. tabella anagrafica splittata per motivi di sicurezza o volume di colonne). Spesso è il sintomo che le due tabelle andrebbero unite in Power Query.
- Many-to-many (:) — nessuna delle due colonne è univoca. Va usata solo quando non esiste una dimension bridge naturale (es. account bancari condivisi da più clienti). È tecnicamente diversa dal pattern classico many-to-many risolto con una bridge table, che resta la soluzione preferibile perché mantiene relazioni 1:* pulite e prevedibili.
Il rischio della cardinalità : nativa è che il motore non può garantire l’unicità: aggregazioni come DISTINCTCOUNT o filtri incrociati possono produrre risultati ambigui o gonfiati, senza alcun messaggio di errore.
Direzione del filtro: single è il default sicuro
La direzione del filtro stabilisce da quale lato viaggia il contesto.
- Single (unidirezionale) — il filtro fluisce dalla dimension (one) verso la fact (many). È il default e va mantenuto come regola: rende la propagazione deterministica e le performance stabili.
- Both (bidirezionale) — il filtro viaggia in entrambe le direzioni. Giustificato in casi specifici e circoscritti:
- una bridge table in un pattern many-to-many, per far propagare il filtro attraverso il ponte;
- uno slicer che deve mostrare solo i valori di una dimension effettivamente presenti nella fact (dimension filtering).
Il bidirezionale abilitato “a tappeto” è la causa più frequente di modelli lenti e di misure che restituiscono totali inattesi, perché moltiplica i percorsi di propagazione. Quando serve un comportamento bidirezionale limitato a una singola misura, è preferibile lasciare la relazione su single e forzare la propagazione in DAX con CROSSFILTER(Table1[col], Table2[col], BOTH), mantenendo il resto del modello prevedibile.
Relazioni attive, inattive e USERELATIONSHIP
Tra due tabelle può esistere una sola relazione attiva (linea continua); eventuali relazioni aggiuntive sono inattive (linea tratteggiata). Il caso classico è il role-playing dimension: una tabella Sales con OrderDate e ShipDate che puntano entrambe a Date. Solo una relazione può essere attiva; l’altra si attiva on-demand dentro una misura:
Shipped Amount =
CALCULATE(
SUM(Sales[Amount]),
USERELATIONSHIP(Sales[ShipDate], 'Date'[Date])
)
USERELATIONSHIP è valido solo dentro CALCULATE (o funzioni che lo incapsulano) e attiva la relazione inattiva solo per quel calcolo.
Ambiguità e percorsi multipli
Quando esistono più percorsi per collegare due tabelle, il motore deve scegliere un cammino univoco; se non può, o disattiva automaticamente una relazione o restituisce risultati che non corrispondono all’intuizione dell’utente. Il rischio cresce combinando filtri bidirezionali con cardinalità many-to-many: si creano percorsi circolari e il contesto si propaga in modo non deterministico. La regola difensiva è: relazioni 1:* con filtro single come struttura portante, e ogni deviazione (both o :) come eccezione documentata e testata.
Trappole tipiche d’esame
- Slicer su una dimension che deve mostrare solo valori usati nella fact → non passare tutto il modello a bidirezionale: imposta il filtro Both solo su quella relazione (o usa CROSSFILTER nella misura specifica).
- Due date nella fact (OrderDate e ShipDate) verso una sola Date table → tieni una relazione attiva e attiva l’altra inattiva nella misura con USERELATIONSHIP, non duplicare la Date table se non necessario.
- Il pattern è many-to-many logico (es. Budget a livello di categoria vs Sales a livello di prodotto) → introduci una bridge table con relazioni 1:*, non una relazione : nativa tra le due fact.
- Totali gonfiati o DISTINCTCOUNT anomalo dopo aver abilitato il bidirezionale → sospetta ambiguità da percorsi multipli; torna a single e forza la propagazione puntuale con CROSSFILTER.
- Performance degradate senza errori evidenti → cerca relazioni both e : lasciate come default: sono la causa più comune di rallentamenti silenziosi.