Esiste un modo per scorrere una variabile di tabella in TSQL senza utilizzare un cursore?
-
09-06-2019 - |
Domanda
Diciamo che ho la seguente variabile di tabella semplice:
declare @databases table
(
DatabaseID int,
Name varchar(15),
Server varchar(15)
)
-- insert a bunch rows into @databases
Dichiarare e utilizzare un cursore è la mia unica opzione se volessi scorrere le righe?C'è un altro modo?
Soluzione
Prima di tutto dovresti essere assolutamente sicuro di dover scorrere ogni riga: le operazioni basate su set verranno eseguite più velocemente in ogni caso che mi viene in mente e normalmente utilizzeranno un codice più semplice.
A seconda dei tuoi dati, potrebbe essere possibile eseguire il loop semplicemente utilizzando le istruzioni select come mostrato di seguito:
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
Un'altra alternativa è utilizzare una tabella temporanea:
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
L'opzione che dovresti scegliere dipende davvero dalla struttura e dal volume dei tuoi dati.
Nota: Se utilizzi SQL Server, ti verrà meglio servito utilizzando:
WHILE EXISTS(SELECT * FROM #Temp)
Utilizzando COUNT
dovrà toccare ogni singola riga della tabella, il EXISTS
basta toccare il primo (vedi La risposta di Josef sotto).
Altri suggerimenti
Solo una breve nota, se utilizzi SQL Server (2008 e versioni successive), gli esempi che hanno:
While (Select Count(*) From #Temp) > 0
Sarebbe meglio servirlo
While EXISTS(SELECT * From #Temp)
Il Conte dovrà toccare ogni singola riga della tabella, la EXISTS
basta toccare il primo.
Ecco come lo faccio:
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
Nessun cursore, nessuna tabella temporanea, nessuna colonna aggiuntiva.La colonna USERID deve essere un numero intero univoco, come lo è la maggior parte delle chiavi primarie.
Definisci la tua tabella temporanea in questo modo:
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
Allora fai questo -
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
Ecco come lo farei:
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
[Modifica] Poiché probabilmente ho saltato la parola "variabile" quando ho letto la domanda per la prima volta, ecco una risposta aggiornata...
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
Se non hai altra scelta che procedere riga per riga creando un cursore FAST_FORWARD.Sarà veloce quanto costruire un ciclo while e molto più facile da mantenere nel lungo periodo.
Fast_Forward Specifica un cursore di forward_only, read_only con ottimizzazioni delle prestazioni abilitate.Non è possibile specificare FAST_FORWARD se viene specificato anche SCROLL o FOR_UPDATE.
Un altro approccio senza dover modificare lo schema o utilizzare le tabelle temporanee:
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
Puoi usare un ciclo while:
While (Select Count(*) From #TempTable) > 0
Begin
Insert Into @Databases...
Delete From #TempTable Where x = x
End
-- [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
Leggero, senza dover creare tabelle aggiuntive, se hai un numero intero ID
sul tavolo
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
Davvero non vedo il motivo per cui dovresti ricorrere all'uso del temuto cursor
.Ma ecco un'altra opzione se utilizzi SQL Server versione 2005/2008
Utilizzo Ricorsione
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
Fornirò la soluzione basata su set.
insert @databases (DatabaseID, Name, Server)
select DatabaseID, Name, Server
From ... (Use whatever query you would have used in the loop or cursor)
Questo è molto più veloce di qualsiasi tecnica di looping ed è più facile da scrivere e mantenere.
Funzionerà nella versione 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
Sono d'accordo con il post precedente sul fatto che le operazioni basate su set in genere funzioneranno meglio, ma se è necessario eseguire l'iterazione sulle righe, ecco l'approccio che adotterei:
- Aggiungi un nuovo campo alla variabile della tabella (Tipo di dati Bit, predefinito 0)
- Inserisci i tuoi dati
- Seleziona la prima riga in alto dove fUsed = 0 (Nota:fused è il nome del campo nel passaggio 1)
- Esegui qualsiasi elaborazione tu debba fare
- Aggiorna il record nella variabile della tabella impostando fUsed = 1 per il record
Seleziona il successivo record inutilizzato dalla tabella e ripeti il processo
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
Questo approccio richiede solo una variabile e non elimina alcuna riga da @databases.So che ci sono molte risposte qui, ma non ne vedo una che utilizzi MIN per ottenere il tuo prossimo ID in questo modo.
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
Preferisco utilizzare Offset Fetch se disponi di un ID univoco puoi ordinare la tabella in base a:
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
In questo modo non ho bisogno di aggiungere campi alla tabella o utilizzare una funzione finestra.
È possibile utilizzare un cursore per fare ciò:
Crea funzione [dbo] .f_teste_loop returns @tabela tabella (COD int, nome varchar (10)) come inizio
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;
FINE
creare procedura [dbo]. [sp_teste_loop] come inizio
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;
FINE
Ecco la mia soluzione, che fa uso di un ciclo infinito, il BREAK
dichiarazione e il @@ROWCOUNT
funzione.Non sono necessari cursori o tabelle temporanee e devo solo scrivere una query per ottenere la riga successiva nel file @databases
tavolo:
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
Questo è il codice che sto usando 2008 R2.Questo codice che sto utilizzando serve per creare indici sui campi chiave (SSNO e EMPR_NO) in tutti i racconti
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
Selezionare @pk = @pk + 1 sarebbe meglio:IMPOSTA @pk += @pk.Evitare di utilizzare SELECT se non si fa riferimento a tabelle ma si stanno semplicemente assegnando valori.
Passo 1:L'istruzione select di seguito crea una tabella temporanea con un numero di riga univoco per ciascun record.
select eno,ename,eaddress,mobno int,row_number() over(order by eno desc) as rno into #tmp_sri from emp
Passaggio 2: dichiarare le variabili richieste
DECLARE @ROWNUMBER INT
DECLARE @ename varchar(100)
Passaggio 3:Prendi il conteggio totale delle righe dalla tabella temporanea
SELECT @ROWNUMBER = COUNT(*) FROM #tmp_sri
declare @rno int
Passaggio 4:Tabella temporanea del ciclo basata sul numero di riga univoco creato in 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