Database
 sql >> Datenbank >  >> RDS >> Database

Machen Sie diese Fehler bei der Verwendung von SQL CURSOR?

Für manche Leute ist es die falsche Frage. SQL-CURSOR IST der Fehler. Der Teufel steckt im Detail! Im Namen von SQL CURSOR kann man in der gesamten SQL-Blogosphäre allerlei Blasphemie lesen.

Wenn Sie genauso denken, warum sind Sie zu dieser Schlussfolgerung gekommen?

Wenn es von einem vertrauenswürdigen Freund und Kollegen stammt, kann ich es Ihnen nicht verübeln. Es passiert. Manchmal viel. Aber wenn dich jemand mit Beweisen überzeugt hat, ist das eine andere Geschichte.

Wir sind uns noch nie begegnet. Du kennst mich nicht als Freund. Aber ich hoffe, dass ich es mit Beispielen erklären und Sie davon überzeugen kann, dass SQL CURSOR seinen Platz hat. Es ist nicht viel, aber diese kleine Stelle in unserem Code hat Regeln.

Aber zuerst möchte ich Ihnen meine Geschichte erzählen.

Ich habe angefangen, mit Datenbanken mit xBase zu programmieren. Das war bis zu meinen ersten zwei Jahren als professioneller Programmierer auf dem College. Ich erzähle Ihnen das, weil wir früher Daten sequenziell verarbeitet haben, nicht in festgelegten Batches wie SQL. Als ich SQL lernte, war das wie ein Paradigmenwechsel. Die Datenbank-Engine entscheidet für mich mit ihren satzbasierten Befehlen, die ich ausgegeben habe. Als ich von SQL CURSOR erfuhr, fühlte es sich an, als wäre ich zu den alten, aber bequemen Methoden zurückgekehrt.

Aber einige ältere Kollegen warnten mich:„Vermeiden Sie SQL CURSOR um jeden Preis!“ Ich bekam ein paar mündliche Erklärungen, und das war es.

SQL CURSOR kann schlecht sein, wenn Sie es für den falschen Job verwenden. Wie mit einem Hammer Holz zu schneiden, es ist lächerlich. Natürlich können Fehler passieren, und darauf wird unser Fokus liegen.

1. Verwenden von SQL CURSOR, wenn satzbasierte Befehle ausreichen

Ich kann das nicht genug betonen, aber DAS ist der Kern des Problems. Als ich zum ersten Mal erfuhr, was SQL CURSOR ist, leuchtete eine Glühbirne auf. „Schleifen! Ich weiß das!" Allerdings nicht, bis es mir Kopfschmerzen bereitete und meine Vorgesetzten mich ausschimpften.

Sie sehen, der Ansatz von SQL ist mengenbasiert. Sie geben einen INSERT-Befehl aus Tabellenwerten aus, und er erledigt die Aufgabe ohne Schleifen in Ihrem Code. Wie ich bereits sagte, ist dies die Aufgabe der Datenbank-Engine. Wenn Sie also eine Schleife erzwingen, um Datensätze zu einer Tabelle hinzuzufügen, umgehen Sie diese Autorität. Es wird hässlich.

Bevor wir ein lächerliches Beispiel ausprobieren, bereiten wir die Daten vor:


SELECT TOP (500)
  val = ROW_NUMBER() OVER (ORDER BY sod.SalesOrderDetailID)
, modified = GETDATE()
, status = 'inserted'
INTO dbo.TestTable
FROM AdventureWorks.Sales.SalesOrderDetail sod
CROSS JOIN AdventureWorks.Sales.SalesOrderDetail sod2

SELECT
 tt.val
,GETDATE() AS modified
,'inserted' AS status
INTO dbo.TestTable2
FROM dbo.TestTable tt
WHERE CAST(val AS VARCHAR) LIKE '%2%'

Die erste Anweisung generiert 500 Datensätze. Der zweite erhält eine Teilmenge davon. Dann sind wir bereit. Wir werden die fehlenden Daten aus TestTable einfügen in TestTable2 mit SQL-CURSOR. Siehe unten:


DECLARE @val INT

DECLARE test_inserts CURSOR FOR 
	SELECT val FROM TestTable tt
	WHERE NOT EXISTS(SELECT val FROM TestTable2 tt1
                 WHERE tt1.val = tt.val)

OPEN test_inserts
FETCH NEXT FROM test_inserts INTO @val
WHILE @@fetch_status = 0
BEGIN
	INSERT INTO TestTable2
	(val, modified, status)
	VALUES
	(@val, GETDATE(),'inserted')

	FETCH NEXT FROM test_inserts INTO @val
END

CLOSE test_inserts
DEALLOCATE test_inserts

So schleifen Sie mit SQL CURSOR, um einen fehlenden Datensatz nacheinander einzufügen. Ziemlich lang, nicht wahr?

Lassen Sie uns nun einen besseren Weg ausprobieren – die satzbasierte Alternative. Hier geht's:


INSERT INTO TestTable2
(val, modified, status)
SELECT val, GETDATE(), status
FROM TestTable tt
WHERE NOT EXISTS(SELECT val FROM TestTable2 tt1
                 WHERE tt1.val = tt.val)

Das ist kurz, übersichtlich und schnell. Wie schnell? Siehe Abbildung 1 unten:

Mit xEvent Profiler in SQL Server Management Studio habe ich die CPU-Zeitangaben, die Dauer und die logischen Lesevorgänge verglichen. Wie Sie in Abbildung 1 sehen können, gewinnt die Verwendung des satzbasierten Befehls zum Einfügen von Datensätzen den Leistungstest. Die Zahlen sprechen für sich. Die Verwendung von SQL CURSOR verbraucht mehr Ressourcen und Verarbeitungszeit.

Versuchen Sie daher, bevor Sie SQL CURSOR verwenden, zuerst einen mengenbasierten Befehl zu schreiben. Es wird sich langfristig besser auszahlen.

Aber was ist, wenn Sie SQL CURSOR benötigen, um die Arbeit zu erledigen?

2. Nicht die geeigneten SQL-CURSOR-Optionen verwenden

Ein weiterer Fehler, den selbst ich in der Vergangenheit gemacht habe, war die Nichtverwendung geeigneter Optionen in DECLARE CURSOR. Es gibt Optionen für Umfang, Modell, Parallelität und ob scrollbar oder nicht. Diese Argumente sind optional und können leicht ignoriert werden. Wenn jedoch SQL CURSOR die einzige Möglichkeit ist, die Aufgabe zu erledigen, müssen Sie Ihre Absicht ausdrücklich angeben.

Fragen Sie sich also:

  • Wenn Sie die Schleife durchlaufen, werden Sie die Zeilen nur vorwärts navigieren oder zur ersten, letzten, vorherigen oder nächsten Zeile wechseln? Sie müssen angeben, ob der CURSOR nur vorwärts oder scrollbar ist. Das ist DECLARE CURSOR FORWARD_ONLY oderDECLARECURSOR SCROLL .
  • Werden Sie die Spalten im CURSOR aktualisieren? Verwenden Sie READ_ONLY, wenn es nicht aktualisierbar ist.
  • Benötigen Sie beim Durchlaufen der Schleife die neuesten Werte? Verwenden Sie STATIC, wenn die Werte keine Rolle spielen, ob sie neu sind oder nicht. Verwenden Sie DYNAMIC, wenn andere Transaktionen Spalten aktualisieren oder Zeilen löschen, die Sie im CURSOR verwenden, und Sie die neuesten Werte benötigen. Hinweis :DYNAMISCH wird teuer.
  • Ist der CURSOR global für die Verbindung oder lokal für den Stapel oder eine gespeicherte Prozedur? Geben Sie an, ob LOCAL oder GLOBAL.

Weitere Informationen zu diesen Argumenten finden Sie in der Referenz von Microsoft Docs.

Beispiel

Lassen Sie uns ein Beispiel ausprobieren, in dem drei CURSORs für die CPU-Zeit, logische Lesevorgänge und die Dauer mit xEvents Profiler verglichen werden. Die erste hat nach DECLARE CURSOR keine geeigneten Optionen. Die zweite ist LOCAL STATIC FORWARD_ONLY READ_ONLY. Der letzte ist LOtyuiCAL FAST_FORWARD.

Hier ist die erste:

-- NOTE: Don't just COPY and PASTE this code then run in your machine. Read and assess.

-- DECLARE CURSOR with no options
SET NOCOUNT ON

DECLARE @command NVARCHAR(2000) = N'SET NOCOUNT ON;'
CREATE TABLE #commands (
	ID INT IDENTITY (1, 1) PRIMARY KEY CLUSTERED
   ,Command NVARCHAR(2000)
);

INSERT INTO #commands (Command)
	VALUES (@command)

INSERT INTO #commands (Command)
	SELECT
	'SELECT ' + CHAR(39) + a.TABLE_SCHEMA + '.' + a.TABLE_NAME 
                  + ' - ' + CHAR(39) 
	          + ' + cast(count(*) as varchar) from ' 
		  + a.TABLE_SCHEMA + '.' + a.TABLE_NAME
	FROM INFORMATION_SCHEMA.tables a
	WHERE a.TABLE_TYPE = 'BASE TABLE';

DECLARE command_builder CURSOR FOR 
  SELECT
	Command
  FROM #commands

OPEN command_builder

FETCH NEXT FROM command_builder INTO @command
WHILE @@fetch_status = 0
BEGIN
	PRINT @command
	FETCH NEXT FROM command_builder INTO @command
END
CLOSE command_builder
DEALLOCATE command_builder

DROP TABLE #commands
GO

Es gibt natürlich eine bessere Option als den obigen Code. Wenn der Zweck nur darin besteht, ein Skript aus vorhandenen Benutzertabellen zu generieren, reicht SELECT aus. Fügen Sie dann die Ausgabe in ein anderes Abfragefenster ein.

Aber wenn Sie ein Skript generieren und sofort ausführen müssen, ist das eine andere Geschichte. Sie müssen das Ausgabeskript auswerten, ob es Ihren Server belasten wird oder nicht. Siehe Fehler Nr. 4 später.

Um Ihnen den Vergleich von drei CURSORs mit unterschiedlichen Optionen zu zeigen, reicht dies aus.

Lassen Sie uns nun einen ähnlichen Code haben, aber mit LOCAL STATIC FORWARD_ONLY READ_ONLY.

--- STATIC LOCAL FORWARD_ONLY READ_ONLY

SET NOCOUNT ON

DECLARE @command NVARCHAR(2000) = N'SET NOCOUNT ON;'
CREATE TABLE #commands (
	ID INT IDENTITY (1, 1) PRIMARY KEY CLUSTERED
   ,Command NVARCHAR(2000)
);

INSERT INTO #commands (Command)
	VALUES (@command)

INSERT INTO #commands (Command)
	SELECT
	'SELECT ' + CHAR(39) + a.TABLE_SCHEMA + '.' + a.TABLE_NAME 
                  + ' - ' + CHAR(39) 
	          + ' + cast(count(*) as varchar) from ' 
		  + a.TABLE_SCHEMA + '.' + a.TABLE_NAME
	FROM INFORMATION_SCHEMA.tables a
	WHERE a.TABLE_TYPE = 'BASE TABLE';

DECLARE command_builder CURSOR LOCAL STATIC FORWARD_ONLY READ_ONLY FOR SELECT
	Command
FROM #commands

OPEN command_builder

FETCH NEXT FROM command_builder INTO @command
WHILE @@fetch_status = 0
BEGIN
	PRINT @command
	FETCH NEXT FROM command_builder INTO @command
END
CLOSE command_builder
DEALLOCATE command_builder

DROP TABLE #commands
GO

Wie Sie oben sehen können, ist der einzige Unterschied zum vorherigen Code das LOCAL STATIC FORWARD_ONLY READ_ONLY Argumente.

Der dritte hat einen LOCAL FAST_FORWARD. Laut Microsoft ist FAST_FORWARD jetzt ein FORWARD_ONLY, READ_ONLY CURSOR mit aktivierten Optimierungen. Wir werden sehen, wie es mit den ersten beiden weitergeht.

Wie vergleichen sie sich? Siehe Abbildung 2:

Derjenige, der weniger CPU-Zeit und Dauer benötigt, ist der LOCAL STATIC FORWARD_ONLY READ_ONLY CURSOR. Beachten Sie auch, dass SQL Server Standardwerte hat, wenn Sie keine Argumente wie STATIC oder READ_ONLY angeben. Das hat schreckliche Konsequenzen, wie Sie im nächsten Abschnitt sehen werden.

Was sp_describe_cursor aufgedeckt hat

sp_describe_cursor ist eine gespeicherte Prozedur vom Master Datenbank, die Sie verwenden können, um Informationen aus dem offenen CURSOR zu erhalten. Und hier ist, was es aus der ersten Reihe von Abfragen ohne CURSOR-Optionen ergab. Siehe Abbildung 3 für das Ergebnis von sp_describe_cursor :

Viel übertreiben? Sie wetten. Der CURSOR aus dem ersten Stapel von Abfragen ist:

  • global zur bestehenden Verbindung.
  • dynamisch, was bedeutet, dass Änderungen in der #commands-Tabelle für Aktualisierungen, Löschungen und Einfügungen nachverfolgt werden.
  • optimistisch, was bedeutet, dass SQL Server einer temporären Tabelle namens CWT eine zusätzliche Spalte hinzugefügt hat. Dies ist eine Prüfsummenspalte zum Nachverfolgen von Änderungen in den Werten der #commands-Tabelle.
  • scrollbar, was bedeutet, dass Sie mit dem Cursor zur vorherigen, nächsten, obersten oder untersten Zeile wechseln können.

Absurd? Ich bin sehr einverstanden. Warum brauchen Sie eine globale Verbindung? Warum müssen Sie Änderungen an der temporären Tabelle #commands nachverfolgen? Haben wir irgendwo anders als zum nächsten Datensatz im CURSOR gescrollt?

Da dies ein SQL-Server für uns feststellt, wird die CURSOR-Schleife zu einem schrecklichen Fehler.

Jetzt wird Ihnen klar, warum die explizite Angabe von SQL-CURSOR-Optionen so wichtig ist. Geben Sie also ab jetzt immer diese CURSOR-Argumente an, wenn Sie einen CURSOR verwenden müssen.

Der Ausführungsplan verrät mehr

Der Actual Execution Plan hat etwas mehr darüber zu sagen, was jedes Mal passiert, wenn ein FETCH NEXT FROM command_builder INTO @command ausgeführt wird. In Abbildung 4 wird eine Zeile in den Clustered Index CWT_PrimaryKey eingefügt in der tempdb Tabelle CWT :

Schreibvorgänge erfolgen in tempdb bei jedem FETCH NEXT. Außerdem gibt es noch mehr. Denken Sie daran, dass der CURSOR in Abbildung 3 OPTIMISTISCH ist? Die Eigenschaften des Clustered Index Scan ganz rechts im Plan zeigen die zusätzliche unbekannte Spalte namens Chk1002 :

Könnte das die Prüfsummenspalte sein? Das Plan-XML bestätigt, dass dies tatsächlich der Fall ist:

Vergleichen Sie nun den tatsächlichen Ausführungsplan von FETCH NEXT, wenn der CURSOR LOCAL STATIC FORWARD_ONLY READ_ONLY ist:

Es verwendet tempdb auch, aber es ist viel einfacher. Unterdessen zeigt Abbildung 8 den Ausführungsplan, wenn LOCAL FAST_FORWARD verwendet wird:

Imbiss

Eine der geeigneten Verwendungen von SQL CURSOR ist das Generieren von Skripten oder das Ausführen einiger Verwaltungsbefehle für eine Gruppe von Datenbankobjekten. Selbst wenn es geringfügige Verwendungen davon gibt, besteht Ihre erste Option darin, den LOCAL STATIC FORWARD_ONLY READ_ONLY CURSOR oder LOCAL FAST_FORWARD zu verwenden. Derjenige mit einem besseren Plan und logischen Lesevorgängen wird gewinnen.

Ersetzen Sie dann je nach Bedarf eines davon durch das passende. Aber weißt du was? Nach meiner persönlichen Erfahrung habe ich nur einen lokalen Nur-Lese-CURSOR mit Vorwärtsdurchlauf verwendet. Ich musste den CURSOR nie global und aktualisierbar machen.

Abgesehen von diesen Argumenten spielt der Zeitpunkt der Ausführung eine Rolle.

3. Verwendung von SQL CURSOR bei täglichen Transaktionen

Ich bin kein Administrator. Aber ich habe eine Vorstellung davon, wie ein ausgelasteter Server aus den DBA-Tools aussieht (oder aus wie vielen Dezibel Benutzer schreien). Wollen Sie unter diesen Umständen weitere Belastungen hinzufügen?

Wenn Sie versuchen, Ihren Code mit einem CURSOR für alltägliche Transaktionen zu erstellen, denken Sie noch einmal darüber nach. CURSORs eignen sich gut für einmalige Läufe auf einem weniger ausgelasteten Server mit kleinen Datensätzen. An einem typischen arbeitsreichen Tag kann ein CURSOR jedoch:

  • Zeilen sperren, insbesondere wenn das Parallelitätsargument SCROLL_LOCKS explizit angegeben wird.
  • Verursacht eine hohe CPU-Auslastung.
  • Verwenden Sie tempdb ausgiebig.

Stellen Sie sich vor, an einem typischen Tag laufen mehrere davon gleichzeitig.

Wir sind kurz vor dem Ende, aber es gibt noch einen weiteren Fehler, über den wir sprechen müssen.

4. Die Auswirkungen von SQL CURSOR nicht einschätzen

Sie wissen, dass CURSOR-Optionen gut sind. Denken Sie, dass es ausreicht, sie zu spezifizieren? Sie haben die Ergebnisse oben bereits gesehen. Ohne die Tools würden wir nicht zu der richtigen Schlussfolgerung kommen.

Außerdem gibt es Code im CURSOR . Je nachdem, was es tut, fügt es mehr zu den verbrauchten Ressourcen hinzu. Diese waren möglicherweise für andere Prozesse verfügbar. Ihre gesamte Infrastruktur, Ihre Hardware und die SQL Server-Konfiguration tragen noch mehr zur Geschichte bei.

Wie sieht es mit dem Datenvolumen aus ? Ich habe SQL CURSOR nur für ein paar hundert Datensätze verwendet. Bei dir kann es anders sein. Das erste Beispiel hat nur 500 Datensätze benötigt, weil ich bereit wäre, auf diese Zahl zu warten. 10.000 oder sogar 1000 haben es nicht geschafft. Sie haben schlecht abgeschnitten.

Letztendlich kann das Überprüfen der logischen Lesevorgänge, egal wie wenig oder mehr, einen Unterschied machen.

Was ist, wenn Sie den Ausführungsplan, die logischen Lesevorgänge oder die verstrichene Zeit nicht überprüfen? Welche schlimmen Dinge können außer dem Einfrieren von SQL Server passieren? Wir können uns nur alle möglichen Weltuntergangsszenarien vorstellen. Du verstehst es.

Schlussfolgerung

SQL CURSOR verarbeitet Daten Zeile für Zeile. Es hat seinen Platz, aber es kann schlecht sein, wenn Sie nicht aufpassen. Es ist wie ein Werkzeug, das selten aus der Werkzeugkiste kommt.

Versuchen Sie also zunächst, das Problem mit satzbasierten Befehlen zu lösen. Es erfüllt die meisten unserer SQL-Anforderungen. Und wenn Sie jemals SQL CURSOR verwenden, verwenden Sie es mit den richtigen Optionen. Schätzen Sie die Auswirkungen mit dem Ausführungsplan, STATISTICS IO und xEvent Profiler ab. Wählen Sie dann den richtigen Zeitpunkt für die Ausführung aus.

All dies wird Ihre Verwendung von SQL CURSOR ein bisschen besser machen.