T-SQL: ciclo attraverso un array di valori noti


89

Ecco il mio scenario:

Diciamo che ho una stored procedure in cui ho bisogno di chiamare un'altra stored procedure su un insieme di ID specifici; C'è un modo per fare questo?

cioè invece di aver bisogno di farlo:

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

Fare qualcosa di simile:

*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

Il mio obiettivo principale qui è semplicemente la manutenibilità (facile rimuovere / aggiungere gli ID man mano che l'azienda cambia), essere in grado di elencare tutti gli ID su una singola riga ... Le prestazioni non dovrebbero essere un grosso problema


correlati, se è necessario iterare su un elenco non intero come varchars, soluzione con cursore: iterate-through-a-list-of-strings-in-sql-server
Pac0

Risposte:


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

Speravo che ci sarebbe stato un modo più elegante, ma penso che questo sarà il più vicino possibile: ho finito per usare un ibrido tra l'uso delle selezioni / unioni qui e il cursore dell'esempio. Grazie!
Giovanni

13
@john: se stai usando il 2008, puoi fare qualcosa come INSERT @ids VALUES (4), (7), (12), (22), (19)
Peter Radocchia

2
Solo per tua informazione, le tabelle di memoria come questa sono generalmente più veloci dei cursori (anche se per 5 valori non riesco a vedere che fa alcuna differenza), ma il motivo principale per cui mi piacciono è che trovo la sintassi simile a quella che troveresti nel codice dell'applicazione , mentre i cursori sembrano (a me) essere relativamente diversi.
Adam Robinson,

sebbene in pratica danneggerà le prestazioni solo molto poco, voglio sottolineare che questo itera attraverso tutti i numeri all'interno dello spazio definito. la soluzione seguente con While exist (Select * From @Ids) ... è logicamente più solida (e più elegante).
Der U

41

Quello che faccio in questo scenario è creare una variabile di tabella per contenere gli ID.

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

- (o chiama un'altra funzione con valori di tabella per generare questa tabella)

Quindi eseguire il ciclo in base alle righe in questa tabella

  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

o...

  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

Ciascuno degli approcci precedenti è molto più veloce di un cursore (dichiarato rispetto alle normali tabelle utente). Le variabili con valori di tabella hanno una cattiva reputazione perché se usate in modo improprio (per tabelle molto larghe con un numero elevato di righe) non hanno prestazioni. Ma se li stai usando solo per contenere un valore chiave o un intero di 4 byte, con un indice (come in questo caso) sono estremamente veloci.


L'approccio precedente è equivalente o più lento di un cursore dichiarato su una variabile di tabella. Non è certo più veloce. Tuttavia, sarebbe più veloce di un cursore dichiarato con opzioni predefinite su tabelle utente normali.
Peter Radocchia

@Peter, ahhh, sì hai ragione, presumo erroneamente che l'uso di un cursore implichi una normale tabella utente, non una variabile di tabella .. Ho modificato per chiarire la distinzione
Charles Bretana

16

usa una variabile cursore statica e una funzione di divisione :

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

I cursori hanno una cattiva reputazione poiché le opzioni predefinite quando dichiarate rispetto alle tabelle utente possono generare molto overhead.

Ma in questo caso l'overhead è minimo, inferiore a qualsiasi altro metodo qui. STATICO indica a SQL Server di materializzare i risultati in tempdb e quindi di iterare su quello. Per piccoli elenchi come questo, è la soluzione ottimale.


7

Puoi provare come di seguito:

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

6
Farei la dichiarazione della lista in questo modo: @list ='4,7,12,22,19' + ','- quindi è del tutto chiaro che la lista deve finire con una virgola (non funziona senza di essa!).
AjV Jsy

5

Di solito utilizzo il seguente approccio

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

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

0

Effettua una connessione al tuo DB usando un linguaggio di programmazione procedurale (qui Python), e fai il ciclo lì. In questo modo puoi anche fare loop complicati.

# 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))
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.