Errores: "La instrucción INSERT EXEC no se puede anidar". y "No se puede utilizar la instrucción ROLLBACK dentro de una instrucción INSERT-EXEC". ¿Cómo solucionar esto?

98

Tengo tres procedimientos almacenados Sp1, Sp2y Sp3.

El primero ( Sp1) ejecutará el segundo ( Sp2) y guardará los datos devueltos en @tempTB1y el segundo ejecutará el tercero ( Sp3) y guardará los datos en @tempTB2.

Si ejecuto el Sp2, funcionará y me devolverá todos mis datos del Sp3, pero el problema está en el Sp1, cuando lo ejecute, mostrará este error:

La instrucción INSERT EXEC no se puede anidar

Intenté cambiar el lugar de execute Sp2y me muestra otro error:

No se puede utilizar la instrucción ROLLBACK dentro de una instrucción INSERT-EXEC.

HAJJAJ
fuente

Respuestas:

101

Este es un problema común cuando se intenta "burbujear" datos de una cadena de procedimientos almacenados. Una restricción en SQL Server es que solo puede tener un INSERT-EXEC activo a la vez. Recomiendo mirar Cómo compartir datos entre procedimientos almacenados, que es un artículo muy completo sobre patrones para solucionar este tipo de problema.

Por ejemplo, una solución alternativa podría ser convertir Sp3 en una función con valores de tabla.

Eddiegroves
fuente
1
enlace roto O sitio que no responde.
SouravA
6
¿Tiene alguna idea de cuál es la razón técnica para no permitirlo? No puedo encontrar ninguna información sobre esto.
jtate
1
Desafortunadamente, esta no suele ser una opción. Muchos tipos de información importante solo están disponibles de manera confiable en los procedimientos almacenados del sistema (porque en ciertos casos la vista de administración respectiva contiene datos obsoletos / no confiables; un ejemplo es la información devuelta por sp_help_jobactivity).
GSerg
21

Esta es la única forma "simple" de hacer esto en SQL Server sin una función creada enrevesada gigante o una llamada de cadena SQL ejecutada, las cuales son soluciones terribles:

  1. crear una tabla temporal
  2. Abra la lista de los datos de su procedimiento almacenado

EJEMPLO:

INSERT INTO #YOUR_TEMP_TABLE
SELECT * FROM OPENROWSET ('SQLOLEDB','Server=(local);TRUSTED_CONNECTION=YES;','set fmtonly off EXEC [ServerName].dbo.[StoredProcedureName] 1,2,3')

Nota : DEBE usar 'set fmtonly off', Y NO PUEDE agregar sql dinámico a esto dentro de la llamada openrowset, ya sea para la cadena que contiene los parámetros de su procedimiento almacenado o para el nombre de la tabla. Es por eso que debe usar una tabla temporal en lugar de variables de tabla, lo que hubiera sido mejor, ya que supera a la tabla temporal en la mayoría de los casos.

Mitch Stokely
fuente
No es obligatorio usar SET FMTONLY OFF. Puede simplemente agregar un IF (1 = 0) que devuelve una tabla vacía con los mismos tipos de datos que el procedimiento normalmente devuelve.
Guillermo Gutiérrez
1
Las tablas temporales y las variables de tabla almacenan sus datos de manera diferente. Se supone que las variables de tabla se usan para conjuntos de resultados pequeños, ya que el optimizador de consultas no mantiene estadísticas en las variables de tabla. Por lo tanto, para conjuntos de datos grandes, casi siempre es mejor usar tablas temporales. Aquí hay un buen artículo de blog al respecto mssqltips.com/sqlservertip/2825/…
gh9
@ gh9 sí, pero de todos modos esta es una idea horrible para conjuntos de resultados grandes. Las estadísticas y el uso de una tabla real en la base de datos temporal pueden causar una sobrecarga significativa. Tengo un procedimiento que devuelve un conjunto de registros con 1 fila de valores actuales (consultando varias tablas) y un procedimiento que almacena eso en una variable de tabla y lo compara con valores en otra tabla con el mismo formato. El cambio de una tabla temporal a una variable de tabla aceleró el tiempo promedio de 8 ms a 2 ms, lo cual es importante cuando se llama varias veces por segundo durante el día y 100.000 veces en un proceso nocturno.
Jason Goemaat
¿Por qué querría que las estadísticas se mantengan en una variable de tabla? El objetivo es crear una tabla temporal en RAM que se destruirá una vez finalizada la consulta. Por definición, las estadísticas creadas en una tabla de este tipo nunca se utilizarían. En general, el hecho de que los datos de una variable de tabla permanezcan en RAM siempre que sea posible los hace más rápidos que las tablas temporales en cualquier escenario donde sus datos sean más pequeños que la cantidad de RAM disponible para SQL Server (que en estos días de más de 100 GB de memoria para nuestro SQL Servidores, casi siempre)
Geoff Griswald
Sin embargo, esto no funciona para procedimientos almacenados extendidos. El error es Los metadatos no se pudieron determinar porque la instrucción 'EXECUTE <nombre del procedimiento> @retval OUTPUT' en el procedimiento ... 'invoca un procedimiento almacenado extendido .
GSerg
11

OK, alentado por jimhark aquí hay un ejemplo del antiguo enfoque de tabla hash única: -

CREATE PROCEDURE SP3 as

BEGIN

    SELECT 1, 'Data1'
    UNION ALL
    SELECT 2, 'Data2'

END
go


CREATE PROCEDURE SP2 as

BEGIN

    if exists (select  * from tempdb.dbo.sysobjects o where o.xtype in ('U') and o.id = object_id(N'tempdb..#tmp1'))
        INSERT INTO #tmp1
        EXEC SP3
    else
        EXEC SP3

END
go

CREATE PROCEDURE SP1 as

BEGIN

    EXEC SP2

END
GO


/*
--I want some data back from SP3

-- Just run the SP1

EXEC SP1
*/


/*
--I want some data back from SP3 into a table to do something useful
--Try run this - get an error - can't nest Execs

if exists (select  * from tempdb.dbo.sysobjects o where o.xtype in ('U') and o.id = object_id(N'tempdb..#tmp1'))
    DROP TABLE #tmp1

CREATE TABLE #tmp1 (ID INT, Data VARCHAR(20))

INSERT INTO #tmp1
EXEC SP1


*/

/*
--I want some data back from SP3 into a table to do something useful
--However, if we run this single hash temp table it is in scope anyway so
--no need for the exec insert

if exists (select  * from tempdb.dbo.sysobjects o where o.xtype in ('U') and o.id = object_id(N'tempdb..#tmp1'))
    DROP TABLE #tmp1

CREATE TABLE #tmp1 (ID INT, Data VARCHAR(20))

EXEC SP1

SELECT * FROM #tmp1

*/
Matt Luckham
fuente
También utilicé esta solución alternativa. ¡Gracias por la idea!
SQL_Guy
Solución fantástica. Esto me ayudó a aprender más sobre el alcance de la tabla temporal. Por ejemplo, no me di cuenta de que puedes usar una tabla temporal en una cadena dynsql si se declara fuera de ella. Concepto similar aquí. Muchas gracias.
jbd
9

Mi solución para este problema siempre ha sido utilizar el principio de que las tablas temporales de hash único están dentro del alcance de cualquier proceso llamado. Entonces, tengo un interruptor de opción en los parámetros de proceso (predeterminado en apagado). Si está activado, el proceso llamado insertará los resultados en la tabla temporal creada en el proceso de llamada. Creo que en el pasado he dado un paso más y puse algo de código en el proceso llamado para verificar si la tabla hash única existe en el alcance, si es así, inserte el código; de lo contrario, devuelva el conjunto de resultados. Parece funcionar bien: la mejor manera de pasar grandes conjuntos de datos entre procesos.

Matt Luckham
fuente
1
Me gusta esta respuesta y apuesto a que obtendrás más votos positivos si me dieras un ejemplo.
jimhark
He estado haciendo esto durante años. Sin embargo, ¿sigue siendo necesario en SQL Azure?
Nick Allan
6

Este truco me funciona.

No tiene este problema en el servidor remoto, porque en el servidor remoto, el último comando de inserción espera a que se ejecute el resultado del comando anterior. No es el caso en el mismo servidor.

Aproveche esa situación para obtener una solución.

Si tiene los permisos adecuados para crear un servidor vinculado, hágalo. Cree el mismo servidor que el servidor vinculado.

  • en SSMS, inicie sesión en su servidor
  • vaya a "Objeto de servidor
  • Haga clic derecho en "Servidores vinculados", luego "Nuevo servidor vinculado"
  • en el diálogo, dé cualquier nombre de su servidor vinculado: por ejemplo: THISSERVER
  • el tipo de servidor es "Otra fuente de datos"
  • Proveedor: Proveedor Microsoft OLE DB para servidor SQL
  • Fuente de datos: su IP, también puede ser solo un punto (.), Porque es localhost
  • Vaya a la pestaña "Seguridad" y elija la tercera "Crear utilizando el contexto de seguridad actual del inicio de sesión"
  • Puede editar las opciones del servidor (tercera pestaña) si lo desea
  • Presione OK, se crea su servidor vinculado

ahora su comando Sql en el SP1 es

insert into @myTempTable
exec THISSERVER.MY_DATABASE_NAME.MY_SCHEMA.SP2

Créame, funciona incluso si tiene una inserción dinámica en SP2

ainasiart
fuente
4

Encontré que una solución es convertir uno de los productos en una función con valores de tabla. Me doy cuenta de que no siempre es posible y presenta sus propias limitaciones. Sin embargo, siempre he podido encontrar al menos uno de los procedimientos un buen candidato para esto. Me gusta esta solución, porque no introduce ningún "truco" en la solución.

Roman K
fuente
pero una desventaja es un problema con el manejo de excepciones si la función es compleja, ¿verdad?
Muflix
2

Encontré este problema al intentar importar los resultados de un proceso almacenado en una tabla temporal, y ese proceso almacenado se insertó en una tabla temporal como parte de su propia operación. El problema es que SQL Server no permite que el mismo proceso escriba en dos tablas temporales diferentes al mismo tiempo.

La respuesta OPENROWSET aceptada funciona bien, pero necesitaba evitar el uso de SQL dinámico o un proveedor OLE externo en mi proceso, así que tomé una ruta diferente.

Una solución fácil que encontré fue cambiar la tabla temporal en mi procedimiento almacenado a una variable de tabla. Funciona exactamente igual que con una tabla temporal, pero ya no entra en conflicto con mi otro inserto de tabla temporal.

Solo para encabezar el comentario, sé que algunos de ustedes están a punto de escribir, advirtiéndome de las variables de tabla como asesinos del rendimiento ... Todo lo que puedo decirles es que en 2020 vale la pena no tener miedo a las variables de tabla. Si esto fue en 2008 y mi base de datos estaba alojada en un servidor con 16 GB de RAM y funcionando con discos duros de 5400 RPM, podría estar de acuerdo con usted. Pero es 2020 y tengo una matriz SSD como mi almacenamiento principal y cientos de gigas de RAM. Podría cargar la base de datos de toda mi empresa en una variable de tabla y aún tener suficiente RAM de sobra.

¡Las variables de tabla están de vuelta en el menú!

Geoff Griswald
fuente
1

Tuve el mismo problema y preocupación por el código duplicado en dos o más sprocs. Terminé agregando un atributo adicional para "modo". Esto permitió que existiera un código común dentro de un sproc y el flujo dirigido por modo y el conjunto de resultados del sproc.

PhoenixAZ
fuente
1

¿Qué tal simplemente almacenar la salida en la tabla estática? Me gusta

-- SubProcedure: subProcedureName
---------------------------------
-- Save the value
DELETE lastValue_subProcedureName
INSERT INTO lastValue_subProcedureName (Value)
SELECT @Value
-- Return the value
SELECT @Value

-- Procedure
--------------------------------------------
-- get last value of subProcedureName
SELECT Value FROM lastValue_subProcedureName

no es lo ideal, pero es tan simple y no es necesario volver a escribir todo.

ACTUALIZACIÓN : la solución anterior no funciona bien con consultas paralelas (acceso asíncrono y multiusuario), por lo tanto, ahora estoy usando tablas temporales

-- A local temporary table created in a stored procedure is dropped automatically when the stored procedure is finished. 
-- The table can be referenced by any nested stored procedures executed by the stored procedure that created the table. 
-- The table cannot be referenced by the process that called the stored procedure that created the table.
IF OBJECT_ID('tempdb..#lastValue_spGetData') IS NULL
CREATE TABLE #lastValue_spGetData (Value INT)

-- trigger stored procedure with special silent parameter
EXEC dbo.spGetData 1 --silent mode parameter

spGetDatacontenido de procedimiento almacenado anidado

-- Save the output if temporary table exists.
IF OBJECT_ID('tempdb..#lastValue_spGetData') IS NOT NULL
BEGIN
    DELETE #lastValue_spGetData
    INSERT INTO #lastValue_spGetData(Value)
    SELECT Col1 FROM dbo.Table1
END

 -- stored procedure return
 IF @silentMode = 0
 SELECT Col1 FROM dbo.Table1
Muflix
fuente
Generalmente, no puede crear un SProc ad-hoc como puede hacerlo con Tablas. Deberá ampliar su ejemplo con más referencias, ya que este enfoque no se conoce ni acepta fácilmente. Además, se parece más a una expresión Lambda que a una ejecución de SProc, que ANSI-SQL no permite para los enfoques de expresión Lambda.
GoldBishop
Funciona, pero descubrí que tampoco funciona bien con consultas paralelas (accesos asíncronos y multiusuario). Por lo tanto, ahora estoy usando el enfoque de tabla temporal. Actualicé mi respuesta.
Muflix
1
La lógica de la tabla Temp es buena, era la referencia de SProc lo que me preocupaba. Los Sproc no pueden inherentemente ser consultados directamente. Las funciones con valores de tabla se pueden consultar directamente desde. Como aludió en su lógica actualizada, el mejor enfoque es una tabla temporal, sesión, instancia o global, y operar desde ese punto.
GoldBishop
0

Declare una variable de cursor de salida al sp interno:

@c CURSOR VARYING OUTPUT

Luego, declare un cursor c a la selección que desea devolver. Luego abre el cursor. Luego establezca la referencia:

DECLARE c CURSOR LOCAL FAST_FORWARD READ_ONLY FOR 
SELECT ...
OPEN c
SET @c = c 

NO cierre ni reasigne.

Ahora llame al sp interno desde el externo proporcionando un parámetro de cursor como:

exec sp_abc a,b,c,, @cOUT OUTPUT

Una vez que se ejecuta el sp interno, @cOUTestá listo para recuperar. Bucle y luego cierre y desasigne.

Stefanos Zilellis
fuente
0

Si puede utilizar otras tecnologías asociadas, como C #, le sugiero que utilice el comando SQL integrado con el parámetro Transaction.

var sqlCommand = new SqlCommand(commandText, null, transaction);

Creé una aplicación de consola simple que demuestra esta capacidad que se puede encontrar aquí: https://github.com/hecked12/SQL-Transaction-Using-C-Sharp

En resumen, C # le permite superar esta limitación en la que puede inspeccionar la salida de cada procedimiento almacenado y usar esa salida como quiera, por ejemplo, puede alimentarlo a otro procedimiento almacenado. Si el resultado es correcto, puede confirmar la transacción; de lo contrario, puede revertir los cambios mediante la reversión.

spidernet12
fuente
-1

En SQL Server 2008 R2, tuve una falta de coincidencia en las columnas de la tabla que causó el error de reversión. Desapareció cuando arreglé mi variable de tabla sqlcmd poblada por la instrucción insert-exec para que coincida con la devuelta por el proceso almacenado. Faltaba el código_org. En un archivo cmd de Windows, carga el resultado del procedimiento almacenado y lo selecciona.

set SQLTXT= declare @resets as table (org_id nvarchar(9), org_code char(4), ^
tin(char9), old_strt_dt char(10), strt_dt char(10)); ^
insert @resets exec rsp_reset; ^
select * from @resets;

sqlcmd -U user -P pass -d database -S server -Q "%SQLTXT%" -o "OrgReport.txt"
usuario3448451
fuente
OP estaba preguntando sobre un error que ocurre al usar declaraciones insert-exec en procedimientos almacenados anidados. Su problema devolvería un error diferente, como "La lista de selección para la instrucción INSERT contiene menos elementos que la lista de inserción. El número de valores SELECT debe coincidir con el número de columnas INSERT".
Losbear
Esto es más una advertencia de que es posible recibir este mensaje por error.
user3448451