codingfreaks

codingfreaks

Experiencing Microsoft

  • Archive
  • Tools
  • About
  • Privacy
  • RSS-Feed
  • Github
  • Youtube

Kalenderwochen in T-SQL

Alexander Schmidt  |  July 14, 2013

Da war wieder mal so ein scheinbr billiges Problem, das mir ein wenig Kopfzerbrechen bereitet hat. Die Lösung könnte dem ein oder anderen bei Abfragen im SQL-Server helfen.


Vorbemerkungen

Möchte man in T-SQL ähnlichen Komfort für Datums-Funktionen haben, wie z.B. in C#, so scheitert man immer mal wieder an scheinbar einfachen Aufgaben. Meine Aufgabe war es, die Datumswerte einer Kalenderwoche (also 7 Stück) über ein Resultset verfügbar zu machen. Fangen wir also zur Erklärung am besten mit dem Ergebnis an:

Abb. 1 So soll es mal funktionieren
Abb. 1 So soll es mal funktionieren

Man kann also erkennen, dass es etwas gibt, dass GetWeekdaysForWeek heißt (bei mir wird das eine Tabellenwert-Funktion), dass sich dies in einem Schema „Utils“ befindet und dass es ein Datum entgegen nimmt. Als Ergebnis bekomme ich ein Resultset, dass als einzige Spalte „DayValue“ anbietet und das immer genau 7 Zeilen liefert. Jede Zeile entspricht hier einem Tag der Kalenderwoche (Ich habe die Abfrage übrigens am 14.07.2013 ausgeführt).

Wer sich das Beispiel genauer ansieht, wird feststellen, dass Montage als erster Tag der Woche angesehen werden. Eine weitere Anforderung ist also, dass mein Tool mit ISO-Datumsfunktionen klar kommt und nicht das amerikanische Modell fährt.

Theorie

Wo ist hier aber nun das Problem? Das Problem sind ein paar Einschränkungen in den T-SQL-Funktionen. Nähern wir uns dem Thema mit ein wenig Grundlagen. Wir könnten uns der Lösung von 2 Seiten her nähern. Die eine wäre, den aktuellen Tagesoffset zu nehmen (z.B. 4, wenn heute ein Donnerstag wäre) und dann die Differenz zum Monat (also 3) zu errechnen. Wir könnten dann ein Result in einer Schleife aufbauen. Man merkt eigentlich bereits beim Lesen, dass das nicht so der Hammer ist und einige Unwägbarkeiten versteckt sind.

Die andere Lösung wäre es, die Kalenderwochen-Nummer zu ermitteln und dann irgendwie rauszubekommen, was der erste Tag dieser Woche ist. Das ist eigentlich der intuitivere Ansatz und war daher auch mein Ziel.

Schritt 1: Kalenderwoche ermitteln

Zunächst möchte ich also die Wochennummer der zu einem Datum (wir nehmen mal GETDATE(), also Heute) ermitteln:

Listing 1
SELECT GETDATE() AS Heute, DATEPART(wk, GETDATE()) AS KW;

Ergebnis:

Ergebnis
Heute                   KW
----------------------- -----------
2013-07-14 15:30:06.540 29

Gut, dass heute ausgerechnet der 14.07.2013 und damit ein Sonntag ist. Das Ergebnis stimmt nämlich nicht, wei ein Blick in den Outlook-Kalender zeigt:

Abb. 2 Outlook-Kalender
Abb. 2 Outlook-Kalender

Profis wissen natürlich sofort, dass die Differenz daher rührt, dass ein amerikanisches Produkt, wie SQL Server standardmäßig immer wie „zu Hause“ rechnet. In den USA ist die KW eines Jahres immer die mit dem 01. Januar. Bei uns wird ISO benutzt, und das heißt grob, dass die Woche mit dem ersten Donnerstag die Kalenderwoche 1 ist. (oder anders die Woche, die als erste mindestens 4 Tage im neuen Jahr hat).

Dafür hat T-SQL mittlerweile etwas an Bord:

Listing 2
SELECT GETDATE() AS Heute, DATEPART(isowk, GETDATE()) AS KW;

Ergebnis:

Ergebnis
Heute                   KW
----------------------- -----------
2013-07-14 15:30:06.540 28

Soweit, so gut. Doch, wie komme ich nun an den ersten Tag (also den Montag) dieser Woche?

Schritt 2: Den Montag ermitteln

Ich hatte die Idee, eine Kombination der Funktionen DATEDIFF und DATEADD zu nutzen. Sehen wir uns mal folgendes an.

Listing 3
SELECT DATEDIFF(ww, '0001-01-01', GETDATE()) AS 'Wochen seit 01.01.0001';

Ergebnis:

Ergebnis
Wochen seit 01.01.0001
----------------------
5924 

Seit dem 01.01.0001 sind also insgesamt 5924 Wochen vergangen. Wenn ich dieses Ergebnis nun wiederum an DATEADD gebe:

Listing 4
SELECT DATEADD(ww, DATEDIFF(ww,'0001-01-01', GETDATE()), '0001-01-01') AS Montag;

Ergebnis:

Ergebnis
Montag
-----------------------
2013-07-15 00:00:00.000

bekomme ich ein Datum zurück, weil DATEADD als dritten Parameter die 0 bekommen hat. Diese steht in diesem Fall für den Anfang der Zeitzählung. Der SQL-Server wird also von Sekunde 0 an 5924 Wochen dazurechnen und landet in der ersten Sekunde des Montags. Das blöde ist nur, dass es der falsche Montag ist, weil hier ja nun meine ISO-Zählung nicht mit drin ist.

Schritt 3: ISO-Erkenntnisse nutzen

Der nächste Schritt ist also, dem SQL-Server unsere in Schritt 1 gesammelten Erkenntnisse nun wieder „überzuhelfen“:

Listing 5
SELECT DATEADD(ww, DATEDIFF(ww, 0, GETDATE()) - (DATEPART(wk, GETDATE()) - DATEPART(isowk, GETDATE())), 0) AS Montag;

Ergebnis:

Ergebnis
Montag
-----------------------
2013-07-08 00:00:00.000

Der Trick ist ganz einfach der, dass wir dem Null-Datum nicht einfach nur die vergangenen Wochen, sondern die verganenen Wochen abzüglich der Wochendifferenz zwischen ISO- und NichtISO-Berechnung übergeben.

Schritt 4: Ein Resultset aufbauen

Der Rest ist nun relativ simpel und macht sich eine UNION-Abfrage zunutze:

Listing 6
WITH WeekDays AS
(
    SELECT DATEADD(ww, DATEDIFF(ww, 0, GETDATE()) - (DATEPART(wk, GETDATE()) - DATEPART(isowk, GETDATE())), 0) DayValue
    UNION ALL
    SELECT  DayValue + 1
    FROM    WeekDays
    WHERE   DayValue + 1 <= DATEADD(ww, DATEDIFF(ww, 0, GETDATE()) - (DATEPART(wk, GETDATE()) - DATEPART(isowk, GETDATE())), 6)
)
SELECT DayValue
FROM WeekDays

Wir bauen zunächst eine Pseudo-Tabelle „WeekDays“ auf. Sie beginnt mit dem Montag und verknüpft diesen per UNION mit allen folgenden 6 Tagen. Zum Schluss wird das Ergebnis als ein Result-Set von „WeekDays“ ermittelt.

Ergebnis
DayValue
-----------------------
2013-07-08 00:00:00.000
2013-07-09 00:00:00.000
2013-07-10 00:00:00.000
2013-07-11 00:00:00.000
2013-07-12 00:00:00.000
2013-07-13 00:00:00.000
2013-07-14 00:00:00.000

Eine Tabellenwert-Funktion rundet das ganze dann wiederverwendbar ab:

Listing 7
CREATE FUNCTION [Util].[GetWeekdaysForWeek]
(
    -- Add the parameters for the function here
    @dayInWeek datetime
)
RETURNS TABLE
AS
RETURN
(
    WITH WeekDays AS
    (
        SELECT DATEADD(ww, DATEDIFF(ww, 0, GETDATE()) - (DATEPART(wk, GETDATE()) - DATEPART(isowk, GETDATE())), 0) DayValue
        UNION ALL
        SELECT  DayValue + 1
        FROM    WeekDays
        WHERE   DayValue + 1 <= DATEADD(ww, DATEDIFF(ww, 0, GETDATE()) - (DATEPART(wk, GETDATE()) - DATEPART(isowk, GETDATE())), 6)
    )
    SELECT DayValue
    FROM WeekDays
)

Nachdem man dieses SQL ausgeführt hat (bitte beim Schema „[Util]“ aufpassen und dieses entweder anpassen oder in der eigenen DB verfügbar machen), kann man zukünftig per

Listing 8
SELECT * FROM [Util].[GetWeekdaysForWeek](GETDATE());

schnell das richtige Ergebnis ausgeben und benutzen.

Einsatzzweck

Wer sich nun immer noch fragt, wofür man diesen ganzen Aufwand eigentlich braucht, der sei darauf verwiesen, dass man öfter mal eine Auswertung über einen Zeitraum fahren möchte. Ein Diagramm, wie das folgende

Abb. 3 Einsatz-Szenario
Abb. 3 Einsatz-Szenario

kann nun direkt über ein T-SQL-Kommando befüllt werden, was ohne die hier gezeigten Tricks eher schwierig wäre.


Alexander Schmidt

Written by Alexander Schmidt who lives and works in Magdeburg building useful things. You should follow him on Youtube