Il modello semantico è il punto in cui le scelte di modellazione diventano performance percepita. In Microsoft Fabric l’architettura medallion porta i dati fino al layer gold, ed è lì — non nel modello — che si costruisce lo star schema definitivo.

Perché lo star schema vince

Il motore VertiPaq (e Direct Lake, che legge direttamente i file Delta di OneLake) è ottimizzato per un pattern preciso: poche tabelle dimensione piccole che filtrano una tabella fatti grande.

Design Effetto sul motore Effetto sulle misure
Star schema Un solo hop di filtro fatti↔dimensione, join scan ottimizzato Misure semplici, SUM + filtri naturali
Snowflake Catena di propagazione multi-hop, più join a runtime DAX più fragile, serve risalire più livelli
Tabella piatta Colonne di dimensione ripetute su milioni di righe: compressione peggiore, memoria alta DISTINCTCOUNT costosi, nessun contesto pulito per slicer

La tabella piatta sembra semplice ma esplode la cardinalità delle colonne testuali: la stessa descrizione prodotto ripetuta 50 milioni di volte comprime peggio di una dimensione da 5.000 righe. Denormalizza le dimensioni nel gold: unisci Category e Subcategory dentro DimProduct con una vista o una tabella Delta nel Warehouse/Lakehouse, invece di lasciare lo snowflake al modello. Il costo si paga una volta in pipeline, non a ogni query.

Regole pratiche per il gold layer

  • Ogni dimensione ha una surrogate key intera; evita chiavi stringa o composite (le relazioni Power BI sono su colonna singola).
  • Le colonne testuali stanno nelle dimensioni, le colonne numeriche additive nei fatti.
  • Nascondi le colonne chiave e le colonne fatti grezze: l’utente deve vedere misure, non campi sommabili per errore.
  • In Direct Lake colonne calcolate e tabelle calcolate DAX non sono supportate: tutto ciò che serve va materializzato a monte (inclusa la date table).

Cardinalità e direzione del filtro

La relazione canonica è uno-a-molti dalla dimensione ai fatti, con cross-filter singolo: il filtro scende dal lato “uno” al lato “molti”. È la configurazione predefinita e quella corretta nel 99% dei casi.

Il bidirezionale propaga anche verso l’alto, e introduce due problemi:

  1. Ambiguità: con più percorsi possibili tra due tabelle il motore non sa quale usare; Power BI blocca l’attivazione o genera risultati non deterministici.
  2. Performance: ogni query deve materializzare il filtro inverso sulla dimensione.

Alternative da preferire:

  • CROSSFILTER dentro CALCULATE per attivare il bidirezionale solo nella misura che ne ha bisogno: Clienti attivi = CALCULATE(DISTINCTCOUNT(DimCustomer[CustomerKey]), CROSSFILTER(FactSales[CustomerKey], DimCustomer[CustomerKey], BOTH))
  • Bridge table per relazioni molti-a-molti reali (es. cliente ↔ account): una tabella ponte con le coppie distinte e due relazioni uno-a-molti, invece di una relazione many-to-many diretta.
  • Row-level security: attenzione, il bidirezionale combinato con RLS espande la superficie di filtro e può esporre righe non previste.

Role-playing dimension

Quando FactSales ha OrderDateKey, ShipDateKey e DueDateKey, non duplicare la date table tre volte: crea una relazione attiva (tipicamente su OrderDateKey) e le altre come relazioni inattive (linea tratteggiata). Poi attivale per misura:

Vendite spedite = CALCULATE([Vendite], USERELATIONSHIP(FactSales[ShipDateKey], DimDate[DateKey]))

USERELATIONSHIP funziona solo dentro CALCULATE/CALCULATETABLE ed è incompatibile con RLS applicata a quella tabella. La duplicazione fisica della dimensione resta valida solo quando l’utente deve incrociare due date nello stesso visual.

La date table fatta bene

Requisiti per Mark as date table: colonna di tipo date/datetime, valori univoci, nessun blank, e un intervallo contiguo che copra anni interi (1 gennaio – 31 dicembre) per ogni anno presente nei fatti. Senza questi requisiti DATESYTD, SAMEPERIODLASTYEAR e le altre funzioni di time intelligence restituiscono risultati errati o vuoti.

Disattiva sempre Auto date/time: crea tabelle date nascoste per ogni colonna data, gonfiando il modello. In Direct Lake la date table va generata nel gold layer (SQL o notebook) con colonne già pronte: Year, QuarterName, MonthName, MonthNumber — e ordina le colonne testuali con Sort by column sulla corrispondente numerica.

Trappole tipiche d’esame

  • Scenario: le misure con TOTALYTD restituiscono blank su alcuni anni → Risposta: la date table non è contigua/non è marcata come date table; estendi il calendario ad anni completi e usa Mark as date table.
  • Scenario: serve filtrare la dimensione prodotto in base ai soli prodotti venduti, ma il modello ha già più percorsi → Risposta: non impostare il bidirezionale sulla relazione; usa CROSSFILTER(..., BOTH) nella singola misura.
  • Scenario: il fatto ha tre chiavi data verso DimDateRisposta: una relazione attiva + relazioni inattive attivate con USERELATIONSHIP in CALCULATE, non tre copie della date table.
  • Scenario: modello Direct Lake, chiedono di aggiungere una colonna calcolata FullName alla dimensione → Risposta: non supportato; aggiungila come colonna nella tabella Delta del gold layer (o passa a Import).
  • Scenario: report lento con un’unica tabella piatta da 200 milioni di righe → Risposta: refactor a star schema nel gold, non aggiunta di indici o aggregazioni improvvisate.
  • Scenario: relazione molti-a-molti tra clienti e conti con doppio conteggio degli importi → Risposta: introduci una bridge table con due relazioni uno-a-molti, evitando la relazione many-to-many diretta.