Czy istnieje sposób na przejście przez zmienną tabelową w TSQL bez użycia kursora?

243

Powiedzmy, że mam następującą prostą zmienną tabelową:

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

Czy deklarowanie i używanie kursora jest moją jedyną opcją, jeśli chcę iterować między wierszami? Czy jest inny sposób?

Promień
źródło
3
Chociaż nie jestem pewien problemu, jaki widzisz w powyższym podejściu; Sprawdź, czy to pomoże .. databasejournal.com/features/mssql/article.php/3111031
Gishu
5
Czy możesz podać nam powód, dla którego chcesz iterować po wierszach, może istnieć inne rozwiązanie, które nie wymaga iteracji (i które w większości przypadków są szybsze z dużym marginesem)
Pop Catalin
zgadzam się z pop ... może nie potrzebować kursora w zależności od sytuacji. ale nie ma problemu z używaniem kursorów, jeśli trzeba
Shawn
3
Nie podajesz, dlaczego chcesz uniknąć kursora. Pamiętaj, że kursor może być najprostszym sposobem na iterację. Być może słyszałeś, że kursory są „złe”, ale tak naprawdę iteracja po tabelach jest zła w porównaniu z operacjami opartymi na zestawach. Jeśli nie możesz uniknąć iteracji, najlepszym sposobem może być kursor. Blokowanie to kolejny problem z kursorami, ale nie jest to istotne, gdy używana jest zmienna tabelowa.
JacquesB
1
Używanie kursora nie jest jedyną opcją, ale jeśli nie możesz uniknąć podejścia wiersz po rzędzie, będzie to najlepsza opcja. Kursory są wbudowaną konstrukcją, która jest bardziej wydajna i mniej podatna na błędy niż tworzenie własnej głupiej pętli WHILE. W większości przypadków wystarczy użyć STATICopcji, aby usunąć ciągłe ponowne sprawdzanie tabel bazowych i blokowania, które są tam domyślnie i powodują, że większość ludzi błędnie uważa, że ​​KURSORY są złe. @JacquesB bardzo blisko: ponowne sprawdzenie, czy wiersz wyników nadal istnieje + blokowanie to problemy. I STATICzwykle to naprawia :-).
Solomon Rutzky

Odpowiedzi:

376

Przede wszystkim powinieneś być absolutnie pewien, że musisz iterować po każdym wierszu - operacje oparte na zestawie wierszy będą działały szybciej w każdym przypadku, o którym mogę pomyśleć i zwykle będą używać prostszego kodu.

W zależności od danych może być możliwe zapętlenie za pomocą SELECTinstrukcji, jak pokazano poniżej:

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

Inną alternatywą jest użycie tabeli tymczasowej:

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

Opcja, którą powinieneś wybrać, naprawdę zależy od struktury i objętości twoich danych.

Uwaga: jeśli korzystasz z programu SQL Server, lepiej byś był obsługiwany za pomocą:

WHILE EXISTS(SELECT * FROM #Temp)

Używanie COUNTbędzie musiało dotykać każdego wiersza w tabeli, EXISTSwystarczy dotknąć pierwszego (patrz odpowiedź Josefa poniżej).

Martynnw
źródło
„Wybierz pierwszą 1 @Id = Id z ATable” powinno być „Wybierz pierwszą 1 @Id = Id z ATable gdzie przetworzone = 0”
Amzath 21.01.11
10
Jeśli używasz programu SQL Server, zapoznaj się z odpowiedzią Josefa poniżej, aby uzyskać drobną korektę powyżej.
Polshgiant,
3
Czy możesz wyjaśnić, dlaczego jest to lepsze niż używanie kursora?
marco-fiset
5
Daj temu głosowanie negatywne. Dlaczego miałby unikać używania kursora? Mówi o iteracji po zmiennej tabeli , a nie tradycyjnej tabeli. Nie wierzę, że tutaj obowiązują normalne wady kursorów. Jeśli przetwarzanie wiersza po rzędzie jest naprawdę wymagane (a jak zauważysz, powinien być pewien, że to pierwsze), wówczas użycie kursora jest znacznie lepszym rozwiązaniem niż te, które tu opisujesz.
peterh
@peterh Masz rację. I w rzeczywistości zwykle można uniknąć tych „normalnych wad”, używając STATICopcji kopiującej zestaw wyników do tabeli tymczasowej, a zatem nie blokujesz już ani nie sprawdzasz ponownie tabel podstawowych :-).
Solomon Rutzky
132

Krótka uwaga, jeśli używasz SQL Server (2008 i wyżej), przykłady, które mają:

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

Lepiej by było

While EXISTS(SELECT * From #Temp)

Hrabia będzie musiał dotknąć każdego wiersza w tabeli, EXISTSwystarczy dotknąć pierwszego.

Josef
źródło
9
To nie jest odpowiedź, ale komentarz / rozszerzenie odpowiedzi Martynw.
Hammad Khan
7
Treść tej notatki wymusza lepszą funkcjonalność formatowania niż komentarz, sugeruję dołączyć ją w odpowiedzi.
Custodio
2
W późniejszych wersjach SQL optymalizator zapytań jest wystarczająco sprytny, aby wiedzieć, że kiedy piszesz pierwszą rzecz, masz na myśli drugą i optymalizuje ją jako taką, aby uniknąć skanowania tabeli.
Dan Def
39

Tak to robię:

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

Bez kursorów, bez tabel tymczasowych, bez dodatkowych kolumn. Kolumna USERID musi być unikalną liczbą całkowitą, podobnie jak większość kluczy podstawowych.

Trevor
źródło
26

Zdefiniuj swój stół tymczasowy w ten sposób -

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

Następnie zrób to -

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
źródło
16

Oto jak bym to zrobił:

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

[Edytuj] Ponieważ prawdopodobnie pominąłem słowo „zmienna”, kiedy po raz pierwszy przeczytałem pytanie, oto zaktualizowana odpowiedź ...


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
źródło
4
więc w zasadzie robisz kursor, ale bez wszystkich zalet kursora
Shawn
1
... bez blokowania tabel używanych podczas przetwarzania ... ponieważ jest to jedna z zalet kursora :)
leoinfo
3
Stoły To stół ZMIENNY - nie jest możliwy jednoczesny dostęp.
DenNukem,
DenNukem, masz rację, myślę, że „pominąłem” słowo „zmienna”, kiedy czytam pytanie w tym czasie ... Dodam kilka uwag do mojej pierwszej odpowiedzi
leoinfo
Muszę się zgodzić z DenNukem i Shawnem. Dlaczego, dlaczego, dlaczego przechodzisz do tych długości, aby uniknąć używania kursora? Znowu: chce iterować zmienną tabeli, a nie tradycyjną tabelę !!!
peterh
10

Jeśli nie masz innego wyboru, niż przejść wiersz po rzędzie, tworząc kursor FAST_FORWARD. Będzie to tak szybkie, jak tworzenie pętli while i znacznie łatwiejsze w utrzymaniu na dłuższą metę.

FAST_FORWARD Określa kursor FORWARD_ONLY, READ_ONLY z włączoną optymalizacją wydajności. FAST_FORWARD nie może zostać określony, jeśli określono również SCROLL lub FOR_UPDATE.


źródło
2
Tak! Jak skomentowałem gdzie indziej, nie widziałem jeszcze żadnych argumentów, dlaczego NIE należy używać kursora, gdy przypadek ma iterować po zmiennej tabeli . FAST_FORWARDKursor jest rozwiązaniem dobrze. (opinia pozytywna)
Peter
5

Inne podejście bez konieczności zmiany schematu lub korzystania z tabel tymczasowych:

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
źródło
4

Możesz użyć pętli while:

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

    Delete From #TempTable Where x = x
End
GateKiller
źródło
4

Będzie to działać w wersji SQL SERVER 2012.

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
źródło
4

Lekki, bez konieczności tworzenia dodatkowych tabel, jeśli masz IDna stole liczbę całkowitą

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
Control Freak
źródło
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
źródło
2

Naprawdę nie widzę sensu, dlaczego musiałbyś uciekać się do używania przerażającego cursor. Ale jest inna opcja, jeśli używasz SQL Server w wersji 2005/2008
Użyj rekurencji

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
źródło
2

Zamierzam dostarczyć rozwiązanie oparte na zestawie.

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

Jest to znacznie szybsze niż jakakolwiek technika zapętlania i jest łatwiejsze do napisania i utrzymania.

HLGEM
źródło
2

Wolę używać funkcji Offset Fetch, jeśli masz unikalny identyfikator, dzięki któremu możesz posortować tabelę według:

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

W ten sposób nie muszę dodawać pól do tabeli ani używać funkcji okna.

Yves A Martin
źródło
2

Można to zrobić za pomocą kursora:

funkcja tworzenia [dbo] .f_teste_loop zwraca tabelę @tabela (cod int, nome varchar (10)) jak początek

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;

koniec

utwórz procedurę [dbo]. [sp_teste_loop] jak zacząć

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;

koniec

Alexandre Pezzutto
źródło
1
Czy pierwotne pytanie nie brzmiało „Bez użycia kursora”?
Fernando Gonzalez Sanchez
1

Zgadzam się z poprzednim postem, że operacje oparte na zestawach będą zazwyczaj działały lepiej, ale jeśli musisz iterować wiersze, oto podejście, które wybrałbym:

  1. Dodaj nowe pole do zmiennej tabeli (bit typu danych, domyślnie 0)
  2. Wprowadź swoje dane
  3. Wybierz wiersz 1, w którym fUsed = 0 (Uwaga: fUsed to nazwa pola w kroku 1)
  4. Wykonaj dowolne przetwarzanie, które musisz wykonać
  5. Zaktualizuj rekord w zmiennej tabeli, ustawiając fUsed = 1 dla rekordu
  6. Wybierz następny nieużywany rekord z tabeli i powtórz proces

    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
źródło
1

Krok 1: Poniższa instrukcja select tworzy tabelę temp z unikalnym numerem wiersza dla każdego rekordu.

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

Krok 2: Zadeklaruj wymagane zmienne

DECLARE @ROWNUMBER INT
DECLARE @ename varchar(100)

Krok 3: Pobierz całkowitą liczbę wierszy z tabeli temp

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

Krok 4: Tabela temp. Pętli oparta na unikalnym numerze wiersza utworzona w temp

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
źródło
1

To podejście wymaga tylko jednej zmiennej i nie usuwa żadnych wierszy z @ baz danych. Wiem, że jest tu wiele odpowiedzi, ale nie widzę takiej, która używa MIN, aby uzyskać twój następny identyfikator w ten sposób.

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
źródło
1

Oto moje rozwiązanie, które korzysta z nieskończonej pętli, BREAKinstrukcji i @@ROWCOUNTfunkcji. Nie są wymagane żadne kursory ani tabela tymczasowa i muszę tylko napisać jedno zapytanie, aby uzyskać następny wiersz w @databasestabeli:

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
źródło
Właśnie zdałem sobie sprawę, że @ControlFreak polecił mi to podejście; Po prostu dodałem komentarze i bardziej szczegółowy przykład.
Mass Dot Net
0

To jest kod, którego używam 2008 R2. Ten kod, którego używam, służy do tworzenia indeksów na kluczowych polach (SSNO i EMPR_NO) we wszystkich opowieściach

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
źródło
0
SELECT @pk = @pk + 1

byłoby lepiej:

SET @pk += @pk

Unikaj używania WYBIERZ, jeśli nie odwołujesz się do tabel, tylko przypisujesz wartości.

Bob Alley
źródło