Erreurs: "L'instruction INSERT EXEC ne peut pas être imbriquée." et "Impossible d'utiliser l'instruction ROLLBACK dans une instruction INSERT-EXEC." Comment résoudre ça?

98

J'ai trois procédures stockées Sp1, Sp2et Sp3.

Le premier ( Sp1) exécutera le second ( Sp2) et enregistrera les données renvoyées dans @tempTB1et le second exécutera le troisième ( Sp3) et enregistrera les données dans @tempTB2.

Si j'exécute le, Sp2cela fonctionnera et il me renverra toutes mes données du Sp3, mais le problème est dans le Sp1, lorsque je l'exécute, il affichera cette erreur:

L'instruction INSERT EXEC ne peut pas être imbriquée

J'ai essayé de changer l'emplacement de execute Sp2et cela m'affiche une autre erreur:

Impossible d'utiliser l'instruction ROLLBACK dans une instruction INSERT-EXEC.

HAJJAJ
la source

Réponses:

102

Il s'agit d'un problème courant lors de la tentative de «bulle» de données à partir d'une chaîne de procédures stockées. Une restriction dans SQL Server est que vous ne pouvez avoir qu'un seul INSERT-EXEC actif à la fois. Je recommande de regarder Comment partager des données entre des procédures stockées qui est un article très complet sur les modèles pour contourner ce type de problème.

Par exemple, une solution pourrait être de transformer Sp3 en une fonction table.

Eddiegroves
la source
1
lien cassé OU site qui ne répond pas.
SouravA
6
avez-vous une idée de la raison technique de ne pas l'autoriser? Je ne trouve aucune information à ce sujet.
jtate
1
Malheureusement, ce n'est souvent pas une option. De nombreux types d'informations importantes ne sont disponibles de manière fiable qu'à partir des procédures stockées du système (car dans certains cas, la vue de gestion respective contient des données non fiables / obsolètes; par exemple, les informations renvoyées par sp_help_jobactivity).
GSerg
21

C'est le seul moyen «simple» de le faire dans SQL Server sans une fonction créée géante alambiquée ou un appel de chaîne SQL exécuté, qui sont tous deux des solutions terribles:

  1. créer une table temporaire
  2. openrowset vos données de procédure stockée dedans

EXEMPLE:

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

Remarque : Vous DEVEZ utiliser 'set fmtonly off', ET vous NE POUVEZ PAS ajouter de SQL dynamique à cela soit à l'intérieur de l'appel openrowset, soit pour la chaîne contenant vos paramètres de procédure stockée, soit pour le nom de la table. C'est pourquoi vous devez utiliser une table temporaire plutôt que des variables de table, ce qui aurait été mieux, car elle surpasse la table temporaire dans la plupart des cas.

Mitch Stokely
la source
Il n'est pas indispensable d'utiliser SET FMTONLY OFF. Vous pouvez simplement ajouter un IF (1 = 0) qui renvoie une table vide avec les mêmes types de données que la procédure renvoie normalement.
Guillermo Gutiérrez
1
Les tables temporaires et les variables de table stockent leurs données différemment. Les variables de table sont censées être utilisées pour les petits ensembles de résultats car l'optimiseur de requête ne gère pas les statistiques sur les variables de table. Ainsi, pour les grands ensembles de données, il est presque toujours préférable d'utiliser des tables temporaires. Voici un bel article de blog à ce sujet mssqltips.com/sqlservertip/2825/…
gh9
@ gh9 oui, mais c'est une idée horrible pour les grands ensembles de résultats de toute façon. Les statistiques et l'utilisation d'une table réelle dans la base de données temporaire peuvent entraîner une surcharge importante. J'ai une procédure qui renvoie un jeu d'enregistrements avec 1 ligne de valeurs actuelles (interrogation de plusieurs tables) et une procédure qui stocke cela dans une variable de table et la compare aux valeurs d'une autre table avec le même format. Le passage d'une table temporaire à une variable de table a accéléré le temps moyen de 8 ms à 2 ms, ce qui est important lorsqu'il est appelé plusieurs fois par seconde tout au long de la journée et 100 000 fois dans un processus nocturne.
Jason Goemaat
Pourquoi voudriez-vous que les statistiques soient conservées sur une variable de table? Le but est de créer une table temporaire dans la RAM qui sera détruite une fois la requête terminée. Par définition, les statistiques créées sur une telle table ne seraient jamais utilisées. En règle générale, le fait que les données d'une variable de table restent dans la RAM chaque fois que possible les rend plus rapides que les tables temporaires dans tout scénario où vos données sont plus petites que la quantité de RAM disponible pour SQL Server (qui de nos jours de 100 Go + pools de mémoire pour notre SQL Serveurs, c'est presque toujours)
Geoff Griswald
Cela ne fonctionne pas pour les procédures stockées étendues. L'erreur est les métadonnées n'ont pas pu être déterminées car l'instruction 'EXECUTE <procedurename> @retval OUTPUT' in procedure ... 'appelle une procédure stockée étendue .
GSerg
11

OK, encouragé par jimhark, voici un exemple de l'ancienne approche de table de hachage unique: -

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
la source
J'ai également utilisé cette solution de contournement. Merci pour l'idée!
SQL_Guy
Fantastique contournement. Cela m'a aidé à en savoir plus sur la portée des tables temporaires. EG, je ne savais pas que vous pouviez utiliser une table temporaire dans une chaîne dynsql si elle était déclarée en dehors. Concept similaire ici. Merci beaucoup.
jbd
9

Mon travail autour de ce problème a toujours été d'utiliser le principe que les tables temporaires de hachage unique sont dans la portée de tout processus appelé. Donc, j'ai un commutateur d'option dans les paramètres de proc (par défaut réglé sur off). Si cette option est activée, le proc appelé insérera les résultats dans la table temporaire créée dans le proc appelant. Je pense que dans le passé, j'ai poussé un peu plus loin et mis du code dans le proc appelé pour vérifier si la table de hachage unique existe dans la portée, si elle le fait, insérez le code, sinon retournez le jeu de résultats. Semble bien fonctionner - meilleure façon de passer de grands ensembles de données entre les processus.

Matt Luckham
la source
1
J'aime cette réponse et je parie que vous obtiendrez plus de votes si vous deviez donner un exemple.
jimhark
Je fais ça depuis des années. Est-ce toujours nécessaire dans SQL Azure?
Nick Allan
6

Cette astuce fonctionne pour moi.

Vous n'avez pas ce problème sur le serveur distant, car sur le serveur distant, la dernière commande d'insertion attend le résultat de la commande précédente à exécuter. Ce n'est pas le cas sur le même serveur.

Profitez de cette situation pour une solution de contournement.

Si vous disposez des autorisations nécessaires pour créer un serveur lié, faites-le. Créez le même serveur que le serveur lié.

  • dans SSMS, connectez-vous à votre serveur
  • aller à "Objet serveur
  • Faites un clic droit sur "Serveurs liés", puis "Nouveau serveur lié"
  • dans la boîte de dialogue, donnez n'importe quel nom de votre serveur lié: par exemple: THISSERVER
  • le type de serveur est "Autre source de données"
  • Fournisseur: fournisseur Microsoft OLE DB pour serveur SQL
  • Source de données: votre IP, cela peut aussi être juste un point (.), Car c'est localhost
  • Allez dans l'onglet "Sécurité" et choisissez le 3ème "Se faire en utilisant le contexte de sécurité actuel de la connexion"
  • Vous pouvez modifier les options du serveur (3e onglet) si vous le souhaitez
  • Appuyez sur OK, votre serveur lié est créé

maintenant votre commande Sql dans le SP1 est

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

Croyez-moi, cela fonctionne même si vous avez un insert dynamique dans SP2

ainasiart
la source
4

J'ai trouvé qu'une solution consiste à convertir l'un des prods en une fonction de table. Je me rends compte que ce n'est pas toujours possible et introduit ses propres limites. Cependant, j'ai toujours pu trouver au moins une des procédures un bon candidat pour cela. J'aime cette solution, car elle n'introduit aucun "hacks" dans la solution.

Roman K
la source
mais un inconvénient est un problème de gestion des exceptions si la fonction est complexe, non?
Muflix
2

J'ai rencontré ce problème en essayant d'importer les résultats d'un processus stocké dans une table temporaire, et ce processus stocké a été inséré dans une table temporaire dans le cadre de sa propre opération. Le problème est que SQL Server n'autorise pas le même processus à écrire dans deux tables temporaires différentes en même temps.

La réponse OPENROWSET acceptée fonctionne bien, mais je devais éviter d'utiliser un SQL dynamique ou un fournisseur OLE externe dans mon processus, j'ai donc emprunté une voie différente.

Une solution de contournement simple que j'ai trouvée était de changer la table temporaire de ma procédure stockée en variable de table. Cela fonctionne exactement de la même manière qu'avec une table temporaire, mais n'est plus en conflit avec mon autre insert de table temporaire.

Juste pour éviter le commentaire, je sais que quelques-uns d'entre vous sont sur le point d'écrire, me mettant en garde contre les variables de table en tant que tueurs de performances ... Tout ce que je peux vous dire, c'est qu'en 2020, cela rapporte des dividendes de ne pas avoir peur des variables de table. Si c'était en 2008 et que ma base de données était hébergée sur un serveur avec 16 Go de RAM et fonctionnant sur des disques durs à 5400 tr / min, je pourrais être d'accord avec vous. Mais c'est 2020 et j'ai une baie SSD comme stockage principal et des centaines de Go de RAM. Je pourrais charger la base de données de toute ma société dans une variable de table et avoir encore beaucoup de RAM à revendre.

Les variables de table sont de retour au menu!

Geoff Griswald
la source
1

J'ai eu le même problème et le même souci concernant le code en double dans deux sprocs ou plus. J'ai fini par ajouter un attribut supplémentaire pour "mode". Cela a permis au code commun d'exister dans un sproc et le flux dirigé par mode et l'ensemble de résultats du sproc.

phénixAZ
la source
1

qu'en est-il simplement de stocker la sortie dans la table statique? Comme

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

ce n'est pas idéal, mais c'est si simple et vous n'avez pas besoin de tout réécrire.

MISE À JOUR : la solution précédente ne fonctionne pas bien avec les requêtes parallèles (accès asynchrone et multi-utilisateur) donc maintenant je suis en utilisant des tables temporaires

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

spGetDatacontenu de procédure stockée imbriquée

-- 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
la source
En règle générale, vous ne pouvez pas créer un SProc ad-hoc comme vous pouvez le faire avec des tables. Vous devrez développer votre exemple avec plus de références, car cette approche n'est pas vraiment connue ou acceptée. En outre, il ressemble plus à une expression Lambda qu'à une exécution SProc, ce qu'ANSI-SQL ne permet pas pour les approches d'expression Lambda.
GoldBishop
Cela fonctionne mais j'ai trouvé que cela ne fonctionnait pas bien avec les requêtes parallèles (accès asynchrone et multi-utilisateur) non plus. Par conséquent, je suis maintenant en utilisant l'approche de la table temporaire. J'ai mis à jour ma réponse.
Muflix
1
La logique de la table Temp est bonne, c'était la référence SProc qui m'intéressait. Les Sproc ne peuvent pas intrinsèquement être interrogés directement. Les fonctions table-Valued peuvent être directement interrogées à partir de. Comme vous l'avez mentionné dans votre logique mise à jour, la meilleure approche est une table temporaire, une session, une instance ou globale, et fonctionne à partir de ce point.
GoldBishop
0

Déclarez une variable de curseur de sortie sur le sp interne:

@c CURSOR VARYING OUTPUT

Déclarez ensuite un curseur c sur la sélection que vous souhaitez retourner. Puis ouvrez le curseur. Puis définissez la référence:

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

NE PAS fermer ou réallouer.

Maintenant, appelez le sp interne à partir de l'extérieur en fournissant un paramètre de curseur comme:

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

Une fois que le sp interne s'exécute, vous @cOUTêtes prêt à aller chercher. Bouclez puis fermez et désallouez.

Stefanos Zilellis
la source
0

Si vous êtes capable d'utiliser d'autres technologies associées telles que C #, je suggère d'utiliser la commande SQL intégrée avec le paramètre Transaction.

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

J'ai créé une application console simple qui démontre cette capacité qui peut être trouvée ici: https://github.com/hecked12/SQL-Transaction-Using-C-Sharp

En bref, C # vous permet de surmonter cette limitation où vous pouvez inspecter la sortie de chaque procédure stockée et utiliser cette sortie comme vous le souhaitez, par exemple, vous pouvez la transmettre à une autre procédure stockée. Si la sortie est correcte, vous pouvez valider la transaction, sinon vous pouvez annuler les modifications à l'aide de la restauration.

spidernet12
la source
-1

Sur SQL Server 2008 R2, j'avais une incompatibilité dans les colonnes de table qui a provoqué l'erreur de restauration. Il a disparu lorsque j'ai corrigé ma variable de table sqlcmd remplie par l'instruction insert-exec pour qu'elle corresponde à celle renvoyée par le proc stocké. Il manquait org_code. Dans un fichier cmd Windows, il charge le résultat de la procédure stockée et le sélectionne.

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"
user3448451
la source
OP demandait une erreur qui se produit lors de l'utilisation d'instructions insert-exec dans des procédures stockées imbriquées. Votre problème renverrait une erreur différente, telle que "La liste de sélection de l'instruction INSERT contient moins d'éléments que la liste d'insertion. Le nombre de valeurs SELECT doit correspondre au nombre de colonnes INSERT."
Losbear
C'est plus un avertissement qu'il est possible d'obtenir ce message par erreur.
user3448451