Bester Ansatz zum Auffüllen der Datumsdimensionstabelle

8

Ich möchte eine Datumsdimensionstabelle in eine SQL Server 2008-Datenbank einfügen. Die Felder in der Tabelle lauten wie folgt:

[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)

Ich habe eine Funktion DateListInRange (D1, D2) geschrieben, die alle Daten zwischen zwei Parameterdaten D1 und D2 einschließlich zurückgibt.

dh. Die Parameter '2014-01-01' und '2014-01-03' würden Folgendes zurückgeben:

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

Ich möchte die DATE_DIM-Tabelle für alle Daten innerhalb eines Bereichs füllen, dh 2010-01-01 bis 2020-01-01. Die meisten Felder können mit den Funktionen SQL 2008 DATEPART, DATENAME und YEAR ausgefüllt werden.

Die Steuerdaten enthalten etwas mehr Logik, von denen einige voneinander abhängig sind. Zum Beispiel: Geschäftsquartal 1 -> Geschäftsmonat muss 1, 2 oder 3 sein Geschäftsquartal 2 -> Geschäftsmonat muss 4, 5 oder 6 sein

Ich kann leicht eine Tabellenwertfunktion schreiben, die ein bestimmtes Datum akzeptiert und dann alle Steuerdaten oder sogar ALLE Felder sogar ausgibt. Dann würde ich nur diese Funktion benötigen, um in jeder Zeile der DateListInRange-Funktion ausgeführt zu werden.

Die Geschwindigkeit ist mir nicht besonders wichtig, da diese nur einige Male im Jahr ausgefüllt werden muss, wenn die Feiertagstabelle geändert wird.

Was ist der beste Weg, dies in SQL zu schreiben?

Derzeit ist es so:

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')

Wenn ich das Gleiche für die Steuerdaten mache, wird es in jedem Fall einige Wiederholungen geben. Eine Aussage könnte vermieden werden, wenn eine Funktion verwendet wird, und möglicherweise die TVF über die Liste der Daten hinweg anwenden.

Bitte beachten Sie, dass ich SQL Server 2008 verwende, sodass viele neuere Datumsfunktionen nur minimal sind.

JohnLinux
quelle

Antworten:

12

UPDATE : Ein allgemeineres Beispiel zum Erstellen und Auffüllen eines Kalenders oder einer Dimensionstabelle finden Sie in diesem Tipp:

Für die spezifische Frage hier ist mein Versuch. Ich werde dies mit der Magie aktualisieren, mit der Sie Dinge wie Fiscal_MonthNumber und Fiscal_MonthName bestimmen, da sie derzeit der einzige nicht intuitive Teil Ihrer Frage sind und die einzigen konkreten Informationen, die Sie tatsächlich nicht aufgenommen haben.

Die "beste" (sprich: effizienteste) Methode zum Auffüllen einer Kalendertabelle, IMHO, ist die Verwendung eines Satzes anstelle einer Schleife. Und Sie können diesen Satz generieren, ohne die Logik in benutzerdefinierte Funktionen zu vergraben, die Ihnen wirklich nichts anderes als die Kapselung bringen - ansonsten ist es nur ein weiteres zu wartendes Objekt. Ich spreche in dieser Blogserie viel ausführlicher darüber:

Wenn Sie Ihre Funktion weiterhin verwenden möchten, stellen Sie sicher, dass es sich nicht um eine Tabellenwertfunktion mit mehreren Anweisungen handelt. das wird überhaupt nicht effizient sein. Sie möchten sicherstellen, dass es inline ist (z. B. eine einzelne RETURNAnweisung und keine explizite @tableDeklaration), WITH SCHEMABINDINGrekursive CTEs hat und nicht verwendet. Außerhalb einer Funktion würde ich Folgendes tun:

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.

Wenn die Tabelle vorhanden ist, können Sie ab einem beliebigen Startdatum eine einzelne, satzbasierte Einfügung von Daten aus so vielen Jahren durchführen, wie Sie möchten. Geben Sie einfach das Startdatum und die Anzahl der Jahre an. Ich verwende eine "gestapelte CTE" -Technik, um Redundanz zu vermeiden, und führe eine ganze Reihe von Berechnungen nur einmal durch. Die Ausgabespalten der früheren CTEs werden später in weiteren Berechnungen verwendet.

-- 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;

Jetzt müssen Sie noch diese Spalten "Feiertag" und "Arbeitstag" bearbeiten - dies wird etwas umständlicher, aber Sie müssen diese drei Spalten mit allen Feiertagen aktualisieren, die in Ihrem Datumsbereich angezeigt werden. Dinge wie der Weihnachtstag sind wirklich einfach:

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

Dinge wie Ostern werden viel schwieriger - ich habe hier vor vielen Jahren einige Ideen gebloggt .

Und natürlich müssen Ihre arbeitsfreien Tage, die absolut nichts mit Feiertagen usw. zu tun haben, direkt von Ihnen aktualisiert werden - SQL Server verfügt nicht über eine integrierte Methode, um den Kalender Ihres Unternehmens zu kennen.

Jetzt habe ich mich absichtlich von der Berechnung einer dieser Spalten ferngehalten, weil Sie so etwas wie die Endbenutzer gesagt haben previously preferred fields they can drag and drop- ich bin mir nicht sicher, ob die Endbenutzer wirklich wissen oder sich darum kümmern, ob die Quelle einer Spalte eine echte Spalte ist, eine berechnete Spalte oder stammt aus einer Ansicht, Abfrage oder Funktion ...

Angenommen , Sie tun wollen Einblick in einige dieser Spalten Berechnung auf Wartung zu erleichtern (und bestehen bleiben , sie zu bezahlen Speicher für Abfragegeschwindigkeit), können Sie in diesem Blick. Nur als Warnung können einige dieser Spalten jedoch nicht als berechnet und beibehalten definiert werden, da sie nicht deterministisch sind. Hier ist ein Beispiel und wie man es umgeht.

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

Ergebnisse:

Meldung 4936, Ebene 16,
Status 1, Zeile 130 Die berechnete Spalte 'DayOfWeek_Number' in der Tabelle 'Test' kann nicht beibehalten werden, da die Spalte nicht deterministisch ist.

Der Grund, warum dies nicht beibehalten werden kann, ist, dass viele datumsbezogene Funktionen von den Sitzungseinstellungen des Benutzers abhängen, wie z DATEFIRST. SQL Server kann die obige Spalte nicht beibehalten, da sie DATEPART(WEEKDAYbei gleichen Daten unterschiedliche Ergebnisse für zwei verschiedene Benutzer liefern sollte, die zufällig unterschiedliche DATEFIRSTEinstellungen haben.

Dann könnten Sie schlau werden und sagen, nun, ich kann es auf die Anzahl der Tage einstellen, Modulo 7, versetzt von einem Tag, von dem ich weiß, dass es ein Samstag ist (sagen wir '2000-01-01'). Also versuchst du:

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

Aber der gleiche Fehler.

Anstatt eine implizite Konvertierung aus einem Zeichenfolgenliteral zu verwenden, das eine Datums- und Uhrzeitangabe in einem eindeutigen Format (für uns, jedoch nicht für SQL Server) darstellt, können wir die Anzahl der Tage zwischen dem "Nulldatum" (1900-01-01) und verwenden Dieses Datum, das wir kennen, ist ein Samstag (2000-01-01). Wenn wir hier eine Ganzzahl verwenden, um den Unterschied in Tagen darzustellen, kann sich SQL Server nicht beschweren, da es keine Möglichkeit gibt, diese Zahl falsch zu interpretieren. Das funktioniert also:

-- 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
);

Erfolg!

Wenn Sie für einige dieser Berechnungen berechnete Spalten verwenden möchten, lassen Sie es mich wissen.

Oh, und noch eine letzte Sache: Ich weiß nicht, warum Sie diesen Tisch jemals schrubben und von Grund auf neu füllen würden. Wie viele dieser Dinge werden sich ändern? Ändern Sie Ihr Geschäftsjahr ständig? Ändern Sie, wie Sie März buchstabieren möchten? Stellen Sie Ihre Woche so ein, dass sie am Montag einer Woche und am Donnerstag der nächsten beginnt? Dies sollte wirklich eine einmalige Tabelle sein, und dann nehmen Sie kleinere Änderungen vor (z. B. das Aktualisieren einzelner Zeilen mit neuen / geänderten Urlaubsinformationen).

Aaron Bertrand
quelle