2009-11-01 8 views
121

¿Cómo se puede llamar a un procedimiento almacenado para cada fila de una tabla, donde las columnas de una fila son parámetros de entrada al SP sin usando un cursor?SQL llamada de procedimiento almacenado para cada fila sin utilizar un cursor

+3

Así, por ejemplo, tiene una tabla de clientes con una columna customerId, y al que desea llamar la SP vez para cada fila de la tabla, que pasa en el customerId correspondiente como parámetro? –

+1

¿Podría explicar por qué no puede usar un cursor? – Andomar

+0

@Gary: Tal vez solo quiero pasar el Nombre del Cliente, no necesariamente la ID. Pero tienes razón. –

Respuesta

163

En general, 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 
+17

como con la respuesta aceptada. USE CON CATION: Dependiendo de su tabla y estructura de índice puede tener un rendimiento muy pobre (O (n^2)) ya que tiene que pedir y buscar en su tabla cada vez que enumera. – csauve

+3

Esto no parece funcionar (la ruptura nunca termina el 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 null en la condición de while sale del ciclo. – dudeNumber4

+0

Estoy de acuerdo con @ dudeNumber4 para SQL Server 2008. El valor de @@ ROWCOUNT parece siempre igual a 0 en cada iteración. Con ese valor, uno pensaría que se rompería cada vez, pero no es así. – MacGyver

2

Si no usa el cursor, creo que tendrá que hacerlo externamente (obtenga la tabla, y luego ejecútelo para cada instrucción y cada vez llame a la sp) Es lo mismo que usar una cursor, pero solo fuera de SQL. ¿Por qué no usarás un cursor?

6

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

Sólo para mayor claridad, me refiero a los casos en que el procedimiento almacenado se puede convertir en una función con valores de tabla.

+10

Buena idea, pero una función no puede llamar a un procedimiento almacenado – Andomar

34

Puedes hacer algo como esto: ordena tu mesa por ej. CustomerID (usando la tabla de ejemplo AdventureWorks Sales.Customer), y iterar sobre aquellos clientes usando un bucle 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 

que debería funcionar con cualquier tabla, siempre y cuando se puede definir algún tipo de ORDER BY en alguna columna.

+12

un ciclo while no es muy diferente de un cursor, por supuesto ... –

+0

@Mitch: sí, cierto - un poco menos sobrecarga. Pero aún así, no está realmente en la mentalidad basada en conjuntos de SQL –

+5

¿Es posible incluso una implementación basada en conjuntos? –

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 ID de cliente y devolviera la suma.

Y usted puede hacer esto:

SELECT CustomerID, CustomerSum.Total 

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

Cuando la función se vería así:

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 a una función no siempre funciona.

+0

En este caso, ¿no existen los permisos de escritura para crear una función? – user2284570

9

la respuesta de Marc es bueno (me comento que si pudiera encontrar la manera de!)
sólo pensé en señalar que puede ser mejor para cambiar el bucle por lo que el SELECT sólo existe una vez (en un caso real en el que necesitaba hacer esto, el SELECT era bastante complejo, y escribirlo dos veces era un tema 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 
+0

El APLICAR se puede usar solo con funciones ... por lo que este enfoque es mucho mejor si no quiere tener que ver con las funciones. – Artur

+0

Necesita 50 representantes para comentar. Siga respondiendo esas preguntas y obtendrá más poder: D http://stackoverflow.com/help/privileges – SvendK

+0

Creo que esta debería ser la respuesta, clara y directa. ¡Muchas gracias! – bomblike

1

Por lo general hago de esta manera cuando se trata de un un buen número de filas:

  1. Seleccionar todos los parámetros sproc en un conjunto de datos con SQL Management Studio
  2. botón derecho del ratón -> Copiar
  3. Pegue en excel
  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 sentencias de Excel en una consulta nueva en SQL Management Studio y ejecútela.
  6. Hecho.

(En conjuntos de datos más grandes, utilizaría una de las soluciones mencionadas anteriormente).

+4

No es muy útil en situaciones de programación, es un truco único. –

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

Esta es una variación de la solución n3rds anteriormente. 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 que sea lo más rápido posible, CustomerID debe 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 

que utilizan este enfoque en algunos varchars tengo que mirar por encima, poniéndolos en una tabla temporal en primer lugar, para darles una identificación.

19
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, así que nunca pondría ese código en producción, pero sí cumple sus requisitos.

+0

¿Cómo hacer lo mismo cuando el procedimiento devuelve un valor que debe establecer el valor de la fila? * (usando un PROCEDIMIENTO en lugar de una función porque [la creación de funciones no está permitida] (http://stackoverflow.com/q/25069132/2284570)) * – user2284570

-1

me gusta hacer algo similar a esto (aunque todavía es muy similar al uso de un cursor)

[code]

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

[/ code]

Tenga en cuenta que Don 'necesita la identidad o la columna isIterated en su tabla de temperatura/variable, solo prefiero hacerlo de esta manera para no tener que eliminar el registro superior de la colección cuando repito el ciclo.

3

Utilizarí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 mesa para recuperar lo que necesites para la acción dentro del ciclo.

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 [email protected] 
    PRINT Convert(NVarChar(10),@RowCnt) +' '+ @Code +' '+ @Name 
END 
+0

Esto es mejor porque no asume el valor que busca es un número entero o se puede comparar sensiblemente. – philw

0

Esta es una variación en las respuestas que se facilitan, sino que debe ser mejor rendimiento, ya que no requiere ORDER BY, contar o MIN/MAX. La única desventaja de este enfoque es que debe crear una tabla temporal para contener todos los Id. (Se supone que tiene lagunas en la lista de IdClientes).

Dicho esto, estoy de acuerdo con @Mark Powell, aunque, en términos generales, un enfoque basado en un conjunto debería ser aún 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 
0

En caso de que el orden es 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 
        [email protected] = 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

una mejor solución para esto es

  1. Copia/código pasado de procedimiento almacenado
  2. unirse a ese código con el tabla para la que desea ejecutarla nuevamente (para cada fila)

Esto le dio 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.

0

Tenía un código de producción que solo podía manejar 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é 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 
Cuestiones relacionadas