¿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?
¿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?
Respuestas:
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
Podría hacer algo como esto: ordene su tabla, por ejemplo, CustomerID (usando la Sales.Customer
tabla 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 BY
en una columna.
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.
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 SELECT
solo exista una vez (en un caso real donde necesito hacer esto, SELECT
era 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
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.
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
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.
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.
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
Por lo general, lo hago de esta manera cuando hay bastantes filas:
(Sin embargo, en conjuntos de datos más grandes, usaría una de las soluciones mencionadas anteriormente).
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;
Una mejor solución para esto es
- Copia / código pasado del procedimiento almacenado
- 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.
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
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
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.