Procedimento armazenado de chamada SQL para cada linha sem usar um cursor


163

Como chamar um procedimento armazenado para cada linha de uma tabela, onde as colunas de uma linha são parâmetros de entrada para o sp sem usar um Cursor?


3
Então, por exemplo, você tem uma tabela Customer com uma coluna customerId e deseja chamar o SP uma vez para cada linha da tabela, passando o customerId correspondente como parâmetro?
9789 Gary McGill #

2
Você poderia explicar por que não pode usar um cursor?
Andomar 1/11/2009

@ Gary: Talvez eu só queira passar o nome do cliente, não necessariamente o ID. Mas você está certo.
Johannes Rudolph

2
@Andomar: puramente científico :-)
Johannes Rudolph

1
Esse problema também me incomoda bastante.
Daniel

Respostas:


200

De um modo geral, eu sempre procuro uma abordagem baseada em conjunto (às vezes às custas de alterar o esquema).

No entanto, esse trecho tem seu lugar.

-- Declare & init (2008 syntax)
DECLARE @CustomerID INT = 0

-- Iterate over all customers
WHILE (1 = 1) 
BEGIN  

  -- Get next customerId
  SELECT TOP 1 @CustomerID = CustomerID
  FROM Sales.Customer
  WHERE CustomerID > @CustomerId 
  ORDER BY CustomerID

  -- Exit loop if no more customers
  IF @@ROWCOUNT = 0 BREAK;

  -- call your sproc
  EXEC dbo.YOURSPROC @CustomerId

END

21
como com a resposta aceita USE WITH CATION: Dependendo da sua tabela e estrutura do índice, ele pode ter um desempenho muito ruim (O (n ^ 2)), pois você precisa solicitar e pesquisar sua tabela toda vez que enumerar.
csauve

3
Isso não parece funcionar (a interrupção nunca sai do loop para mim - o trabalho está concluído, mas a consulta gira no loop). Inicializar o id e verificar nulo na condição while sai do loop.
precisa saber é o seguinte

8
@@ ROWCOUNT pode ser lido apenas uma vez. Mesmo instruções IF / PRINT o definirão como 0. O teste para @@ ROWCOUNT deve ser feito 'imediatamente' após a seleção. Gostaria de verificar novamente seu código / ambiente. technet.microsoft.com/en-us/library/ms187316.aspx
Mark Powell

3
While não são melhores do que os cursores, tenha cuidado, eles podem ser ainda pior: techrepublic.com/blog/the-enterprise-cloud/...
Jaime

1
@Brennan Pope Use a opção LOCAL para um CURSOR e ele será destruído em caso de falha. Use LOCAL FAST_FORWARD e existem quase zero motivos para não usar CURSORs para esse tipo de loop. Definitivamente superaria esse loop WHILE.
Martin

39

Você poderia fazer algo assim: ordenar sua tabela, por exemplo, CustomerID (usando a Sales.Customertabela de exemplo AdventureWorks ) e iterar sobre esses clientes usando um loop WHILE:

-- define the last customer ID handled
DECLARE @LastCustomerID INT
SET @LastCustomerID = 0

-- define the customer ID to be handled now
DECLARE @CustomerIDToHandle INT

-- select the next customer to handle    
SELECT TOP 1 @CustomerIDToHandle = CustomerID
FROM Sales.Customer
WHERE CustomerID > @LastCustomerID
ORDER BY CustomerID

-- as long as we have customers......    
WHILE @CustomerIDToHandle IS NOT NULL
BEGIN
    -- call your sproc

    -- set the last customer handled to the one we just handled
    SET @LastCustomerID = @CustomerIDToHandle
    SET @CustomerIDToHandle = NULL

    -- select the next customer to handle    
    SELECT TOP 1 @CustomerIDToHandle = CustomerID
    FROM Sales.Customer
    WHERE CustomerID > @LastCustomerID
    ORDER BY CustomerID
END

Isso deve funcionar com qualquer tabela, desde que você possa definir algum tipo de ORDER BYem alguma coluna.


@ Mitch: sim, é verdade - um pouco menos sobrecarga. Mas ainda assim - não é realmente na mentalidade baseada em conjunto de SQL
marc_s

6
Uma implementação baseada em conjunto é possível?
Johannes Rudolph

Eu não sei de nenhuma maneira de conseguir isso, realmente - é uma tarefa muito processual para começar ....
marc_s

2
@marc_s executa um procedimento de função / loja para cada item de uma coleção, que soa como o pão com manteiga das operações baseadas em conjuntos. O problema surge provavelmente por não ter resultados de cada um deles. Veja "map" na maioria das linguagens de programação funcionais.
Daniel

4
re: Daniel. Uma função sim, um procedimento armazenado não. Um procedimento armazenado por definição pode ter efeitos colaterais e efeitos colaterais não são permitidos em consultas. Da mesma forma, um "mapa" adequado em uma linguagem funcional proíbe efeitos colaterais.
Csauve

28
DECLARE @SQL varchar(max)=''

-- MyTable has fields fld1 & fld2

Select @SQL = @SQL + 'exec myproc ' + convert(varchar(10),fld1) + ',' 
                   + convert(varchar(10),fld2) + ';'
From MyTable

EXEC (@SQL)

Ok, então eu nunca colocaria esse código em produção, mas ele atende aos seus requisitos.


Como fazer a mesma coisa quando o procedimento retorna um valor que deve definir o valor da linha? (usando um procedimento em vez de uma função porque da criação da função não é permitida )
user2284570

@WeihuiGuo porque o Code criado dinamicamente usando strings é HORRÍVELmente propenso a falhas e uma dor total na bunda para depuração. Você nunca deve absolutamente fazer algo assim fora de um caso que não tem chance de se tornar parte rotineira de um ambiente de produção
Marie

11

A resposta de Marc é boa (eu comentaria se pudesse descobrir como fazê-lo!)
Apenas pensei em apontar que talvez seja melhor alterar o loop para que o SELECTúnico exista uma vez (em um caso real em que eu precisei fazer isso, SELECTera bastante complexo e escrevê-lo duas vezes era um problema de manutenção arriscado).

-- define the last customer ID handled
DECLARE @LastCustomerID INT
SET @LastCustomerID = 0
-- define the customer ID to be handled now
DECLARE @CustomerIDToHandle INT
SET @CustomerIDToHandle = 1

-- as long as we have customers......    
WHILE @LastCustomerID <> @CustomerIDToHandle
BEGIN  
  SET @LastCustomerId = @CustomerIDToHandle
  -- select the next customer to handle    
  SELECT TOP 1 @CustomerIDToHandle = CustomerID
  FROM Sales.Customer
  WHERE CustomerID > @LastCustomerId 
  ORDER BY CustomerID

  IF @CustomerIDToHandle <> @LastCustomerID
  BEGIN
      -- call your sproc
  END

END

O APPLY pode ser usado apenas com funções ... portanto, essa abordagem é muito melhor se você não quiser ter a ver com funções.
Artur

Você precisa de 50 representantes para comentar. Mantenha responder a essas perguntas e você vai ter mais poder: D stackoverflow.com/help/privileges
SvendK

Eu acho que essa deve ser a resposta, clara e direta. Muito obrigado!
bomblike 15/05

7

Se você pode transformar o procedimento armazenado em uma função que retorna uma tabela, poderá usar a aplicação cruzada.

Por exemplo, digamos que você tenha uma tabela de clientes e deseje calcular a soma de seus pedidos, criaria uma função que pegasse um CódigoDoCliente e retornasse a soma.

E você pode fazer isso:

SELECT CustomerID, CustomerSum.Total

FROM Customers
CROSS APPLY ufn_ComputeCustomerTotal(Customers.CustomerID) AS CustomerSum

Onde a função se pareceria:

CREATE FUNCTION ComputeCustomerTotal
(
    @CustomerID INT
)
RETURNS TABLE
AS
RETURN
(
    SELECT SUM(CustomerOrder.Amount) AS Total FROM CustomerOrder WHERE CustomerID = @CustomerID
)

Obviamente, o exemplo acima pode ser feito sem uma função definida pelo usuário em uma única consulta.

A desvantagem é que as funções são muito limitadas - muitos dos recursos de um procedimento armazenado não estão disponíveis em uma função definida pelo usuário e a conversão de um procedimento armazenado em uma função nem sempre funciona.


No caso de não existirem permissões de gravação para criar uma função?
User2284570

7

Eu usaria a resposta aceita, mas outra possibilidade é usar uma variável de tabela para armazenar um conjunto numerado de valores (nesse caso, apenas o campo ID de uma tabela) e percorrer os números de linha com um JOIN na tabela para recupere o que for necessário para a ação dentro do loop.

DECLARE @RowCnt int; SET @RowCnt = 0 -- Loop Counter

-- Use a table variable to hold numbered rows containg MyTable's ID values
DECLARE @tblLoop TABLE (RowNum int IDENTITY (1, 1) Primary key NOT NULL,
     ID INT )
INSERT INTO @tblLoop (ID)  SELECT ID FROM MyTable

  -- Vars to use within the loop
  DECLARE @Code NVarChar(10); DECLARE @Name NVarChar(100);

WHILE @RowCnt < (SELECT COUNT(RowNum) FROM @tblLoop)
BEGIN
    SET @RowCnt = @RowCnt + 1
    -- Do what you want here with the data stored in tblLoop for the given RowNum
    SELECT @Code=Code, @Name=LongName
      FROM MyTable INNER JOIN @tblLoop tL on MyTable.ID=tL.ID
      WHERE tl.RowNum=@RowCnt
    PRINT Convert(NVarChar(10),@RowCnt) +' '+ @Code +' '+ @Name
END

Isso é melhor porque não assume que o valor que você procura é um número inteiro ou pode ser comparado com sensibilidade.
PhilW

Exatamente o que eu estava procurando.
Raithlin 21/09/18

6

Para o SQL Server 2005 em diante, você pode fazer isso com o CROSS APPLY e uma função com valor de tabela.

Apenas para maior clareza, estou me referindo aos casos em que o procedimento armazenado pode ser convertido em uma função com valor de tabela.


12
Boa idéia, mas a função não pode chamar um procedimento armazenado
Andomar

3

Esta é uma variação da solução n3rds acima. Nenhuma classificação usando ORDER BY é necessária, pois MIN () é usado.

Lembre-se de que CustomerID (ou qualquer outra coluna numérica usada para o progresso) deve ter uma restrição exclusiva. Além disso, para torná-lo o mais rápido possível, o CustomerID deve estar indexado.

-- Declare & init
DECLARE @CustomerID INT = (SELECT MIN(CustomerID) FROM Sales.Customer); -- First ID
DECLARE @Data1 VARCHAR(200);
DECLARE @Data2 VARCHAR(200);

-- Iterate over all customers
WHILE @CustomerID IS NOT NULL
BEGIN  

  -- Get data based on ID
  SELECT @Data1 = Data1, @Data2 = Data2
    FROM Sales.Customer
    WHERE [ID] = @CustomerID ;

  -- call your sproc
  EXEC dbo.YOURSPROC @Data1, @Data2

  -- Get next customerId
  SELECT @CustomerID = MIN(CustomerID)
    FROM Sales.Customer
    WHERE CustomerID > @CustomerId 

END

Eu uso essa abordagem em alguns varchars que preciso examinar, colocando-os em uma tabela temporária primeiro, para fornecer um ID.


2

Se você não usar o cursor, acho que precisará fazê-lo externamente (pegue a tabela e execute cada instrução e sempre que chamar sp). É o mesmo que usar um cursor, mas apenas fora SQL Por que você não usa um cursor?


2

Essa é uma variação das respostas já fornecidas, mas deve ter um desempenho melhor porque não requer ORDER BY, COUNT ou MIN / MAX. A única desvantagem dessa abordagem é que você precisa criar uma tabela temporária para armazenar todos os IDs (a suposição é de que existem lacunas na sua lista de IDs do cliente).

Dito isto, eu concordo com @Mark Powell, embora, de um modo geral, uma abordagem baseada em conjunto ainda deva ser melhor.

DECLARE @tmp table (Id INT IDENTITY(1,1) PRIMARY KEY NOT NULL, CustomerID INT NOT NULL)
DECLARE @CustomerId INT 
DECLARE @Id INT = 0

INSERT INTO @tmp SELECT CustomerId FROM Sales.Customer

WHILE (1=1)
BEGIN
    SELECT @CustomerId = CustomerId, @Id = Id
    FROM @tmp
    WHERE Id = @Id + 1

    IF @@rowcount = 0 BREAK;

    -- call your sproc
    EXEC dbo.YOURSPROC @CustomerId;
END

1

Eu costumo fazer dessa maneira quando há algumas linhas:

  1. Selecione todos os parâmetros sproc em um conjunto de dados com o SQL Management Studio
  2. Clique com o botão direito do mouse -> Copiar
  3. Cole para o Excel
  4. Crie instruções sql de linha única com uma fórmula como '= "EXEC schema.mysproc @ param =" & A2' em uma nova coluna do Excel. (Onde A2 é sua coluna do Excel que contém o parâmetro)
  5. Copie a lista de instruções do Excel para uma nova consulta no SQL Management Studio e execute.
  6. Feito.

(Em conjuntos de dados maiores, eu usaria uma das soluções mencionadas acima).


4
Não é muito útil em situações de programação, é um corte único.
Warren P

1

DELIMITER //

CREATE PROCEDURE setFakeUsers (OUT output VARCHAR(100))
BEGIN

    -- define the last customer ID handled
    DECLARE LastGameID INT;
    DECLARE CurrentGameID INT;
    DECLARE userID INT;

    SET @LastGameID = 0; 

    -- define the customer ID to be handled now

    SET @userID = 0;

    -- select the next game to handle    
    SELECT @CurrentGameID = id
    FROM online_games
    WHERE id > LastGameID
    ORDER BY id LIMIT 0,1;

    -- as long as we have customers......    
    WHILE (@CurrentGameID IS NOT NULL) 
    DO
        -- call your sproc

        -- set the last customer handled to the one we just handled
        SET @LastGameID = @CurrentGameID;
        SET @CurrentGameID = NULL;

        -- select the random bot
        SELECT @userID = userID
        FROM users
        WHERE FIND_IN_SET('bot',baseInfo)
        ORDER BY RAND() LIMIT 0,1;

        -- update the game
        UPDATE online_games SET userID = @userID WHERE id = @CurrentGameID;

        -- select the next game to handle    
        SELECT @CurrentGameID = id
         FROM online_games
         WHERE id > LastGameID
         ORDER BY id LIMIT 0,1;
    END WHILE;
    SET output = "done";
END;//

CALL setFakeUsers(@status);
SELECT @status;

1

Uma solução melhor para isso é

  1. Cópia / código antigo do Procedimento Armazenado
  2. Associe esse código à tabela para a qual você deseja executá-lo novamente (para cada linha)

Foi assim que você obteve uma saída em formato de tabela limpa. Enquanto se você executa o SP para cada linha, obtém um resultado de consulta separado para cada iteração que é feia.


0

Caso o pedido seja importante

--declare counter
DECLARE     @CurrentRowNum BIGINT = 0;
--Iterate over all rows in [DataTable]
WHILE (1 = 1)
    BEGIN
        --Get next row by number of row
        SELECT TOP 1 @CurrentRowNum = extendedData.RowNum
                    --here also you can store another values
                    --for following usage
                    --@MyVariable = extendedData.Value
        FROM    (
                    SELECT 
                        data.*
                        ,ROW_NUMBER() OVER(ORDER BY (SELECT 0)) RowNum
                    FROM [DataTable] data
                ) extendedData
        WHERE extendedData.RowNum > @CurrentRowNum
        ORDER BY extendedData.RowNum

        --Exit loop if no more rows
        IF @@ROWCOUNT = 0 BREAK;

        --call your sproc
        --EXEC dbo.YOURSPROC @MyVariable
    END

0

Eu tinha um código de produção que só podia lidar com 20 funcionários por vez, abaixo está a estrutura do código. Acabei de copiar o código de produção e removi as coisas abaixo.

ALTER procedure GetEmployees
    @ClientId varchar(50)
as
begin
    declare @EEList table (employeeId varchar(50));
    declare @EE20 table (employeeId varchar(50));

    insert into @EEList select employeeId from Employee where (ClientId = @ClientId);

    -- Do 20 at a time
    while (select count(*) from @EEList) > 0
    BEGIN
      insert into @EE20 select top 20 employeeId from @EEList;

      -- Call sp here

      delete @EEList where employeeId in (select employeeId from @EE20)
      delete @EE20;
    END;

  RETURN
end

-1

Eu gosto de fazer algo semelhante a isso (embora ainda seja muito parecido com o uso de um cursor)

[código]

-- Table variable to hold list of things that need looping
DECLARE @holdStuff TABLE ( 
    id INT IDENTITY(1,1) , 
    isIterated BIT DEFAULT 0 , 
    someInt INT ,
    someBool BIT ,
    otherStuff VARCHAR(200)
)

-- Populate your @holdStuff with... stuff
INSERT INTO @holdStuff ( 
    someInt ,
    someBool ,
    otherStuff
)
SELECT  
    1 , -- someInt - int
    1 , -- someBool - bit
    'I like turtles'  -- otherStuff - varchar(200)
UNION ALL
SELECT  
    42 , -- someInt - int
    0 , -- someBool - bit
    'something profound'  -- otherStuff - varchar(200)

-- Loop tracking variables
DECLARE @tableCount INT
SET     @tableCount = (SELECT COUNT(1) FROM [@holdStuff])

DECLARE @loopCount INT
SET     @loopCount = 1

-- While loop variables
DECLARE @id INT
DECLARE @someInt INT
DECLARE @someBool BIT
DECLARE @otherStuff VARCHAR(200)

-- Loop through item in @holdStuff
WHILE (@loopCount <= @tableCount)
    BEGIN

        -- Increment the loopCount variable
        SET @loopCount = @loopCount + 1

        -- Grab the top unprocessed record
        SELECT  TOP 1 
            @id = id ,
            @someInt = someInt ,
            @someBool = someBool ,
            @otherStuff = otherStuff
        FROM    @holdStuff
        WHERE   isIterated = 0

        -- Update the grabbed record to be iterated
        UPDATE  @holdAccounts
        SET     isIterated = 1
        WHERE   id = @id

        -- Execute your stored procedure
        EXEC someRandomSp @someInt, @someBool, @otherStuff

    END

[/código]

Observe que você não precisa da identidade ou da coluna isIterated na sua tabela temporária / variável, eu prefiro fazê-lo dessa maneira, para não precisar excluir o registro superior da coleção enquanto iteramos no loop.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.