¿Cómo ejecuto un procedimiento almacenado una vez para cada fila devuelta por consulta?

206

Tengo un procedimiento almacenado que altera los datos del usuario de cierta manera. Lo paso user_id y hace lo suyo. Quiero ejecutar una consulta en una tabla y luego para cada user_id encuentro ejecutar el procedimiento almacenado una vez en ese user_id

¿Cómo escribiría una consulta para esto?

MetaGuru
fuente
55
Es necesario especificar lo RDBMS - la respuesta será diferente para SQL Server, Oracle, MySQL, etc.
Gary.Ray
55
Lo más probable es que no necesite un procedimiento almacenado en absoluto. ¿Puede describir "qué" hace exactamente el procedimiento almacenado? Quizás todo el proceso se pueda expresar como una única declaración de actualización. El patrón "hacer una vez para cada registro" generalmente debe evitarse, si es posible.
Tomalak
¿Qué base de datos estás usando?
Usuario SO
1
Debería leer este artículo ... el elemento 2 dice: NO use cursores codeproject.com/KB/database/sqldodont.aspx...mind También estoy en contra de la optimización prematura.
Michael Prewecki
77
@MichaelPrewecki: Si sigue leyendo en ese artículo mal escrito, verá que el elemento 10 es "NO use cursores del lado del servidor a menos que sepa lo que está haciendo". Creo que este es un caso de "Sé lo que estoy haciendo".
Gabe

Respuestas:

246

usa un cursor

ADENDA: [Ejemplo de cursor 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

en MS SQL, aquí hay un artículo de ejemplo

tenga en cuenta que los cursores son más lentos que las operaciones basadas en conjuntos, pero más rápidos que los bucles while manuales; más detalles en esta pregunta SO

ANEXO 2: si va a procesar más que unos pocos registros, primero introdúzcalos en una tabla temporal y pase el cursor sobre la tabla temporal; esto evitará que SQL se convierta en bloqueos de tabla y acelere la operación

APÉNDICE 3: y, por supuesto, si puede incorporar lo que esté haciendo su procedimiento almacenado a cada ID de usuario y ejecutar todo como una sola declaración de actualización de SQL, sería óptimo

Steven A. Lowe
fuente
21
Te perdiste 'abrir cur' después de la declaración; esto me estaba dando errores 'el cursor no está abierto'. No tengo el representante para hacer una edición.
Fiona - myaccessible.website
55
Puedes agradecer a las personas votando sus comentarios. Quién sabe, tal vez así tendrán el representante para hacer la edición, ¡la próxima vez! :-)
Robino
Asegúrese de verificar sus índices en las cláusulas JOINS y WHERE en los campos utilizados en su procedimiento almacenado. Aceleré dramáticamente llamando a mi SP en un bucle después de agregar índices apropiados.
Mateo
1
Gracias por el recordatorio de usar la tabla temporal para evitar posibles problemas de bloqueo causados ​​por una ejecución prolongada.
a Tony
A veces, el procedimiento almacenado es demasiado grande o complicado para alinearlo sin arriesgarse a introducir errores. Cuando el rendimiento no es la máxima prioridad, la ejecución del SP en un bucle de cursor suele ser la opción más práctica.
Suncat2000 el
55

¡intenta cambiar tu método si necesitas hacer un bucle!

dentro del procedimiento almacenado principal, cree una tabla #temp que contenga los datos que necesita procesar. Llame al procedimiento almacenado secundario, la tabla #temp estará visible y podrá procesarla, con suerte trabajando con todo el conjunto de datos y sin un cursor o bucle.

esto realmente depende de lo que esté haciendo este procedimiento almacenado secundario. Si está ACTUALIZANDO, puede "actualizar desde" unirse a la tabla #temp y hacer todo el trabajo en una declaración sin un bucle. Lo mismo se puede hacer para INSERT y DELETEs. Si necesita hacer múltiples actualizaciones con IFs, puede convertirlas en múltiples UPDATE FROMcon la tabla #temp y usar declaraciones CASE o condiciones WHERE.

Cuando trabaje en una base de datos, intente perder la mentalidad de bucle, es una pérdida de rendimiento real, causará bloqueo / bloqueo y ralentizará el procesamiento. Si realiza un bucle en todas partes, su sistema no escalará muy bien y será muy difícil de acelerar cuando los usuarios comiencen a quejarse de actualizaciones lentas.

Publique el contenido de este procedimiento al que desea llamar en un bucle, y apostaría 9 de cada 10 veces, podría escribirlo para trabajar en un conjunto de filas.

KM.
fuente
3
+1 para una muy buena solución alternativa, suponiendo que controlas la proc del niño
Steven A. Lowe
un poco de pensamiento, ¡esta solución es muy superior!
encc
77
Las operaciones basadas en conjuntos son siempre preferibles. Sin embargo, tenga en cuenta que modificar un SP no siempre es una opción; piense en las soluciones proporcionadas por el proveedor. Es posible que algunos usuarios ni siquiera tengan visibilidad, dejando solo las opciones de cursor o bucle. En mi tienda, nuestros desarrolladores pueden ver todo, pero hay muchos obstáculos para aclarar si una solución se construye fuera de la aplicación del proveedor debido a disparadores, procesos anidados, número de registros manipulados, etc. Muchas veces su mejor opción, debido a La complejidad de la aplicación es simplemente pasar el cursor por los registros.
Steve Mangiameli
11

Se necesitará algo así como sustituciones para sus tablas y nombres de campo.

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
fuente
2
mientras que los bucles son más lentos que los cursores
Steven A. Lowe
La construcción o declaración SQL de declarar cursor no es compatible (??)
MetaGuru
9

Puedes hacerlo con una consulta dinámica.

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

¿No se puede hacer esto con una función definida por el usuario para replicar lo que sea que esté haciendo su procedimiento almacenado?

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

donde udfMyFunction es una función que realiza que toma la ID de usuario y hace lo que sea necesario hacer con ella.

Ver http://www.sqlteam.com/article/user-defined-functions para obtener más información.

Estoy de acuerdo en que los cursores realmente deben evitarse siempre que sea posible. ¡Y generalmente es posible!

(por supuesto, mi respuesta presupone que solo está interesado en obtener la salida del SP y que no está cambiando los datos reales. Me parece que "altera los datos del usuario de cierta manera" un poco ambiguo de la pregunta original, así que pensé en ofrecer esto como una posible solución. ¡Depende completamente de lo que estés haciendo!)

secuencia aleatoria
fuente
1
OP: "procedimiento almacenado que altera los datos del usuario de cierta manera" MSDN : Las funciones definidas por el usuario no se pueden usar para realizar acciones que modifiquen el estado de la base de datos. Sin embargo, SQLSVR 2014 no parece tener un problema con él
johnny 5
6

Use una tabla variable o una tabla temporal.

Como se mencionó anteriormente, un cursor es el último recurso. Principalmente porque usa muchos recursos, emite bloqueos y puede ser una señal de que simplemente no estás entendiendo cómo usar SQL correctamente.

Nota al margen: una vez encontré una solución que usaba cursores para actualizar filas en una tabla. Después de un cierto escrutinio, resultó que todo podría ser reemplazado con un solo comando ACTUALIZAR. Sin embargo, en este caso, donde se debe ejecutar un procedimiento almacenado, un solo comando SQL no funcionará.

Cree una variable de tabla como esta (si está trabajando con muchos datos o tiene poca memoria, use una tabla temporal en su lugar):

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

los id es importante.

Reemplazar parentychild con algunos buenos datos, por ejemplo, identificadores relevantes o el conjunto completo de datos para ser operado.

Insertar datos en la tabla, por ejemplo:

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

Declara algunas variables:

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

Y finalmente, cree un ciclo while sobre los datos en la tabla:

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

La primera selección obtiene datos de la tabla temporal. La segunda selección actualiza el @id.MINdevuelve nulo si no se seleccionaron filas.

Un enfoque alternativo es hacer un bucle mientras la tabla tiene filas SELECT TOP 1y eliminar la fila seleccionada de la tabla temporal:

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

Me gusta la forma de consulta dinámica de Dave Rincon, ya que no usa cursores y es pequeña y fácil. Gracias Dave por compartir.

Pero para mis necesidades en Azure SQL y con un "distintivo" en la consulta, tuve que modificar el código de esta manera:

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 esto ayude a alguien...

Tom
fuente