it-swarm.com.de

Cursorersatz für Neulinge

Ich würde gerne wissen, was der allgemeine Ersatz für einen Cursor ist. Die allgemeine Implementierung eines Cursors, den ich unterwegs sehe, ist

DECLARE @variable INT, @sqlstr NVARCHAR(MAX)

DECLARE cursor_name CURSOR
FOR select_statement --essentially to get an array for @variable 
                     --usually it's a subset of unique ids for accounts, clients, parts, etc

OPEN cursor_name
FETCH NEXT FROM cursor_name INTO @variable
WHILE @@FETCH_STATUS = 0
BEGIN
     SET @sqlstr = N'
     /* some query that uses '+ str(@variable) +' to do dirty work
     such as: go through all our accounts, if it''s some subset (possible new cursor), 
     go through those accounts and connect this way, 
     map those fields and add it to our big uniform table */
     '

     EXEC sp_executesql @sqlstr
FETCH NEXT FROM cursor_name INTO @variable
END

CLOSE cursor_name
DEALLOCATE cursor_name

Da so viele Leute Anti-Cursor sind (mit einem Nicken an SO: Warum hassen Leute Cursor ), was ist der allgemeine Ersatz für die allgemeine Implementierung (vorzugsweise SQL Server)?

6
undrline

Es kommt darauf an ™

Die Fähigkeit, einen oder mehrere Cursor zu umgehen, hängt davon ab, was in diesem Cursor ausgeführt wird. Ohne zu wissen, was darin vor sich geht, gibt es keine Möglichkeit zu sagen. Es kann sein, dass es keine Problemumgehung gibt und Sie eine zeilenweise Verarbeitung durchführen müssen.

Nachfolgend einige Beispiele.

Funktioniert nicht in Sets

Dieses Beispiel ist das grundlegendste und ist einfach die Tatsache, dass Sie Ihr gesamtes Dataset oder Teile Ihres Datasets auf einmal abfragen können, aber der Cursor wurde erstellt und fragt die Daten zeilenweise ab. Übliche, durch die dies ersetzt werden kann, sind JOIN, CROSS APPLY/OUTER APPLY Und andere.

Betrachten Sie den folgenden Datensatz:

CREATE TABLE dbo.Lotr(LotrId int, CharacterName varchar(255), Val varchar(255));
CREATE TABLE dbo.LotrAttributes(LotrATtributeId int, LotrId int, AttrVal varchar(255));

INSERT INTO dbo.Lotr(LotrId,CharacterName,Val)
VALUES(1,'Frodo','Ring')
,(2,'Gandalf','Staff');

INSERT INTO dbo.LotrAttributes(LotrId,LotrATtributeId,AttrVal)
VALUES(1,1,'RingAttribute1')
,(1,2,'RingAttribute2')
,(1,3,'RingAttribute3')
,(2,4,'StaffAttribute1')
,(2,5,'StaffAttribute2');

Man könnte versuchen, jeden Datensatz zu finden, und er stimmt separat überein, indem man die Tabelle Lotr durchläuft.

Cursor:

DECLARE @LotrID int
DECLARE C CURSOR FOR SELECT LotrId from dbo.Lotr;
OPEN C
FETCH NEXT FROM C INTO @LotrID;
WHILE @@FETCH_STATUS = 0
BEGIN
SELECT LotrATtributeId from dbo.LotrAttributes where LotrId = @LotrID;
FETCH NEXT FROM C INTO @LotrID;
END
CLOSE C
DEALLOCATE C

Das Ergebnis sind zwei Ergebnismengen

LotrATtributeId
1
2
3
LotrATtributeId
4
5

Wenn dieses inner join Verwendet wird, erhalten wir das gleiche Ergebnis wie eine Ergebnismenge.

SELECT LotrATtributeId from dbo.Lotr L
INNER JOIN dbo.LotrAttributes LA 
ON L.LotrId = LA.LotrId;

LotrATtributeId
1
2
3
4
5

String-Manipulation

Häufig wird FOR XML PATH('') verwendet, um Zeichenfolgenmanipulationen innerhalb von Cursorn zu ersetzen.

Datensatz

CREATE TABLE dbo.Lotr(LotrId int, CharacterName varchar(255), Val varchar(255));
CREATE TABLE dbo.LotrAttributes(LotrATtributeId int, LotrId int, AttrVal varchar(255));

INSERT INTO dbo.Lotr(LotrId,CharacterName,Val)
VALUES(1,'Frodo','Ring');

INSERT INTO dbo.LotrAttributes(LotrId,LotrATtributeId,AttrVal)
VALUES(1,1,'RingAttribute1')
,(1,2,'RingAttribute2')
,(1,3,'RingAttribute3');

Doppelcursor mit String-Manipulation

DECLARE @LotrId int, @CharacterName varchar(255), @Val varchar(255)
DECLARE @LotrATtributeId int, @AttrVal varchar(255)
DECLARE C CURSOR FOR
SELECT LotrId,CharacterName, Val FROM dbo.Lotr
OPEN C
FETCH NEXT FROM C INTO @LotrId,@CharacterName,@Val
WHILE @@FETCH_STATUS = 0
BEGIN

        SET @CharacterName +='|'+ @Val

        DECLARE D CURSOR FOR
        SELECT LotrATtributeId, AttrVal FROM dbo.LotrAttributes where LotrId = @LotrId
        OPEN D
        FETCH NEXT FROM D INTO @LotrATtributeId,@AttrVal
        WHILE @@FETCH_STATUS = 0
        BEGIN
        SET @CharacterName +='['[email protected]+ '],'

        FETCH NEXT FROM D INTO @LotrATtributeId,@AttrVal
        END
        CLOSE D 
        DEALLOCATE D

FETCH NEXT FROM C INTO @LotrId,@CharacterName,@Val
END
CLOSE C
DEALLOCATE C
SELECT LEFT(@CharacterName,len(@charactername)-1);

Ergebnis

(No column name)
Frodo|Ring[RingAttribute1],[RingAttribute2],[RingAttribute3],

Entfernen der Cursor mit FOR XML PATH ('')

SELECT L.Charactername +'|'+ L.Val + (SELECT stuff((SELECT ','+QUOTENAME(AttrVal) FROM dbo.LotrAttributes LA WHERE LA.LotrId = L.LotrId FOR XML PATH('')), 1, 1, ''))
FROM
dbo.Lotr L;

* *

Die eigentliche Problemumgehung besteht darin, herauszufinden, warum die Daten auf diese Weise dargestellt werden, und die Anwendung zu ändern, um sie nicht in diesem Format zu benötigen, und sie irgendwo zu speichern.

Wenn Ihre Hände gebunden sind, wäre dies das nächstbeste.


Fügen Sie die Top-10-Werte in eine temporäre Tabelle ein, basierend auf den IDs in einer anderen Tabelle

Daten

CREATE TABLE dbo.sometable (InsertTableId int, val varchar (255)); CREATE TABLE dbo.Top10Table (Top10TableId int, InsertTableId int, val varchar (255));

INSERT INTO dbo.sometable(InsertTableId,val)
VALUES(1,'bla')
,(2,'blabla');
INSERT INTO dbo.Top10Table(Top10TableId,InsertTableId,Val)
VALUES(1,1,'WUW')
,(2,1,'WUW')
,(3,1,'WUW');

Cursor

CREATE TABLE #Top10Values(Top10TableId int, InsertTableId int, val varchar(255))

    DECLARE @InsertTableId int;
    DECLARE C CURSOR FOR select InsertTableId from dbo.sometable;
    OPEN C
    FETCH NEXT FROM C INTO @InsertTableId;
    WHILE @@FETCH_STATUS =0
    BEGIN
    INSERT INTO #Top10Values(Top10TableId,InsertTableId,val)
    SELECT top(10) Top10TableId,InsertTableId,Val FROM dbo.Top10Table 
    where InsertTableId = @InsertTableId
    ORDER BY Top10TableId 

    FETCH NEXT FROM C INTO @InsertTableId;
    END
    CLOSE C
    DEALLOCATE C

    SELECT * FROM  #Top10Values;
    DROP TABLE #Top10Values;

Ergebnis

Top10TableId    InsertTableId   val
1   1   WUW
2   1   WUW
3   1   WUW

Ersetzen des Cursors durch CROSS APPLY Und ein CTE

CREATE TABLE #Top10Values(Top10TableId int, InsertTableId int, val varchar(255));
;WITH CTE 
AS
(
select InsertTableId  from dbo.sometable
)

INSERT INTO #Top10Values(Top10TableId,InsertTableId,val)
SELECT  T1T.Top10TableId,T1T.InsertTableId,T1T.Val 
FROM 
CTE
CROSS APPLY (SELECT TOP (10) Top10TableId,InsertTableId,Val from dbo.Top10Table T1T
WHERE T1T.InsertTableId = CTE.InsertTableId
) T1T ;

SELECT * FROM  #Top10Values;
DROP TABLE #Top10Values;

Andere Beispiele

  • Ein Beispiel zum Ersetzen eines Cursors zur Auswahl eines dynamischen Satzes von Artikeln pro Lieferant mithilfe von CROSS APPLYhier .
  • Ein Beispiel für die Verwendung von Fensterfunktionen zum Ersetzen eines Cursors hier .

Manchmal gibt es keine andere Wahl

Wenn Sie nicht in Sätzen arbeiten können und zeilenweise arbeiten müssen, können Sie den Cursor trotzdem optimieren.

Eine der größten Änderungen bei der Beschleunigung des Cursors ist das Hinzufügen von LOCAL FAST_FORWARD.

DECLARE C CURSOR LOCAL FAST_FORWARD FOR SELECT LotrId from dbo.Lotr

Schauen Sie sich diesen Blogpost von @AaronBertrand an, wo er die möglichen Leistungsunterschiede erklärt, wenn Cursoreinstellungen wie LOCAL & FAST_FORWARD Verwenden oder nicht.

6
Randi Vertongen

Es gibt keinen "allgemeinen Ersatz" - Sie haben alle "schmutzigen Arbeiten" hier versteckt, daher ist es schwer zu sagen, ob es in diesem Fall überhaupt einen spezifischen Ersatz gibt . Es gibt sicherlich einige spezielle Fälle, in denen Sie eine Reihe von Zeilen zeilenweise verarbeiten, unabhängig davon, ob Sie einen Cursor, eine while-Schleife oder einen anderen iterativen Prozess verwenden oder in einen satzbasierten Prozess konvertieren, der Alle Zeilen auf einmal sind viel besser. Es gibt jedoch auch andere Dinge, die nur zeilenweise ausgeführt werden müssen, z. B. das Ausführen einer gespeicherten Prozedur oder eines dynamischen SQL pro Zeile, dieselbe Abfrage über mehrere Datenbanken hinweg usw.

Cursor oder nicht, die Probleme, auf die Sie anspielen und die Sie verknüpfen, sind die gleichen, unabhängig davon, ob Sie den Deklarationscursor oder eine andere Schleifenstruktur verwenden (siehe dieser Beitrag ), und sind irrelevant, wenn Sie etwas tun müssen sowieso eine Reihe nach der anderen zu tun. Wenn Sie also einige spezifische Details zur Funktionsweise dieses Cursors angeben, erhalten Sie möglicherweise Ratschläge zum Entfernen des Cursors (oder zu einem nicht möglichen), aber Sie suchen nach einem magischen Ansatz, bei dem Sie alle Cursor eliminieren können zu allen Szenarien wird für Sie ziemlich frustrierend sein.

Der allgemeine Rat für neue Leute, die die Sprache eingeben, IMHO, sollte sein, immer darüber nachzudenken, was Sie tun müssen , um eine Reihe von Zeilen zu erstellen, im Gegensatz zu Was müssen Sie mit jeder Zeile in einer Menge tun ? Der Unterschied in der Sprache ist subtil, aber entscheidend. Wenn Benutzer das Problem als einen Datensatz anstelle einer Reihe einzelner Zeilen betrachten, ist es möglicherweise weniger wahrscheinlich, dass sie standardmäßig einen Cursor verwenden. Aber wenn sie aus verschiedenen Arten der Programmierung stammen - wobei iterativ der beste/einzige Weg ist -, anstatt ihnen einfach beizubringen, dass SQL Server nicht für diese Funktionsweise optimiert ist, weiß ich nicht, ob es einen Weg gibt, dies offensichtlich zu machen oder automatisch.

Ihre Frage verlangt immer noch einen allgemeinen Ersatz, und ich glaube immer noch, dass es so etwas nicht gibt.

6
Aaron Bertrand

Doug Lane hat eine Reihe von Videos mit dem Titel "T-SQL Level Up" auf YouTube gedreht. Ein Teil der Serie untersucht einen allgemeinen Ansatz zum Entfernen von Cursorn, der ungefähr so ​​aussieht:

  • Entfernen Sie alle Cursorsprachen (Cursor deklarieren, öffnen, abrufen, während, schließen, freigeben usw.) und andere Variablendeklarationen
  • Identifizieren Sie Orte, an denen satzbasierte Operationen kombiniert werden können (Variablen, die mit einem SELECT gefüllt sind und später in einem INSERT verwendet werden, können beispielsweise durch eine INSERT INTO...SELECT - Anweisung ersetzt werden).
  • Verschieben Sie die bedingte Logik (IF...ELSE) In WHERE -Klauseln, CASE Anweisungen, Unterabfragen usw.

Wie die anderen großartigen Antworten hier gezeigt haben, gibt es dafür keine Silberkugel. Aber diese Videos sind meiner Meinung nach ein wirklich intuitiver Ansatz zur Lösung des Problems.

Doug durchläuft drei Cursor-Ersetzungen mit zunehmender Komplexität in jedem Teil. Ich würde es sehr empfehlen, sie anzuschauen (da das ganze Geschäft im Video besser rüberkommt):

1
Josh Darnell