Ошибки: «Оператор INSERT EXEC не может быть вложенным». и «Невозможно использовать оператор ROLLBACK в операторе INSERT-EXEC». Как это решить?

98

У меня есть три хранимых процедуры Sp1, Sp2и Sp3.

Первый ( Sp1) выполнит второй ( Sp2) и сохранит возвращенные данные, @tempTB1а второй выполнит третий ( Sp3) и сохранит данные в @tempTB2.

Если я выполню, Sp2он будет работать, и он вернет мне все мои данные из Sp3, но проблема в том Sp1, что когда я его выполню, он отобразит эту ошибку:

Оператор INSERT EXEC не может быть вложенным

Я попытался изменить место, execute Sp2и он показал мне еще одну ошибку:

Невозможно использовать инструкцию ROLLBACK в инструкции INSERT-EXEC.

HAJJAJ
источник

Ответы:

102

Это обычная проблема при попытке «всплыть» данными из цепочки хранимых процедур. Ограничение в SQL Server состоит в том, что вы можете иметь активным только один INSERT-EXEC одновременно. Я рекомендую посмотреть Как обмениваться данными между хранимыми процедурами, которая представляет собой очень подробную статью о шаблонах, позволяющих обойти этот тип проблемы.

Например, можно было бы превратить Sp3 в функцию с табличным значением.

Eddiegroves
источник
1
неработающая ссылка ИЛИ не отвечающий сайт.
SouravA 08
6
Вы хоть представляете, какова техническая причина отказа? Я не могу найти никакой информации по этому поводу.
jtate
1
К сожалению, это очень часто не вариант. Многие типы важной информации надежно доступны только из системных хранимых процедур (потому что в определенных случаях соответствующее представление управления содержит ненадежные / устаревшие данные; примером является информация, возвращаемая функцией sp_help_jobactivity).
GSerg
21

Это единственный «простой» способ сделать это в SQL Server без какой-либо гигантской запутанной созданной функции или выполненного вызова строки sql, оба из которых являются ужасными решениями:

  1. создать временную таблицу
  2. openrowset ваши данные хранимой процедуры в нем

ПРИМЕР:

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

Примечание : вы ДОЛЖНЫ использовать 'set fmtonly off', И вы НЕ МОЖЕТЕ добавить к нему динамический sql ни внутри вызова openrowset, ни для строки, содержащей параметры вашей хранимой процедуры, ни для имени таблицы. Вот почему вам нужно использовать временную таблицу, а не табличные переменные, что было бы лучше, поскольку в большинстве случаев она выполняет временную таблицу.

Митч Стокли
источник
Не обязательно использовать SET FMTONLY OFF. Вы можете просто добавить IF (1 = 0), который возвращает пустую таблицу с теми же типами данных, которые обычно возвращает процедура.
Гильермо Гутьеррес
1
Временные таблицы и переменные таблицы хранят свои данные по-разному. Табличные переменные должны использоваться для небольших наборов результатов, поскольку оптимизатор запросов не поддерживает статистику по табличным переменным. Поэтому для больших наборов данных почти всегда лучше использовать временные таблицы. Вот хорошая статья об этом в блоге mssqltips.com/sqlservertip/2825/…
gh9
@ gh9 да, но в любом случае это ужасная идея для больших наборов результатов. Статистика и использование реальной таблицы во временной базе данных может вызвать значительные накладные расходы. У меня есть процедура, которая возвращает набор записей с 1 строкой текущих значений (запрашивает несколько таблиц) и процедура, которая сохраняет это в переменной таблицы и сравнивает ее со значениями в другой таблице с тем же форматом. Переход от временной таблицы к табличной переменной ускорил среднее время с 8 мс до 2 мс, что важно, когда она вызывается несколько раз в секунду в течение дня и 100 000 раз в ночное время.
Джейсон Гоэмаат
Зачем вам нужно вести статистику по табличной переменной? Все дело в том, чтобы создать временную таблицу в ОЗУ, которая будет уничтожена после завершения запроса. По определению, любая статистика, созданная для такой таблицы, никогда не будет использоваться. Как правило, тот факт, что данные в табличной переменной остаются в ОЗУ везде, где это возможно, делает их быстрее, чем временные таблицы, в любом сценарии, где ваши данные меньше, чем объем ОЗУ, доступный для SQL Server (который в наши дни пулов памяти более 100 ГБ для нашего SQL Серверы, почти всегда)
Джефф Грисвальд
Однако это не работает для расширенных хранимых процедур. Ошибка: метаданные не могут быть определены, потому что оператор EXECUTE <procedurename> @retval OUTPUT в процедуре ... вызывает расширенную хранимую процедуру .
GSerg
11

Хорошо, поощряемый jimhark, вот пример старого подхода с использованием единой хеш-таблицы:

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

*/
Мэтт Лакхэм
источник
Я тоже использовал этот обходной путь. Спасибо за идею!
SQL_Guy
Фантастический обходной путь. Это помогло мне узнать больше об области видимости временной таблицы. Например, я не понимал, что вы можете использовать временную таблицу в строке dynsql, если она была объявлена ​​вне ее. Аналогичная концепция здесь. Большое спасибо.
jbd
9

Моя работа для решения этой проблемы всегда заключалась в использовании принципа, согласно которому отдельные временные хеш-таблицы входят в область действия любых вызываемых процессов. Итак, у меня есть переключатель опций в параметрах процесса (по умолчанию выключено). Если это включено, вызываемая процедура вставляет результаты во временную таблицу, созданную в вызывающей процедуре. Я думаю, что в прошлом я сделал еще один шаг и поместил некоторый код в вызываемую процедуру, чтобы проверить, существует ли единая хеш-таблица в области видимости, если она есть, затем вставьте код, иначе верните набор результатов. Кажется, работает хорошо - лучший способ передачи больших наборов данных между процессами.

Мэтт Лакхэм
источник
1
Мне нравится этот ответ, и я готов поспорить, что вы получите больше голосов, если предоставите и покажете пример.
jimhark
Я занимаюсь этим годами. Но нужно ли это в SQL Azure?
Ник Аллан
6

У меня этот трюк работает.

У вас нет этой проблемы на удаленном сервере, потому что на удаленном сервере последняя команда вставки ожидает выполнения результата предыдущей команды. На одном сервере это не так.

Воспользуйтесь этой ситуацией для обходного пути.

Если у вас есть право на создание связанного сервера, сделайте это. Создайте тот же сервер, что и связанный сервер.

  • в SSMS войдите на свой сервер
  • перейдите в "Объект сервера"
  • Щелкните правой кнопкой мыши «Связанные серверы», затем «Новый связанный сервер».
  • в диалоговом окне укажите любое имя связанного сервера: например: ЭТОТ СЕРВЕР
  • тип сервера - «Другой источник данных»
  • Поставщик: поставщик Microsoft OLE DB для SQL-сервера
  • Источник данных: ваш IP-адрес, также может быть точка (.), Потому что это localhost
  • Перейдите на вкладку «Безопасность» и выберите третью «Сделать с использованием текущего контекста безопасности входа».
  • Вы можете редактировать параметры сервера (3-я вкладка), если хотите
  • Нажмите ОК, ваш связанный сервер создан

теперь ваша команда Sql в SP1

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

Поверьте, работает даже у вас есть динамическая вставка в SP2

Ainasiart
источник
4

Я нашел решение - преобразовать одно из запросов в функцию с табличным значением. Я понимаю, что это не всегда возможно, и вводит свои ограничения. Однако мне всегда удавалось найти хотя бы одну из процедур, подходящую для этого. Мне нравится это решение, потому что оно не вносит никаких "взломов" в решение.

Роман К
источник
но недостатком является проблема с обработкой исключений, если функция сложная, верно?
Muflix
2

Я столкнулся с этой проблемой при попытке импортировать результаты хранимой процедуры во временную таблицу, и что хранимая процедура вставлена ​​во временную таблицу как часть своей собственной операции. Проблема в том, что SQL Server не позволяет одному и тому же процессу одновременно записывать в две разные временные таблицы.

Принятый ответ OPENROWSET работает нормально, но мне нужно было избегать использования динамического SQL или внешнего поставщика OLE в моем процессе, поэтому я пошел другим путем.

Я нашел один простой обходной путь - заменить временную таблицу в моей хранимой процедуре табличной переменной. Он работает точно так же, как и с временной таблицей, но больше не конфликтует с моей другой вставкой временной таблицы.

Просто чтобы избежать комментария, я знаю, что некоторые из вас собираются написать, предупреждая меня об использовании табличных переменных как убийц производительности ... Все, что я могу вам сказать, это то, что в 2020 году не бояться табличных переменных выгодно. Если бы это был 2008 год, и моя база данных размещалась на сервере с 16 ГБ ОЗУ и с жесткими дисками 5400 об / мин, я мог бы согласиться с вами. Но сейчас 2020 год, и у меня есть SSD-массив в качестве основного хранилища и сотни гигабайт оперативной памяти. Я мог бы загрузить всю базу данных моей компании в табличную переменную и при этом иметь достаточно свободной оперативной памяти.

Табличные переменные снова в меню!

Джефф Грисвальд
источник
1

У меня была такая же проблема и беспокойство по поводу дублирования кода в двух или более sprocs. В итоге я добавил дополнительный атрибут для «режима». Это позволяло существовать общему коду внутри одного sproc и потока, направленного в режим, и набора результатов sproc.

PhoenixAZ
источник
1

как насчет того, чтобы просто сохранить вывод в статической таблице? подобно

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

это не идеально, но это так просто, и вам не нужно все переписывать.

ОБНОВЛЕНИЕ : предыдущее решение не работает с параллельными запросами (асинхронный и многопользовательский доступ), поэтому теперь я использую временные таблицы

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

spGetDataсодержимое вложенных хранимых процедур

-- 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
Муфликс
источник
Как правило, вы не можете создать специализированный SProc, как это можно сделать с помощью таблиц. Вам нужно будет расширить свой пример дополнительными ссылками, так как этот подход не так легко известен или принят. Кроме того, он больше напоминает лямбда-выражение, чем выполнение SProc, которое ANSI-SQL не допускает для подходов с лямбда-выражением.
GoldBishop
Он работает, но я обнаружил, что он плохо работает с параллельными запросами (асинхронным и многопользовательским доступом). Поэтому теперь я использую подход с временными таблицами. Я обновил свой ответ.
Muflix
1
Логика таблицы Temp хороша, меня интересовала ссылка на SProc. По сути, Sproc не может быть запрошен напрямую. Из табличных функций можно напрямую запрашивать. Должно быть, как вы упомянули в обновленной логике, лучший подход - это временная таблица, сеанс, экземпляр или глобальный, и работать с этой точки.
GoldBishop
0

Объявите переменную курсора вывода для внутреннего sp:

@c CURSOR VARYING OUTPUT

Затем объявите курсор c для выбора, который вы хотите вернуть. Затем откройте курсор. Затем установите ссылку:

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

НЕ закрывайте и не перераспределяйте.

Теперь вызовите внутренний sp из внешнего, задав параметр курсора, например:

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

Как только внутренний sp выполняется, ваш @cOUTготов к загрузке. Зациклить, а затем закрыть и освободить.

Стефанос Зилеллис
источник
0

Если вы можете использовать другие связанные технологии, такие как C #, я предлагаю использовать встроенную команду SQL с параметром Transaction.

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

Я создал простое консольное приложение, демонстрирующее эту способность, которое можно найти здесь: https://github.com/hecked12/SQL-Transaction-Using-C-Sharp

Короче говоря, C # позволяет преодолеть это ограничение, когда вы можете проверять вывод каждой хранимой процедуры и использовать этот вывод, как вам нравится, например, вы можете передать его другой хранимой процедуре. Если результат в порядке, вы можете зафиксировать транзакцию, в противном случае вы можете отменить изменения с помощью отката.

паутина12
источник
-1

В SQL Server 2008 R2 у меня было несоответствие в столбцах таблицы, которое вызывало ошибку отката. Это исчезло, когда я исправил мою табличную переменную sqlcmd, заполненную оператором insert-exec, чтобы она соответствовала той, которая возвращалась сохраненной процедурой. Отсутствовал org_code. В файле cmd Windows он загружает результат хранимой процедуры и выбирает его.

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"
пользователь3448451
источник
OP спрашивал об ошибке, которая возникает при использовании операторов insert-exec во вложенных хранимых процедурах. Ваша проблема вернет другую ошибку, например «Список выбора для оператора INSERT содержит меньше элементов, чем список вставки. Количество значений SELECT должно соответствовать количеству столбцов INSERT».
Losbear
Это скорее предупреждение, что это сообщение может быть получено ошибочно.
user3448451 09