Datumsberechnungen wirken harmlos, bis die Abfrage die Datensätze von heute nicht findet, die Kalenderwoche zum Jahreswechsel danebenliegt oder die Summe der Arbeitszeiten plötzlich 07:59 statt 08:00 lautet. Alle drei Fälle haben dieselbe Ursache: Access speichert Datum und Uhrzeit anders, als die angezeigte Darstellung vermuten lässt. Dieser Beitrag zeigt, was dabei schiefgehen kann, und liefert die Funktionen, die Ihnen die üblichen Fallen abnehmen.
Wie Access Datum und Zeit speichert
Ein Wert des Datentyps Date ist intern eine Fließkommazahl vom Typ Double. Für die in heutigen Anwendungen üblichen Datumswerte ab dem 30. Dezember 1899 zählt der Ganzzahlanteil die Tage seit diesem Ausgangsdatum, der Nachkommaanteil gibt die Tageszeit als Bruchteil eines Tages an. Listing 1 macht das sichtbar.
Public Sub DatumAlsZahl() Debug.Print CDbl(#4/5/2026#) Debug.Print CDbl(#4/5/2026 12:00:00 PM#) Debug.Print CDbl(#4/5/2026 6:00:00 AM#) Debug.Print CDbl(#12/30/1899#) End Sub
Die Ausgabe im Direktbereich:
46117 46117,5 46117,25 0
Der 5. April 2026 ist also der Tag Nummer 46117, zwölf Uhr mittags entspricht einem halben Tag, sechs Uhr morgens einem Viertel. Aus dieser Darstellung folgen unmittelbar zwei nützliche Rechenregeln: Ein Tag mehr heißt schlicht + 1, und die Differenz zweier Datumswerte, multipliziert mit 24, ergibt die Stundenzahl dazwischen. Beachten Sie die Datumsliterale im Code. Zwischen den Rautezeichen erwartet VBA das amerikanische Format mit Monat vor Tag. Schreiben Sie #5.4.2026#, wandelt der Editor das stillschweigend in #5/4/2026# um – und das ist der 4. Mai, nicht der 5. April.
Der versteckte Zeitanteil
Genau hier liegt die häufigste Fehlerquelle überhaupt. Ein Feld vom Typ Datum/Uhrzeit, das mit Now gefüllt wurde, enthält eben auch die Uhrzeit. Die Prozedur in Listing 2 führt vor, was das für einen Vergleich bedeutet.
Public Sub ZeitanteilFalle() Dim datGespeichert As Date datGespeichert = #4/5/2026 9:37:12 AM# Debug.Print datGespeichert = #4/5/2026# Debug.Print Int(datGespeichert) = #4/5/2026# Debug.Print DateValue(datGespeichert) = #4/5/2026# End Sub
Hier ist die Ausgabe dieser Prozedur:
Falsch Wahr Wahr
Der erste Vergleich scheitert, weil 46117,4008 nun einmal nicht 46117 ist. Für aktuelle, positive Datumswerte schneiden sowohl Int als auch DateValue den Zeitanteil ab; DateValue drückt die fachliche Absicht deutlicher aus. In einer Abfrage sollten Sie allerdings keine der beiden Funktionen auf das Feld anwenden, weil Access dann für jeden Datensatz rechnen muss und den Index nicht mehr nutzen kann. Formulieren Sie stattdessen einen Bereich, wie ihn foglendes Listing zeigt:
SELECT *
FROM tblAuftraege
WHERE Erfasst >= #2026-04-05#
AND Erfasst < #2026-04-06#
Die obere Grenze steht bewusst auf kleiner als der Folgetag und nicht auf Between. Ein Between #2026-04-05# And #2026-04-06# würde die Datensätze vom 6. April um Punkt Mitternacht mit einschließen, weil Between an beiden Enden einschließend arbeitet.
Datumsangaben in SQL-Ausdrücke einbauen
Setzen Sie eine solche Bedingung per VBA zusammen, brauchen Sie das Datum als Zeichenkette. Ein schlichtes Verketten mit dem Datumswert ist problematisch, weil VBA dabei gewöhnlich ein von den Regionaleinstellungen abhängiges Datumsformat erzeugt und Access den 5. April dann als 4. Mai liest. Die Funktion SqlDatum aus Listing 2 nimmt Ihnen die Umwandlung ab.
Public Function SqlDatum(ByVal varDatum As Variant, Optional ByVal blnMitZeit As Boolean = False) As String If IsNull(varDatum) Then SqlDatum = "Null" ElseIf Not IsDate(varDatum) Then Err.Raise 13, "SqlDatum", "Der übergebene Wert ist kein gültiges Datum." ElseIf blnMitZeit Then SqlDatum = Format$(CDate(varDatum), "\#yyyy\-mm\-dd hh\:nn\:ss\#") Else SqlDatum = Format$(CDate(varDatum), "\#yyyy\-mm\-dd\#") End If End Function
Listing 1: SqlDatum liefert ein Datum in der von Access erwarteten Schreibweise
Die Funktion erzeugt das ISO-Format mit Jahr, Monat und Tag, das Access unabhängig von den Ländereinstellungen richtig auswertet. Die umgekehrten Schrägstriche im Formatausdruck sorgen dafür, dass Raute, Bindestrich und Doppelpunkt als Zeichen ausgegeben und nicht als Formatanweisung gedeutet werden. Beim Doppelpunkt ist das zwingend, denn ohne Maskierung setzt VBA dort das landestypische Zeitzeichen ein.
Die Prüfung mit IsDate weist ungeeignete Werte bereits in der Hilfsfunktion mit einer verständlichen Fehlermeldung zurück. Ohne sie liefe eine versehentlich übergebene Zeichenkette entweder in einen Fehler bei CDate oder in einen SQL-Ausdruck, dessen Ursache sich später nur mühsam zurückverfolgen lässt.
Ein Null-Wert wird zum SQL-Schlüsselwort Null. Das ist bei Wertzuweisungen verwendbar, etwa in einer Aktualisierungsabfrage. In Vergleichsbedingungen muss ein Nullwert dagegen mit Is Null beziehungsweise Is Not Null geprüft werden – ein WHERE Lieferdatum = Null liefert grundsätzlich keine Treffer.
Debug.Print SqlDatum(#4/5/2026#) Debug.Print SqlDatum(#4/5/2026 9:37:12 AM#, True) Debug.Print SqlDatum(Null) #2026-04-05# #2026-04-05 09:37:12# Null
Damit lässt sich der Tagesbereich aus der obigen Abfrage sauber zusammensetzen:
Public Sub Tagesbereich() Dim db As DAO.Database Dim rst As DAO.Recordset Dim strSQL As String Dim datTag As Date datTag = #4/5/2026# strSQL = "SELECT * FROM tblAuftraege WHERE Erfasst >= " _ & SqlDatum(datTag) & " AND Erfasst < " _ & SqlDatum(datTag + 1) Set db = CurrentDb Set rst = db.OpenRecordset(strSQL, dbOpenDynaset) Do While Not rst.EOF Debug.Print rst.Fields(0), rst.Fields(1) rst.MoveNext Loop End Sub
Noch robuster sind parametrisierte Abfragen. Dabei übergibt DAO den Datumswert typisiert, sodass weder das regionale Format noch die Schreibweise des Datumsliterals eine Rolle spielt. Folgendes Listing zeigt, wie das aussieht:
Public Sub TagesbereichParametrisiert() Dim db As DAO.Database Dim qdf As DAO.QueryDef Dim rst As DAO.Recordset Dim datTag As Date datTag = #4/5/2026# Set db = CurrentDb Set qdf = db.CreateQueryDef("", _ "PARAMETERS pVon DateTime, pBis DateTime; " & _ "SELECT * FROM tblAuftraege " & _ "WHERE Erfasst >= pVon AND Erfasst < pBis") qdf.Parameters("pVon") = datTag qdf.Parameters("pBis") = DateAdd("d", 1, datTag) Set rst = qdf.OpenRecordset Do While Not rst.EOF Debug.Print rst.Fields(0), rst.Fields(1) rst.MoveNext Loop End Sub
Der leere Name im Aufruf von CreateQueryDef erzeugt eine temporäre Abfrage, die nicht im Navigationsbereich erscheint. Die beiden Parameter werden mit DateTime deklariert und anschließend als echte Datumswerte zugewiesen – eine Zeichenkette entsteht dabei an keiner Stelle.
Die Kalenderwoche nach ISO 8601
Die eingebaute Funktion DatePart kennt das Intervall ww und liefert damit eine Kalenderwoche. Übergeben Sie zusätzlich die Parameter vbMonday und vbFirstFourDays, entspricht die Wochennummer der Zählung nach ISO 8601. Beide Angaben sind Pflicht: Ohne sie beginnt die Woche am Sonntag und die erste Woche des Jahres mit dem 1. Januar – das ist die amerikanische Zählung und für deutsche Auswertungen unbrauchbar.
Was DatePart dagegen nicht liefert, ist das Wochenjahr. Und genau daran scheitern Auswertungen, wie Listing 6 zeigt.
Public Sub KalenderwocheFalle() Debug.Print Year(#12/31/2007#) & "-KW" _ & DatePart("ww", #12/31/2007#, _ vbMonday, vbFirstFourDays) Debug.Print Year(#1/1/2021#) & "-KW" _ & DatePart("ww", #1/1/2021#, _ vbMonday, vbFirstFourDays) End Sub
Hier ist das Ergebnis der Prozedur:
2007-KW1 2021-KW53
Beide Angaben sind falsch. Der 31. Dezember 2007 gehört zur Woche 1 des Jahres 2008, der 1. Januar 2021 zur Woche 53 des Jahres 2020. Die Wochennummern stimmen also – nur das Jahr passt nicht dazu, weil Year das Kalenderjahr liefert und nicht das Wochenjahr. Ein Jahr 2021 mit einer Woche 53 gibt es überhaupt nicht.
In der Praxis fällt das erst auf, wenn eine Auswertung über den Jahreswechsel läuft. Eine Gruppierung nach Jahr und Woche erzeugt dann Gruppen, die es nicht gibt, und verteilt die Datensätze einer einzigen Woche auf zwei Zeilen.
Die Norm löst das über eine einfache Regel: Eine Woche gehört zu dem Jahr, in dem ihr Donnerstag liegt. Damit lassen sich Wochennummer und Wochenjahr aus derselben Rechnung ableiten. Die Funktion IsoKalenderwoche aus Listing 3 übernimmt den ersten Teil.
Public Function IsoKalenderwoche(ByVal datDatum As Date) As Long Dim datDonnerstag As Date ' Donnerstag derselben Woche ermitteln datDonnerstag = DateValue(datDatum) - Weekday(datDatum, vbMonday) + 4 IsoKalenderwoche = Int((datDonnerstag - DateSerial(Year(datDonnerstag), 1, 1)) / 7) + 1 End Function
Listing 2: IsoKalenderwoche liefert die Wochennummer nach ISO 8601
Die Funktion ermittelt zuerst den Donnerstag der Woche, in der das übergebene Datum liegt. Weekday mit dem Parameter vbMonday liefert für Montag die 1, für Sonntag die 7; die Rechnung Datum minus Wochentag plus 4 landet damit immer auf dem Donnerstag. Der Aufruf von DateValue entfernt einen etwaigen Zeitanteil, der für die Kalenderwoche ohne Bedeutung ist. Anschließend ermittelt sie die Position dieses Donnerstags innerhalb des Jahres, teilt durch sieben und addiert eins.
Das Wochenjahr fällt dabei nebenbei ab: Es ist schlicht das Jahr des Donnerstags. Die Funktion IsoKalenderjahr aus Listing 4 gibt es zurück.
Public Function IsoKalenderjahr(ByVal datDatum As Date) As Long IsoKalenderjahr = Year(DateValue(datDatum) - Weekday(datDatum, vbMonday) + 4) End Function
Listing 3: IsoKalenderjahr liefert das Jahr, zu dem die Woche gehört
Für die beiden Problemfälle von oben sieht das nun so aus:
Debug.Print IsoKalenderjahr(#12/31/2007#) & "-KW" _ & IsoKalenderwoche(#12/31/2007#) Debug.Print IsoKalenderjahr(#1/1/2021#) & "-KW" _ & IsoKalenderwoche(#1/1/2021#) Debug.Print IsoKalenderjahr(#1/3/2016#) & "-KW" _ & IsoKalenderwoche(#1/3/2016#)
Die Ergebnisse lauten nun:
2008-KW1 2020-KW53 2015-KW53
Wenn Sie nach Kalenderwochen gruppieren, verwenden Sie immer beide Funktionen zusammen. Eine Auswertung, die nur nach der Wochennummer gruppiert, wirft die Woche 53 aus 2015 und die Woche 53 aus 2020 in denselben Topf.
Das Alter berechnen
Auch hier führt die naheliegende Lösung in die Irre. DateDiff mit dem Intervall yyyy zählt die Jahreswechsel zwischen zwei Datumsangaben, nicht die erreichten Geburtstage. Für eine am 15. Juni 1980 geborene Person liefert es am 14. Juni 2026 bereits 46 statt 45. Die Funktion Alter aus Listing 5 korrigiert das.
Public Function Alter(ByVal datGeburt As Date, Optional ByVal datStichtag As Date) As Long Dim datGeburtstag As Date If datStichtag = 0 Then datStichtag = Date If datGeburt > datStichtag Then Err.Raise 5, "Alter", "Das Geburtsdatum liegt nach dem Stichtag." End If Alter = DateDiff("yyyy", datGeburt, datStichtag) ' Geburtstag im Stichjahr datGeburtstag = DateSerial(Year(datStichtag), Month(datGeburt), Day(datGeburt)) If datGeburtstag > datStichtag Then Alter = Alter - 1 End If End Function
Listing 4: Alter berechnet das vollendete Lebensalter zu einem Stichtag
Die Funktion nimmt zunächst die Differenz der Jahreszahlen und zieht eine Eins ab, falls der Geburtstag im Stichjahr noch bevorsteht. Ein Geburtsdatum nach dem Stichtag weist sie mit einem Fehler zurück, weil ein negatives Alter kein sinnvolles Ergebnis wäre.
Unser exklusives Angebot für Dich!
(Gilt für den Abschluss eines Jahres-Abonnements im ersten Jahr, danach 189,-/Jahr)
Hier geht’s weiter →Die ersten 4 Wochen kostenlos testen – voller Zugriff auf alle Artikel, vollständigen Code und Beispieldatenbanken. Kein Risiko: Wenn es nicht passt, kündigst Du einfach innerhalb der ersten vier Wochen.
Hast Du eine konkrete Frage zu Deiner eigenen Access-Anwendung?
Vielleicht stellt Deine Anwendung Dich vor eine Herausforderung, zu der Du bisher keine Lösung findest. Schlechte Performance, kein ausreichender Zugriffsschutz, Du bist unsicher über Dein Datenmodell oder Dein Code liefert unerklärliche Fehler?
In unserem kostenlosen Access-Audit schaut sich André Minhorst persönlich gemeinsam mit Dir Deine Lösung per Zoom an – und zeigt Dir, wo Datenmodell, VBA-Code, Ergonomie und Sicherheit Optimierungspotenzial bieten.
Jetzt kostenloses Access-Audit anfordern →