T-SQL: boucle sur un tableau de valeurs connues

89

Voici mon scénario:

Disons que j'ai une procédure stockée dans laquelle j'ai besoin d'appeler une autre procédure stockée sur un ensemble d'identifiants spécifiques; Y a-t-il un moyen de faire cela?

c'est-à-dire au lieu d'avoir à le faire:

exec p_MyInnerProcedure 4
exec p_MyInnerProcedure 7
exec p_MyInnerProcedure 12
exec p_MyInnerProcedure 22
exec p_MyInnerProcedure 19

Faire quelque chose comme ça:

*magic where I specify my list contains 4,7,12,22,19*

DECLARE my_cursor CURSOR FAST_FORWARD FOR
*magic select*

OPEN my_cursor 
FETCH NEXT FROM my_cursor INTO @MyId
WHILE @@FETCH_STATUS = 0
BEGIN

exec p_MyInnerProcedure @MyId

FETCH NEXT FROM my_cursor INTO @MyId
END

Mon objectif principal ici est simplement la maintenabilité (facile à supprimer / ajouter des identifiants au fur et à mesure que l'entreprise change), être capable de lister tous les identifiants sur une seule ligne ... Les performances ne devraient pas être un aussi gros problème

John
la source
connexe, si vous avez besoin d'itérer sur une liste non entière comme varchars, solution avec curseur: itérer-à-travers-une-liste-de-chaînes-dans-sql-server
Pac0

Réponses:

105
declare @ids table(idx int identity(1,1), id int)

insert into @ids (id)
    select 4 union
    select 7 union
    select 12 union
    select 22 union
    select 19

declare @i int
declare @cnt int

select @i = min(idx) - 1, @cnt = max(idx) from @ids

while @i < @cnt
begin
     select @i = @i + 1

     declare @id = select id from @ids where idx = @i

     exec p_MyInnerProcedure @id
end
Adam Robinson
la source
J'espérais qu'il y aurait une manière plus élégante, mais je pense que ce sera aussi proche que possible: j'ai fini par utiliser un hybride entre l'utilisation de select / unions ici et le curseur de l'exemple. Merci!
John
13
@john: si vous utilisez 2008, vous pouvez faire quelque chose comme INSERT @ids VALUES (4), (7), (12), (22), (19)
Peter Radocchia
2
Juste pour info, les tables mémoire comme celle-ci sont généralement plus rapides que les curseurs (bien que pour 5 valeurs, je ne vois guère que cela fasse une différence), mais la principale raison pour laquelle je les aime est que je trouve la syntaxe similaire à ce que vous trouverez dans le code d'application , alors que les curseurs me paraissent relativement différents.
Adam Robinson
bien que cela nuise très peu en pratique aux performances, je tiens à souligner que cela se répète à travers tous les nombres dans l'espace défini. la solution ci-dessous avec While existe (Select * From @Ids) ... est logiquement plus saine (et plus élégante).
Der U
41

Ce que je fais dans ce scénario, c'est créer une variable de table pour contenir les identifiants.

  Declare @Ids Table (id integer primary Key not null)
  Insert @Ids(id) values (4),(7),(12),(22),(19)

- (ou appelez une autre fonction de table pour générer cette table)

Puis boucle en fonction des lignes de ce tableau

  Declare @Id Integer
  While exists (Select * From @Ids)
    Begin
      Select @Id = Min(id) from @Ids
      exec p_MyInnerProcedure @Id 
      Delete from @Ids Where id = @Id
    End

ou...

  Declare @Id Integer = 0 -- assuming all Ids are > 0
  While exists (Select * From @Ids
                where id > @Id)
    Begin
      Select @Id = Min(id) 
      from @Ids Where id > @Id
      exec p_MyInnerProcedure @Id 
    End

L'une ou l'autre des approches ci-dessus est beaucoup plus rapide qu'un curseur (déclaré par rapport aux tables utilisateur normales). Les variables table ont une mauvaise réputation car lorsqu'elles sont utilisées de manière incorrecte (pour des tables très larges avec un grand nombre de lignes) elles ne sont pas performantes. Mais si vous les utilisez uniquement pour contenir une valeur de clé ou un entier de 4 octets, avec un index (comme dans ce cas), ils sont extrêmement rapides.

Charles Bretana
la source
L'approche ci-dessus est équivalente ou plus lente qu'un curseur déclaré sur une variable de table. Ce n'est certainement pas plus rapide. Ce serait cependant plus rapide qu'un curseur déclaré avec des options par défaut sur les tables utilisateur normales.
Peter Radocchia
@Peter, ahhh, oui vous avez raison, je suppose à tort que l'utilisation d'un curseur implique une table utilisateur normale, pas une variable de table .. J'ai édité pour clarifier la distinction
Charles Bretana
16

utilisez une variable de curseur statique et une fonction de fractionnement :

declare @comma_delimited_list varchar(4000)
set @comma_delimited_list = '4,7,12,22,19'

declare @cursor cursor
set @cursor = cursor static for 
  select convert(int, Value) as Id from dbo.Split(@comma_delimited_list) a

declare @id int
open @cursor
while 1=1 begin
  fetch next from @cursor into @id
  if @@fetch_status <> 0 break
  ....do something....
end
-- not strictly necessary w/ cursor variables since they will go out of scope like a normal var
close @cursor
deallocate @cursor

Les curseurs ont une mauvaise réputation car les options par défaut lorsqu'elles sont déclarées sur les tables utilisateur peuvent générer beaucoup de surcharge.

Mais dans ce cas, la surcharge est assez minime, inférieure à toutes les autres méthodes ici. STATIC indique à SQL Server de matérialiser les résultats dans tempdb, puis d'itérer dessus. Pour de petites listes comme celle-ci, c'est la solution optimale.

Peter Radocchia
la source
7

Vous pouvez essayer comme ci-dessous:

declare @list varchar(MAX), @i int
select @i=0, @list ='4,7,12,22,19,'

while( @i < LEN(@list))
begin
    declare @item varchar(MAX)
    SELECT  @item = SUBSTRING(@list,  @i,CHARINDEX(',',@list,@i)-@i)
    select @item

     --do your stuff here with @item 
     exec p_MyInnerProcedure @item 

    set @i = CHARINDEX(',',@list,@i)+1
    if(@i = 0) set @i = LEN(@list) 
end
Ramakrishna Talla
la source
6
Je ferais cette déclaration de liste comme ceci: @list ='4,7,12,22,19' + ','- il est donc tout à fait clair que la liste doit se terminer par une virgule (cela ne fonctionne pas sans elle!).
AjV Jsy
5

J'utilise généralement l'approche suivante

DECLARE @calls TABLE (
    id INT IDENTITY(1,1)
    ,parameter INT
    )

INSERT INTO @calls
select parameter from some_table where some_condition -- here you populate your parameters

declare @i int
declare @n int
declare @myId int
select @i = min(id), @n = max(id) from @calls
while @i <= @n
begin
    select 
        @myId = parameter
    from 
        @calls
    where id = @i

        EXECUTE p_MyInnerProcedure @myId
    set @i = @i+1
end
Kristof
la source
2
CREATE TABLE #ListOfIDs (IDValue INT)

DECLARE @IDs VARCHAR(50), @ID VARCHAR(5)
SET @IDs = @OriginalListOfIDs + ','

WHILE LEN(@IDs) > 1
BEGIN
SET @ID = SUBSTRING(@IDs, 0, CHARINDEX(',', @IDs));
INSERT INTO #ListOfIDs (IDValue) VALUES(@ID);
SET @IDs = REPLACE(',' + @IDs, ',' + @ID + ',', '')
END

SELECT * 
FROM #ListOfIDs
Moshe
la source
0

Connectez-vous à votre base de données en utilisant un langage de programmation procédural (ici Python), et faites la boucle là-bas. De cette façon, vous pouvez également faire des boucles compliquées.

# make a connection to your db
import pyodbc
conn = pyodbc.connect('''
                        Driver={ODBC Driver 13 for SQL Server};
                        Server=serverName;
                        Database=DBname;
                        UID=userName;
                        PWD=password;
                      ''')
cursor = conn.cursor()

# run sql code
for id in [4, 7, 12, 22, 19]:
  cursor.execute('''
    exec p_MyInnerProcedure {}
  '''.format(id))
LoMaPh
la source