Benutzer mit den meisten Antworten
Schleife ohne Cursor

Frage
-
Hey,
im Moment arbeite ich an diversen SQL-Scripts und bin noch relativ neu auf dem Gebiet.
Für ein größeres Projekt sollte ich ein kleines Script schreiben, dass einfache Nummern (1, 2, 3, ...) in Spaltenbezeichnungen im Excel-Format (A, B, C, ...) umwandelt. Meine erste Lösung basierte auf einem Cursor. Da ich aber nun gelesen habe, dass Cursor bei großen Datenmengen sehr uneffizient sind, habe ich eine Funktion daraus gemacht und es wird nur noch ein UPDATE ausgeführt.
Das Problem, dass ich jetzt habe ist, dass wir dieses Script in ein eigenes Framework einbauen wollten, aber unser SQL-Interpreter mit dem Erstellen der Funktion nicht zurecht kommt. Woran es genau liegt, wissen wir leider noch nicht, aber daran wird bereits gearbeitet. Da wir aber erstmal so zurecht kommen müssen, lautet meine Frage:
Kennt jemand eine Methode um eine Funktion und einen Cursor zu umgehen?
Hier das ursprüngliche Script, welches einen Cursor verwendet:
DECLARE @nNumeric INT, @nValue INT, @nOriginal INT, @cLetter CHAR, @cString VARCHAR(10); DECLARE csExcel CURSOR FOR SELECT ORIGINAL, CONVERSION FROM EXCEL_CONVERSION ORDER BY ORIGINAL; OPEN csExcel; FETCH NEXT FROM csExcel INTO @nOriginal, @cString; WHILE @@FETCH_STATUS = 0 BEGIN SET @nNumeric = @nOriginal; SET @cString = ''; WHILE @nNumeric > 0 BEGIN SET @nValue = (@nNumeric - 1) % 26 SET @cLetter = CHAR(@nValue + 65) SET @cString = @cLetter + @cString SET @nNumeric = (@nNumeric - (@nValue + 1)) / 26 END; UPDATE EXCEL_CONVERSION SET CONVERSION = @cString WHERE ORIGINAL = @nOriginal; FETCH NEXT FROM csExcel INTO @nOriginal, @cString; END; CLOSE csExcel; DEALLOCATE csExcel;
Ich würde mich echt sehr über Hinweise freuen. Falls Ihr noch mehr Infos braucht, einfach Fragen. Danke!
Grüße
Antworten
-
Ich will es mal so formulieren:
Einen Cursor durch eine WHILE -Loop zu ersetzen, ist vergleichbar der Heilung der Pest durch Cholera.... ;-)
Cursor und WHILE-Loop sind beides zeilenbasierte Operationen, die mit zunehmender Zeilenanzahl immer langsamer werden. Das Schachteln von WHILE-Loops (oder cursors) skaliert das Ganze noch deutlicher.
Anbei eine Variante, die völlig ohne Schleifen auskommt (Dauer auf meinem PC: 94ms):
DECLARE @t table (ORIGINAL INT, CONVERSION CHAR(4)) INSERT INTO @t SELECT TOP 20000 row_number() over(order by (select null)) pos,'' p FROM master..spt_values cross join (SELECT TOP 20 row_number() over(order by (select null)) pos ,''p FROM master..spt_values )x UPDATE @t SET CONVERSION = CASE WHEN ORIGINAL > 18278 THEN CHAR(((((ORIGINAL-1)/26 -1)/26 -1)/26-1)%26 + 65) ELSE '' END + CASE WHEN ORIGINAL > 702 THEN CHAR ((((ORIGINAL-1)/26 -1)/26-1) %26 + 65) ELSE '' END + CASE WHEN ORIGINAL > 26 THEN CHAR (((ORIGINAL-1)/26 -1) %26 + 65) ELSE '' END + CHAR((ORIGINAL-1)%26 + 65) FROM @t
- Als Antwort markiert Kedishen Donnerstag, 24. Juli 2014 12:23
Alle Antworten
-
Hallo Kedishen,
Allgemeine Informationen über Cursors und wie diese die Leistung Ihrer Anwendung beeinflüssen können, schauen Sie sich den Artikel unter [1]. Im Allgemein können Cursors in nested Schleifen vermieden werden, indem diese mit joins ersetzt werden. Für Zeilenoperationen können Sie sich den Artikel unter [2] anschauen. Dort werden verschiedene Strategien vorgestellt, welche diese Funktionalität ohne Cursors darstellen.
[1] http://blogs.msdn.com/b/sqlprogrammability/archive/2008/03/18/increase-your-sql-server-performance-by-replacing-cursors-with-set-operations.aspx
[2] http://searchsqlserver.techtarget.com/tip/Avoid-cursors-in-SQL-Server-with-these-methods-to-loop-over-records
Viele Grüße
Hristo Valev
App-Entwickler-Hotline für MSDN Online Deutschland
Disclaimer:
Bitte haben Sie Verständnis dafür, dass wir hier auf Rückfragen gar nicht oder nur sehr zeitverzögert antworten können.
Bitte nutzen Sie für Rückfragen oder neue Fragen den telefonischen Weg über die App-Entwickler-Hotline: http://www.msdn-online.de/Hotline
Es gelten für die App-Entwickler-Hotline und dieses Posting diese Nutzungsbedingungen, Hinweise zu Markenzeichen, Informationen zur Datensicherheit sowie die gesonderten Nutzungsbedingungen für die App-Entwickler-Hotline. -
Hallo Kedishen,
es wäre für andere nett, wenn Du Deine Lösung zeigen würdest.
Wobei man sich hier das Leben dauerhaft einfacher machen sollte.
Excel unterstützt maximal 16384 Spalten, was keine große Anzahl für eine SQL Server Tabelle ist. Anstatt die Werte häufiger neu zu berechnen, kann man sie einmalig in einer Tabelle ablegen.
Damit es schnell in beide Richtungen funktioniert, sollten beide Spalten (oben ORIGINAL, CONVERSION) mit einem eindeutigen Index versehen werden.
Danach können Zugriffe in die eine wie andere Richtung mit reinem SQL über JOIN oder APPLY implementiert werden.
Vom Prinzip das gleiche wie
Gruß Elmar
P. S.: Mit Index und FAST_FORWARD Cursor und gesetztem SET NOCOUNT ON braucht die "Schleife" für 16384 Werte weniger als eine Sekunde.
-
Hey Elmar,
kein Problem. Mittlerweile habe ich noch mehr daran rumgebastelt. Ich benutze jetzt eine temporäre Tabelle auf der alle laufenden Updates gemacht werden und erst zum Schluss gleiche ich dann mit der Haupttabelle ab.
DECLARE @nRowCount INT, @nIndex INT, @nOriginal INT, @nNumeric INT, @nValue INT, @cLetter CHAR, @cString VARCHAR(10); CREATE TABLE #tblTemp(ROWNUMBER INT, COL1 INT, COL2 VARCHAR(10)); INSERT INTO #tblTemp(ROWNUMBER, COL1) SELECT ROW_NUMBER() OVER (ORDER BY ORIGINAL ASC), ORIGINAL FROM EXCEL_CONVERSION WHERE CONVERSION IS NULL OR CONVERSION = ''; SET @nRowCount = @@ROWCOUNT; SET @nIndex = 1; CREATE CLUSTERED INDEX idxTemp ON #tblTemp(COL1) WITH FILLFACTOR = 100; WHILE @nIndex <= @nRowCount BEGIN SELECT @nOriginal = COL1 FROM #tblTemp WHERE ROWNUMBER = @nIndex; SET @cString = ''; SET @nNumeric = @nOriginal; WHILE (@nNumeric > 0) BEGIN SET @nValue = ((@nNumeric) - 1) % 26; SET @cLetter = CHAR(@nValue + 65); SET @cString = @cLetter + @cString; SET @nNumeric = (@nNumeric - (@nValue + 1)) / 26; END; SET @nIndex = @nIndex + 1; UPDATE #tblTemp SET COL2 = @cString WHERE COL1 = @nOriginal END; UPDATE EXCEL_CONVERSION SET CONVERSION = temp.COL2 FROM #tblTemp temp WHERE CONVERSION = temp.COL1 AND (CONVERSION IS NULL OR CONVERSION = '') DROP TABLE #tblTemp;
Danke für den Rat mit der festen Tabelle, leider soll das so dynamisch bleiben, da es der Auftraggeber so wünscht. Ansonsten hätte ich das hier sofort in die Tat umgesetzt. In Index-Nutzung muss ich mich noch einarbeiten. Ich lese immer wieder, dass man dadurch gut an Zeit gewinnen kann.
Ich werde noch weiter gucken, was ich daran optimieren kann und wenn jemand Einfälle hat, dann bitte einfach posten.
Grüße
P.S.: Ich bin momentan bei circa 30 Sekunden für 20000 Datensätzen.
-
Hallo,
eine kleine permanente Tabelle sollte nicht stören.
Zunächst aber Dein erste Version leicht angepasst. Wichtig eigentlich nur das SET NOCOUNT ON aktiviert ist (sonst gibt es zuviel "Gelaber"). Die Tabelle hat als Primary Key ORIGINAL und der Cursor ist auf einen FAST_FORWARD umgestellt:
USE tempdb; GO SET NOCOUNT ON; GO CREATE TABLE EXCEL_CONVERSION ( ORIGINAL int NOT NULL CONSTRAINT PK_EXCEL_CONVERSION PRIMARY KEY, CONVERSION varchar(10) NOT NULL); -- eine langweilige Variante zum Füllen DECLARE @i int = 1; WHILE @i < 16385 BEGIN INSERT INTO EXCEL_CONVERSION (ORIGINAL, CONVERSION) VALUES(@i, ''); SET @i += 1 END; GO DECLARE @starttime DATETIME2 = SYSUTCDATETIME(); DECLARE @nNumeric INT, @nValue INT, @nOriginal INT, @cLetter CHAR, @cString VARCHAR(10); -- geändert in FAST_FORWARD und ORDER BY weggelassen, da irrelevant DECLARE csExcel CURSOR FAST_FORWARD FOR SELECT ORIGINAL, CONVERSION FROM EXCEL_CONVERSION; OPEN csExcel; FETCH NEXT FROM csExcel INTO @nOriginal, @cString; WHILE @@FETCH_STATUS = 0 BEGIN SET @nNumeric = @nOriginal; SET @cString = ''; WHILE @nNumeric > 0 BEGIN SET @nValue = (@nNumeric - 1) % 26 SET @cLetter = CHAR(@nValue + 65) SET @cString = @cLetter + @cString SET @nNumeric = (@nNumeric - (@nValue + 1)) / 26 END; UPDATE EXCEL_CONVERSION SET CONVERSION = @cString WHERE ORIGINAL = @nOriginal; FETCH NEXT FROM csExcel INTO @nOriginal, @cString; END; CLOSE csExcel; DEALLOCATE csExcel; SELECT 'CURSOR', DATEDIFF(ms, @starttime , SYSUTCDATETIME()); GO --SELECT * FROM EXCEL_CONVERSION ORDER BY ORIGINAL; GO DROP TABLE EXCEL_CONVERSION; GO
Das braucht bei mir Pi * Daumen etwa 800 ms.
Die von mir vorgeschlagene Variante würde eine Konvertierungstabelle anlegen. Die eigentliche Änderung wäre ein einziges UPDATE:
CREATE TABLE ExcelColumns ( ColumnValue int NOT NULL CONSTRAINT PK_ExcelColumns PRIMARY KEY, ColumnReference varchar(3) NOT NULL CONSTRAINT UK_ExcelColumns UNIQUE); GO WITH Numbers AS ( SELECT number AS nvalue, CASE WHEN number = 0 THEN '' ELSE CHAR(number + 64) END AS cvalue FROM master..spt_values WHERE type = 'P' AND number BETWEEN 0 AND 26 ) INSERT INTO ExcelColumns(ColumnValue, ColumnReference) SELECT TOP (16384) (n1.nvalue * 26 * 26) + (n2.nvalue * 26) + n3.nvalue AS nvalues, n1.cvalue + n2.cvalue + n3.cvalue AS cvalues FROM Numbers AS n1 OUTER APPLY (SELECT * FROM Numbers WHERE n1.nvalue = 0 OR (n1.nvalue > 0 AND nvalue > 0)) AS n2 OUTER APPLY (SELECT * FROM Numbers WHERE nvalue BETWEEN 1 AND 26) AS n3 ORDER BY nvalues; GO DECLARE @starttime DATETIME2 = SYSUTCDATETIME(); UPDATE EXCEL_CONVERSION SET CONVERSION = (SELECT ColumnReference FROM ExcelColumns WHERE ColumnValue = EXCEL_CONVERSION.ORIGINAL); SELECT 'SQL', DATEDIFF(ms, @starttime , SYSUTCDATETIME()); GO DROP TABLE ExcelColumns; GO
Die ExcelColumns Tabelle sollte man permanent einrichten, wenn es häufiger gebraucht wird, aber auch temporär wäre das Ganze noch schneller.
Das UPDATE (ohne Anlage) braucht etwa 60 ms, mit Anlage etwa 230 ms.
Gruß Elmar
-
Ich will es mal so formulieren:
Einen Cursor durch eine WHILE -Loop zu ersetzen, ist vergleichbar der Heilung der Pest durch Cholera.... ;-)
Cursor und WHILE-Loop sind beides zeilenbasierte Operationen, die mit zunehmender Zeilenanzahl immer langsamer werden. Das Schachteln von WHILE-Loops (oder cursors) skaliert das Ganze noch deutlicher.
Anbei eine Variante, die völlig ohne Schleifen auskommt (Dauer auf meinem PC: 94ms):
DECLARE @t table (ORIGINAL INT, CONVERSION CHAR(4)) INSERT INTO @t SELECT TOP 20000 row_number() over(order by (select null)) pos,'' p FROM master..spt_values cross join (SELECT TOP 20 row_number() over(order by (select null)) pos ,''p FROM master..spt_values )x UPDATE @t SET CONVERSION = CASE WHEN ORIGINAL > 18278 THEN CHAR(((((ORIGINAL-1)/26 -1)/26 -1)/26-1)%26 + 65) ELSE '' END + CASE WHEN ORIGINAL > 702 THEN CHAR ((((ORIGINAL-1)/26 -1)/26-1) %26 + 65) ELSE '' END + CASE WHEN ORIGINAL > 26 THEN CHAR (((ORIGINAL-1)/26 -1) %26 + 65) ELSE '' END + CHAR((ORIGINAL-1)%26 + 65) FROM @t
- Als Antwort markiert Kedishen Donnerstag, 24. Juli 2014 12:23
-
Danke für die vielen guten und schnellen Beiträge!
Ich habe jetzt die Lösung von LMU92 genommen, da es quasi genau das ist, was wir bei diesem Projekt brauchen.
Eure Beiträge helfen mir echt gut zu verstehen, was alles so möglich ist mit SQL. Ich hoffe bald komme ich von selbst auf solche Lösungen. Auch dafür danke an euch.
Grüße