Procedimiento almacenado de llamadas SQL para cada fila sin usar un cursor

163

¿Cómo se puede llamar a un procedimiento almacenado para cada fila en una tabla, donde las columnas de una fila son parámetros de entrada al sp sin usar un cursor?

Johannes Rudolph
fuente
3
Entonces, por ejemplo, tiene una tabla Customer con una columna customerId, y desea llamar al SP una vez para cada fila de la tabla, pasando el customerId correspondiente como parámetro.
Gary McGill el
2
¿Podría explicar por qué no puede usar un cursor?
Andomar 01 de
@ Gary: Tal vez solo quiero pasar el nombre del cliente, no necesariamente la identificación. Pero tienes razón.
Johannes Rudolph el
2
@Andomar: puramente científico :-)
Johannes Rudolph el
1
Este problema también me molesta mucho.
Daniel

Respuestas:

200

En términos generales, siempre busco un enfoque basado en conjuntos (a veces a expensas de cambiar el esquema).

Sin embargo, este fragmento tiene su 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
Mark Powell
fuente
21
como con la respuesta aceptada. USE CON PRECAUCIÓN: Dependiendo de su estructura de tabla e índice, puede tener un rendimiento muy bajo (O (n ^ 2)) ya que debe ordenar y buscar en su tabla cada vez que enumera.
csauve
3
Esto no parece funcionar (break nunca sale del ciclo para mí; el trabajo está hecho pero la consulta gira en el ciclo). La inicialización de la identificación y la comprobación de nulo en la condición while sale del ciclo.
dudeNumber4
8
@@ ROWCOUNT solo se puede leer una vez. Incluso las declaraciones IF / PRINT lo establecerán en 0. La prueba para @@ ROWCOUNT debe hacerse 'inmediatamente' después de la selección. Volvería a verificar su código / entorno. technet.microsoft.com/en-us/library/ms187316.aspx
Mark Powell
3
Si bien los bucles no son mejores que los cursores, tenga cuidado, pueden ser aún peores: techrepublic.com/blog/the-enterprise-cloud/…
Jaime
1
@Brennan Pope Use la opción LOCAL para un CURSOR y se destruirá en caso de falla. Use LOCAL FAST_FORWARD y hay casi cero razones para no usar CURSOR para este tipo de bucles. Definitivamente superaría este ciclo WHILE.
Martin
39

Podría hacer algo como esto: ordene su tabla, por ejemplo, CustomerID (usando la Sales.Customertabla de ejemplo AdventureWorks ) e itere sobre esos clientes usando un ciclo 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

Eso debería funcionar con cualquier tabla siempre que pueda definir algún tipo de ORDER BYen una columna.

marc_s
fuente
@Mitch: sí, cierto, un poco menos sobrecarga. Pero aún así, no está realmente en la mentalidad basada en conjuntos de SQL
marc_s
66
¿Es posible una implementación basada en conjuntos?
Johannes Rudolph el
No sé de ninguna manera de lograr que, en realidad - que es una tarea muy procedimental, para empezar ....
marc_s
2
@marc_s ejecuta una función / procedimiento de tienda para cada elemento de una colección, que suena como el pan y la mantequilla de las operaciones basadas en conjuntos. El problema surge probablemente de no tener resultados de cada uno de ellos. Ver "mapa" en la mayoría de los lenguajes de programación funcionales.
Daniel
44
re: Daniel Una función sí, un procedimiento almacenado no. Un procedimiento almacenado por definición puede tener efectos secundarios, y los efectos secundarios no están permitidos en las consultas. Del mismo modo, un "mapa" adecuado en un lenguaje funcional prohíbe los efectos secundarios.
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, entonces nunca pondría ese código en producción, pero sí satisface tus requisitos.

Thomas Gabriel
fuente
¿Cómo hacer lo mismo cuando el procedimiento devuelve un valor que debería establecer el valor de la fila? (usando un PROCEDIMIENTO en lugar de una función porque la creación de funciones no está permitida )
usuario2284570
@WeihuiGuo porque el Código creado dinámicamente usando cadenas es HORRIBLEMENTE propenso a fallas y un dolor total en el trasero para depurar. Absolutamente nunca debe hacer algo como esto fuera de una sola vez que no tiene posibilidad de convertirse en una parte rutinaria de un entorno de producción
Marie
11

La respuesta de Marc es buena (¡lo comentaría si pudiera resolver cómo hacerlo!)
Solo pensé en señalar que puede ser mejor cambiar el ciclo para que SELECTsolo exista una vez (en un caso real donde necesito hacer esto, SELECTera bastante complejo, y escribirlo dos veces era un problema de mantenimiento arriesgado).

-- 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
Maxxx
fuente
La APLICACIÓN solo se puede usar con funciones ... por lo que este enfoque es mucho mejor si no desea tener que ver con las funciones.
Artur
Necesitas 50 repeticiones para comentar. Siga respondiendo esas preguntas y obtendrá más potencia: D stackoverflow.com/help/privileges
SvendK
Creo que esta debería ser la respuesta, simple y directa. ¡Muchas gracias!
bomba
7

Si puede convertir el procedimiento almacenado en una función que devuelve una tabla, puede usar la aplicación cruzada.

Por ejemplo, supongamos que tiene una tabla de clientes y desea calcular la suma de sus pedidos, crearía una función que tomara un CustomerID y devolviera la suma.

Y podrías hacer esto:

SELECT CustomerID, CustomerSum.Total

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

Donde se vería la función:

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

Obviamente, el ejemplo anterior podría hacerse sin una función definida por el usuario en una sola consulta.

El inconveniente es que las funciones son muy limitadas: muchas de las características de un procedimiento almacenado no están disponibles en una función definida por el usuario, y la conversión de un procedimiento almacenado en una función no siempre funciona.

David Griffiths
fuente
En el caso, ¿no hay permisos de escritura para crear una función?
user2284570
7

Usaría la respuesta aceptada, pero otra posibilidad es usar una variable de tabla para contener un conjunto de valores numerados (en este caso, solo el campo ID de una tabla) y recorrerlos por Número de fila con un JOIN a la tabla para recupere lo que necesite para la acción dentro del bucle.

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
AjV Jsy
fuente
Esto es mejor porque no asume que el valor que busca es un número entero o se puede comparar con sensatez.
philw
Exactamente lo que estaba buscando.
Raithlin
6

Para SQL Server 2005 en adelante, puede hacer esto con CROSS APPLY y una función con valores de tabla.

Solo por claridad, me refiero a aquellos casos en los que el procedimiento almacenado se puede convertir en una función con valores de tabla.

Trigo Mitch
fuente
12
Buena idea, pero una función no puede llamar a un procedimiento almacenado
Andomar
3

Esta es una variación de la solución n3rds anterior. No se necesita ordenar usando ORDER BY, ya que se usa MIN ().

Recuerde que CustomerID (o cualquier otra columna numérica que use para el progreso) debe tener una restricción única. Además, para hacerlo lo más rápido posible se debe indexar CustomerID.

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

Utilizo este enfoque en algunos varchars que necesito revisar, colocándolos primero en una tabla temporal, para darles una identificación.

beruic
fuente
2

Si no sabe qué usar un cursor, creo que tendrá que hacerlo externamente (obtenga la tabla y luego ejecute para cada instrucción y cada vez que llame al sp) Es lo mismo que usar un cursor, pero solo afuera SQL ¿Por qué no usarás un cursor?

Dani
fuente
2

Esta es una variación de las respuestas ya proporcionadas, pero debería funcionar mejor porque no requiere ORDER BY, COUNT o MIN / MAX. La única desventaja con este enfoque es que debe crear una tabla temporal para contener todos los ID (se supone que tiene vacíos en su lista de ID de cliente).

Dicho esto, estoy de acuerdo con @Mark Powell, aunque, en términos generales, un enfoque basado en conjuntos aún debería ser mejor.

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
Adriaan de Beer
fuente
1

Por lo general, lo hago de esta manera cuando hay bastantes filas:

  1. Seleccione todos los parámetros sproc en un conjunto de datos con SQL Management Studio
  2. Haga clic derecho -> Copiar
  3. Pegar para sobresalir
  4. Cree sentencias sql de una sola fila con una fórmula como '= "EXEC schema.mysproc @ param =" & A2' en una nueva columna de Excel. (Donde A2 es su columna de Excel que contiene el parámetro)
  5. Copie la lista de declaraciones de Excel en una nueva consulta en SQL Management Studio y ejecútela.
  6. Hecho.

(Sin embargo, en conjuntos de datos más grandes, usaría una de las soluciones mencionadas anteriormente).

Jonas Stensved
fuente
44
No es muy útil en situaciones de programación, es un truco único.
Warren P
1

DELIMITADOR //

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;
radixxko
fuente
1

Una mejor solución para esto es

  1. Copia / código pasado del procedimiento almacenado
  2. Unir ese código con la tabla para la que desea ejecutarlo nuevamente (para cada fila)

Este fue un resultado limpio con formato de tabla. Mientras que si ejecuta SP para cada fila, obtiene un resultado de consulta separado para cada iteración que es feo.

Hammad Khan
fuente
0

En caso de que el orden sea 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
isxaker
fuente
0

Tenía un código de producción que solo podía manejar a 20 empleados a la vez, a continuación se muestra el marco para el código. Acabo de copiar el código de producción y eliminé las cosas a continuación.

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
William Egge
fuente
-1

Me gusta hacer algo similar a esto (aunque todavía es muy similar a usar un 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]

Tenga en cuenta que no necesita la identidad o la columna isIterated en su tabla temporal / variable, solo prefiero hacerlo de esta manera para no tener que eliminar el registro superior de la colección mientras itero a través del ciclo.

Kritner
fuente