Original von Narayana Vyas Kondreddi; deutsche Übersetzung von Frank Kalis
Traditionell waren SQL Server Datenbankentwickler es gewohnt, SELECT zu verwenden um Werte einer Variablen zuzuweisen. Dies funktionierte einwandfrei und war eine absolut gültige Praktik in SQL Server 6.5. Microsoft veröffentlichte SQL Server 7.0 in 1999. Diese Version führte das SET Statement ein, um Variablen zu initialisieren und Werte zuzuweisen. In der onlinedokumentation konnte man ebenfalls nachlesen: "Es ist empfohlen, dass SET @local_variable anstelle SELECT @local_variable verwendet wird zur Variablenzuweisung."
Dies führte zu einiger Verwirrung in der Datenbankentwicklergemeinde, da Microsoft nie erläuterte, warum SET bevorzugt werden sollte. In diesem Artikel möchte ich die wichtigsten Unterschiede zwischen SET und SELECT erläutern und Sie auf Dinge hinweisen, die Sie bei der jeweiligen Verwendung beachten sollten.
Falls Sie ein Anfänger in T-SQL sind, wird Ihnen das folgende Beispiel einen Eindruck vermitteln, wovon ich spreche:
/* Declaring variables */
DECLARE @Variable1 AS int, @Variable2 AS int
/* Setting @Variable1 to a value of 1 using SELECT */
SELECT @Variable1 = 1
/* Setting @Variable2 to a value of 2 using SET */
SET @Variable2 = 2
Nun zu den Unterschieden! Sind Standards wichtig für Sie? Beantworten Sie diese Frage mit 'Ja', dann sollten Sie SET verwenden. Aus dem einfachen Grund, dass SET ANSI Standard ist, um Werte zu Variablen zuzuweisen, während SELECT kein Standard ist.
Ein weiterer grundlegender Unterschied zwischen SET und SELECT ist, dass Sie SELECT verwenden können, um gleichzeitig mehreren Variablen Werte zuzuweisen, während SET nur die Zuweisung eines Wertes zu einer Variablen erlaubt. So wie hier:
/* Declaring variables */
DECLARE @Variable1 AS int, @Variable2 AS int
/* Initializing two variables at once */
SELECT @Variable1 = 1, @Variable2 = 2
/* The same can be done using SET, but two SET statements are needed */
SET @Variable1 = 1
SET @Variable2 = 2
So weit, so gut. Haben Sie aber jemals Fehlerbehandlungcode in T-SQL geschrieben, wissen Sie sehr wahrscheinlich, dass die Systemvariablen @@ERROR und @@ROWCOUNT in einem einzigen Statemetn angefangen werden müssen, direkt nach einem Data Manipulation (DML) Statement wie INSERT; UPDATE, DELETE oder anderen. Andernfalls werden diese Systemvariablen auf 0 zurückgesetzt. Wollen Sie also an Standards festhalten und SET in diesem Szenario verwenden, haben Sie kein Glück. Nachfolgendes Beispiel verdeutlicht da Problem:
DECLARE @Error int, @RowCount int
SELECT price/0 FROM dbo.titles
SET @RowCount = @@ROWCOUNT
SET @Error = @@ERROR
SELECT @Error AS Error
GO
Wenn Sie obigen Code in der Pubs Beispieldatenbank ausführen, zeigt die Variable @@ERROR einen Wert von 0, trotz des 'Division durch 0' Fehler 8134. Für so ein Szenario sollten Sie Standards vergessen und SELECT verwenden:
DECLARE @Error int, @RowCount int
SELECT price/0 FROM dbo.titles
SELECT @RowCount = @@ROWCOUNT, @Error = @@ERROR
SELECT @Error AS Error
Bestehen Sie aber auf die Verwendung von SET gibt es dennoch eine Lösung. Hier ein Beispiel (nicht besonders lesbar und auch nicht empfohlen):
DECLARE @ErrorAndRowcount AS varchar(25), @Error int, @RowCount int
SELECT price/0 FROM dbo.titles
/* Capturing @@ERROR and @@ROWCOUNT into a dot separated string */
SET @ErrorAndRowcount = CAST(@@ERROR AS varchar(12)) + '.' + CAST(@@ROWCOUNT AS varchar(12))
/* one way to separate the string into error and rowcount variables */
SET @Error = CAST(PARSENAME(@ErrorAndRowcount, 2) AS int)
SET @RowCount = CAST(PARSENAME(@ErrorAndRowcount, 1) AS int)
SELECT @Error AS Error, @RowCount AS Row_Count
/* Another way of splitting the string into error and rowcount variables */
SET @Error = CAST(LEFT(@ErrorAndRowcount, CHARINDEX('.', @ErrorAndRowcount)-1) AS int)
SET @RowCount = CAST(RIGHT(@ErrorAndRowcount, CHARINDEX('.', REVERSE(@ErrorAndRowcount))-1) AS int)
SELECT @Error AS Error, @RowCount AS Row_Count
GO
Ein weiterer Unterschied zwischen SET und SELECT: Verwenden Sie eine Abfrage, um die Variable zu füllen, führt SET zu einem Fehler, falls die Abfrage mehr als einen Wert zurückgibt. SELECT hingegen weist einen der zurückgegebenen Werte der Variablen zu und verschleiert damit die Tatsache, dass mehr als eine Zeile zurückgegeben wurde. Eine Folge hiervon, können Fehler in Ihrem Quellcode sein, die durch die Verwendung von SELECT unbemerkt bleiben; diese Art von Fehler sind schwer auszumachen. Hier ist ein Beispiel:
/* Consider the following table with two rows */
SET NOCOUNT on
CREATE TABLE #Test (i int, j varchar(10))
INSERT INTO #Test (i, j) VALUES (1, 'First Row')
INSERT INTO #Test (i, j) VALUES (1, 'Second Row')
GO
/* Following SELECT will return two rows, but the variable gets its value from one of those rows, without an error.
This may not be what you were expecting. Since no error is returned,
you will never know that two rows existed for the condition, WHERE i = 1 */
DECLARE @j varchar(10)
SELECT @j = j FROM #Test WHERE i = 1
SELECT @j
GO
/* If you rewrite the same query, but use SET instead, for variable initialization, you will see the following error */
DECLARE @j varchar(10)
SET @j = (SELECT j FROM #Test WHERE i = 1)
SELECT @j
Server: Msg 512, Level 16, State 1, Line -1074284106
Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.
Aufgrund der obigen Ergebnisse, rate ich Ihnen stets SET zu verwenden, wenn Sie eine Abfrage verwenden, um Variablen zu füllen, um sicherzustellen, dass nur eine Zeile zurückgegeben wird. Sollten Sie aus irgendeinem Grund SET nicht *mögen*, können Sie das gleiche Verhalten mit SELECT erreichen:
DECLARE @j varchar(10)
SELECT @j = (SELECT j FROM #Test WHERE i = 1)
SELECT @j
Hier ist ein weiterer Unterschied im Hinblick auf Variablenzuweisung auf Basis einer Abfrage, speziell dann, wenn die Abfrage keinen Datensatz zurückgibt. Führen Sie folgendes Beispiel in der Pubs Beispieldatenbank aus und Sie werden sehen, was ich meine:
/* Returns NULL */
DECLARE @Title varchar(80)
SET @Title = 'Not Found'
SET @Title =
(
SELECT title
FROM dbo.titles
WHERE title_id = 'InvalitTitleID'
)
SELECT @Title
GO
/* Returns the string literal 'Not Found' */
DECLARE @Title varchar(80)
SET @Title = 'Not Found'
SELECT @Title = title
FROM dbo.titles
WHERE title_id = 'InvalitTitleID'
SELECT @Title
GO
Last, but not least! Gibt es Performance Unterschiede zwischen SET und SELECT? Ist eines schneller oder langsamer als das andere? Die meisten Datenbankentwickler und -administratoren sind sich hierbei nicht sicher. Also, habe ich einen Test durchgeführt mit aufschlussreichen Ergebnissen Ausgewählt wurde ein Entwicklungsserver für diesen Test. Alle Applikationen wurden beendet; ebenso wurden alle nicht notwendigen Dienste gestoppt. Der SQL Server Agent wurde ebenfalls gestoppt, um zu verhindern, dass keine Jobs während des Tests gestartet werden konnten. Ferner wurde der Server vom Netzwerk genommen. Ergebnis war ein isolierter Rechner auf dem nicht ausser dem SQL Server lief. Als nächstes habe ich ein Test Skript erstellt, welches kontinuierlich innerhalb einer Schleife (mit konfigurierbarer Wiederholungsrate) Werte zu Variablen mit SET und SELECT zuweist und die Zeit misst, die jeder Schleifendurchlauf benötigt.
Hier das Ergebnis:
Aus Performance Gesichtspunkten ist kaum ein Unterschied zwischen SET und SELECT, was das Initialisieren und Wertezuweisen betrifft. ABER, eine interessante Entdeckung konnte ich machen. Wie Sie wissen, kann ein einziges SELECT Statement gleichzeitig Werte mehreren Variablen zuweisen. Diese Eigenschaft macht SELECT zum Sieger über SET. Ein einziges SELECT Statement, welches Werte zu drei verschiedenen Variablen zuweist, ist wesentlich schneller als drei verschiedene SET Statements. In diesem Szenario ist SELECT mindest doppelt so schnell wie SET. Schlussfolgerung also ist: Wenn Sie eine Schleife in Ihrer Stored Procedure haben, die Werte von verschiedenen Variablen manipuliert und Sie möchten das Maximale an Performance aus dieser Schleife herausholen, sollten Sie alle Variablenmanipulationen in einem einzigen SELECT Statement unterbringen (oder verwandte Variablen in wenigen SELECT Statements) wie folgt:
SELECT @TestVar1 = @TestVar1 + 1, @TestVar2 = @TestVar2 - 1, @CTR = @CTR + 1
Diesen Test habe ich mit SQL Server 7.0, 2000 und Yukon Beta 1 durchgeführt. Die Ergebnisse waren konsistent. Ich habe diesen Test sogar auf Einzel- und Multiprozessor Maschinen durchgeführt. Mit identischen Ergebnissen. Möchten Sie diesen Test für sich einmal durchführen, können Sie gerne das nachfolgende Skript verwenden. Ein Hinweis am Rand sei aber erlaubt: Lassen Sie das Skript nicht auf einem Produktionsserver laufen, da es zu einer Prozessorauslastung von 100% während des Tests führen kann. Falls Sie ausserdem der Meinung sind, der Test würde zu lange dauern, verringern Sie den Wert der Variablen @TimesToLoop2 um die Anzahl der Iterationen zu verändern. Am Ende des Tests zeigt das Skript an, wie viel Zeit (in Sekunden) es gedauert hat, Werte zu Variablen mit SET, SELECT und mehrfache Zuweisung mit SELECT zuzuweisen. Hier ist das Skript:
DECLARE @Test1 int, @Test2 int, @Test3 int, @TestVar1 int, @TestVar2 int
DECLARE @Loop int, @Start datetime, @CTR int, @TimesToLoop1 int, @TimesToLoop2 int
SET @Test1 = 0
SET @Test2 = 0
SET @Test3 = 0
SET @Loop = 0
SET @TestVar2 = 0
SET @TimesToLoop1 = 10
SET @TimesToLoop2 = 50000
WHILE @Loop < @TimesToLoop1
BEGIN
SET @Start = CURRENT_TIMESTAMP
SET @CTR = 0
/* Testing the performance of SET */
WHILE @CTR < @TimesToLoop2
BEGIN
SET @TestVar1 = 1
SET @TestVar2 = @TestVar2 - @TestVar1
SET @CTR = @CTR + 1
END
SET @Loop = @Loop + 1
SET @Test1 = @Test1 + DATEDIFF(ms, @Start, CURRENT_TIMESTAMP)
END
SET @Loop = 0
SET @TestVar2 = 0
WHILE @Loop < @TimesToLoop1
BEGIN
SELECT @Start = CURRENT_TIMESTAMP
SELECT @CTR = 0
/* Testing the performance of SELECT */
WHILE @CTR < @TimesToLoop2
BEGIN
SELECT @TestVar1 = 1
SELECT @TestVar2 = @TestVar2 - @TestVar1
SELECT @CTR = @CTR + 1
END
SELECT @Loop = @Loop + 1
SELECT @Test2 = @Test2 + DATEDIFF(ms, @Start, CURRENT_TIMESTAMP)
END
SET @Loop = 0
SET @TestVar2 = 0
WHILE @Loop < @TimesToLoop1
BEGIN
SELECT @Start = CURRENT_TIMESTAMP, @CTR = 0
/* Testing the performance of SELECT with multiple variable assignments */
WHILE @CTR < @TimesToLoop2
BEGIN
SELECT @TestVar1 = 1, @TestVar2 = @TestVar2 - @TestVar1, @CTR = @CTR + 1
END
SELECT @Loop = @Loop + 1, @Test3 = @Test3 + DATEDIFF(ms, @Start, CURRENT_TIMESTAMP)
END
SELECT (@Test1/CAST(@TimesToLoop1 AS decimal(7,2)))/1000.00 AS [SET],
(@Test2/CAST(@TimesToLoop1 AS decimal(7,2)))/1000.00 AS [SELECT],
(@Test3/CAST(@TimesToLoop1 AS decimal(7,2)))/1000.00 AS [SELECT with Multiple Assignments]
Den Originalartikel und viele weitere finden Sie
hier