Die Abfrage läuft in Access seit Jahren fehlerfrei – bis die Tabellen auf den SQL Server umziehen. Plötzlich liefert Format im Gruppierungsausdruck eine Fehlermeldung, Like “M*” findet keinen einzigen Datensatz, und ein Aufruf von Nz endet mit der Meldung, der Server kenne diese Funktion nicht. Wer Access mit einem SQL Server als Backend betreibt, kommt früher oder später nicht mehr um T-SQL herum, die Abfragesprache des Servers. Der erste Teil dieser Reihe zeigt, wo sich T-SQL von dem SQL unterscheidet, das wir aus Access kennen, und wie wir T-SQL direkt aus Access heraus ausführen.
Was diese Reihe vorhat
Access spricht seine eigene SQL-Mundart, oft noch Jet-SQL genannt, obwohl die Jet-Engine längst von der ACE-Engine abgelöst wurde. Solange wir mit Access-Tabellen arbeiten, spielt der Unterschied keine Rolle. Sobald das Backend auf einem SQL Server liegt, ändert sich die Lage: Eine Access-Abfrage auf verknüpfte Tabellen schickt Access nicht einfach an den Server. Access übersetzt sie über den ODBC-Treiber so weit wie möglich in T-SQL und erledigt den Rest selbst – nachdem es die benötigten Daten vom Server geholt hat. Was dabei schiefgehen kann, sehen wir weiter unten an echten Fehlermeldungen.
Die folgenden Teile dieser Reihe führen durch die Sprache T-SQL. Jeder Teil geht von einer Technik aus, die wir in Access beherrschen, und zeigt das Gegenstück im SQL Server: von SELECT über UPDATE und MERGE, Sichten, gespeicherte Prozeduren und Trigger bis zu Indizes und Berechtigungen. Dieser erste Teil legt das Fundament. Wir richten die Beispieldatenbank ein, lernen SQL Server Management Studio kennen, vergleichen die wichtigsten Unterschiede der beiden Dialekte und führen T-SQL aus Access und aus VBA heraus aus.
Die Beispieldatenbank -Bestellverwaltung_TSQL
Als Beispiel dient durch die ganze Reihe eine kleine Bestellverwaltung: Kunden bestellen bei einem Händler für Lebensmittel, Bürobedarf und Technik, Mitarbeiter betreuen die Bestellungen, Lieferanten beliefern das Sortiment. Die Datenbank heißt Bestellverwaltung_TSQL und besteht aus diesen Tabellen:
- tblKategorien: die Warengruppen als zweistufige Hierarchie, über das Fremdschlüsselfeld OberKategorieID auf sich selbst verwiesen
- tblLieferanten: sieben Lieferanten aus Deutschland, Österreich und der Schweiz
- tblProdukte: dreißig Produkte mit Preis, Lagerbestand, dem Ja/Nein-Feld Auslaufartikel und einer optionalen Beschreibung
- tblKunden: zweiunddreißig Kunden, teils Firmen, teils Privatpersonen, mit leeren Feldern und Sonderzeichen in den Namen
- tblMitarbeiter: acht Mitarbeiter, über VorgesetzterID ebenfalls hierarchisch verknüpft
- tblBestellstatus: die fünf Bearbeitungsstufen einer Bestellung
- tblBestellungen: 330 Bestellungen von Januar 2024 bis September 2026
- tblBestelldetails: die 825 Positionen der Bestellungen
Die Namen folgen einer festen Konvention: Tabellen beginnen mit tbl, der Primärschlüssel einer Tabelle heißt wie die Tabelle in der Einzahl mit angehängtem ID (etwa ProduktID in tblProdukte), und Fremdschlüsselfelder tragen denselben Namen wie das Feld, auf das sie verweisen (KategorieID in tblProdukte verweist auf tblKategorien).
Das Skript Bestellverwaltung_TSQL.sql aus dem Download legt die Datenbank samt Daten an. Wir führen es in SQL Server Management Studio (kurz SSMS) auf einer lokalen SQL Server Express-Instanz aus.
Es löscht eine vorhandene Datenbank gleichen Namens und erzeugt sie neu. Danach zeigt der Objekt-Explorer die acht Tabellen (siehe Bild 2).

Bild 1: Die Tabellen der Datenbank Bestellverwaltung_TSQL im Objekt-Explorer von SSMS
Das Access-Frontend SQLServer_EinstiegVonJetSQLZuTSQL.accdb enthält die acht Tabellen als Verknüpfungen mit den Namen dbo_tblKategorien, dbo_tblKunden und so weiter. Wie man solche Verknüpfungen von Hand oder per VBA anlegt, haben wir im Beitrag SQL Server: Tabellen per VBA verknüpfen (www.vbentwickler.de/485) gezeigt.
Das erste SELECT in SSMS
Ein Abfragefenster öffnen wir in SSMS mit Neue Abfrage. Wichtig ist die Datenbank, gegen die die Anweisung läuft: Sie steht in der Auswahlliste links neben dem Ausführen-Schalter. Alternativ stellen wir der Anweisung ein USE Bestellverwaltung_TSQL voran. Die folgende Abfrage liefert die ersten drei Kunden, alphabetisch nach Nachnamen sortiert:
SELECT TOP (3) KundeID, Firma, Nachname, Ort FROM dbo.tblKunden ORDER BY Nachname;
Fast alles daran kennen wir aus Access. Neu ist dbo: Das ist das Schema, zu dem die Tabelle gehört. Jedes Objekt im SQL Server liegt in einem Schema, und wenn wir nichts anderes angelegt haben, heißt es dbo. Das Semikolon am Ende empfiehlt Microsoft für jede Anweisung, und wir setzen es hier konsequent. Mit F5 oder dem Schalter Ausführen erscheint das Ergebnis im Ergebnisbereich (siehe Bild 1).

Bild 2: Das erste SELECT im Abfragefenster von SSMS, darunter das Ergebnis
Ein Wort zu GO: In SSMS-Skripten stoßen wir ständig auf diese Zeile. GO ist kein T-SQL-Befehl, sondern ein Trennzeichen für SSMS und das Kommandozeilenwerkzeug sqlcmd.
Es teilt ein Skript in einzelne Stapel (Batches), die nacheinander an den Server gehen. Das Skript der Beispieldatenbank nutzt es zum Beispiel, damit die Datenbank angelegt ist, bevor das erste CREATE TABLE an sie gerichtet wird. In Pass-Through-Abfragen, die wir weiter unten kennenlernen, gehört GO nicht hinein.
Zeichenketten und Platzhalter
Der auffälligste Unterschied zeigt sich schon bei einem Textkriterium. In Access fassen wir Text in doppelte Anführungszeichen und verwenden * und ? als Platzhalter.
Die Abfrage nach Kunden aus Köln, deren Nachname mit S beginnt, lautet in der SQL-Ansicht einer Access-Abfrage so:
SELECT KundeID, Vorname, Nachname, Ort FROM dbo_tblKunden WHERE Ort = "Köln" AND Nachname Like "S*";
In T-SQL stehen Zeichenketten in einfachen Anführungszeichen, und die Platzhalter heißen % für beliebig viele Zeichen und _ für genau ein Zeichen. Außerdem setzen wir vor Texte, die Umlaute oder andere Unicode-Zeichen enthalten können, das Präfix N.
Es kennzeichnet das Literal als Unicode-Text (nvarchar) und verhindert, dass ein Zeichen durch die Zeichensatzumsetzung verloren geht. Die T-SQL-Fassung der Abfrage sieht so aus:
SELECT KundeID, Vorname, Nachname, Ort FROM dbo.tblKunden WHERE Ort = N'Köln' AND Nachname LIKE N'S%';
Die doppelten Anführungszeichen sind in T-SQL nicht verboten, aber sie bedeuten etwas anderes: Bei eingeschaltetem QUOTED_IDENTIFIER – das ist die Voreinstellung in SSMS und beim ODBC-Treiber – kennzeichnen sie einen Objektnamen, so wie bei Access die eckigen Klammern. Ein “Köln” sucht der Server deshalb als Spaltennamen. Wie die Fehlermeldung dazu aussieht, sehen wir im Abschnitt über Pass-Through-Abfragen.
Ein Apostroph im Text verdoppeln wir. Der Kunde Sean O’Brien findet sich so:
SELECT KundeID, Vorname, Nachname FROM dbo.tblKunden WHERE Nachname = N'O''Brien';
Besonders tückisch ist der Platzhalter *. Er löst keinen Fehler aus, er ist in T-SQL schlicht ein gewöhnliches Zeichen. Die Abfrage WHERE Nachname LIKE N’M*’ sucht daher nach Nachnamen, die genau auf M* enden, und liefert null Datensätze, statt Meier und Müller zu finden. Wer Abfragen aus Access übernimmt, sollte jedes Like auf diese Platzhalter hin prüfen.
Datumswerte
In Access schreiben wir Datumsliterale in Rautezeichen und im amerikanischen Format: #9/15/2026#. T-SQL kennt diese Schreibweise nicht.
Der Server beantwortet sie mit der Meldung Falsche Syntax in der Nähe von “#”. Ein Datum steht in T-SQL als Zeichenkette in einfachen Anführungszeichen, und damit es unabhängig von den Spracheinstellungen des Servers richtig gelesen wird, schreiben wir es als ‘20260915’ (Jahr, Monat, Tag ohne Trennzeichen).
Die zweite Falle betrifft die Uhrzeit. Die Spalte Bestelldatum in tblBestellungen hat den Datentyp datetime2(0) und speichert Datum und Uhrzeit. Die Bestellung vom 15. September 2026 ist um 11:30:00 Uhr eingegangen. Eine Abfrage mit Gleichheit auf den Tag findet sie daher nicht, denn ‘20260915’ bedeutet Mitternacht. Drei Wege, die Bestellungen eines Tages zu zählen, vergleicht Listing 1.
SELECT COUNT(*) FROM dbo.tblBestellungen WHERE Bestelldatum = '20260915'; SELECT COUNT(*) FROM dbo.tblBestellungen WHERE Bestelldatum >= '20260915' AND Bestelldatum < '20260916'; SELECT COUNT(*) FROM dbo.tblBestellungen WHERE CAST(Bestelldatum AS date) = '20260915';
Listing 1: Drei Wege, die Bestellungen eines Tages zu zählen
Die erste Abfrage liefert 0, die zweite und die dritte liefern 1. Die zweite Schreibweise, ein halboffenes Intervall, ist die Standardlösung: Sie vergleicht das Feld unverändert mit Grenzwerten und lässt dem Server damit alle Möglichkeiten, einen Index zu nutzen.
Die dritte ist bequemer zu lesen, wandelt aber den Feldinhalt um – bei anderen Umwandlungen als dem Schnitt auf das Datum kann der Server dann keinen Index mehr verwenden, wie wir im Teil über Indizes sehen werden.
NULL und das Verketten von Texten
Mit NULL verhält sich T-SQL grundsätzlich wie Access: Ein Vergleich mit NULL ist weder wahr noch falsch. Das Kriterium WHERE Firma = NULL liefert deshalb nie einen Datensatz, die Frage nach leeren Feldern lautet WHERE Firma IS NULL. In der Beispieldatenbank sind bei sechzehn Kunden keine Firmen hinterlegt – die Privatkunden. Die Abfrage mit = NULL zählt 0, die Abfrage mit IS NULL zählt 16.
Interessanter wird es beim Zusammensetzen von Texten. In Access gibt es zwei Operatoren: & verkettet und behandelt Null als leeren Text, + verkettet und gibt Null zurück, wenn ein Teil Null ist. T-SQL hat nur +, und der verhält sich wie das + in Access. Hinzu kommt die Funktion CONCAT, die NULL wie einen leeren Text behandelt. Wir wollen Kunden als “Müller (Müller Bürobedarf GmbH)” anzeigen, Privatkunden dagegen nur mit dem Nachnamen. Listing 2 zeigt drei Varianten.
SELECT TOP (4)
Nachname + N' (' + Firma + N')' AS MitPlus,
CONCAT(Nachname, N' (', Firma, N')') AS MitConcat,
Nachname + ISNULL(N' (' + Firma + N')', N'') AS Sauber
FROM dbo.tblKunden
ORDER BY KundeID;
Listing 2: Drei Varianten, Nachname und Firma zu verketten
Bei Privatkunden liefert die erste Spalte NULL, die zweite den unschönen Text “Schmidt ()” mit leeren Klammern, und erst die dritte Spalte ist richtig: Weil ein + mit NULL wieder NULL ergibt, ist der gesamte Klammerausdruck NULL, wenn die Firma fehlt, und ISNULL ersetzt ihn durch einen leeren Text. Dieses Muster kennen Access-Entwickler schon, in Access schreiben wir es als Nachname & (” (” + Firma + “)”).
Ja/Nein, Zahlen und Datentypen
Für die Felder unserer Tabellen müssen wir wissen, welcher Access-Datentyp welchem SQL-Server-Typ entspricht. Die wichtigsten Zuordnungen:
- AutoWert: int mit IDENTITY
- Zahl (Long Integer): int
- Zahl (Integer): smallint
- Währung: money oder, besser kontrollierbar, decimal(9,2)
- Ja/Nein: bit
- Kurzer Text: nvarchar(n)
- Langer Text: nvarchar(max)
- Datum/Uhrzeit: date für reine Datumswerte, datetime2 für Datum mit Uhrzeit
Die Definition einer Tabelle liest der Server in Systemsichten aus. Die Abfrage aus Listing 3 zeigt die Spalten von tblProdukte mit Typ, Länge und der Angabe, ob NULL erlaubt ist.
SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH AS Laenge, IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = N'tblProdukte' ORDER BY ORDINAL_POSITION;
Listing 3: Die Spalten der Tabelle tblProdukte mit Datentyp und NULL-Zulässigkeit
Das Ergebnis sehen wir in Bild 3.

Bild 3: Die Spalten von tblProdukte; bei Beschreibung steht die Länge -1 für nvarchar(max)
Das Feld Auslaufartikel ist ein bit, in Access das Ja/Nein-Feld. Dort ist Wahr der Wert -1, in T-SQL ist ein gesetztes bit die 1. Das hat konkrete Folgen: Die Bedingung WHERE Auslaufartikel = -1, aus Access gewohnt, liefert in T-SQL keinen Fehler, aber auch keinen Datensatz.
Die Bedingung WHERE Auslaufartikel = True scheitert mit dem Fehler Ungültiger Spaltenname “True”, denn T-SQL kennt kein True. Richtig ist WHERE Auslaufartikel = 1. Auch IIF(Auslaufartikel, …) funktioniert nicht, weil ein bit in T-SQL kein Wahrheitswert ist; dort muss ein Vergleich stehen.
Funktionen: Was aus Access bleibt
Viele Funktionen, die wir in Access-Abfragen verwenden, stammen aus VBA und gibt es im SQL Server nicht. Die Datenbank-Engine kennt dafür eigene Gegenstücke. Eine erste Übersicht:
- Nz: ISNULL oder COALESCE; COALESCE akzeptiert mehr als zwei Argumente und ist Standard-SQL
- Iif: IIF in der Schreibweise von T-SQL (der Vergleich muss ausdrücklich stehen) oder die allgemeinere CASE WHEN … THEN … ELSE … END-Anweisung
- Format: FORMAT für Zahlen und Datumswerte, bei großen Mengen langsamer als CONVERT mit Stilnummer
- Date/Now: CAST(GETDATE() AS date) beziehungsweise GETDATE oder SYSDATETIME
- Left/Mid/Len: LEFT, SUBSTRING und LEN; Mid entspricht SUBSTRING mit Startposition und Länge
- InStr: CHARINDEX; die Parameter stehen in umgekehrter Reihenfolge (gesuchter Text zuerst)
- DateAdd/DateDiff: DATEADD und DATEDIFF; der Datumsteil steht ohne Anführungszeichen, etwa DATEDIFF(DAY, Bestelldatum, Lieferdatum)
- Year/Month/Day: gleichnamige Funktionen
- DLookup/DSum/DCount: gibt es nicht; wir ersetzen sie durch Unterabfragen oder Verknüpfungen, davon handelt der dritte Teil
Ein Beispiel mit drei dieser Funktionen zeigt Listing 4: die ersten vier Produkte mit Beschreibung, Auslauf-Kennzeichen und formatiertem Preis.
SELECT TOP (4)
Produkt,
ISNULL(Beschreibung, N'(keine)') AS Beschreibung,
IIF(Auslaufartikel = 1, N'ja', N'nein') AS Auslauf,
FORMAT(Einzelpreis, 'N2', 'de-DE') AS Preis,
Lagerbestand
FROM dbo.tblProdukte
ORDER BY ProduktID;
Listing 4: Die Funktionen ISNULL, IIF und FORMAT in einer Abfrage
Das Ergebnis steht in Bild 4. Das dritte Argument von FORMAT legt die Kultur fest und damit Dezimalkomma und Tausenderpunkt unabhängig von der Spracheinstellung des Servers.

Bild 4: Die Produktliste mit ISNULL, IIF und FORMAT im Ergebnisraster von SSMS
Warum Access-Abfragen auf SQL-Server-Tabellen scheitern können
Eine Abfrage wie die in Listing 5, die wir in Access für unsere Auswertung der Monatsumsätze 2026 anlegen würden, ist reines Access-SQL auf den verknüpften Tabellen.
SELECT Format([b].[Bestelldatum],"yyyy-mm") AS Monat,
Sum([d].[Menge]*[d].[Einzelpreis]*(1-[d].[Rabatt])) AS Umsatz
FROM dbo_tblBestellungen AS b INNER JOIN dbo_tblBestelldetails AS d
ON b.BestellungID = d.BestellungID
WHERE b.Bestelldatum >= #1/1/2026# AND b.BestellstatusID <> 5
GROUP BY Format([b].[Bestelldatum],"yyyy-mm")
ORDER BY Format([b].[Bestelldatum],"yyyy-mm");
Listing 5: Monatsumsätze 2026 in Access-SQL
Sie steht als qryMonatsumsatzJet im Frontend der Beispieldatenbank.
Unser exklusives Angebot für Dich!
(Gilt für den Abschluss eines Jahres-Abonnements.)
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.
Oder 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 →