Existe-t-il un moyen de parcourir une variable de table dans TSQL sans utiliser de curseur?


243

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 voulais parcourir les lignes? Y a-t-il un autre moyen?


3
Bien que je ne sois pas sûr du problème que vous voyez avec l'approche ci-dessus; Voir si cela aide .. databasejournal.com/features/mssql/article.php/3111031
Gishu

5
Pourriez-vous nous fournir la raison pour laquelle vous souhaitez itérer sur les lignes, une autre solution qui ne nécessite pas d'itération pourrait exister (et qui est plus rapide dans une large mesure dans la plupart des cas)
Pop Catalin

d'accord avec pop ... peut ne pas avoir besoin d'un curseur selon la situation. mais il n'y a aucun problème à utiliser des curseurs si vous en avez besoin
Shawn

3
Vous ne dites pas pourquoi vous voulez éviter un curseur. Sachez qu'un curseur peut être le moyen le plus simple d'itérer. Vous avez peut-être entendu dire que les curseurs sont «mauvais», mais c'est vraiment l'itération sur les tables qui est mauvaise par rapport aux opérations basées sur des ensembles. Si vous ne pouvez pas éviter l'itération, un curseur peut être le meilleur moyen. Le verrouillage est un autre problème avec les curseurs, mais ce n'est pas pertinent lors de l'utilisation d'une variable de table.
JacquesB

1
L'utilisation d'un curseur n'est pas votre seule option, mais si vous n'avez aucun moyen d'éviter une approche ligne par ligne, ce sera votre meilleure option. Les curseurs sont une construction intégrée qui est plus efficace et moins sujette aux erreurs que de faire votre propre boucle WHILE idiote. La plupart du temps, il vous suffit d'utiliser l' STATICoption pour supprimer la relecture constante des tables de base et le verrouillage qui sont là par défaut et amènent la plupart des gens à croire à tort que les curseurs sont mauvais. @JacquesB très proche: revérifier pour voir si la ligne de résultat existe toujours + verrouillage sont les problèmes. Et STATICcorrige généralement cela :-).
Solomon Rutzky

Réponses:


376

Tout d'abord, vous devez être absolument sûr que vous devez parcourir chaque ligne - les opérations basées sur les ensembles fonctionneront plus rapidement dans tous les cas auxquels je pense et utiliseront normalement du code plus simple.

Selon vos données, il peut être possible de boucler en utilisant uniquement des SELECTinstructions, 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)

En utilisant COUNT devra toucher chaque ligne du tableau, le EXISTSseul besoin de toucher la première (voir la réponse de Josef ci-dessous).


"Sélectionnez Top 1 @Id = Id From ATable" devrait être "Sélectionnez Top 1 @Id = Id From ATable Where Processed = 0"
Amzath

10
Si vous utilisez SQL Server, consultez la réponse de Josef ci-dessous pour un petit ajustement à ce qui précède.
Polshgiant

3
Pouvez-vous expliquer pourquoi c'est mieux que d'utiliser un curseur?
marco-fiset

5
A donné à celui-ci un downvote. Pourquoi devrait-il éviter d'utiliser un curseur? Il parle d'itérer sur une variable de table , pas une table traditionnelle. Je ne crois pas que les inconvénients normaux des curseurs s'appliquent ici. Si le traitement ligne par ligne est vraiment nécessaire (et comme vous le signalez, il devrait en être certain en premier), alors l'utilisation d'un curseur est une bien meilleure solution que celles que vous décrivez ici.
peterh

@peterh Vous avez raison. Et en fait, vous pouvez généralement éviter ces "inconvénients normaux" en utilisant l' STATICoption qui copie le jeu de résultats dans une table temporaire, et donc vous ne verrouillez ou ne revérifiez plus les tables de base :-).
Solomon Rutzky

132

Juste une petite note, 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 comte devra toucher chaque ligne de la table, le EXISTSseul besoin de toucher la première.


9
Ce n'est pas une réponse mais un commentaire / amélioration sur la réponse de Martynw.
Hammad Khan

7
Le contenu de cette note impose une meilleure fonctionnalité de mise en forme qu'un commentaire, je suggère de l'ajouter à la réponse.
Custodio

2
Dans les versions ultérieures de SQL, l'optimiseur de requêtes est suffisamment intelligent pour savoir que lorsque vous écrivez la première chose, vous voulez réellement dire la seconde et l'optimise en tant que telle pour éviter l'analyse de la table.
Dan Def

39

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.


26

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

16

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

[Modifier] Parce que j'ai probablement sauté le mot "variable" lors de ma première lecture de 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

4
Donc, fondamentalement, vous faites un curseur, mais sans tous les avantages d'un curseur
Shawn

1
... sans verrouiller les tables qui sont utilisées lors du traitement ... car c'est l'un des avantages d'un curseur :)
leoinfo

3
Les tables? C'est une table VARIABLE - il n'y a pas d'accès simultané possible.
DenNukem

DenNukem, vous avez raison, je pense que j'ai "sauté" le mot "variable" quand j'ai lu la question à ce moment-là ... J'ajouterai quelques notes à ma réponse initiale
leoinfo

Je dois être d'accord avec DenNukem et Shawn. Pourquoi, pourquoi, pourquoi allez-vous à ces longueurs pour éviter d'utiliser un curseur? Encore une fois: il veut parcourir une variable de table, pas une table traditionnelle !!!
peterh

10

Si vous n'avez pas d'autre choix que d'aller ligne par ligne en créant un curseur FAST_FORWARD. Ce sera aussi rapide que de créer une boucle de temps et beaucoup plus facile à entretenir sur le long terme.

FAST_FORWARD Spécifie un curseur FORWARD_ONLY, READ_ONLY avec les optimisations de performances activées. FAST_FORWARD ne peut pas être spécifié si SCROLL ou FOR_UPDATE est également spécifié.


2
Ouais! Comme je l'ai commenté ailleurs, je n'ai pas encore vu d'arguments expliquant pourquoi NE PAS utiliser un curseur lorsque le cas consiste à parcourir une variable de table . Un FAST_FORWARDcurseur est une bonne solution. (vote positif)
peterh

5

Une autre approche sans avoir à modifier votre 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

Vous pouvez utiliser une boucle while:

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

    Delete From #TempTable Where x = x
End

4

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 

4

Léger, sans avoir à faire de tableaux supplémentaires, si vous avez un entier IDsur 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
-- [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

2

Je ne vois vraiment pas pourquoi vous auriez besoin d'utiliser redouté cursor. Mais voici une autre option si vous utilisez SQL Server version 2005/2008
Use Recursion

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

Je vais fournir la solution basée sur un ensemble.

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 toute technique de boucle et plus facile à écrire et à maintenir.


2

Je préfère utiliser l'Offset Fetch si vous avez un ID 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 des champs à la table ou d'utiliser une fonction de fenêtre.


2

Il est possible d'utiliser un curseur pour cela:

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

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] as begin

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


1
La question d'origine n'était-elle pas «sans utiliser de curseur»?
Fernando Gonzalez Sanchez

1

Je suis d'accord avec le post précédent que les opérations basées sur des ensembles fonctionneront généralement mieux, mais si vous avez besoin d'itérer sur les lignes, voici l'approche que je prendrais:

  1. Ajoutez un nouveau champ à votre variable de table (bit de type de données, par défaut 0)
  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. Effectuez le traitement dont vous avez besoin
  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é dans le tableau 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

É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 de la table temporaire

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

Étape 4: table de temp 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

1

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 prochaine ID comme ça.

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

Voici ma solution, qui utilise une boucle infinie, l' BREAKinstruction et la @@ROWCOUNTfonction. Aucun curseur ou table temporaire n'est nécessaire, et je n'ai besoin que d'écrire une requête pour obtenir la ligne suivante dans la @databasestable:

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

Je viens de réaliser que @ControlFreak a recommandé cette approche avant moi; J'ai simplement ajouté des commentaires et un exemple plus détaillé.
Mass Dot Net

0

C'est le code que j'utilise 2008 R2. Ce code que j'utilise est de construire des index sur les champs clés (SSNO & EMPR_NO) n 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
SELECT @pk = @pk + 1

serait mieux:

SET @pk += @pk

Évitez d'utiliser SELECT si vous ne faites pas référence à des tables ou si vous affectez simplement des valeurs.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.