T-SQL: Loop por meio de uma matriz de valores conhecidos

90

Aqui está o meu cenário:

Digamos que eu tenha um procedimento armazenado no qual preciso chamar outro procedimento armazenado em um conjunto de IDs específicos; existe uma maneira de fazer isso?

ou seja, em vez de precisar fazer isso:

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

Fazendo algo assim:

*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

Meu objetivo principal aqui é simplesmente manutenção (fácil de remover / adicionar ids conforme o negócio muda), ser capaz de listar todos os ids em uma única linha ... O desempenho não deve ser um problema tão grande

John
fonte
relacionado, se você precisar iterar em uma lista não inteira como varchars, solução com cursor: iterar por meio de uma lista-de-strings-em-sql-server
Pac0

Respostas:

106
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
fonte
Eu esperava que houvesse uma maneira mais elegante, mas acho que será o mais próximo que posso chegar: Acabei usando um híbrido entre usar select / unions aqui e o cursor do exemplo. Obrigado!
João,
13
@john: se estiver usando 2008, você pode fazer algo como INSERT @ids VALUES (4), (7), (12), (22), (19)
Peter Radocchia
2
Apenas para sua informação, tabelas de memória como esta são geralmente mais rápidas do que cursores (embora para 5 valores eu dificilmente consiga ver que fazem alguma diferença), mas a maior razão de eu gostar delas é que acho a sintaxe semelhante à que você encontraria no código do aplicativo , enquanto os cursores parecem (para mim) relativamente diferentes.
Adam Robinson
embora na prática prejudique muito pouco o desempenho, quero salientar que isso ocorre em todos os números dentro do espaço definido. a solução abaixo com While exists (Select * From @Ids) ... é logicamente mais sólida (e mais elegante).
Der U de
41

O que faço neste cenário é criar uma variável de tabela para conter os Ids.

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

- (ou chame outra função com valor de tabela para gerar esta tabela)

Em seguida, faça um loop com base nas linhas desta tabela

  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

Qualquer uma das abordagens acima é muito mais rápida do que um cursor (declarado em relação à (s) Tabela (s) de usuário normal). Variáveis ​​com valor de tabela têm uma má reputação porque, quando usadas incorretamente (para tabelas muito largas com grande número de linhas), elas não apresentam desempenho. Mas se você os estiver usando apenas para manter um valor-chave ou um inteiro de 4 bytes, com um índice (como neste caso) eles são extremamente rápidos.

Charles Bretana
fonte
A abordagem acima é equivalente ou mais lenta do que um cursor declarado em uma variável de tabela. Certamente não é mais rápido. No entanto, seria mais rápido do que um cursor declarado com opções padrão em tabelas de usuários regulares.
Peter Radocchia,
@Peter, ahhh, sim, você está correto, presumo incorretamente que usar um cursor implica em uma tabela de usuário regular, não uma variável de tabela. Editei para deixar clara a distinção
Charles Bretana
16

use uma variável de cursor estática e uma função de divisão :

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

Os cursores têm má reputação, pois as opções padrão, quando declaradas nas tabelas do usuário, podem gerar muita sobrecarga.

Mas, neste caso, a sobrecarga é mínima, menor do que qualquer outro método aqui. STATIC diz ao SQL Server para materializar os resultados em tempdb e, em seguida, iterar sobre isso. Para pequenas listas como essa, é a solução ideal.

Peter Radocchia
fonte
7

Você pode tentar o seguinte:

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
fonte
6
Eu faria essa declaração de lista assim: @list ='4,7,12,22,19' + ','- então é totalmente claro que a lista deve terminar com uma vírgula (ela não funciona sem ela!).
AjV Jsy
5

Eu costumo usar a seguinte abordagem

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

Faça uma conexão com seu banco de dados usando uma linguagem de programação procedural (aqui Python) e faça o loop ali. Dessa forma, você também pode fazer loops complicados.

# 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
fonte