Ein MSSQL Cursor durchläuft das Ergebnis einer Abfrage Zeile für Zeile. Das ist nützlich, wenn jeder Datensatz einen eigenen, aufeinanderfolgenden Verarbeitungsschritt benötigt. Für normale Auswertungen und viele Änderungen ist eine setbasierte SQL-Anweisung jedoch einfacher und häufig effizienter. In diesem Beitrag lernst du einen vollständigen Cursor-Ablauf kennen, siehst die wichtigsten Cursor-Optionen und vergleichst ihn mit einer Alternative ohne Schleife.

Die Beispiele gelten für Microsoft SQL Server und Transact-SQL (T-SQL). Sie verwenden eine lokale temporäre Tabelle, damit du keine vorhandenen Anwendungstabellen veränderst.

Was ist ein Cursor in SQL Server?

Eine SELECT-Anweisung liefert normalerweise eine Ergebnismenge. Ein Cursor stellt einen Zeiger auf diese Ergebnismenge bereit: Mit FETCH holst du jeweils eine Zeile und kannst ihre Werte in Variablen verarbeiten. Danach wechselst du zur nächsten Zeile.

Der typische Ablauf lautet DECLARE → OPEN → FETCH → verarbeiten → CLOSE → DEALLOCATE. Ein Cursor ist nicht automatisch die richtige Wahl, nur weil mehrere Zeilen betroffen sind. SQL Server kann Filter, Joins, Aggregationen und Änderungen oft für alle passenden Zeilen in einer einzigen Anweisung ausführen. Wenn du die Abfragegrundlagen auffrischen möchtest, lies die Einführung in SELECT im SQL Server.

MSSQL Cursor: Beispieldaten vorbereiten

Führe diesen Block einmal in einem SQL-Server-Abfragefenster aus und bleibe für die folgenden Beispiele in derselben Verbindung. Beim erneuten Ausführen wird nur die lokale Übungstabelle neu angelegt:

IF OBJECT_ID('tempdb..#Mitarbeiter') IS NOT NULL
    DROP TABLE #Mitarbeiter;

CREATE TABLE #Mitarbeiter (
    MitarbeiterID int NOT NULL PRIMARY KEY,
    Name nvarchar(100) NOT NULL,
    Abteilung nvarchar(50) NOT NULL,
    Jahresgehalt decimal(10, 2) NOT NULL
);

INSERT INTO #Mitarbeiter
    (MitarbeiterID, Name, Abteilung, Jahresgehalt)
VALUES
    (1, N'Anna Weber', N'IT', 48000.00),
    (2, N'Ben Meier', N'Vertrieb', 52000.00),
    (3, N'Clara Roth', N'IT', 56000.00);

Die Tabelle #Mitarbeiter existiert nur in dieser Sitzung. Sie ist klein, damit du den Ablauf gut verfolgen kannst; aus ihrer Laufzeit lassen sich keine Aussagen über große produktive Tabellen ableiten.

Vollständiges T-SQL-Beispiel: Zeilen mit einem Cursor lesen

Der folgende Cursor gibt die drei Namen in der Reihenfolge ihrer IDs aus. Du kannst den Block nach den Beispieldaten vollständig kopieren und ausführen:

DECLARE @MitarbeiterID int;
DECLARE @Name nvarchar(100);

DECLARE mitarbeiter_cursor CURSOR LOCAL FAST_FORWARD FOR
    SELECT MitarbeiterID, Name
    FROM #Mitarbeiter
    ORDER BY MitarbeiterID;

OPEN mitarbeiter_cursor;

FETCH NEXT FROM mitarbeiter_cursor
INTO @MitarbeiterID, @Name;

WHILE @@FETCH_STATUS = 0
BEGIN
    PRINT CONCAT(@MitarbeiterID, N': ', @Name);

    FETCH NEXT FROM mitarbeiter_cursor
    INTO @MitarbeiterID, @Name;
END;

CLOSE mitarbeiter_cursor;
DEALLOCATE mitarbeiter_cursor;

Die Ausgabe findest du in SQL Server Management Studio im Reiter Meldungen. PRINT ist hier nur eine einfache Übungsausgabe; für einen Bericht ist ein einzelnes SELECT geeigneter.

Warum steht FETCH vor und innerhalb der Schleife?

Der erste FETCH NEXT holt die erste Zeile. Erst danach kann @@FETCH_STATUS sinnvoll geprüft werden. Im Schleifenkörper verarbeitest du die aktuelle Zeile und holst anschließend die nächste. Wenn keine weitere Zeile vorhanden ist, liefert der Abruf keinen erfolgreichen Status und die Schleife endet. Bei einer leeren Ergebnismenge wird der Schleifenkörper gar nicht ausgeführt.

Die Werte in INTO müssen zur Anzahl und Reihenfolge der ausgewählten Spalten passen. ORDER BY MitarbeiterID macht die gewünschte Bearbeitungsreihenfolge ausdrücklich sichtbar.

Was bedeuten LOCAL und FAST_FORWARD?

LOCAL begrenzt den Namen des Cursors auf den aktuellen Batch, die Prozedur oder den Trigger. Mit dieser ausdrücklichen Angabe bist du nicht von der Datenbankvorgabe für den Cursor-Geltungsbereich abhängig.

FAST_FORWARD kombiniert FORWARD_ONLY und READ_ONLY mit Optimierungen. Du kannst damit nur vorwärts mit FETCH NEXT laufen und keine positionierten Änderungen über diesen Cursor ausführen. Das bedeutet nicht, dass in derselben Sitzung keine separate UPDATE-Anweisung möglich wäre. FAST_FORWARD ist für einen einfachen lesenden Durchlauf oft ein passender Ausgangspunkt, aber keine garantierte Bestleistung für jede Abfrage. Microsoft beschreibt die Details unter DECLARE CURSOR.

Welche Cursor-Typen gibt es?

Cursor-Eigenschaften bestimmen, ob du zurückblättern kannst und wie sich Änderungen an den zugrunde liegenden Daten auf spätere Abrufe auswirken. Die folgenden Begriffe beschreiben unterschiedliche Aspekte; READ_ONLY ist zum Beispiel keine eigenständige Bewegungsrichtung.

Option Verhalten Wichtige Grenze
FAST_FORWARD Vorwärtsgerichteter, schreibgeschützter Cursor mit Optimierungen Nur FETCH NEXT; keine positionierten Änderungen
STATIC Arbeitet mit einer Momentaufnahme der Ergebnismenge in tempdb Spätere Änderungen an den Basistabellen erscheinen normalerweise nicht in den Abrufen
KEYSET Fixiert Zugehörigkeit und Reihenfolge der Zeilen beim Öffnen; Werte bestehender Zeilen können aktualisiert sichtbar werden Neu hinzukommende Zeilen gehören nicht automatisch zum geöffneten Keyset
DYNAMIC Kann Änderungen an Werten und Zeilenbestand während des Durchlaufs widerspiegeln Sichtbarkeit hängt unter anderem von Commit und Transaktionsisolation ab; keine feste Momentaufnahme
FORWARD_ONLY Erlaubt nur die Bewegung zur nächsten Zeile Legt allein noch nicht fest, ob der Cursor schreibgeschützt ist
READ_ONLY Unterbindet positionierte Änderungen über den Cursor Ist keine allgemeine Sperre gegen separate Änderungen an der Tabelle

Ein STATIC-Cursor ist nicht pauschal schneller als ein DYNAMIC-Cursor, und ein dynamischer Cursor ist keine Garantie für „Echtzeitdaten“. Abfrage, Datenmenge, Isolation und benötigte Bewegungsrichtung bestimmen die Kosten. Für den Einstieg reicht meist die bewusste Wahl eines lesenden LOCAL FAST_FORWARD-Cursors.

@@FETCH_STATUS, CLOSE und DEALLOCATE richtig verwenden

@@FETCH_STATUS beschreibt den letzten FETCH auf der aktuellen Verbindung: 0 bedeutet erfolgreich, -1 fehlgeschlagen oder Ende der Ergebnismenge, -2 eine fehlende Zeile. Prüfe den Wert direkt nach FETCH. Die Funktion bezieht sich nicht auf einen bestimmten Cursor; ein weiterer Cursor-Abruf in einer aufgerufenen Prozedur kann sie verändern. Die Microsoft-Dokumentation zu @@FETCH_STATUS erläutert diesen Fall.

CLOSE schließt den geöffneten Cursor und gibt das aktuelle Resultset frei. DEALLOCATE entfernt die Cursor-Referenz und gibt die zugehörigen Strukturen frei, sobald keine weitere Referenz besteht. Beides gehört nach dem Durchlauf dazu. In produktiven Routinen solltest du Fehler mit TRY...CATCH behandeln und bei einem Abbruch nur tatsächlich geöffnete beziehungsweise noch vorhandene Cursor schließen und freigeben. Ein CLOSE auf einen bereits geschlossenen Cursor ist selbst ein Fehler.

Wann ist eine setbasierte Abfrage besser?

Für die reine Namensliste ist kein Cursor nötig. Eine einzelne Abfrage ist kürzer und liefert ein zusammenhängendes Resultset:

SELECT MitarbeiterID, Name
FROM #Mitarbeiter
ORDER BY MitarbeiterID;

Auch viele Änderungen lassen sich ohne Zeilenschleife formulieren. Diese Beispieltransaktion erhöht nur in der temporären Tabelle die IT-Gehälter um fünf Prozent und nimmt die Änderung anschließend wieder zurück:

BEGIN TRANSACTION;

UPDATE #Mitarbeiter
SET Jahresgehalt = Jahresgehalt * 1.05
WHERE Abteilung = N'IT';

SELECT MitarbeiterID, Name, Jahresgehalt
FROM #Mitarbeiter
ORDER BY MitarbeiterID;

ROLLBACK TRANSACTION;

Ein Cursor mit einem UPDATE pro Mitarbeiter würde dieselbe Aufgabe ausführlicher lösen und mehr einzelne Verarbeitungsschritte verursachen. Prüfe daher zuerst, ob sich die Anforderung als SELECT, UPDATE, DELETE, INSERT ... SELECT oder mit Fensterfunktionen ausdrücken lässt.

Wann ist ein MSSQL Cursor dennoch sinnvoll?

Ein Cursor kann passen, wenn ein Arbeitsschritt zwingend für jede Zeile einzeln und in bestimmter Folge ausgeführt werden muss, beispielsweise weil eine vorhandene Prozedur pro Datensatz aufgerufen wird oder spätere Schritte vom Ergebnis des vorherigen abhängen. Auch dann solltest du prüfen, ob die eigentliche Arbeit außerhalb einer langen Datenbanktransaktion stattfinden kann.

Bei größeren Datenmengen können zeilenweise Aufrufe Laufzeit, Sperren und Log-Aufwand erhöhen. Wähle nur die benötigten Spalten und Zeilen, beschränke den Cursor-Geltungsbereich, plane die Fehlerbehandlung und miss die Wirkung unter realistischen Bedingungen. Für die Diagnose langsamer Abfragen hilft der Beitrag zur Analyse mit Query Store und Ausführungsplänen.

Häufige Fehler bei einem Cursor in SQL Server

  • Den ersten FETCH vergessen: Vor dem ersten Abruf ist @@FETCH_STATUS keine zuverlässige Bedingung für deinen neuen Cursor.
  • FETCH an die falsche Stelle setzen: Verarbeite die gerade geholte Zeile und rufe erst danach die nächste ab, sonst überspringst du Daten.
  • Cursor nicht freigeben: Schließe ihn nach der Nutzung und entferne die Referenz mit DEALLOCATE.
  • READ_ONLY als generelles Schreibverbot verstehen: Es betrifft positionierte Änderungen über den Cursor.
  • Jede Zeile einzeln aktualisieren: Prüfe zuerst, ob eine setbasierte Anweisung dieselbe fachliche Aufgabe erfüllt.
  • Eine lange Transaktion um den gesamten Durchlauf legen: Das kann Sperren und Konkurrenz mit anderen Sitzungen verstärken. Wähle die Transaktionsgrenze nach der erforderlichen Konsistenz.

Häufige Fragen zum MSSQL Cursor

Muss ich einen Cursor immer mit CLOSE und DEALLOCATE beenden?

Für einen geöffneten, benannten Cursor ist das die klare und empfohlene Vorgehensweise. CLOSE beendet das aktuelle Resultset; DEALLOCATE entfernt die Referenz. Bei Fehlerbehandlung musst du vorher prüfen, in welchem Zustand sich der Cursor befindet.

Ist FAST_FORWARD immer der schnellste Cursor?

Nein. Er ist für vorwärtsgerichtete, lesende Durchläufe optimiert. Die tatsächliche Leistung hängt von Abfrage, Datengröße und Ausführungsplan ab. Häufig ist eine Lösung ohne Cursor die bessere Wahl.

Kann ein READ_ONLY-Cursor die Tabelle vor Änderungen schützen?

Nein. Die Option verhindert positionierte Änderungen über diesen Cursor. Sie ersetzt weder Datenbankberechtigungen noch Transaktionsregeln.

Fazit: MSSQL Cursor bewusst einsetzen

Ein MSSQL Cursor ist ein Werkzeug für echte Zeile-für-Zeile-Verarbeitung. Das vollständige Muster beginnt mit DECLARE und OPEN, holt die erste Zeile vor der Schleife und endet mit CLOSE und DEALLOCATE. Für gewöhnliche Listen und gleichartige Änderungen reicht eine setbasierte SQL-Anweisung oft aus. Entscheide anhand der fachlichen Anforderung und prüfe die Leistung mit realistischen Daten.