Tabella Calendario in Power BI: come crearla (DAX, Power Query, 3 metodi)
Una tabella calendario (o date table) è una tabella dedicata con una riga per ogni giorno, senza buchi, collegata alla colonna data della tabella dei fatti e marcata come "tabella delle date" nel modello. Senza di lei le funzioni di time intelligence — TOTALYTD, SAMEPERIODLASTYEAR, DATEADD — restituiscono risultati sbagliati o incompleti. Si crea in tre modi: in DAX con CALENDAR/CALENDARAUTO, in Power Query con M, oppure lasciando fare a Power BI con l'Auto date/time (l'unica opzione da evitare).
Perché non basta la colonna data delle vendite
La tentazione è ovvia: ho già Vendite[Data], ci metto un filtro e via. Il problema è che quella colonna contiene solo i giorni in cui hai venduto qualcosa.
Conseguenze concrete:
- Il 25 dicembre non esiste nel modello. Una media mobile a 7 giorni salta quel giorno e restituisce una media su 6.
SAMEPERIODLASTYEARdeve mappare un periodo sul periodo equivalente dell'anno prima: se l'anno prima quel giorno non c'è, il confronto è vuoto o parziale.- Non hai colonne per slicer sensati (Trimestre, Anno fiscale, Settimana ISO, Weekend sì/no).
- Se hai due tabelle fatti (Vendite e Acquisti) con date diverse, non hai un asse temporale comune su cui filtrarle entrambe.
Le funzioni di time intelligence in DAX assumono una serie continua di giorni. Se la serie ha buchi, il risultato non dà errore: dà un numero sbagliato. È il tipo di bug peggiore.
Metodo 1 — DAX: CALENDAR vs CALENDARAUTO
Modeling → New table. Due funzioni generano l'elenco di date:
CALENDAR(inizio, fine) |
CALENDARAUTO([mese_fine_fiscale]) |
|
|---|---|---|
| Range | Lo decidi tu | Dedotto dalle colonne data del modello |
| Controllo | Totale | Zero |
| Rischio | Dimenticare di estenderlo | Una DataNascita del 1935 ti genera 90 anni di righe |
| Anno fiscale | Gestito a mano | Parametro opzionale (es. CALENDARAUTO(6) per anno luglio–giugno) |
CALENDARAUTO() scansiona tutte le colonne di tipo data/datetime del modello e prende min e max. È comodo finché non hai una colonna anagrafica con date lontane: a quel punto ti ritrovi una tabella da 30.000 righe per un dataset che copre 3 anni. Non è drammatico in termini di memoria, ma sporca tutti gli slicer.
L'approccio più robusto è CALENDAR con il range derivato dalla tabella dei fatti, esteso ad anni interi:
Calendario =
VAR DataMin = DATE ( YEAR ( MIN ( Vendite[Data] ) ), 1, 1 )
VAR DataMax = DATE ( YEAR ( MAX ( Vendite[Data] ) ), 12, 31 )
RETURN
ADDCOLUMNS (
CALENDAR ( DataMin, DataMax ),
"Anno", YEAR ( [Date] ),
"MeseNum", MONTH ( [Date] ),
"NomeMese", FORMAT ( [Date], "mmmm", "it-IT" ),
"MeseBreve", FORMAT ( [Date], "mmm", "it-IT" ),
"TrimestreNum", QUARTER ( [Date] ),
"Trimestre", "Q" & QUARTER ( [Date] ),
"AnnoTrimestre", YEAR ( [Date] ) & "-Q" & QUARTER ( [Date] ),
"AnnoMese", FORMAT ( [Date], "yyyy-mm" ),
"AnnoMeseNum", YEAR ( [Date] ) * 100 + MONTH ( [Date] ),
"GiornoSettimanaNum", WEEKDAY ( [Date], 2 ),
"GiornoSettimana", FORMAT ( [Date], "dddd", "it-IT" ),
"IsWeekend", WEEKDAY ( [Date], 2 ) > 5,
"SettimanaISO", WEEKNUM ( [Date], 21 )
)
Tre dettagli che valgono la pena:
- Il terzo argomento di
FORMATè il locale. Senza"it-IT"i nomi dei mesi seguono la lingua del modello: apri il.pbixsu una macchina in inglese e ti ritrovi "January". Fissare il locale rende il risultato deterministico. WEEKDAY([Date], 2)usa la convenzione europea: lunedì = 1, domenica = 7. Quindi> 5è il weekend.WEEKNUM([Date], 21)è la settimana ISO 8601 (quella che usa il resto d'Europa).WEEKNUMsenza secondo argomento usa la convenzione americana e ti darà numeri diversi da quelli del gestionale.
AnnoMese come testo "2026-03" si ordina correttamente in alfabetico, quindi non serve una sort column. Se invece vuoi il formato "mar 2026", ti serve AnnoMeseNum come colonna di ordinamento.
Anno fiscale (esempio: luglio–giugno)
Aggiungi due colonne dentro lo stesso ADDCOLUMNS:
"AnnoFiscale",
IF ( MONTH ( [Date] ) >= 7, YEAR ( [Date] ) + 1, YEAR ( [Date] ) ),
"MeseFiscaleNum",
IF ( MONTH ( [Date] ) >= 7, MONTH ( [Date] ) - 6, MONTH ( [Date] ) + 6 ),
"TrimestreFiscale",
"FQ" & ROUNDUP (
IF ( MONTH ( [Date] ) >= 7, MONTH ( [Date] ) - 6, MONTH ( [Date] ) + 6 ) / 3,
0
)
Verifica mentale: 15 agosto 2026 → AnnoFiscale 2027, MeseFiscaleNum 2, FQ1. 30 giugno 2026 → AnnoFiscale 2026, MeseFiscaleNum 12, FQ4. Corretto.
Poi ordina NomeMese per MeseFiscaleNum se vuoi che luglio compaia per primo nei visual.
Metodo 2 — Power Query (M)
In produzione preferisco M. Motivo pratico: la tabella diventa una query come le altre, la puoi unire con un file di festività aziendali, riusare via template o dataflow, e — dettaglio non banale — con Direct Lake o modelli Fabric la calendario può vivere come tabella Delta condivisa in OneLake, cosa impossibile con una tabella calcolata DAX.
let
DataInizio = #date(2021, 1, 1),
DataFine = #date(2027, 12, 31),
NumGiorni = Duration.Days(DataFine - DataInizio) + 1,
ElencoDate = List.Dates(DataInizio, NumGiorni, #duration(1, 0, 0, 0)),
Tabella = Table.FromList(ElencoDate, Splitter.SplitByNothing(), {"Date"}),
Tipizzata = Table.TransformColumnTypes(Tabella, {{"Date", type date}}),
Anno = Table.AddColumn(Tipizzata, "Anno", each Date.Year([Date]), Int64.Type),
MeseNum = Table.AddColumn(Anno, "MeseNum", each Date.Month([Date]), Int64.Type),
NomeMese = Table.AddColumn(MeseNum, "NomeMese", each Date.MonthName([Date], "it-IT"), type text),
TrimNum = Table.AddColumn(NomeMese, "TrimestreNum", each Date.QuarterOfYear([Date]), Int64.Type),
Trimestre = Table.AddColumn(TrimNum, "Trimestre", each "Q" & Text.From(Date.QuarterOfYear([Date])), type text),
AnnoMese = Table.AddColumn(Trimestre, "AnnoMese",
each Text.From(Date.Year([Date])) & "-" &
Text.PadStart(Text.From(Date.Month([Date])), 2, "0"), type text),
AnnoMeseN = Table.AddColumn(AnnoMese, "AnnoMeseNum",
each Date.Year([Date]) * 100 + Date.Month([Date]), Int64.Type),
GgSettNome = Table.AddColumn(AnnoMeseN, "GiornoSettimana", each Date.DayOfWeekName([Date], "it-IT"), type text),
GgSettNum = Table.AddColumn(GgSettNome, "GiornoSettimanaNum",
each Date.DayOfWeek([Date], Day.Monday) + 1, Int64.Type),
IsWeekend = Table.AddColumn(GgSettNum, "IsWeekend",
each Date.DayOfWeek([Date], Day.Monday) >= 5, type logical),
AnnoFisc = Table.AddColumn(IsWeekend, "AnnoFiscale",
each if Date.Month([Date]) >= 7 then Date.Year([Date]) + 1 else Date.Year([Date]), Int64.Type),
MeseFisc = Table.AddColumn(AnnoFisc, "MeseFiscaleNum",
each if Date.Month([Date]) >= 7 then Date.Month([Date]) - 6 else Date.Month([Date]) + 6, Int64.Type)
in
MeseFisc
Nota su Date.DayOfWeek([Date], Day.Monday): restituisce 0–6 con lunedì = 0, quindi + 1 per avere 1–7 e >= 5 per sabato/domenica.
Se vuoi un range dinamico invece di date fisse, sostituisci le prime due righe con parametri (Gestione parametri in Power Query) oppure calcola DataFine da DateTime.LocalNow():
DataFine = #date(Date.Year(DateTime.LocalNow()) + 1, 12, 31),
Attenzione: DateTime.LocalNow() non fa query folding e usa il fuso orario del gateway/servizio, non il tuo. Per range dipendenti da "oggi" è più prevedibile DateTime.FixedLocalNow().
Metodo 3 — Auto date/time (da disattivare)
Power BI, di default, crea una mini-tabella data nascosta per ogni colonna di tipo data del modello. Funziona, ma:
- Una tabella nascosta per colonna: se hai DataOrdine, DataSpedizione, DataFattura e DataNascita, sono quattro tabelle di cui non hai controllo.
- Contiene solo Anno/Trimestre/Mese/Giorno. Niente settimana ISO, niente anno fiscale, niente festività.
- Gonfia le dimensioni del
.pbixe il tempo di refresh senza dartene alcun beneficio.
Disattivalo: File → Opzioni → File corrente → Caricamento dati → Data/ora automatica → OFF, e anche nella sezione Globale per i file nuovi.
Mark as date table: perché è obbligatorio
Dopo aver creato la tabella: vista Modello → seleziona la tabella → Contrassegna come tabella delle date → scegli la colonna Date.
Non è un vezzo estetico. Cosa cambia davvero:
- Le funzioni di time intelligence smettono di applicare il filtro implicito su Anno/Trimestre/Mese delle tabelle auto date/time e lavorano sulla tua tabella.
- Power BI valida la colonna: deve essere di tipo date, univoca, senza blank e continua. Se la validazione fallisce, ti dice esattamente cosa manca — è un test gratuito sulla qualità della tabella.
- Le funzioni tipo
DATESYTDassumono che l'ultimo giorno dell'anno sia il 31/12 della tabella. Con anno fiscale non standard passi il parametro (DATESYTD(Calendario[Date], "30/6")), ma serve comunque una date table marcata.
Poi crea la relazione: Calendario[Date] (lato uno) → Vendite[Data] (lato molti), direzione singola.
Errori comuni
Generare la calendario dalle date dei fatti. DISTINCT(Vendite[Data]) o VALUES(Vendite[Data]) produce una tabella con buchi. Fa esattamente il contrario di quello che serve. Usa sempre CALENDAR o List.Dates.
Range che non copre anni interi. Se la tabella parte dal 15/03/2024, i calcoli YTD del 2024 sono sbagliati per costruzione: manca gennaio-febbraio. Sempre dal 1° gennaio al 31 dicembre (o dal primo all'ultimo giorno dell'anno fiscale).
Colonna di tipo datetime invece di date. Un datetime con la componente oraria genera migliaia di valori distinti per giorno: la relazione non è più uno-a-molti pulita, la cardinalità esplode e "Contrassegna come tabella delle date" fallisce. Trasforma in date in Power Query, non con una colonna calcolata.
Sort by column sulla colonna sbagliata. NomeMese ordinato per AnnoMeseNum dà l'errore "Non è possibile ordinare … perché contiene più di un valore distinto": "Gennaio" corrisponde sia a 202601 sia a 202701. La colonna di ordinamento deve avere una relazione 1:1 con quella ordinata. Quindi: NomeMese → MeseNum, AnnoMese → AnnoMeseNum, GiornoSettimana → GiornoSettimanaNum.
Relazioni multiple verso la stessa tabella. Vendite ha DataOrdine e DataSpedizione: puoi collegarle entrambe alla calendario, ma una sola relazione resta attiva. La seconda è tratteggiata e va attivata su richiesta:
Fatturato per Data Spedizione =
CALCULATE (
SUM ( Vendite[Importo] ),
USERELATIONSHIP ( Calendario[Date], Vendite[DataSpedizione] )
)
Se ti serve incrociare i due assi nello stesso visual (spedizioni per mese di ordine), USERELATIONSHIP non basta: devi duplicare la calendario in una seconda tabella (role-playing dimension).
Filtrare la data sulla tabella dei fatti invece che sulla calendario. Scrivere CALCULATE(SUM(Vendite[Importo]), YEAR(Vendite[Data]) = 2026) funziona, ma bypassa la calendario e rompe la coerenza con gli slicer. Una volta che hai una date table, tutti i filtri temporali passano da lì.
Lasciare visibile la colonna data dei fatti. Se Vendite[Data] resta visibile, prima o poi qualcuno la trascina in un visual e ottiene numeri che non tornano con gli altri report. Nascondila.
Se vuoi vedere la modellazione a monte di tutto questo — star schema, relazioni, granularità — il Livello Base del corso Power BI Lab è gratuito e parte da lì; la tabella calendario e la time intelligence sono nel Livello Intermedio (dettagli e prezzi).