Il miglior approccio per popolare la tabella delle dimensioni della data


8

Sto cercando di popolare una tabella delle dimensioni della data in un database di SQL Server 2008. I campi nella tabella sono i seguenti:

[DateId]                    INT IDENTITY(1,1) PRIMARY KEY
[DateTime]                  DATETIME
[Date]                      DATE
[DayOfWeek_Number]          TINYINT
[DayOfWeek_Name]            VARCHAR(9)
[DayOfWeek_ShortName]       VARCHAR(3)
[Week_Number]               TINYINT
[Fiscal_DayOfMonth]         TINYINT
[Fiscal_Month_Number]       TINYINT
[Fiscal_Month_Name]         VARCHAR(12)
[Fiscal_Month_ShortName]    VARCHAR(3)
[Fiscal_Quarter]            TINYINT     
[Fiscal_Year]               INT
[Calendar_DayOfMonth]       TINYINT
[Calendar_Month Number]     TINYINT     
[Calendar_Month_Name]       VARCHAR(9)
[Calendar_Month_ShortName]  VARCHAR(3)
[Calendar_Quarter]          TINYINT
[Calendar_Year]             INT
[IsLeapYear]                BIT
[IsWeekDay]                 BIT
[IsWeekend]                 BIT
[IsWorkday]                 BIT
[IsHoliday]                 BIT
[HolidayName]               VARCHAR(255)

Ho scritto una funzione DateListInRange (D1, D2) che restituisce tutte le date tra due date di parametro D1 e D2 incluse.

vale a dire. i parametri '2014-01-01' e '2014-01-03' restituiranno:

2014-01-01
2014-01-02
2014-01-03

Voglio popolare la tabella DATE_DIM per tutte le date in un intervallo, ovvero dal 2010-01-01 al 2020-01-01. La maggior parte dei campi può essere popolata con le funzioni DATEPART, DATENAME e YEAR di SQL 2008.

I dati fiscali contengono un po 'più di logica, alcuni dei quali dipendono l'uno dall'altro. Ad esempio: Quarto fiscale 1 -> Il mese fiscale deve essere 1, 2 o 3 Quarto fiscale 2 -> Il mese fiscale deve essere 4, 5 o 6

Posso facilmente scrivere una funzione con valori di tabella che accetta una data specifica e quindi restituisce tutti i dati fiscali o TUTTI i campi. Quindi avrei solo bisogno di questa funzione per funzionare su ogni riga della funzione DateListInRange.

Non mi preoccupo molto della velocità, poiché sarà necessario popolarla solo poche volte l'anno quando il tavolo delle vacanze viene modificato.

Qual è il modo migliore per scrivere questo in SQL?

Attualmente è così:

SELECT 
    [Date],
    CAST([Date] AS DATE)                AS [Date],
    DATEPART(W,[Date])                  AS [DayOfWeek_Number], -- First day of week is sunday
    DATENAME(W,[Date])                  AS [DayOfWeek_Name],
    SUBSTRING(DATENAME(DW,[Date]),1,3)  AS [DayOfWeek_ShortName],
    DATEPART(WK, [Date])                AS [WeekNumber],
    DATEPART(M, [Date])                 AS [Calendar_Month_Number],
    DATENAME(M, [Date])                 AS [Calendar_Month_Name],
    SUBSTRING(DATENAME(M, [Date]),1,3)  AS [Calendar_Month_ShortName],
    DATEPART(QQ, [Date])                AS [Calendar_Quarter],
    YEAR([Date])                        AS [Calendar_Year],

    CASE WHEN
    (
        (YEAR([Date]) % 4 = 0) AND (YEAR([Date]) % 100 != 0) 
        OR
        (YEAR([Date]) % 400 = 0)
    )
    THEN 1 ELSE 0 
    END                                     AS [IsLeapYear],

    CASE WHEN
    (
        DATEPART(W,[Date]) = 1 OR DATEPART(W,[Date]) = 7
    )
    THEN 0 ELSE 1
    END                                     AS [IsWeekDay]
FROM [DateListForRange] 
('2014-01-01','2014-01-31')

Se faccio lo stesso per i dati fiscali, ci sarebbe un bel po 'di ripetizione in ogni caso, la dichiarazione potrebbe essere evitata usando una funzione e forse l'applicazione incrociata del TVF sull'elenco delle date.

Si noti che sto usando SQL Server 2008, quindi molte nuove funzionalità di data sono minime.

Risposte:


12

AGGIORNAMENTO : per un esempio più generico di creazione e popolamento di un calendario o di una tabella di dimensioni, vedere questo suggerimento:

Per la domanda specifica a portata di mano, ecco il mio tentativo. Lo aggiornerò con la magia che usi per determinare cose come Fiscal_MonthNumber e Fiscal_MonthName, perché in questo momento sono l'unica parte non intuitiva della tua domanda, ed è l'unica informazione tangibile che in realtà non hai incluso.

Il modo "migliore" (leggi: più efficiente) per popolare una tabella di calendario, IMHO, è utilizzare un set, piuttosto che un ciclo. E puoi generare questo set senza seppellire la logica in funzioni definite dall'utente, che in realtà non ti fanno guadagnare altro che incapsulamento, altrimenti è solo un altro oggetto da mantenere. Ne parlo in modo molto più dettagliato in questa serie di blog:

Se si desidera continuare a utilizzare la propria funzione, assicurarsi che non sia una funzione con valori di tabella con più istruzioni; non sarà affatto efficiente. Vuoi assicurarti che sia in linea (ad es. Abbia una singola RETURNistruzione e nessuna @tabledichiarazione esplicita ), abbia WITH SCHEMABINDINGe non usi CTE ricorsivi. Al di fuori di una funzione, ecco come lo farei:

CREATE TABLE dbo.DateDimension
(
  [Date]                      DATE PRIMARY KEY,
  [DayOfWeek_Number]          TINYINT,
  [DayOfWeek_Name]            VARCHAR(9),
  [DayOfWeek_ShortName]       VARCHAR(3),
  [Week_Number]               TINYINT,
  [Fiscal_DayOfMonth]         TINYINT,
  [Fiscal_Month_Number]       TINYINT,
  [Fiscal_Month_Name]         VARCHAR(12),
  [Fiscal_Month_ShortName]    VARCHAR(3),
  [Fiscal_Quarter]            TINYINT,     
  [Fiscal_Year]               SMALLINT,
  [Calendar_DayOfMonth]       TINYINT,
  [Calendar_Month Number]     TINYINT,     
  [Calendar_Month_Name]       VARCHAR(9),
  [Calendar_Month_ShortName]  VARCHAR(3),
  [Calendar_Quarter]          TINYINT,
  [Calendar_Year]             SMALLINT, 
  [IsLeapYear]                BIT,
  [IsWeekDay]                 BIT,
  [IsWeekend]                 BIT,
  [IsWorkday]                 BIT,
  [IsHoliday]                 BIT,
  [HolidayName]               VARCHAR(255)
);
-- add indexes, constraints, etc.

Con la tabella in atto, è possibile eseguire un singolo inserimento basato su set di tutti gli anni di dati desiderati da qualsiasi data di inizio scelta. Basta specificare la data di inizio e il numero di anni. Uso una tecnica "CTE in pila" per evitare la ridondanza ed eseguire una sola serie di calcoli una sola volta; le colonne di output dei precedenti CTE vengono successivamente utilizzate in ulteriori calcoli in seguito.

-- these are important:
SET LANGUAGE US_ENGLISH;
SET DATEFIRST 7;

DECLARE @start DATE = '20100101', @years TINYINT = 20;

;WITH src AS
(
  -- you don't need a function for this...
  SELECT TOP (DATEDIFF(DAY, @start, DATEADD(YEAR, @years, @start)))
    d = DATEADD(DAY, ROW_NUMBER() OVER (ORDER BY s1.number)-1, @start)
   FROM master.dbo.spt_values AS s1
   CROSS JOIN master.dbo.spt_values AS s2
   -- your own numbers table works much better here, but this'll do
),
w AS 
(
  SELECT d, 
    wd      = DATEPART(WEEKDAY,d), 
    wdname  = DATENAME(WEEKDAY,d), 
    wnum    = DATEPART(ISO_WEEK,d),
    qnum    = DATEPART(QUARTER, d),
    y       = YEAR(d),
    m       = MONTH(d),
    mname   = DATENAME(MONTH,d),
    md      = DAY(d)
  FROM src
),
q AS
(
  SELECT *, 
    wdsname   = LEFT(wdname,3),
    msname    = LEFT(mname,3),
    IsWeekday = CASE WHEN wd IN (1,7) THEN 0 ELSE 1 END,
    fq1 = DATEADD(DAY,25,DATEADD(MONTH,2,DATEADD(YEAR,YEAR(d)-1900,0)))
  FROM w
),
q1 AS
(
  SELECT *, 
    -- useless, just inverse of IsWeekday, but okay:
    IsWeekend = CASE WHEN IsWeekday = 1 THEN 0 ELSE 1 END,
    fq = COALESCE(NULLIF(DATEDIFF(QUARTER,DATEADD(DAY,6,fq1),d) 
         + CASE WHEN md >= 26 AND m%3 = 0 THEN 2 ELSE 1 END,0),4)
    FROM q
)
--INSERT dbo.DimWithDateAllPersisted(Date)
SELECT 
  DateKey = d,
  DayOfWeek_Number = wd,
  DayOfWeek_Name = wdname,
  DayOfWeek_ShortName = wdsname,
  Week_Number = wnum,
  -- I'll update these four lines when I have usable info
  Fiscal_DayOfMonth      = 0,--'?magic?',
  Fiscal_Month_Number    = 0,--'?magic?',
  Fiscal_Month_Name      = 0,--'?magic?',
  Fiscal_Month_ShortName = 0,--'?magic?',
  Fiscal_Quarter = fq,
  Fiscal_Year = CASE WHEN fq = 4 AND m < 3 THEN y-1 ELSE y END,
  Calendar_DayOfMonth = md,
  Calendar_Month_Number = m,
  Calendar_Month_Name = mname,
  Calendar_Month_ShortName = msname,
  Calendar_Quarter = qnum,
  Calendar_Year = y,
  IsLeapYear = CASE 
    WHEN (y%4 = 0 AND y%100 != 0) OR (y%400 = 0) THEN 1 ELSE 0 END,
  IsWeekday,
  IsWeekend,
  IsWorkday = CASE WHEN IsWeekday = 1 THEN 1 ELSE 0 END,
  IsHoliday = 0,
  HolidayName = ''
FROM q1;

Ora, hai ancora queste colonne "ferie" e "giorni feriali" da affrontare: questo diventa un po 'più ingombrante, ma devi aggiornare quelle tre colonne con tutte le festività che compaiono nell'intervallo di date. Cose come il giorno di Natale sono davvero facili:

UPDATE dbo.DateDimension
  SET IsWorkday = 0, IsHoliday = 1, HolidayName = 'Christmas'
  WHERE Calendar_Month_Number = 12 AND Calendar_DayOfMonth = 25;

Le cose come la Pasqua diventano molto più complicate - ho scritto alcune idee sul blog qui molti anni fa .

E, naturalmente, i giorni non lavorativi della tua azienda che non hanno assolutamente nulla a che fare con i giorni festivi ecc. Devono essere aggiornati direttamente da te - SQL Server non avrà un modo integrato per conoscere il calendario della tua azienda.

Ora, ho volutamente tenuto lontano dal calcolare una di queste colonne, perché hai detto qualcosa come gli utenti finali hanno previously preferred fields they can drag and drop: non sono sicuro che gli utenti finali sappiano davvero o si preoccupino se l'origine di una colonna è una colonna reale, una colonna calcolata o proviene da una vista, query o funzione ...

Supponendo che fare voler esaminare calcolo alcune di queste colonne per facilitare il vostro manutenzione (e persistere per stoccaggio retribuzione per velocità di query), si può guardare in quello. Tuttavia, proprio come un avvertimento, alcune di queste colonne non possono essere definite come calcolate e persistenti perché non sono deterministiche. Ecco un esempio e come aggirarlo.

CREATE TABLE dbo.Test
(
  [date] DATE PRIMARY KEY,
  DayOfWeek_Number AS DATEPART(WEEKDAY, [date]) PERSISTED
);

risultati:

Messaggio 4936, livello 16, stato 1, riga 130
La colonna calcolata "DayOfWeek_Number" nella tabella "Test" non può essere mantenuta poiché la colonna non è deterministica.

Il motivo per cui non è possibile persistere è perché molte funzioni relative alla data si basano sulle impostazioni della sessione dell'utente, come DATEFIRST. SQL Server non può persistere nella colonna sopra perché DATEPART(WEEKDAYdovrebbe dare risultati diversi - dati gli stessi dati - per due utenti diversi che hanno DATEFIRSTimpostazioni diverse .

Allora potresti diventare intelligente, e dire, beh, posso impostarlo come il numero di giorni, modulo 7, offset da un giorno che so essere un sabato (diciamo, '2000-01-01'). Quindi provi:

CREATE TABLE dbo.Test
(
  [date] DATE PRIMARY KEY,
  DayOfWeek_Number AS 
    COALESCE(NULLIF(DATEDIFF(DAY,'20000101',[date])%7,0),7) PERSISTED
);

Ma stesso errore.

Invece di utilizzare una conversione implicita da una stringa letterale che rappresenta una data e ora in un formato non ambiguo (per noi, ma non SQL Server), possiamo usare il numero di giorni tra la "data zero" (1900-01-01) e quella data che conosciamo è un sabato (2000-01-01). Se utilizziamo un numero intero qui per rappresentare la differenza in giorni, SQL Server non può lamentarsi, perché non c'è modo di interpretare erroneamente quel numero. Quindi funziona:

-- SELECT DATEDIFF(DAY, 0, '20000101');  -- 36524

CREATE TABLE dbo.Test
(
  [date] DATE PRIMARY KEY,
  DayOfWeek_Number AS 
    COALESCE(NULLIF(DATEDIFF(DAY,36524,[date])%7,0),7) PERSISTED
    -----------------------------^^^^^  only change
);

Successo!

Se sei interessato a perseguire colonne calcolate per alcuni di questi calcoli, fammi sapere.

Oh, e un'ultima cosa: non so perché dovresti mai pulire questo tavolo e ripopolarlo da zero. Quante di queste cose cambieranno? Hai intenzione di modificare costantemente l'anno fiscale? Cambia il modo in cui vuoi scrivere marzo? Impostare la settimana in modo che inizi lunedì una settimana e giovedì il prossimo? Questa dovrebbe davvero essere una tabella build-it-up-once e quindi apportare piccole modifiche (come l'aggiornamento di singole righe con informazioni sulle vacanze nuove / modificate).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.