Existe-t-il un moyen de parcourir une variable de table en SQL sans utiliser de curseur?
-
09-06-2019 - |
Question
Disons que j'ai la variable de table simple suivante:
declare @databases table
(
DatabaseID int,
Name varchar(15),
Server varchar(15)
)
-- insert a bunch rows into @databases
Est-ce que déclarer et utiliser un curseur est ma seule option si je veux parcourir les lignes? Y a-t-il un autre moyen?
La solution
Tout d'abord, vous devez être absolument sûr de devoir parcourir chaque ligne. Les opérations basées sur les ensembles de données s'exécuteront plus rapidement dans tous les cas auxquels je peux penser et utiliseront normalement un code plus simple.
En fonction de vos données, il peut être possible d'effectuer une boucle en utilisant uniquement les instructions select, comme indiqué ci-dessous:
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
Une autre alternative consiste à utiliser une table temporaire:
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'option à choisir dépend vraiment de la structure et du volume de vos données.
Remarque: Si vous utilisez SQL Server, vous feriez mieux de vous servir de:
WHILE EXISTS(SELECT * FROM #Temp)
L'utilisation de COUNT
doit toucher chaque ligne du tableau. Le code EXISTS
ne doit toucher que la première (voir La réponse de Josef ci-dessous).
Autres conseils
Juste une note rapide, si vous utilisez SQL Server (2008 et supérieur), les exemples suivants:
While (Select Count(*) From #Temp) > 0
serait mieux servi avec
While EXISTS(SELECT * From #Temp)
Le décompte devra toucher chaque ligne du tableau, le EXISTS
ne doit toucher que la première.
Voici comment je le fais:
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
Pas de curseur, pas de table temporaire, pas de colonne supplémentaire. La colonne USERID doit être un entier unique, comme le sont la plupart des clés primaires.
Définissez votre table temporaire comme ceci -
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
Ensuite, faites ceci -
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
Voici comment je le ferais:
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
[Éditer] Parce que j'ai probablement sauté le mot "variable" la première fois que j'ai lu la question, voici une réponse mise à jour ...
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
Si vous n'avez pas le choix, allez ligne par ligne en créant un curseur FAST_FORWARD. Ce sera aussi rapide que de construire une boucle while et beaucoup plus facile à maintenir sur le long terme.
FAST_FORWARD Spécifie un curseur FORWARD_ONLY, READ_ONLY avec l'optimisation des performances activée. FAST_FORWARD ne peut pas être spécifié si SCROLL ou FOR_UPDATE est également spécifié.
Une autre approche sans avoir à changer de schéma ni à utiliser de tables temporaires:
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
Vous pouvez utiliser une boucle 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
Léger, sans avoir à créer de tables supplémentaires, si vous avez un ID
entier sur la table
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
Je ne vois vraiment pas pourquoi il faudrait recourir à un redouté curseur
.
Mais voici une autre option si vous utilisez SQL Server version 2005/2008
Utiliser Récursivité
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
Je vais fournir la solution basée sur les ensembles.
insert @databases (DatabaseID, Name, Server)
select DatabaseID, Name, Server
From ... (Use whatever query you would have used in the loop or cursor)
Ceci est beaucoup plus rapide que n'importe quelle technologie en boucle et est plus facile à écrire et à maintenir.
Cela fonctionnera dans la version 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
Je suis d'accord avec l'article précédent pour dire que les opérations basées sur les ensembles donneront généralement de meilleurs résultats, mais si vous devez effectuer une itération sur les lignes, voici l'approche que je prendrais:
- Ajouter un nouveau champ à votre variable de table (Data Type Bit, 0 par défaut)
- Insérez vos données
- Sélectionnez la première ligne où fUsed = 0 (Remarque: fUsed est le nom du champ à l'étape 1)
- Effectuez tous les traitements nécessaires
- Mettez à jour l'enregistrement dans votre variable de table en définissant fUsed = 1 pour l'enregistrement
-
Sélectionnez le prochain enregistrement inutilisé de la table et répétez le processus
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
Cette approche nécessite une seule variable et ne supprime aucune ligne de @databases. Je sais qu'il y a beaucoup de réponses ici, mais je n'en vois pas qui utilise MIN pour obtenir votre prochain identifiant comme celui-ci.
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
Je préfère utiliser la fonction de récupération d'offset si vous avez un identifiant unique. Vous pouvez trier votre table par:
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
De cette façon, je n'ai pas besoin d'ajouter de champs à la table ni d'utiliser une fonction de fenêtre.
Il est possible d'utiliser un curseur pour faire ceci:
crée une fonction [dbo] .f_teste_loop retourne la table @tabela ( cod int, Nom Varchar (10) ) comme commencer
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;
fin
créer la procédure [dbo]. [sp_teste_loop] comme commencer
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;
fin
Voici ma solution, qui utilise une boucle infinie, l'instruction BREAK
et la fonction @@ ROWCOUNT
. Aucun curseur ni table temporaire ne sont nécessaires et il ne me reste plus qu'à écrire une requête pour obtenir la ligne suivante de la table @databases
:
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
C’est le code que j’utilise 2008 R2. Ce code que j’utilise est de construire des index sur des champs clés (SSNO & EMPR_NO) dans tous les contes
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
Sélectionnez @pk = @pk + 1 serait mieux: SET @pk + = @pk. Évitez d’utiliser SELECT si vous ne faites pas référence à des tables, c’est-à-dire des valeurs.
Étape 1: L'instruction select ci-dessous crée une table temporaire avec un numéro de ligne unique pour chaque enregistrement.
select eno,ename,eaddress,mobno int,row_number() over(order by eno desc) as rno into #tmp_sri from emp
Étape 2: Déclarez les variables requises
DECLARE @ROWNUMBER INT
DECLARE @ename varchar(100)
Étape 3: Prenez le nombre total de lignes dans la table temporaire
SELECT @ROWNUMBER = COUNT(*) FROM #tmp_sri
declare @rno int
Étape 4: Table temporaire des boucles basée sur un numéro de ligne unique créé dans 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