Como executo um procedimento armazenado uma vez para cada linha retornada pela consulta?

206

Eu tenho um procedimento armazenado que altera os dados do usuário de uma certa maneira. Eu passo-o user_id e faz é coisa. Eu quero executar uma consulta em uma tabela e, em seguida, para cada user_id eu acho executar o procedimento armazenado uma vez nesse user_id

Como eu escreveria uma consulta para isso?

MetaGuru
fonte
5
Você precisa especificar o RDBMS - a resposta será diferente para SQL Server, Oracle, MySQL, etc.
Gary.Ray
5
As chances são de que você não precise de um procedimento armazenado. Você pode descrever "o que" o procedimento armazenado faz exatamente? Talvez todo o processo possa ser expresso como uma única declaração de atualização. O padrão "faça uma vez para cada registro" geralmente deve ser evitado, se possível.
21449 Tomalak
Qual banco de dados você está usando?
Assim o usuário
1
Você deve ler este artigo ... o item 2 diz: NÃO use cursores codeproject.com/KB/database/sqldodont.aspx...mind Também sou contra a otimização prematura.
25140 Michael Prewecki
7
@ MichaelPrewecki: Se você ler mais um artigo mal escrito, verá que o item 10 é "NÃO use cursores do lado do servidor, a menos que você saiba o que está fazendo". Eu acho que este é um caso de "eu sei o que estou fazendo".
Gabe

Respostas:

246

use um cursor

ADENDO: [Exemplo de cursor do MS SQL]

declare @field1 int
declare @field2 int
declare cur CURSOR LOCAL for
    select field1, field2 from sometable where someotherfield is null

open cur

fetch next from cur into @field1, @field2

while @@FETCH_STATUS = 0 BEGIN

    --execute your sproc on each row
    exec uspYourSproc @field1, @field2

    fetch next from cur into @field1, @field2
END

close cur
deallocate cur

no MS SQL, aqui está um artigo de exemplo

observe que os cursores são mais lentos que as operações baseadas em conjuntos, mas mais rápidos que os loops manuais manuais; mais detalhes nesta questão SO

ADENDO 2: se você estiver processando mais do que apenas alguns registros, puxe-os para uma tabela temporária primeiro e passe o cursor sobre a tabela temporária; isso impedirá que o SQL passe para bloqueios de tabela e acelere a operação

ADENDO 3: e, é claro, se você puder incorporar o que o procedimento armazenado está fazendo a cada ID de usuário e executar tudo como uma única instrução de atualização SQL, isso seria ideal

Steven A. Lowe
fonte
21
Você perdeu o 'cur aberto' após a declaração - isso estava me dando erros 'o cursor não está aberto'. Eu não tenho o representante para fazer uma edição.
Fiona - myaccessible.website 24/03
5
Você pode agradecer às pessoas votando positivamente no seu comentário. Quem sabe, talvez eles tenham o representante para fazer a edição, da próxima vez! :-)
Robino
Verifique seus índices nas cláusulas JOINS e WHERE nos campos usados ​​no seu procedimento armazenado. Eu acelerei dramaticamente chamando meu SP em um loop depois de adicionar índices apropriados.
Matthew
1
Agradecemos o lembrete de usar a tabela temporária para evitar possíveis problemas de bloqueio causados ​​por longa execução.
Tony
Às vezes, o procedimento armazenado é muito grande ou complicado para alinhar sem arriscar a introdução de bugs. Onde o desempenho não é a prioridade final, executar o SP em um loop de cursor geralmente é a opção mais prática.
Suncat2000
55

tente alterar seu método se precisar fazer um loop!

dentro do procedimento armazenado pai, crie uma tabela #temp que contém os dados que você precisa processar. Chame o procedimento armazenado filho, a tabela #temp estará visível e você poderá processá-lo, trabalhando com todo o conjunto de dados e sem um cursor ou loop.

isso realmente depende do que esse procedimento armazenado filho está fazendo. Se você está atualizando, pode "atualizar de" ingressando na tabela #temp e fazer todo o trabalho em uma instrução sem um loop. O mesmo pode ser feito para INSERT e DELETEs. Se você precisar fazer várias atualizações com IFs, poderá convertê-las em váriasUPDATE FROM com a tabela #temp e usar instruções CASE ou condições WHERE.

Ao trabalhar em um banco de dados, tente perder a mentalidade de fazer loop, é uma perda real de desempenho, causará bloqueio / bloqueio e atrasará o processamento. Se você fizer um loop em qualquer lugar, seu sistema não será muito bem dimensionado e será muito difícil acelerar quando os usuários começarem a reclamar de atualizações lentas.

Poste o conteúdo deste procedimento que você deseja chamar em um loop, e aposto 9 em 10 vezes, você pode escrever para trabalhar em um conjunto de linhas.

KM.
fonte
3
+1 para uma boa solução, assumindo que você controlar a criança sproc
Steven A. Lowe
um pouco de pensamento, esta solução é de longe superior!
encc
7
Operações baseadas em conjuntos são sempre preferíveis. No entanto, lembre-se de que modificar um SP nem sempre é uma opção - pense nas soluções fornecidas pelo fornecedor. Alguns usuários podem nem ter visibilidade, deixando apenas as opções de cursor ou loop. Na minha loja, nossos desenvolvedores podem ver tudo, mas há muitos obstáculos a serem resolvidos se uma solução for criada fora do aplicativo do fornecedor devido a gatilhos, procs aninhados, número de registros sendo manipulados etc. Muitas vezes, sua melhor opção, devido a complexidade do aplicativo, é simplesmente percorrer os registros.
Steve Mangiameli
11

Algo como essas substituições será necessário para suas tabelas e nomes de campos.

Declare @TableUsers Table (User_ID, MyRowCount Int Identity(1,1)
Declare @i Int, @MaxI Int, @UserID nVarchar(50)

Insert into @TableUser
Select User_ID
From Users 
Where (My Criteria)
Select @MaxI = @@RowCount, @i = 1

While @i <= @MaxI
Begin
Select @UserID = UserID from @TableUsers Where MyRowCount = @i
Exec prMyStoredProc @UserID
Select

 @i = @i + 1, @UserID = null
End
u07ch
fonte
2
while são mais lentos que cursores
Steven A. Lowe
O Declare cursor construção instrução SQL ou não é suportado (??)
MetaGuru
9

Você pode fazer isso com uma consulta dinâmica.

declare @cadena varchar(max) = ''
select @cadena = @cadena + 'exec spAPI ' + ltrim(id) + ';'
from sysobjects;
exec(@cadena);
Dave Rincon
fonte
6

Isso não pode ser feito com uma função definida pelo usuário para replicar o que o procedimento armazenado está fazendo?

SELECT udfMyFunction(user_id), someOtherField, etc FROM MyTable WHERE WhateverCondition

onde udfMyFunction é uma função que você cria, que recebe o ID do usuário e faz o que for necessário.

Consulte http://www.sqlteam.com/article/user-defined-functions para obter um pouco mais de experiência

Concordo que os cursores realmente devem ser evitados sempre que possível. E geralmente é possível!

(é claro, minha resposta pressupõe que você só está interessado em obter a saída do SP e que não está alterando os dados reais. Acho que "altera os dados do usuário de uma certa maneira" um pouco ambíguo da pergunta original, então pensei em oferecer isso como uma solução possível. Depende totalmente do que você está fazendo!)

sequência aleatória
fonte
1
OP: "procedimento armazenado que altera os dados do usuário de uma certa maneira" MSDN : Funções definidas pelo usuário não podem ser usadas para executar ações que modificam o estado do banco de dados. No entanto, o SQLSVR 2014 parece não ter um problema com ele
johnny
6

Use uma variável de tabela ou uma tabela temporária.

Como mencionado anteriormente, um cursor é o último recurso. Principalmente porque usa muitos recursos, bloqueia problemas e pode ser um sinal de que você simplesmente não está entendendo como usar o SQL corretamente.

Nota lateral: Uma vez me deparei com uma solução que usava cursores para atualizar linhas em uma tabela. Após algum exame, descobriu-se que a coisa toda poderia ser substituída por um único comando UPDATE. No entanto, nesse caso, onde um procedimento armazenado deve ser executado, um único comando SQL não funcionará.

Crie uma variável de tabela como esta (se você estiver trabalhando com muitos dados ou tiver pouca memória, use uma tabela temporária ):

DECLARE @menus AS TABLE (
    id INT IDENTITY(1,1),
    parent NVARCHAR(128),
    child NVARCHAR(128));

o id é importante.

Substitua parentechild por alguns bons dados, por exemplo, identificadores relevantes ou todo o conjunto de dados a ser operado.

Inserir dados na tabela, por exemplo:

INSERT INTO @menus (parent, child) 
  VALUES ('Some name',  'Child name');
...
INSERT INTO @menus (parent,child) 
  VALUES ('Some other name', 'Some other child name');

Declare algumas variáveis:

DECLARE @id INT = 1;
DECLARE @parentName NVARCHAR(128);
DECLARE @childName NVARCHAR(128);

E, finalmente, crie um loop while sobre os dados da tabela:

WHILE @id IS NOT NULL
BEGIN
    SELECT @parentName = parent,
           @childName = child 
        FROM @menus WHERE id = @id;

    EXEC myProcedure @parent=@parentName, @child=@childName;

    SELECT @id = MIN(id) FROM @menus WHERE id > @id;
END

A primeira seleção busca dados da tabela temporária. A segunda seleção atualiza o @id.MINretorna nulo se nenhuma linha foi selecionada.

Uma abordagem alternativa é fazer um loop enquanto a tabela possui linhas SELECT TOP 1e remover a linha selecionada da tabela temporária:

WHILE EXISTS(SELECT 1 FROM @menuIDs) 
BEGIN
    SELECT TOP 1 @menuID = menuID FROM @menuIDs;

    EXEC myProcedure @menuID=@menuID;

    DELETE FROM @menuIDs WHERE menuID = @menuID;
END;
Erk
fonte
3

Eu gosto da maneira de consulta dinâmica de Dave Rincon, pois não usa cursores e é pequeno e fácil. Obrigado Dave por compartilhar.

Mas, para as minhas necessidades no SQL do Azure e com um "distinto" na consulta, tive que modificar o código da seguinte maneira:

Declare @SQL nvarchar(max);
-- Set SQL Variable
-- Prepare exec command for each distinctive tenantid found in Machines 
SELECT @SQL = (Select distinct 'exec dbo.sp_S2_Laser_to_cache ' + 
              convert(varchar(8),tenantid) + ';' 
              from Dim_Machine
              where iscurrent = 1
              FOR XML PATH(''))

--for debugging print the sql 
print @SQL;

--execute the generated sql script
exec sp_executesql @SQL;

Espero que isso ajude alguém...

Tom
fonte