Как получить ответ от хранимой процедуры до ее завершения?

8

Мне нужно вернуть частичный результат (как простой выбор) из хранимой процедуры, прежде чем она будет завершена.

Возможно ли это сделать?

Если да, то как это сделать?

Если нет, то какой обходной путь?

РЕДАКТИРОВАТЬ: У меня есть несколько частей процедуры. В первой части я вычисляю несколько строк. Я использую их позже в процедуре для выполнения дополнительных операций. Проблема в том, что строка нужна вызывающей стороне как можно скорее. Поэтому мне нужно вычислить эту строку и передать ее обратно (каким-то образом из выбора, например), а затем продолжить работу. Звонящий получает свою ценную строку гораздо быстрее.

Абонент - это веб-сервис.

Богдан Богданов
источник
Предполагая, что полная блокировка таблицы не произошла или явная транзакция не была объявлена, вы сможете запустить SELECT в отдельном сеансе без проблем.
Стив Мангиамели
В общем, это единственный способ увидеть это сейчас, но я не думаю, что это будет намного быстрее (есть и другие проблемы), @SteveMangiameli
Богдан Богданов
Разделить это на два SP? Передайте вывод от первого ко второму.
папараццо
Не очень быстрое решение, поэтому мы от него отказались, @Paparazzi
Богдан Богданов

Ответы:

11

Вы, вероятно, ищете RAISERRORкоманду с NOWAITопцией.

По замечаниям :

RAISERROR может использоваться в качестве альтернативы PRINT для возврата сообщений вызывающим приложениям.

Это не возвращает результаты из SELECTоператора, но позволит вам передавать сообщения / строки обратно клиенту. Если вы хотите быстро вернуть подмножество данных, которые вы выбираете, вы можете рассмотреть FASTподсказку запроса.

Указывает, что запрос оптимизирован для быстрого поиска первых number_rows. Это неотрицательное целое число. После возвращения первых number_rows запрос продолжает выполнение и выдает полный набор результатов.

Добавил Шеннон Северанс в комментарии:

Из Обработки ошибок и транзакций в SQL Server Эрланда Соммарского:

Имейте в виду, что некоторые API и инструменты могут накапливаться на их стороне, тем самым сводя на нет эффект WITH NOWAIT.

Смотрите исходную статью для полного контекста.

Erik
источник
FASTЯ решил эту проблему для меня, когда мне нужно было синхронизировать выполнение хранимой процедуры и кода C #, чтобы усугубить и воспроизвести состояние гонки. Проще потреблять наборы результатов программно, чем использовать что-то вроде RAISERROR(). Когда я начал читать ваш ответ, мне показалось, что вы говорили, что это невозможно сделать SELECT, так что, возможно, это можно уточнить?
Бинки
5

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

Если ваш абонент взаимодействует с базой данных асинхронно или является многопоточным или многопоточным, так что вы можете открыть второй сеанс, пока первый еще выполняется, вы можете создать таблицу для хранения частичных данных и обновить их по мере выполнения процедуры. Затем это может быть прочитано вторым сеансом с установленным уровнем изоляции транзакции 1, чтобы позволить ему читать незафиксированные изменения:

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT * FROM progress_table

1: согласно комментариям и последующему обновлению в ответе srutzky, установка уровня изоляции не требуется, если отслеживаемый процесс не заключен в транзакцию, хотя я склонен устанавливать его по привычке в таких обстоятельствах, поскольку это не вызывает вред, когда не требуется в этих случаях

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

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT * FROM progress_table WHERE process = <current_process_uuid>

Я использовал этот метод для мониторинга длительных ручных процессов в SSMS. Я не могу решить, пахнет ли он слишком сильно для меня, чтобы рассмотреть его использование в производстве ...

Дэвид Спиллетт
источник
1
Это один из вариантов, но мне не нравится в данный момент. Я надеюсь, что появятся другие варианты.
Богдан Богданов
5

OP уже попытался отправить несколько наборов результатов (не MARS) и увидел, что он действительно ожидает завершения хранимой процедуры, прежде чем возвращать какие-либо наборы результатов. Учитывая эту ситуацию, вот несколько вариантов:

  1. Если ваши данные достаточно малы, чтобы поместиться в пределах 128 байт, вы, скорее всего, можете использовать SET CONTEXT_INFOих, чтобы сделать это значение видимым через SELECT [context_info] FROM [sys].[dm_exec_requests] WHERE [session_id] = @SessionID;. Вам просто нужно выполнить быстрый запрос перед запуском хранимой процедуры SELECT @@SPID;и получить его через SqlCommand.ExecuteScalar.

    Я только что проверил это, и это работает.

  2. Аналогично предложению @ David для помещения данных в таблицу «прогресса», но без необходимости возиться с проблемами очистки или параллелизма / разделения процессов:

    1. Создайте новый Guidкод приложения и передайте его в качестве параметра хранимой процедуре. Сохраните этот Guid в переменной, так как он будет использоваться несколько раз.
    2. В хранимой процедуре создайте глобальную временную таблицу, используя этот Guid как часть имени таблицы, что-то вроде CREATE TABLE ##MyProcess_{GuidFromApp};. Таблица может иметь любые столбцы с любыми типами данных, которые вам нужны.
    3. Когда у вас есть данные, вставьте их в эту глобальную временную таблицу.

    4. В коде приложения, начинают пытаться читать данные, но обернуть SELECTв IF EXISTSтак что не будет ошибкой , если таблица не была создана еще:

      IF (OBJECT_ID('tempdb..[##MyProcess_{0}]')
          IS NOT NULL)
      BEGIN
        SELECT * FROM [##MyProcess_{0}];
      END;

    С String.Format(), вы можете заменить {0}на значение в переменной Guid. Проверьте, если Reader.HasRowsи если true, то прочитайте результаты, иначе позвоните Thread.Sleep()или как угодно, чтобы потом опросить снова.

    Льготы:

    • Эта таблица изолирована от других процессов, поскольку только код приложения знает конкретное значение Guid, поэтому не нужно беспокоиться о других процессах. Другой процесс будет иметь свою собственную глобальную временную таблицу.
    • Поскольку это таблица, все строго напечатано.
    • Поскольку это временная таблица, по завершении сеанса, выполняющего хранимую процедуру, таблица будет очищена автоматически.
    • Потому что это глобальная временная таблица:
      • это доступно другим Сессиям, точно так же как постоянный стол
      • он переживет окончание подпроцесса, в котором он создан (т.е. вызов EXEC/ sp_executesql)


    Я проверил это, и он работает как ожидалось. Вы можете попробовать это сами с помощью следующего примера кода.

    На одной вкладке запроса выполните следующее, а затем выделите 3 строки в блок-комментарии и выполните это:

    CREATE
    --ALTER
    PROCEDURE #GetSomeInfoBackQuickly
    (
      @MessageTableName NVARCHAR(50) -- might not always be a GUID
    )
    AS
    SET NOCOUNT ON;
    
    DECLARE @SQL NVARCHAR(MAX) = N'CREATE TABLE [##MyProcess_' + @MessageTableName
                 + N'] (Message1 NVARCHAR(50), Message2 NVARCHAR(50), SomeNumber INT);';
    
    -- Do some calculations
    
    EXEC (@SQL);
    
    SET @SQL = N'INSERT INTO [##MyProcess_' + @MessageTableName
    + N'] (Message1, Message2, SomeNumber) VALUES (@Msg1, @Msg2, @SomeNum);';
    
    DECLARE @SomeNumber INT = CRYPT_GEN_RANDOM(2);
    
    EXEC sp_executesql
        @SQL,
        N'@Msg1 NVARCHAR(50), @Msg2 NVARCHAR(50), @SomeNum INT',
        @Msg1 = N'wow',
        @Msg2 = N'yadda yadda yadda',
        @SomeNum = @SomeNumber;
    
    WAITFOR DELAY '00:00:10.000';
    
    SET @SomeNumber = CRYPT_GEN_RANDOM(3);
    EXEC sp_executesql
        @SQL,
        N'@Msg1 NVARCHAR(50), @Msg2 NVARCHAR(50), @SomeNum INT',
        @Msg1 = N'wow',
        @Msg2 = N'yadda yadda yadda',
        @SomeNum = @SomeNumber;
    
    WAITFOR DELAY '00:00:10.000';
    GO
    /*
    DECLARE @TempTableID NVARCHAR(50) = NEWID();
    RAISERROR('%s', 10, 1, @TempTableID) WITH NOWAIT;
    
    EXEC #GetSomeInfoBackQuickly @TempTableID;
    */

    Перейдите на вкладку «Сообщения» и скопируйте напечатанный GUID. Затем откройте другую вкладку запроса и выполните следующую команду, поместив GUID, скопированный с вкладки «Сообщения другого сеанса», в инициализацию переменной в строке 1:

    DECLARE @TempTableID NVARCHAR(50) = N'GUID-from-other-session';
    
    EXEC (N'SELECT * FROM [##MyProcess_' + @TempTableID + N']');

    Продолжай бить F5. Вы должны увидеть 1 запись в течение первых 10 секунд, а затем 2 записи в течение следующих 10 секунд.

  3. Вы можете использовать SQLCLR для обратного вызова вашего приложения через веб-сервис или каким-либо другим способом.

  4. Вы могли бы , возможно , использовать PRINT/ RAISERROR(..., 1, 10) WITH NOWAITпередать строки обратно сразу, но это будет немного сложнее из - за следующие проблемы:

    • «Сообщение» выход ограничен либо VARCHAR(8000)илиNVARCHAR(4000)
    • Сообщения не отправляются так же, как результаты. Чтобы захватить их, вам нужно настроить обработчик событий. В этом случае вы можете создать переменную в виде статической коллекции, чтобы получать сообщения, которые будут доступны всем частям кода. Или, может быть, каким-то другим способом. У меня есть один или два примера в других ответах, показывающих, как перехватывать сообщения, и я буду ссылаться на них позже, когда найду их.
    • Сообщения по умолчанию также не отправляются, пока процесс не завершится. Однако это поведение можно изменить, установив для свойства SqlConnection.FireInfoMessageEventOnUserErrors значение true. В документации говорится:

      Когда вы устанавливаете FireInfoMessageEventOnUserErrors в true , ошибки, которые ранее рассматривались как исключения, теперь обрабатываются как события InfoMessage. Все события запускаются немедленно и обрабатываются обработчиком событий. Если для параметра FireInfoMessageEventOnUserErrors установлено значение false, события InfoMessage обрабатываются в конце процедуры.

      Недостатком здесь является то, что большинство ошибок SQL больше не будет вызывать SqlException. В этом случае вам необходимо протестировать дополнительные свойства событий, которые передаются в обработчик событий сообщений. Это справедливо для всего соединения, что делает вещи немного сложнее, но не управляемыми.

    • Все сообщения отображаются на одном уровне без отдельного поля или свойства, чтобы отличить одно от другого. Порядок, в котором они получены, должен совпадать с порядком их отправки, но не уверен, достаточно ли это надежно. Возможно, вам понадобится добавить тег или что-то, что вы сможете затем проанализировать. Таким образом, вы, по крайней мере, можете быть уверены, какой из них есть какой.

Соломон Руцкий
источник
2
Я пытаюсь это После вычисления строки я возвращаю ее как простой выбор и продолжаю процедуру. Проблема в том, что он возвращает все наборы одновременно (я полагаю, после RETURNутверждения). Так что это не работает.
Богдан Богданов
2
@BogdanBogdanov Вы используете .NET и SqlConnection? Сколько данных вы хотите передать обратно? Какие типы данных? Вы пробовали PRINTили RAISERROR WITH NOWAIT?
Соломон Руцкий
Сейчас попробую. Мы используем .NET Web Service.
Богдан Богданов
«Поскольку это глобальная временная таблица, вам не нужно беспокоиться об уровнях изоляции транзакций» - это действительно правильно? На временные таблицы IIRC, даже глобальные, должны распространяться те же ограничения ACID, что и на любую другую таблицу. Не могли бы вы подробно рассказать, как вы тестировали поведение?
Дэвид Спиллетт
@DavidSpillett Теперь, когда я думаю об этом, уровень изоляции действительно не является проблемой, и то же самое относится и к вашему предложению. Пока таблица не создана внутри транзакции. Я только что обновил свой ответ кодом примера.
Соломон Руцкий
0

Если ваша хранимая процедура должна выполняться в фоновом режиме (то есть асинхронно), вам следует использовать Service Broker. Это немного затрудняет настройку, но как только вы это сделаете, вы сможете запустить хранимую процедуру (неблокирующую) и прослушивать сообщения о ходе выполнения столько времени, сколько захотите.

саржа
источник