web-dev-qa-db-fra.com

Existe-t-il un moyen de parcourir une variable de table en SQL sans utiliser de curseur?

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

La déclaration et l'utilisation d'un curseur sont-elles ma seule option si je souhaite parcourir les lignes? Y a-t-il un autre moyen?

225
Ray Vega

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 de faire une boucle en utilisant simplement 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 que vous devez choisir dépend vraiment de la structure et du volume de vos données.

Remarque: Si vous utilisez SQL Server, vous seriez mieux servi en utilisant:

WHILE EXISTS(SELECT * FROM #Temp)

L'utilisation de COUNT devra toucher chaque ligne du tableau, le EXISTS ne doit toucher que la première (voir réponse de Josef ci-dessous).

339
Martynnw

Juste une note rapide, si vous utilisez SQL Server (2008 et supérieur), les exemples qui ont:

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

Serait mieux servi avec

While EXISTS(SELECT * From #Temp)

Le compte doit toucher chaque ligne du tableau, la EXISTS ne doit toucher que la première.

128
Josef

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 curseurs, pas de tables temporaires, pas de colonnes supplémentaires. La colonne USERID doit être un entier unique, comme le sont la plupart des clés primaires.

37
Trevor

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

Alors fais 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
22
Seibar

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

[Edit] Comme j'ai probablement oublié 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
16
leoinfo

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é.

10
Wes Brown

Une autre approche sans avoir à changer de schéma ou à utiliser des 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
4
SReiderB

Léger, sans avoir à créer de tables supplémentaires, si vous avez un entier ID 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
3
Control Freak

Vous pouvez utiliser une boucle while:

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

    Delete From #TempTable Where x = x
End
3
GateKiller
-- [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
Syed Umar Ahmed

Il est possible d'utiliser un curseur pour faire ceci:

la fonction create [dbo] .f_teste_loop renvoie la table @tabela (cod int, nome varchar (10)) comme début.

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] en tant que début

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

2
Alexandre Pezzutto

Je ne vois vraiment pas pourquoi il faudrait recourir à l'utilisation redoutée cursor. Mais voici une autre option si vous utilisez SQL Server version 2005/2008
Utilisation Récursion

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
2
Sung M. Kim

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 
2
OrganicCoder

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)

C'est beaucoup plus rapide que n'importe quelle technologie en boucle et il est plus facile à écrire et à maintenir.

2
HLGEM

Je préfère utiliser la récupération excentrée 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 ou d'utiliser une fonction de fenêtre.

2
Yves A Martin

Cette approche ne nécessite qu'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 ID 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
1
Sean

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 me suffit d'é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
1
Mass Dot Net

Je suis d'accord avec l'article précédent pour dire que les opérations basées sur les ensembles seront généralement plus performantes, mais si vous devez effectuer une itération sur les lignes, voici l'approche que je choisirais:

  1. Ajouter un nouveau champ à votre variable de table (Data Type Bit, 0 par défaut)
  2. Insérez vos données
  3. Sélectionnez la première ligne où fUsed = 0 (Remarque: fUsed est le nom du champ à l'étape 1)
  4. Effectuer tout le traitement que vous devez faire
  5. Mettez à jour l'enregistrement dans votre variable de table en définissant fUsed = 1 pour l'enregistrement.
  6. 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
    
1
Tim Lentine

É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: Prendre le nombre total de lignes dans la table temporaire

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

Étape 4: Table temporaire de boucle 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
0
Srinivas Maale

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

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 simplement assigner des valeurs.

0
Bob Alley