none
Schleife ohne Cursor RRS feed

  • 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

    Dienstag, 22. Juli 2014 11:08

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
    Mittwoch, 23. Juli 2014 19:21

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
    App-Entwickler-Hotline: Schnelle & kompetente Hilfe für Entwickler: kostenfrei!

    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.

    • Als Antwort markiert Kedishen Dienstag, 22. Juli 2014 13:52
    • Tag als Antwort aufgehoben Kedishen Donnerstag, 24. Juli 2014 12:24
    Dienstag, 22. Juli 2014 13:07
  • Hey Hristo,

    danke für die schnelle Antwort!

    Die beiden Artikel haben mir sehr geholfen und nun habe ich eine Lösung ohne Cursor erstellt, die auch in unserem Framework funktioniert. Vielen Dank dafür.

    Grüße

    Dienstag, 22. Juli 2014 13:51
  • 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

    http://sqlserver2000.databases.aspfaq.com/why-should-i-consider-using-an-auxiliary-numbers-table.html

    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.

    Dienstag, 22. Juli 2014 14:40
    Beantworter
  • 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.

    Mittwoch, 23. Juli 2014 12:39
  • 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

    Mittwoch, 23. Juli 2014 18:57
    Beantworter
  • 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
    Mittwoch, 23. Juli 2014 19:21
  • 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

    Donnerstag, 24. Juli 2014 12:29