2008-09-15 16 views
186

Powiedzmy mam następujący prosty stół zmiennej:Czy istnieje sposób przechwytywania zmiennej tabeli w TSQL bez użycia kursora?

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

deklaruje i za pomocą kursora moim jedynym rozwiązaniem, jeśli chciałem wykonać iterację wierszy? Czy istnieje inny sposób?

+3

mógł Pan podać nam dlaczego chcesz iteracyjne nad rzędów, inne rozwiązanie nie wymagają iteracji, która mogłaby istnieć (i które są w większości przypadków szybsze z większego marginesu) –

+0

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 potrzebujesz – Shawn

+0

http://wiki.lessthandot.com/index.php/Cursors_and_How_to_Avoid_Them – HLGEM

Odpowiedz

267

Przede wszystkim należy mieć absolutną pewność, trzeba wykonać iterację każdego wiersza - operacje zestaw oparty wykona szybciej w każdym przypadku mogę myśleć i będzie normalnie użyj prostszego kodu.

zależności od danych może być możliwe do pętli tylko przy użyciu select, 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 należy wybrać tak naprawdę zależy od struktury i ilości danych.

Uwaga: Jeśli używasz SQL Server można byłoby lepiej podawane przy użyciu:

WHILE EXISTS(SELECT * FROM #Temp) 

Korzystanie COUNT będzie musiał dotknąć każdy pojedynczy wiersz w tabeli, EXISTS wystarczy tylko dotknąć pierwszy (patrz poniżej: Josef's answer).

+0

"Wybierz pierwszą 1 @Id = ID z ATable" powinno być "Wybierz pierwszą 1 @Id = ID z ATable, gdzie Przetwarzane = 0" – Amzath

+9

Jeśli używasz programu SQL Server, zobacz odpowiedź Josefa poniżej, aby wprowadzić drobne poprawki do powyższego. – Polshgiant

+2

Czy możesz wyjaśnić, dlaczego jest to lepsze niż użycie kursora? –

2

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

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

    Delete From #TempTable Where x = x 
End 
14

Oto jak zrobiłbym to:

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] Bo prawdopodobnie pominięte słowo „zmienna”, kiedy pierwszy raz przeczytać pytanie, tutaj jest zaktualizowaną 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 
+4

, więc w zasadzie robisz kursor, ale bez wszystkich zalet kursora – Shawn

+1

... bez blokowania tabel, które są używane podczas przetwarzania ... ponieważ jest to jedna z * zalet * kursora :) – leoinfo

+3

Tabele? Jest to tabela VARIABLE - nie ma równoczesnego dostępu. – DenNukem

0

zgadzam się z poprzedniego postu, które wyróżniają oparte operacje zazwyczaj osiągają lepsze wyniki, ale jeśli trzeba iteracyjne tu wierszy jest podejście wziąłbym:

  1. Dodaj nowe pole do zmiennej tabeli (dane typu bit, domyślnie 0)
  2. wstawić swoje dane
  3. Wybierz TOP 1 wiersz gdzie Fused = 0 (Uwaga: Fused to nazwa pola w punkcie 1)
  4. wykonać cokolwiek przetwarzanie trzeba zrobić
  5. zaktualizować rekord w zmiennej tabeli przez ustawienie Fused = 1 do protokołu
  6. Wybierz następny nieużywany rekord z tabeli i powtórzyć 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 
    
108

Wystarczy krótka notatka, jeśli używasz SQL Server, przykłady, które mają:

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

byłoby lepiej podawane z

While EXISTS(SELECT * From #Temp) 

Hrabia będzie musiał dotknąć każdy pojedynczy wiersz w tabeli, EXISTS tylko musi dotknąć pierwszego.

+6

To nie jest odpowiedź, ale komentarz/ulepszenie w odpowiedzi na Martynw. –

+6

Treść tej notatki wymusza lepsze funkcje formatowania niż komentarz, proponuję dołączyć w odpowiedzi. – Custodio

+1

W późniejszych wersjach SQL optymalizator zapytań jest na tyle sprytny, by wiedzieć, że kiedy piszesz pierwszą rzecz, masz na myśli drugą i optymalizuje ją jako taką, aby uniknąć skanowania tabeli. –

7

Jeśli nie masz wyboru, możesz przejść wiersz po wierszu, tworząc kursor FAST_FORWARD. Będzie to równie szybkie jak budowanie pętli i dużo łatwiejsze do utrzymania na dłuższą metę.

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

+1

Tak! Jak skomentowałem w innym miejscu, nie widziałem jeszcze argumentów, dlaczego ** NIE ** używać kursora, gdy przypadek ma się powtarzać nad ** zmienną tabeli **. Kursor 'FAST_FORWARD' jest dobrym rozwiązaniem. (upvote) – peterh

15

Zdefiniuj temp tabeli tak -

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 

Wtedy to zrobić -

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 
2

ja naprawdę nie widzę sensu dlaczego trzeba uciekać się do używania bał cursor. Ale tu jest inna opcja, jeśli używasz SQL Server w wersji 2005/2008
Zastosowanie rekursji

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 
31

To jak to zrobić:

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 

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

1

Mam zamiar 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 szybszy proces niż technika pętlowa i jest łatwiejsze do napisania i obsługi.

2
-- [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 
3

Innym podejściem bez konieczności zmiany schematu lub przy użyciu 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 
1

ta będzie działać w wersji SQL Server 2012.

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

while(@Rowcount>0) 
    begin 
select @[email protected]; 
SELECT * FROM AddressTable order by AddressId desc OFFSET @Rowcount ROWS FETCH NEXT 1 ROWS ONLY; 
end 
0

To jest kod, którego używam 2008 R2. Ten kod, który używam jest budowanie indeksów na kluczowych obszarach (SSNO & EMPR_NO) n wszystkie opowieści

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 
0

Wybierz @pk = @pk + 1 będzie lepiej: SET @pk + = @pk. Unikaj używania SELECT, jeśli nie odwołujesz się do tabel, tylko przypisujesz wartości.

0

Krok 1: Poniżej instrukcji select tworzy tabelę tymczasową 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: Declare wymagane zmienne

DECLARE @ROWNUMBER INT 
DECLARE @ename varchar(100) 

Krok 3: przejąć całkowitą wierszy liczyć z tabeli temp

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

Krok 4: Pętla tabeli temp na podstawie unikalnego numeru wiersza stworzyć w temp

while @rownumber>0 
begin 
    set @[email protected] 
    select @ename=ename from #tmp_sri where [email protected] **// You can take columns data from here as many as you want** 
    set @[email protected] 
    print @ename **// instead of printing, you can write insert, update, delete statements** 
end 
2

Lekkie, bez konieczności dokonywania dodatkowych tabel, jeśli masz całkowitą ID na stole

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

    --Process @anything 

END 
0

Takie podejście wymaga tylko jednej zmiennej, a nie usuwać żadnych wierszy z @databases. Wiem, że jest tu wiele odpowiedzi, ale nie widzę takiego, który używa MIN, aby otrzymać kolejny 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 
1

Wolę użyciu Offset Fetch jeśli masz unikatowy identyfikator można sortować 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 

ten sposób nie trzeba dodawać pola do stołu lub użyć okna funkcjonować.

+0

pokazuje mi błąd składni na "FETCH NEXT 1 ROW;" –

1

Jest możliwe użycie kursora, aby to zrobić:

tworzyć funkcja [dbo] .f_teste_loop powraca @tabela stół ( cod int nome varchar (10) ) jak rozpocząć

procedura
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

tworzyć [dbo].[Sp_teste_loop] jak rozpocząć

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

+0

Nie było oryginalne pytanie "Bez używania kursora"? –

1

Oto moje rozwiązanie, które sprawia, że ​​korzystanie z nieskończoną pętlę, oświadczenie BREAK, a funkcja @@ROWCOUNT. Brak kursorów lub tymczasowej tabeli są niezbędne, a ja tylko trzeba napisać jedno zapytanie dostać następnego wiersza w @databases tabeli:

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 
+0

Właśnie zdałem sobie sprawę, że ** @ ControlFreak ** zalecił mi to podejście; Po prostu dodałem komentarze i bardziej szczegółowy przykład. –