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.