Erros: “A instrução INSERT EXEC não pode ser aninhada.” e “Não é possível usar a instrução ROLLBACK em uma instrução INSERT-EXEC.” Como resolver isso?

98

Eu tenho três procedimentos armazenados Sp1, Sp2e Sp3.

O primeiro ( Sp1) executará o segundo ( Sp2) e salvará os dados retornados no @tempTB1e o segundo executará o terceiro ( Sp3) e salvará os dados no @tempTB2.

Se eu executar o Sp2ele funcionará e ele retornará todos os meus dados do Sp3, mas o problema está no Sp1, quando eu executar ele exibirá este erro:

A instrução INSERT EXEC não pode ser aninhada

Tentei mudar o local de execute Sp2e ele me mostrou outro erro:

Não é possível usar a instrução ROLLBACK em uma instrução INSERT-EXEC.

HAJJAJ
fonte

Respostas:

101

Este é um problema comum ao tentar fazer uma "bolha" de dados de uma cadeia de procedimentos armazenados. Uma restrição no SQL Server é que você só pode ter um INSERT-EXEC ativo por vez. Recomendo consultar Como compartilhar dados entre procedimentos armazenados, que é um artigo muito completo sobre padrões para contornar esse tipo de problema.

Por exemplo, uma solução alternativa poderia ser transformar o Sp3 em uma função com valor de tabela.

Eddiegroves
fonte
1
link quebrado OU site que não responde.
SouravA
6
tem ideia de qual é o motivo técnico para não permitir? Não consigo encontrar nenhuma informação sobre isso.
jtate de
1
Infelizmente, isso muitas vezes não é uma opção. Muitos tipos de informações importantes só estão disponíveis de forma confiável a partir de procedimentos armazenados do sistema (porque em certos casos a respectiva exibição de gerenciamento contém dados não confiáveis ​​/ obsoletos; um exemplo são as informações retornadas por sp_help_jobactivity).
GSerg
21

Esta é a única maneira "simples" de fazer isso no SQL Server sem alguma função criada complicada gigante ou chamada de string sql executada, sendo que ambas são soluções terríveis:

  1. criar uma mesa temporária
  2. openrowset seus dados de procedimento armazenados nele

EXEMPLO:

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 : Você DEVE usar 'set fmtonly off' E NÃO PODE adicionar sql dinâmico a ele dentro da chamada openrowset, seja para a string contendo seus parâmetros de procedimento armazenado ou para o nome da tabela. É por isso que você deve usar uma tabela temporária em vez de variáveis ​​de tabela, o que teria sido melhor, já que executa uma tabela temporária na maioria dos casos.

Mitch Stokely
fonte
Não é obrigatório usar SET FMTONLY OFF. Você pode apenas adicionar um IF (1 = 0) que retorna uma tabela vazia com os mesmos tipos de dados que o procedimento normalmente retorna.
Guillermo Gutiérrez
1
As tabelas temporárias e as variáveis ​​da tabela armazenam seus dados de maneira diferente. As variáveis ​​da tabela devem ser usadas para pequenos conjuntos de resultados, pois o otimizador de consulta não mantém estatísticas sobre as variáveis ​​da tabela. Portanto, para grandes conjuntos de dados, quase sempre é melhor usar tabelas temporárias. Aqui está um bom artigo de blog sobre isso mssqltips.com/sqlservertip/2825/…
gh9
@ gh9 sim, mas essa é uma ideia horrível para grandes conjuntos de resultados. As estatísticas e o uso de uma tabela real no banco de dados temporário podem causar sobrecarga significativa. Eu tenho um procedimento que retorna um conjunto de registros com 1 linha de valores atuais (consultando várias tabelas) e um procedimento que armazena isso em uma variável de tabela e compara com valores em outra tabela com o mesmo formato. Mudar de uma tabela temporária para uma variável de tabela acelerou o tempo médio de 8 ms para 2 ms, o que é importante quando é chamado várias vezes por segundo ao longo do dia e 100.000 vezes em um processo noturno.
Jason Goemaat
Por que você deseja que as estatísticas sejam mantidas em uma variável de tabela? O objetivo é criar uma tabela temporária na RAM que será destruída após o término da consulta. Por definição, quaisquer estatísticas criadas em tal tabela nunca seriam usadas. Geralmente, o fato de os dados em uma variável de tabela permanecerem na RAM sempre que possível os torna mais rápidos do que as Tabelas temporárias em qualquer cenário onde seus dados sejam menores do que a quantidade de RAM disponível para o SQL Server (que atualmente é de 100 GB + pools de memória para nosso SQL Servidores, quase sempre)
Geoff Griswald
Porém, isso não funciona para procedimentos armazenados estendidos. O erro é Os metadados não puderam ser determinados porque a instrução 'EXECUTE <procedurename> @retval OUTPUT' in procedure ... 'chama um procedimento armazenado estendido .
GSerg
11

OK, encorajado por Jimhark, aqui está um exemplo da antiga abordagem de tabela 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
fonte
Eu usei essa solução alternativa também. Obrigado pela idéia!
SQL_Guy
Solução fantástica. Isso me ajudou a aprender mais sobre o escopo da tabela temporária. EG, eu não sabia que você pode usar uma tabela temporária em uma string dynsql se ela foi declarada fora dela. Conceito semelhante aqui. Muito obrigado.
jbd
9

Minha solução para esse problema sempre foi usar o princípio de que tabelas temporárias de hash simples estão no escopo de qualquer procs chamada. Então, eu tenho um switch de opção nos parâmetros proc (padrão definido como off). Se estiver ativado, o proc chamado irá inserir os resultados na tabela temporária criada no proc de chamada. Acho que no passado eu dei um passo adiante e coloquei algum código no proc chamado para verificar se a tabela hash única existe no escopo, se existir, então insira o código, caso contrário, retorne o conjunto de resultados. Parece funcionar bem - a melhor maneira de passar grandes conjuntos de dados entre procs.

Matt Luckham
fonte
1
Eu gosto dessa resposta e aposto que você obterá mais votos se der um exemplo.
Jimhark
Venho fazendo isso há anos. Ainda é necessário no SQL Azure?
Nick Allan
6

Este truque funciona para mim.

Você não tem esse problema no servidor remoto, porque no servidor remoto, o último comando de inserção aguarda o resultado do comando anterior para ser executado. Não é o caso no mesmo servidor.

Aproveite essa situação para uma solução alternativa.

Se você tiver a permissão certa para criar um servidor vinculado, faça isso. Crie o mesmo servidor como servidor vinculado.

  • no SSMS, faça login no seu servidor
  • vá para "Objeto de servidor
  • Clique com o botão direito em "Servidores Vinculados" e em "Novo Servidor Vinculado"
  • na caixa de diálogo, forneça qualquer nome de seu servidor vinculado: por exemplo: THISSERVER
  • o tipo de servidor é "Outra fonte de dados"
  • Provedor: Provedor Microsoft OLE DB para servidor SQL
  • Fonte de dados: seu IP, também pode ser apenas um ponto (.), Pois é localhost
  • Vá até a aba "Segurança" e escolha a 3ª "Ser feito utilizando o contexto de segurança atual do login"
  • Você pode editar as opções do servidor (3ª guia) se quiser
  • Pressione OK, seu servidor vinculado é criado

agora seu comando Sql no SP1 é

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

Acredite em mim, funciona mesmo que você tenha um insert dinâmico no SP2

ainasiart
fonte
4

Eu descobri que uma solução alternativa é converter um dos prods em uma função com valor de tabela. Percebo que nem sempre é possível e apresenta suas próprias limitações. No entanto, sempre consegui encontrar pelo menos um dos procedimentos um bom candidato para isso. Gosto desta solução porque não introduz nenhum "hacks" na solução.

Roman K
fonte
mas uma desvantagem é um problema com o tratamento de exceções se a função for complexa, certo?
Muflix
2

Eu encontrei esse problema ao tentar importar os resultados de um Stored Proc em uma tabela temporária e que Stored Proc inseriu em uma tabela temporária como parte de sua própria operação. O problema é que o SQL Server não permite que o mesmo processo grave em duas tabelas temporárias diferentes ao mesmo tempo.

A resposta aceita do OPENROWSET funciona bem, mas eu precisava evitar o uso de SQL dinâmico ou um provedor OLE externo em meu processo, então fui por um caminho diferente.

Uma solução alternativa fácil que descobri foi alterar a tabela temporária em meu procedimento armazenado para uma variável de tabela. Ele funciona exatamente da mesma forma que funcionava com uma mesa temporária, mas não entra mais em conflito com minha outra inserção de tabela temporária.

Só para evitar o comentário, sei que alguns de vocês estão prestes a escrever, avisando-me que as Variáveis ​​de Tabela são destruidoras de desempenho ... Tudo o que posso dizer a vocês é que em 2020 vale a pena não ter medo das Variáveis ​​de Tabela. Se estivéssemos em 2008 e meu banco de dados estivesse hospedado em um servidor com 16 GB de RAM e HDDs de 5400 RPM, talvez eu concorde com você. Mas é 2020 e tenho uma matriz SSD como meu armazenamento principal e centenas de GB de RAM. Eu poderia carregar o banco de dados de toda a minha empresa em uma variável de tabela e ainda ter bastante RAM de sobra.

As variáveis ​​de tabela estão de volta ao menu!

Geoff Griswald
fonte
1

Eu tive o mesmo problema e preocupação com código duplicado em dois ou mais sprocs. Acabei adicionando um atributo adicional para "modo". Isso permitiu a existência de código comum dentro de um sproc e do fluxo direcionado de modo e conjunto de resultados do sproc.

PhoenixAZ
fonte
1

que tal apenas armazenar a saída na tabela estática? Gostar

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

não é o ideal, mas é tão simples e você não precisa reescrever tudo.

ATUALIZAÇÃO : a solução anterior não funciona bem com consultas paralelas (acesso assíncrono e multiusuário), portanto agora estou usando tabelas temporárias

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

spGetDataconteúdo de procedimento armazenado aninhado

-- 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
fonte
Geralmente, você não pode criar um SProc ad-hoc como faz com as tabelas. Você precisará expandir seu exemplo com mais referências, pois essa abordagem não é realmente conhecida ou aceita. Além disso, ele se assemelha mais a uma Expressão Lambda do que a uma execução SProc, que ANSI-SQL não permite para abordagens de Expressão Lambda.
GoldBishop
Funciona, mas descobri que também não funciona bem com consultas paralelas (acessos assíncronos e multiusuário). Portanto, agora estou usando a abordagem de tabela temporária. Eu atualizei minha resposta.
Muflix de
1
A lógica da tabela Temp é boa, era a referência SProc que me preocupava. Os Sprocs não podem ser consultados diretamente de forma inerente. Funções com valor de tabela podem ser consultadas diretamente de. Deve como você aludiu em sua lógica atualizada, a melhor abordagem é uma Tabela Temporária, sessão, instância ou global e operar a partir desse ponto.
GoldBishop
0

Declare uma variável de cursor de saída para o sp interno:

@c CURSOR VARYING OUTPUT

Em seguida, declare um cursor c para a seleção que deseja retornar. Em seguida, abra o cursor. Em seguida, defina a referência:

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

NÃO feche ou realoque.

Agora chame o sp interno do externo fornecendo um parâmetro de cursor como:

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

Depois que o sp interno for executado, você @cOUTestará pronto para buscar. Faça um loop, feche e desaloque.

Stefanos Zilellis
fonte
0

Se você puder usar outras tecnologias associadas, como C #, sugiro usar o comando SQL integrado com o parâmetro Transaction.

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

Eu criei um aplicativo de console simples que demonstra essa capacidade que pode ser encontrado aqui: https://github.com/hecked12/SQL-Transaction-Using-C-Sharp

Resumindo, C # permite que você supere essa limitação onde você pode inspecionar a saída de cada procedimento armazenado e usar essa saída como quiser, por exemplo, você pode alimentá-la para outro procedimento armazenado. Se a saída estiver ok, você pode confirmar a transação, caso contrário, você pode reverter as alterações usando rollback.

spidernet12
fonte
-1

No SQL Server 2008 R2, tive uma incompatibilidade nas colunas da tabela que causou o erro de reversão. Ele foi embora quando eu corrigi minha variável de tabela sqlcmd preenchida pela instrução insert-exec para corresponder àquela retornada pelo proc armazenado. Estava faltando org_code. Em um arquivo cmd do Windows, ele carrega o resultado do procedimento armazenado e o seleciona.

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
fonte
OP estava perguntando sobre um erro que ocorre ao usar instruções insert-exec em procedimentos armazenados aninhados. Seu problema retornaria um erro diferente, como "A lista de seleção para a instrução INSERT contém menos itens do que a lista de inserção. O número de valores SELECT deve corresponder ao número de colunas INSERT."
Losbear
Este é mais um aviso de que é possível receber esta mensagem erroneamente.
user3448451