Fehler: "INSERT EXEC-Anweisung kann nicht verschachtelt werden." und "Die ROLLBACK-Anweisung kann nicht in einer INSERT-EXEC-Anweisung verwendet werden." Wie kann man das lösen?

98

Ich habe drei gespeicherte Prozeduren Sp1, Sp2und Sp3.

Das erste ( Sp1) führt das zweite ( Sp2) aus und speichert zurückgegebene Daten in @tempTB1und das zweite führt das dritte ( Sp3) aus und speichert Daten in @tempTB2.

Wenn ich das ausführe Sp2, funktioniert es und es gibt mir alle meine Daten von dem zurück Sp3, aber das Problem liegt darin Sp1, dass bei der Ausführung der folgende Fehler angezeigt wird:

Die Anweisung INSERT EXEC kann nicht verschachtelt werden

Ich habe versucht, den Ort zu ändern, execute Sp2und es wird mir ein weiterer Fehler angezeigt:

Die ROLLBACK-Anweisung kann nicht in einer INSERT-EXEC-Anweisung verwendet werden.

HAJJAJ
quelle

Antworten:

99

Dies ist ein häufiges Problem, wenn versucht wird, Daten aus einer Kette gespeicherter Prozeduren zu "sprudeln". Eine Einschränkung in SQL Server besteht darin, dass jeweils nur ein INSERT-EXEC aktiv sein kann. Ich empfehle, sich mit dem Austausch von Daten zwischen gespeicherten Prozeduren zu befassen. Dies ist ein sehr ausführlicher Artikel über Muster, um diese Art von Problem zu umgehen.

Eine Lösung könnte beispielsweise darin bestehen, Sp3 in eine Funktion mit Tabellenwert umzuwandeln.

eddiegroves
quelle
1
defekter Link ODER nicht reagierende Site.
SouravA
6
Haben Sie eine Idee, was der technische Grund dafür ist, dass Sie dies nicht zulassen? Ich kann keine Informationen dazu finden.
jtate
Leider ist dies sehr oft keine Option. Viele Arten wichtiger Informationen sind nur in gespeicherten Systemprozeduren zuverlässig verfügbar (da in bestimmten Fällen die jeweilige Verwaltungsansicht unzuverlässige / veraltete Daten enthält; ein Beispiel sind die von zurückgegebenen Informationen ). sp_help_jobactivity
GSerg
21

Dies ist die einzige "einfache" Möglichkeit, dies in SQL Server ohne eine riesige, verschlungene, erstellte Funktion oder einen ausgeführten SQL-String-Aufruf zu tun. Beides sind schreckliche Lösungen:

  1. Erstellen Sie eine temporäre Tabelle
  2. Öffnen Sie Ihre gespeicherten Prozedurdaten

BEISPIEL:

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

Hinweis : Sie MÜSSEN 'set fmtonly off' verwenden, und Sie können diesem weder innerhalb des openrowset-Aufrufs dynamisches SQL hinzufügen, weder für die Zeichenfolge, die Ihre Parameter für gespeicherte Prozeduren enthält, noch für den Tabellennamen. Aus diesem Grund müssen Sie eine temporäre Tabelle anstelle von Tabellenvariablen verwenden, was besser gewesen wäre, da in den meisten Fällen eine temporäre Tabelle ausgeführt wird.

Mitch Stokely
quelle
Es ist kein Muss, SET FMTONLY OFF zu verwenden. Sie können einfach eine IF (1 = 0) hinzufügen, die eine leere Tabelle mit denselben Datentypen zurückgibt, die die Prozedur normalerweise zurückgibt.
Guillermo Gutiérrez
1
Temporäre Tabellen und Tabellenvariablen speichern ihre Daten unterschiedlich. Tabellenvariablen sollten für kleine Ergebnismengen verwendet werden, da das Abfrageoptimierungsprogramm keine Statistiken zu Tabellenvariablen verwaltet. Für große Datenmengen ist es daher fast immer besser, Temp-Tabellen zu verwenden. Hier ist ein schöner Blog-Artikel darüber mssqltips.com/sqlservertip/2825/…
gh9
@ gh9 ja, aber das ist sowieso eine schreckliche Idee für große Ergebnismengen. Die Statistik und Verwendung einer tatsächlichen Tabelle in der temporären Datenbank kann einen erheblichen Overhead verursachen. Ich habe eine Prozedur, die ein Recordset mit 1 Zeile aktueller Werte zurückgibt (mehrere Tabellen abfragt) und eine Prozedur, die diese in einer Tabellenvariablen speichert und mit Werten in einer anderen Tabelle mit demselben Format vergleicht. Der Wechsel von einer temporären Tabelle zu einer Tabellenvariablen beschleunigte die durchschnittliche Zeit von 8 ms auf 2 ms. Dies ist wichtig, wenn sie den ganzen Tag über mehrmals pro Sekunde und in einem nächtlichen Prozess 100.000 Mal aufgerufen wird.
Jason Goemaat
Warum sollten Statistiken für eine Tabellenvariable gepflegt werden? Der springende Punkt ist, eine temporäre Tabelle im RAM zu erstellen, die nach Abschluss der Abfrage zerstört wird. Per Definition würden Statistiken, die für eine solche Tabelle erstellt wurden, niemals verwendet. Im Allgemeinen macht die Tatsache, dass Daten in einer Tabellenvariablen, wo immer möglich, im RAM verbleiben, sie in jedem Szenario, in dem Ihre Daten kleiner sind als die für SQL Server verfügbare RAM-Menge (in diesen Tagen mehr als 100 GB Speicherpools für unser SQL), schneller als temporäre Tabellen Server, ist fast immer)
Geoff Griswald
Dies funktioniert jedoch nicht für erweiterte gespeicherte Prozeduren. Der Fehler lautet: Die Metadaten konnten nicht ermittelt werden, da die Anweisung 'EXECUTE <Prozedurname> @retval OUTPUT' in procedure ... 'eine erweiterte gespeicherte Prozedur aufruft .
GSerg
11

OK, ermutigt von Jimhark hier ist ein Beispiel für den alten Single-Hash-Table-Ansatz: -

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
quelle
Ich habe diese Umgehung auch verwendet. Danke für die Idee!
SQL_Guy
Fantastische Abhilfe. Dies hat mir geholfen, mehr über das temporäre Scoping von Tabellen zu erfahren. EG, ich wusste nicht, dass Sie eine temporäre Tabelle in einer Dynsql-Zeichenfolge verwenden können, wenn sie außerhalb deklariert wurde. Ähnliches Konzept hier. Vielen Dank.
jbd
9

Meine Problemumgehung bestand immer darin, das Prinzip zu verwenden, dass einzelne Hash-temporäre Tabellen für alle aufgerufenen Prozesse gelten. Ich habe also einen Optionsschalter in den Proc-Parametern (standardmäßig ausgeschaltet). Wenn dies aktiviert ist, fügt der aufgerufene Prozess die Ergebnisse in die temporäre Tabelle ein, die im aufrufenden Prozess erstellt wurde. Ich denke, in der Vergangenheit bin ich noch einen Schritt weiter gegangen und habe Code in den aufgerufenen Prozess eingefügt, um zu überprüfen, ob die einzelne Hash-Tabelle im Gültigkeitsbereich vorhanden ist. Wenn dies der Fall ist, wird der Code eingefügt, andernfalls wird die Ergebnismenge zurückgegeben. Scheint gut zu funktionieren - die beste Möglichkeit, große Datenmengen zwischen Prozessen zu übertragen.

Matt Luckham
quelle
1
Ich mag diese Antwort und ich wette, Sie erhalten mehr Stimmen, wenn Sie ein Beispiel geben.
Jimhark
Ich mache das schon seit Jahren. Ist es in SQL Azure dennoch erforderlich?
Nick Allan
6

Dieser Trick funktioniert bei mir.

Dieses Problem tritt auf dem Remoteserver nicht auf, da auf dem Remoteserver der letzte Einfügebefehl auf die Ausführung des Ergebnisses des vorherigen Befehls wartet. Dies ist auf demselben Server nicht der Fall.

Profitieren Sie von dieser Situation für eine Problemumgehung.

Wenn Sie die richtige Berechtigung zum Erstellen eines Verbindungsservers haben, tun Sie dies. Erstellen Sie denselben Server wie den Verbindungsserver.

  • Melden Sie sich in SSMS bei Ihrem Server an
  • Gehen Sie zu "Server Object
  • Klicken Sie mit der rechten Maustaste auf "Verbindungsserver" und dann auf "Neuer Verbindungsserver".
  • Geben Sie im Dialogfeld einen beliebigen Namen Ihres Verbindungsservers an: zB: THISSERVER
  • Der Servertyp ist "Andere Datenquelle".
  • Anbieter: Microsoft OLE DB-Anbieter für SQL Server
  • Datenquelle: Ihre IP, es kann auch nur ein Punkt (.) Sein, weil es localhost ist
  • Gehen Sie zur Registerkarte "Sicherheit" und wählen Sie die dritte "Mit dem aktuellen Sicherheitskontext des Logins erstellen".
  • Sie können die Serveroptionen (3. Registerkarte) bearbeiten, wenn Sie möchten
  • Drücken Sie OK, Ihr Verbindungsserver wird erstellt

Jetzt lautet Ihr SQL-Befehl im SP1

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

Glauben Sie mir, es funktioniert auch, wenn Sie eine dynamische Einfügung in SP2 haben

ainasiart
quelle
4

Ich fand eine Lösung, um eines der Produkte in eine Tabellenwertfunktion umzuwandeln. Mir ist klar, dass dies nicht immer möglich ist, und es gibt seine eigenen Einschränkungen. Es ist mir jedoch immer gelungen, mindestens eines der Verfahren als guten Kandidaten dafür zu finden. Ich mag diese Lösung, weil sie keine "Hacks" in die Lösung einführt.

Roman K.
quelle
Ein Nachteil ist jedoch ein Problem bei der Ausnahmebehandlung, wenn die Funktion komplex ist, oder?
Muflix
2

Dieses Problem trat auf, als ich versuchte, die Ergebnisse eines gespeicherten Prozesses in eine temporäre Tabelle zu importieren, und dieser gespeicherte Prozess wurde als Teil seiner eigenen Operation in eine temporäre Tabelle eingefügt. Das Problem ist, dass SQL Server nicht zulässt, dass derselbe Prozess gleichzeitig in zwei verschiedene temporäre Tabellen schreibt.

Die akzeptierte OPENROWSET-Antwort funktioniert einwandfrei, aber ich musste vermeiden, Dynamic SQL oder einen externen OLE-Anbieter in meinem Prozess zu verwenden, also bin ich einen anderen Weg gegangen.

Eine einfache Problemumgehung bestand darin, die temporäre Tabelle in meiner gespeicherten Prozedur in eine Tabellenvariable zu ändern. Es funktioniert genauso wie bei einer temporären Tabelle, steht jedoch nicht mehr in Konflikt mit meiner anderen temporären Tabelleneinfügung.

Nur um den Kommentar abzuwenden, ich weiß, dass einige von Ihnen im Begriff sind zu schreiben und mich vor Tabellenvariablen als Leistungskiller warnen ... Ich kann Ihnen nur sagen, dass es sich im Jahr 2020 auszahlt, keine Angst vor Tabellenvariablen zu haben. Wenn dies 2008 war und meine Datenbank auf einem Server mit 16 GB RAM und 5400 U / min-Festplatten gehostet wurde, stimme ich Ihnen möglicherweise zu. Aber es ist 2020 und ich habe ein SSD-Array als primären Speicher und Hunderte von GB RAM. Ich könnte die Datenbank meines gesamten Unternehmens in eine Tabellenvariable laden und trotzdem genügend RAM zur Verfügung haben.

Tabellenvariablen sind wieder im Menü!

Geoff Griswald
quelle
1

Ich hatte das gleiche Problem und die gleiche Besorgnis über doppelten Code in zwei oder mehr Sprocs. Am Ende habe ich ein zusätzliches Attribut für "Modus" hinzugefügt. Dies ermöglichte es, dass gemeinsamer Code innerhalb eines Sprocs und des modengesteuerten Flusses und der Ergebnismenge des Sprocs vorhanden war.

phoenixAZ
quelle
1

Was ist mit dem Speichern der Ausgabe in der statischen Tabelle? Mögen

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

Es ist nicht ideal, aber es ist so einfach und Sie müssen nicht alles neu schreiben.

UPDATE : Die vorherige Lösung funktioniert nicht gut mit parallelen Abfragen (asynchroner und Mehrbenutzerzugriff), daher verwende ich jetzt temporäre Tabellen

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

verschachtelter spGetDataInhalt der gespeicherten Prozedur

-- 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
quelle
Im Allgemeinen können Sie kein SProc-Ad-hoc wie mit Tabellen erstellen. Sie müssen Ihr Beispiel um weitere Referenzen erweitern, da dieser Ansatz nicht ohne weiteres bekannt oder akzeptiert ist. Außerdem ähnelt es eher einem Lambda-Ausdruck als einer SProc-Ausführung, die in ANSI-SQL keine Lambda-Ausdrucksansätze zulässt.
GoldBishop
Es funktioniert, aber ich habe festgestellt, dass es auch bei parallelen Abfragen (asynchrone und Mehrbenutzerzugriffe) nicht gut funktioniert. Daher verwende ich jetzt den temporären Tabellenansatz. Ich habe meine Antwort aktualisiert.
Muflix
1
Die Temp-Tabellenlogik ist gut, es war die SProc-Referenz, mit der ich mich befasst habe. Sprocs können nicht von Natur aus direkt abgefragt werden. Tabellenwertfunktionen können direkt von abgefragt werden. Der beste Ansatz ist eine temporäre Tabelle, Sitzung, Instanz oder global, und Sie müssen von diesem Punkt aus arbeiten.
Goldbischof
0

Deklarieren Sie eine Ausgabecurservariable zum inneren sp:

@c CURSOR VARYING OUTPUT

Deklarieren Sie dann einen Cursor c auf die Auswahl, die Sie zurückgeben möchten. Öffnen Sie dann den Cursor. Stellen Sie dann die Referenz ein:

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

NICHT schließen oder neu zuweisen.

Rufen Sie nun das innere sp vom äußeren auf und geben Sie einen Cursorparameter wie folgt ein:

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

Sobald der innere sp ausgeführt wird, können Sie ihn @cOUTabrufen. Schleife und dann schließen und freigeben.

Stefanos Zilellis
quelle
0

Wenn Sie andere zugehörige Technologien wie C # verwenden können, empfehle ich die Verwendung des integrierten SQL-Befehls mit dem Transaktionsparameter.

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

Ich habe eine einfache Konsolen-App erstellt, die diese Fähigkeit demonstriert. Sie finden sie hier: https://github.com/hecked12/SQL-Transaction-Using-C-Sharp

Kurz gesagt, mit C # können Sie diese Einschränkung überwinden, indem Sie die Ausgabe jeder gespeicherten Prozedur überprüfen und diese Ausgabe verwenden können, wie Sie möchten. Sie können sie beispielsweise einer anderen gespeicherten Prozedur zuführen. Wenn die Ausgabe in Ordnung ist, können Sie die Transaktion festschreiben. Andernfalls können Sie die Änderungen mithilfe des Rollbacks zurücksetzen.

spidernet12
quelle
-1

Unter SQL Server 2008 R2 hatte ich eine Nichtübereinstimmung in Tabellenspalten, die den Rollback-Fehler verursachte. Es verschwand, als ich meine von der insert-exec-Anweisung aufgefüllte sqlcmd-Tabellenvariable so korrigierte, dass sie mit der vom gespeicherten Prozess zurückgegebenen übereinstimmt. Es fehlte org_code. In einer Windows-Cmd-Datei wird das Ergebnis der gespeicherten Prozedur geladen und ausgewählt.

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
quelle
OP fragte nach einem Fehler, der bei der Verwendung von insert-exec-Anweisungen in verschachtelten gespeicherten Prozeduren auftritt. Ihr Problem würde einen anderen Fehler zurückgeben, z. B. "Die Auswahlliste für die INSERT-Anweisung enthält weniger Elemente als die Einfügeliste. Die Anzahl der SELECT-Werte muss mit der Anzahl der INSERT-Spalten übereinstimmen."
Losbear
Dies ist eher eine Warnung, dass es möglich ist, diese Nachricht fälschlicherweise zu erhalten.
user3448451