Gibt es eine Möglichkeit, eine Tabellenvariable in TSQL ohne Verwendung eines Cursors zu durchlaufen?

243

Angenommen, ich habe die folgende einfache Tabellenvariable:

declare @databases table
(
    DatabaseID    int,
    Name        varchar(15),   
    Server      varchar(15)
)
-- insert a bunch rows into @databases

Ist das Deklarieren und Verwenden eines Cursors meine einzige Option, wenn ich die Zeilen durchlaufen möchte? Gibt es eine andere Art und Weise?

Strahl
quelle
3
Obwohl ich nicht sicher bin, welches Problem Sie mit dem obigen Ansatz sehen; Sehen Sie, ob dies hilft .. databasejournal.com/features/mssql/article.php/3111031
Gishu
5
Könnten Sie uns den Grund nennen, warum Sie über die Zeilen iterieren möchten? Möglicherweise gibt es eine andere Lösung, für die keine Iteration erforderlich ist (und die in den meisten Fällen um ein Vielfaches schneller ist)
Pop Catalin,
stimme mit pop überein ... benötigt je nach Situation möglicherweise keinen Cursor. Aber es gibt kein Problem mit der Verwendung von Cursorn, wenn Sie müssen
Shawn
3
Sie geben nicht an, warum Sie einen Cursor vermeiden möchten. Beachten Sie, dass ein Cursor möglicherweise die einfachste Methode zum Iterieren ist. Sie haben vielleicht gehört, dass Cursor "schlecht" sind, aber es ist wirklich eine Iteration über Tabellen, die im Vergleich zu satzbasierten Operationen schlecht ist. Wenn Sie die Iteration nicht vermeiden können, ist ein Cursor möglicherweise der beste Weg. Das Sperren ist ein weiteres Problem bei Cursorn, das jedoch bei Verwendung einer Tabellenvariablen nicht relevant ist.
JacquesB
1
Die Verwendung eines Cursors ist nicht Ihre einzige Option. Wenn Sie jedoch keine Möglichkeit haben, einen zeilenweisen Ansatz zu vermeiden, ist dies die beste Option. CURSORs sind ein integriertes Konstrukt, das effizienter und weniger fehleranfällig ist als Ihre eigene dumme WHILE-Schleife. Meistens müssen Sie nur die STATICOption verwenden, um die ständige Überprüfung der Basistabellen und die Sperren, die standardmäßig vorhanden sind, zu entfernen und die meisten Leute fälschlicherweise glauben zu lassen, dass CURSORs böse sind. @JacquesB ganz in der Nähe: Überprüfen Sie erneut, ob die Ergebniszeile noch vorhanden ist. + Sperren sind die Probleme. Und STATICnormalerweise behebt das :-).
Solomon Rutzky

Antworten:

376

Zuallererst sollten Sie absolut sicher sein, dass Sie jede zeilensatzbasierte Operation durchlaufen müssen. Sie wird in jedem denkbaren Fall schneller ausgeführt und verwendet normalerweise einfacheren Code.

Abhängig von Ihren Daten kann es möglich sein, nur die folgenden SELECTAnweisungen zu verwenden:

Declare @Id int

While (Select Count(*) From ATable Where Processed = 0) > 0
Begin
    Select Top 1 @Id = Id From ATable Where Processed = 0

    --Do some processing here

    Update ATable Set Processed = 1 Where Id = @Id 

End

Eine andere Alternative ist die Verwendung einer temporären Tabelle:

Select *
Into   #Temp
From   ATable

Declare @Id int

While (Select Count(*) From #Temp) > 0
Begin

    Select Top 1 @Id = Id From #Temp

    --Do some processing here

    Delete #Temp Where Id = @Id

End

Die Option, die Sie auswählen sollten, hängt wirklich von der Struktur und dem Volumen Ihrer Daten ab.

Hinweis: Wenn Sie SQL Server verwenden, sollten Sie Folgendes verwenden:

WHILE EXISTS(SELECT * FROM #Temp)

Mit COUNTmuss jede einzelne Zeile in der Tabelle berührt werden, die EXISTSeinzige muss nur die erste berühren (siehe Josefs Antwort unten).

Martynnw
quelle
"Wählen Sie Top 1 @Id = ID von ATable" sollte "Wählen Sie Top 1 @Id = ID von ATable, wo verarbeitet = 0" sein
Amzath
10
Wenn Sie SQL Server verwenden, finden Sie in Josefs Antwort unten eine kleine Änderung der obigen Informationen.
Polshgiant
3
Können Sie erklären, warum dies besser ist als die Verwendung eines Cursors?
Marco-Fiset
5
Hat diesem einen Downvote gegeben. Warum sollte er es vermeiden, einen Cursor zu benutzen? Er spricht über das Iterieren über Tabellenvariablen , nicht über eine traditionelle Tabelle. Ich glaube nicht, dass hier die normalen Nachteile von Cursorn zutreffen. Wenn eine zeilenweise Verarbeitung wirklich erforderlich ist (und wie Sie darauf hinweisen, sollte er sich dessen zuerst sicher sein), ist die Verwendung eines Cursors eine viel bessere Lösung als die hier beschriebenen.
Peterh
@ Peter Du bist richtig. Tatsächlich können Sie diese "normalen Nachteile" normalerweise vermeiden, indem Sie die STATICOption verwenden, mit der die Ergebnismenge in eine temporäre Tabelle kopiert wird, und daher werden Basistabellen nicht mehr gesperrt oder erneut überprüft :-).
Solomon Rutzky
132

Nur eine kurze Anmerkung, wenn Sie SQL Server (2008 und höher) verwenden, die folgenden Beispiele:

While (Select Count(*) From #Temp) > 0

Wäre besser mit serviert

While EXISTS(SELECT * From #Temp)

Der Count muss jede einzelne Zeile in der Tabelle EXISTSberühren , die einzige muss nur die erste berühren.

Josef
quelle
9
Dies ist keine Antwort, sondern ein Kommentar / eine Verbesserung der Martynw-Antwort.
Hammad Khan
7
Der Inhalt dieser Notiz erzwingt eine bessere Formatierungsfunktionalität als ein Kommentar. Ich würde vorschlagen, an die Antwort anzuhängen.
Custodio
2
In späteren Versionen von SQL ist der Abfrageoptimierer klug genug zu wissen, dass Sie beim Schreiben des ersten Dings tatsächlich das zweite meinen und es als solches optimieren, um den Tabellenscan zu vermeiden.
Dan Def
39

So mache ich es:

declare @RowNum int, @CustId nchar(5), @Name1 nchar(25)

select @CustId=MAX(USERID) FROM UserIDs     --start with the highest ID
Select @RowNum = Count(*) From UserIDs      --get total number of records
WHILE @RowNum > 0                          --loop until no more records
BEGIN   
    select @Name1 = username1 from UserIDs where USERID= @CustID    --get other info from that row
    print cast(@RowNum as char(12)) + ' ' + @CustId + ' ' + @Name1  --do whatever

    select top 1 @CustId=USERID from UserIDs where USERID < @CustID order by USERID desc--get the next one
    set @RowNum = @RowNum - 1                               --decrease count
END

Keine Cursor, keine temporären Tabellen, keine zusätzlichen Spalten. Die USERID-Spalte muss eine eindeutige Ganzzahl sein, wie es die meisten Primärschlüssel sind.

Trevor
quelle
26

Definieren Sie Ihre temporäre Tabelle wie folgt:

declare @databases table
(
    RowID int not null identity(1,1) primary key,
    DatabaseID    int,
    Name        varchar(15),   
    Server      varchar(15)
)

-- insert a bunch rows into @databases

Dann mach das -

declare @i int
select @i = min(RowID) from @databases
declare @max int
select @max = max(RowID) from @databases

while @i <= @max begin
    select DatabaseID, Name, Server from @database where RowID = @i --do some stuff
    set @i = @i + 1
end
Seibar
quelle
16

So würde ich es machen:

Select Identity(int, 1,1) AS PK, DatabaseID
Into   #T
From   @databases

Declare @maxPK int;Select @maxPK = MAX(PK) From #T
Declare @pk int;Set @pk = 1

While @pk <= @maxPK
Begin

    -- Get one record
    Select DatabaseID, Name, Server
    From @databases
    Where DatabaseID = (Select DatabaseID From #T Where PK = @pk)

    --Do some processing here
    -- 

    Select @pk = @pk + 1
End

[Bearbeiten] Da ich beim ersten Lesen der Frage wahrscheinlich das Wort "Variable" übersprungen habe, finden Sie hier eine aktualisierte Antwort ...


declare @databases table
(
    PK            int IDENTITY(1,1), 
    DatabaseID    int,
    Name        varchar(15),   
    Server      varchar(15)
)
-- insert a bunch rows into @databases
--/*
INSERT INTO @databases (DatabaseID, Name, Server) SELECT 1,'MainDB', 'MyServer'
INSERT INTO @databases (DatabaseID, Name, Server) SELECT 1,'MyDB',   'MyServer2'
--*/

Declare @maxPK int;Select @maxPK = MAX(PK) From @databases
Declare @pk int;Set @pk = 1

While @pk <= @maxPK
Begin

    /* Get one record (you can read the values into some variables) */
    Select DatabaseID, Name, Server
    From @databases
    Where PK = @pk

    /* Do some processing here */
    /* ... */ 

    Select @pk = @pk + 1
End
Leoinfo
quelle
4
Im Grunde genommen machst du einen Cursor, aber ohne alle Vorteile eines Cursors
Shawn
1
... ohne die Tabellen zu sperren, die während der Verarbeitung verwendet werden ... da dies einer der Vorteile eines Cursors ist :)
Leoinfo
3
Tische? Es ist eine Tabelle VARIABLE - es ist kein gleichzeitiger Zugriff möglich.
DenNukem
DenNukem, Sie haben Recht, ich glaube, ich habe das Wort "Variable" "übersprungen", als ich die Frage zu diesem Zeitpunkt las ... Ich werde meiner ersten Antwort einige Anmerkungen hinzufügen
Leoinfo
Ich muss DenNukem und Shawn zustimmen. Warum, warum, warum gehen Sie zu diesen Längen, um die Verwendung eines Cursors zu vermeiden? Nochmals: Er möchte eine Tabellenvariable durchlaufen, keine traditionelle Tabelle !!!
Peterh
10

Wenn Sie keine andere Wahl haben, als Zeile für Zeile einen FAST_FORWARD-Cursor zu erstellen. Es ist so schnell wie der Aufbau einer while-Schleife und auf lange Sicht viel einfacher zu warten.

FAST_FORWARD Gibt einen FORWARD_ONLY-, READ_ONLY-Cursor mit aktivierten Leistungsoptimierungen an. FAST_FORWARD kann nicht angegeben werden, wenn auch SCROLL oder FOR_UPDATE angegeben ist.


quelle
2
Ja! Wie ich an anderer Stelle kommentiert habe, habe ich noch keine Argumente dafür gesehen, warum NICHT ein Cursor verwendet werden soll, wenn der Fall über eine Tabellenvariable iteriert werden soll . Ein FAST_FORWARDCursor ist eine feine Lösung. (Gegenstimme)
Peterh
5

Ein anderer Ansatz, ohne dass Sie Ihr Schema ändern oder temporäre Tabellen verwenden müssen:

DECLARE @rowCount int = 0
  ,@currentRow int = 1
  ,@databaseID int
  ,@name varchar(15)
  ,@server varchar(15);

SELECT @rowCount = COUNT(*)
FROM @databases;

WHILE (@currentRow <= @rowCount)
BEGIN
  SELECT TOP 1
     @databaseID = rt.[DatabaseID]
    ,@name = rt.[Name]
    ,@server = rt.[Server]
  FROM (
    SELECT ROW_NUMBER() OVER (
        ORDER BY t.[DatabaseID], t.[Name], t.[Server]
       ) AS [RowNumber]
      ,t.[DatabaseID]
      ,t.[Name]
      ,t.[Server]
    FROM @databases t
  ) rt
  WHERE rt.[RowNumber] = @currentRow;

  EXEC [your_stored_procedure] @databaseID, @name, @server;

  SET @currentRow = @currentRow + 1;
END
SReiderB
quelle
4

Sie können eine while-Schleife verwenden:

While (Select Count(*) From #TempTable) > 0
Begin
    Insert Into @Databases...

    Delete From #TempTable Where x = x
End
GateKiller
quelle
4

Dies funktioniert in der SQL Server 2012-Version.

declare @Rowcount int 
select @Rowcount=count(*) from AddressTable;

while( @Rowcount>0)
  begin 
 select @Rowcount=@Rowcount-1;
 SELECT * FROM AddressTable order by AddressId desc OFFSET @Rowcount ROWS FETCH NEXT 1 ROWS ONLY;
end 
OrganicCoder
quelle
4

Leicht, ohne zusätzliche Tabellen erstellen zu müssen, wenn Sie eine Ganzzahl IDauf der Tabelle haben

Declare @id int = 0, @anything nvarchar(max)
WHILE(1=1) BEGIN
  Select Top 1 @anything=[Anything],@id=@id+1 FROM Table WHERE ID>@id
  if(@@ROWCOUNT=0) break;

  --Process @anything

END
Kontroll-Freak
quelle
3
-- [PO_RollBackOnReject]  'FININV10532'
alter procedure PO_RollBackOnReject
@CaseID nvarchar(100)

AS
Begin
SELECT  *
INTO    #tmpTable
FROM   PO_InvoiceItems where CaseID = @CaseID

Declare @Id int
Declare @PO_No int
Declare @Current_Balance Money


While (Select ROW_NUMBER() OVER(ORDER BY PO_LineNo DESC) From #tmpTable) > 0
Begin
        Select Top 1 @Id = PO_LineNo, @Current_Balance = Current_Balance,
        @PO_No = PO_No
        From #Temp
        update PO_Details
        Set  Current_Balance = Current_Balance + @Current_Balance,
            Previous_App_Amount= Previous_App_Amount + @Current_Balance,
            Is_Processed = 0
        Where PO_LineNumber = @Id
        AND PO_No = @PO_No
        update PO_InvoiceItems
        Set IsVisible = 0,
        Is_Processed= 0
        ,Is_InProgress = 0 , 
        Is_Active = 0
        Where PO_LineNo = @Id
        AND PO_No = @PO_No
End
End
Syed Umar Ahmed
quelle
2

Ich verstehe wirklich nicht den Punkt, warum Sie auf gefürchtete zurückgreifen müssten cursor. Wenn Sie SQL Server Version 2005/2008 verwenden, verwenden
Sie die Rekursion

declare @databases table
(
    DatabaseID    int,
    Name        varchar(15),   
    Server      varchar(15)
)

--; Insert records into @databases...

--; Recurse through @databases
;with DBs as (
    select * from @databases where DatabaseID = 1
    union all
    select A.* from @databases A 
        inner join DBs B on A.DatabaseID = B.DatabaseID + 1
)
select * from DBs
dance2die
quelle
2

Ich werde die setbasierte Lösung bereitstellen.

insert  @databases (DatabaseID, Name, Server)
select DatabaseID, Name, Server 
From ... (Use whatever query you would have used in the loop or cursor)

Dies ist weitaus schneller als jede Schleifentechnik und einfacher zu schreiben und zu warten.

HLGEM
quelle
2

Ich bevorzuge die Verwendung des Offset-Abrufs, wenn Sie eine eindeutige ID haben, nach der Sie Ihre Tabelle sortieren können:

DECLARE @TableVariable (ID int, Name varchar(50));
DECLARE @RecordCount int;
SELECT @RecordCount = COUNT(*) FROM @TableVariable;

WHILE @RecordCount > 0
BEGIN
SELECT ID, Name FROM @TableVariable ORDER BY ID OFFSET @RecordCount - 1 FETCH NEXT 1 ROW;
SET @RecordCount = @RecordCount - 1;
END

Auf diese Weise muss ich der Tabelle keine Felder hinzufügen oder eine Fensterfunktion verwenden.

Yves A Martin
quelle
2

Hierzu kann ein Cursor verwendet werden:

create function [dbo] .f_teste_loop gibt als Anfang die Tabelle @tabela (cod int, nome varchar (10)) zurück

insert into @tabela values (1, 'verde');
insert into @tabela values (2, 'amarelo');
insert into @tabela values (3, 'azul');
insert into @tabela values (4, 'branco');

return;

Ende

Erstellen Sie die Prozedur [dbo]. [sp_teste_loop] als Beginn

DECLARE @cod int, @nome varchar(10);

DECLARE curLoop CURSOR STATIC LOCAL 
FOR
SELECT  
    cod
   ,nome
FROM 
    dbo.f_teste_loop();

OPEN curLoop;

FETCH NEXT FROM curLoop
           INTO @cod, @nome;

WHILE (@@FETCH_STATUS = 0)
BEGIN
    PRINT @nome;

    FETCH NEXT FROM curLoop
           INTO @cod, @nome;
END

CLOSE curLoop;
DEALLOCATE curLoop;

Ende

Alexandre Pezzutto
quelle
1
War die ursprüngliche Frage nicht "Ohne Cursor"?
Fernando Gonzalez Sanchez
1

Ich stimme dem vorherigen Beitrag zu, dass satzbasierte Operationen normalerweise eine bessere Leistung erbringen. Wenn Sie jedoch die Zeilen durchlaufen müssen, würde ich folgende Vorgehensweise wählen:

  1. Fügen Sie Ihrer Tabellenvariablen ein neues Feld hinzu (Datentypbit, Standard 0)
  2. Geben Sie Ihre Daten ein
  3. Wählen Sie die oberste 1 Zeile aus, in der fUsed = 0 ist (Hinweis: fUsed ist der Name des Felds in Schritt 1).
  4. Führen Sie die erforderliche Verarbeitung durch
  5. Aktualisieren Sie den Datensatz in Ihrer Tabellenvariablen, indem Sie für den Datensatz fUsed = 1 festlegen
  6. Wählen Sie den nächsten nicht verwendeten Datensatz aus der Tabelle aus und wiederholen Sie den Vorgang

    DECLARE @databases TABLE  
    (  
        DatabaseID  int,  
        Name        varchar(15),     
        Server      varchar(15),   
        fUsed       BIT DEFAULT 0  
    ) 
    
    -- insert a bunch rows into @databases
    
    DECLARE @DBID INT
    
    SELECT TOP 1 @DBID = DatabaseID from @databases where fUsed = 0 
    
    WHILE @@ROWCOUNT <> 0 and @DBID IS NOT NULL  
    BEGIN  
        -- Perform your processing here  
    
        --Update the record to "used" 
    
        UPDATE @databases SET fUsed = 1 WHERE DatabaseID = @DBID  
    
        --Get the next record  
        SELECT TOP 1 @DBID = DatabaseID from @databases where fUsed = 0   
    END
Tim Lentine
quelle
1

Schritt 1: Unter der select-Anweisung wird eine temporäre Tabelle mit einer eindeutigen Zeilennummer für jeden Datensatz erstellt.

select eno,ename,eaddress,mobno int,row_number() over(order by eno desc) as rno into #tmp_sri from emp 

Schritt 2: Deklarieren Sie die erforderlichen Variablen

DECLARE @ROWNUMBER INT
DECLARE @ename varchar(100)

Schritt 3: Nehmen Sie die Gesamtzahl der Zeilen aus der temporären Tabelle

SELECT @ROWNUMBER = COUNT(*) FROM #tmp_sri
declare @rno int

Schritt 4: Schleifentemp-Tabelle basierend auf der in Temp erstellten eindeutigen Zeilennummer

while @rownumber>0
begin
  set @rno=@rownumber
  select @ename=ename from #tmp_sri where rno=@rno  **// You can take columns data from here as many as you want**
  set @rownumber=@rownumber-1
  print @ename **// instead of printing, you can write insert, update, delete statements**
end
Srinivas Maale
quelle
1

Dieser Ansatz erfordert nur eine Variable und löscht keine Zeilen aus @databases. Ich weiß, dass es hier viele Antworten gibt, aber ich sehe keine, die MIN verwendet, um Ihre nächste ID wie diese zu erhalten.

DECLARE @databases TABLE
(
    DatabaseID    int,
    Name        varchar(15),   
    Server      varchar(15)
)

-- insert a bunch rows into @databases

DECLARE @CurrID INT

SELECT @CurrID = MIN(DatabaseID)
FROM @databases

WHILE @CurrID IS NOT NULL
BEGIN

    -- Do stuff for @CurrID

    SELECT @CurrID = MIN(DatabaseID)
    FROM @databases
    WHERE DatabaseID > @CurrID

END
Sean
quelle
1

Hier ist meine Lösung, die eine Endlosschleife, die BREAKAnweisung und die @@ROWCOUNTFunktion verwendet. Es sind keine Cursor oder temporäre Tabellen erforderlich, und ich muss nur eine Abfrage schreiben, um die nächste Zeile in der @databasesTabelle zu erhalten:

declare @databases table
(
    DatabaseID    int,
    [Name]        varchar(15),   
    [Server]      varchar(15)
);


-- Populate the [@databases] table with test data.
insert into @databases (DatabaseID, [Name], [Server])
select X.DatabaseID, X.[Name], X.[Server]
from (values 
    (1, 'Roger', 'ServerA'),
    (5, 'Suzy', 'ServerB'),
    (8675309, 'Jenny', 'TommyTutone')
) X (DatabaseID, [Name], [Server])


-- Create an infinite loop & ensure that a break condition is reached in the loop code.
declare @databaseId int;

while (1=1)
begin
    -- Get the next database ID.
    select top(1) @databaseId = DatabaseId 
    from @databases 
    where DatabaseId > isnull(@databaseId, 0);

    -- If no rows were found by the preceding SQL query, you're done; exit the WHILE loop.
    if (@@ROWCOUNT = 0) break;

    -- Otherwise, do whatever you need to do with the current [@databases] table row here.
    print 'Processing @databaseId #' + cast(@databaseId as varchar(50));
end
Mass Dot Net
quelle
Ich habe gerade festgestellt, dass @ControlFreak diesen Ansatz vor mir empfohlen hat. Ich habe einfach Kommentare und ein ausführlicheres Beispiel hinzugefügt.
Mass Dot Net
0

Dies ist der Code, den ich 2008 R2 verwende. Dieser Code, den ich verwende, dient zum Erstellen von Indizes für Schlüsselfelder (SSNO & EMPR_NO) in allen Geschichten

if object_ID('tempdb..#a')is not NULL drop table #a

select 'IF EXISTS (SELECT name FROM sysindexes WHERE name ='+CHAR(39)+''+'IDX_'+COLUMN_NAME+'_'+SUBSTRING(table_name,5,len(table_name)-3)+char(39)+')' 
+' begin DROP INDEX [IDX_'+COLUMN_NAME+'_'+SUBSTRING(table_name,5,len(table_name)-3)+'] ON '+table_schema+'.'+table_name+' END Create index IDX_'+COLUMN_NAME+'_'+SUBSTRING(table_name,5,len(table_name)-3)+ ' on '+ table_schema+'.'+table_name+' ('+COLUMN_NAME+') '   'Field'
,ROW_NUMBER() over (order by table_NAMe) as  'ROWNMBR'
into #a
from INFORMATION_SCHEMA.COLUMNS
where (COLUMN_NAME like '%_SSNO_%' or COLUMN_NAME like'%_EMPR_NO_')
    and TABLE_SCHEMA='dbo'

declare @loopcntr int
declare @ROW int
declare @String nvarchar(1000)
set @loopcntr=(select count(*)  from #a)
set @ROW=1  

while (@ROW <= @loopcntr)
    begin
        select top 1 @String=a.Field 
        from #A a
        where a.ROWNMBR = @ROW
        execute sp_executesql @String
        set @ROW = @ROW + 1
    end 
howmnsk
quelle
0
SELECT @pk = @pk + 1

wäre besser:

SET @pk += @pk

Vermeiden Sie die Verwendung von SELECT, wenn Sie nicht auf Tabellen verweisen, sondern nur Werte zuweisen.

Bob Alley
quelle