Como obter resposta do procedimento armazenado antes de terminar?

8

Eu preciso retornar o resultado parcial (como seleção simples) de um procedimento armazenado antes de ser concluído.

É possível fazer isso?

Se sim, como fazer isso?

Caso contrário, alguma solução alternativa?

EDIT: Eu tenho várias partes do procedimento. Na primeira parte, calculo várias seqüências. Eu os uso posteriormente no procedimento para fazer operações adicionais. O problema é que a string é necessária pelo chamador o mais rápido possível. Então, eu preciso calcular essa sequência e passá-la de volta (de alguma forma, de uma seleção, por exemplo) e continuar trabalhando. O chamador obtém sua valiosa sequência muito mais rapidamente.

O chamador é um serviço da Web.

Bogdan Bogdanov
fonte
Supondo que não tenha ocorrido um bloqueio completo da tabela ou que uma transação explícita não tenha sido declarada, você poderá executar o SELECT em uma sessão separada sem problemas.
21416 Steve Jobsiamiam
Em geral, este é única maneira que eu vê-lo agora, mas eu não acho que vai ser muito mais rápido (também há outras questões), @SteveMangiameli
Bogdan Bogdanov
Dividi-lo em dois SP? Passe a saída do primeiro para o segundo.
Paparazzo
Não solução muito rápida, por isso que nós disaceded-lo, @Paparazzi
Bogdan Bogdanov

Respostas:

11

Você provavelmente está procurando o RAISERRORcomando com a NOWAITopção

Pelas observações :

O RAISERROR pode ser usado como uma alternativa ao PRINT para retornar mensagens aos aplicativos de chamada.

Isso não retorna os resultados de uma SELECTinstrução, mas permite que você repasse mensagens / strings de volta ao cliente. Se você deseja retornar um subconjunto rápido dos dados selecionados, considere a FASTdica de consulta.

Especifica que a consulta é otimizada para recuperação rápida do primeiro número_rows. Este é um número inteiro não negativo. Depois que os primeiros number_rows são retornados, a consulta continua a execução e produz seu conjunto de resultados completo.

Adicionado por Shannon Severance em um comentário:

Do tratamento de erros e transações no SQL Server por Erland Sommarskog:

Cuidado, porém, que algumas APIs e ferramentas podem ser armazenadas em buffer, anulando o efeito de WITH NOWAIT.

Veja o artigo de origem para o contexto completo.

Erik
fonte
FASTresolveu o problema para mim em um problema em que eu precisava sincronizar a execução de um procedimento armazenado e código C # para exacerbar e reproduzir uma condição de corrida. É mais fácil consumir conjuntos de resultados programaticamente do que usar algo parecido RAISERROR(). Quando comecei a ler sua resposta, parecia que você estava dizendo que isso não pode ser resolvido SELECT, então talvez isso possa ser esclarecido?
27619 binki
5

ATUALIZAÇÃO: Veja a resposta de strutzky ( acima ) e os comentários de pelo menos um exemplo em que isso não se comporta como eu espero e descrevo aqui. Vou ter que experimentar / ler mais para atualizar meu entendimento quando o tempo permitir ...

Se o chamador interage com o banco de dados de forma assíncrona ou é encadeado / com vários processos, para que você possa abrir uma segunda sessão enquanto a primeira ainda está em execução, você pode criar uma tabela para armazenar os dados parciais e atualizá-los à medida que o procedimento avança. Isso pode ser lido por uma segunda sessão com o nível de isolamento de transação 1 definido para permitir a leitura de alterações não confirmadas:

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT * FROM progress_table

1: de acordo com os comentários e a atualização subsequente na resposta de srutzky, não é necessário definir o nível de isolamento se o processo que está sendo monitorado não estiver envolvido em uma transação, embora eu tenha tendência a deixar o hábito em circunstâncias que não causem prejudicar quando não é necessário nesses casos

Obviamente, se você puder ter vários processos operando dessa maneira (o que provavelmente se o seu servidor da Web aceitar usuários simultâneos e for muito raro que não seja esse o caso), será necessário identificar as informações de progresso desse processo de alguma maneira . Talvez passe ao procedimento um UUID recém-cunhado como uma chave, adicione-o à tabela de progresso e leia com:

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

Eu usei esse método para monitorar processos manuais de longa execução no SSMS. Não posso decidir se "cheira" demais para eu considerar usá-lo na produção ...

David Spillett
fonte
11
Esta é uma opção, mas não gosto no momento. Espero que outras opções apareçam.
Bogdan Bogdanov
5

O OP já tentou enviar vários conjuntos de resultados (não o MARS) e viu que realmente espera que o Procedimento Armazenado seja concluído antes de retornar qualquer conjunto de resultados. Com essa situação em mente, aqui estão algumas opções:

  1. Se seus dados forem pequenos o suficiente para caber em 128 bytes, você provavelmente poderá usar o SET CONTEXT_INFOque deve tornar esse valor visível via SELECT [context_info] FROM [sys].[dm_exec_requests] WHERE [session_id] = @SessionID;. Você só precisaria executar uma consulta rápida antes de executar o Procedimento Armazenado SELECT @@SPID;e obter essa via SqlCommand.ExecuteScalar.

    Acabei de testar isso e funciona.

  2. Semelhante à sugestão de @ David de colocar os dados em uma tabela de "progresso", mas sem precisar mexer nos problemas de limpeza ou simultaneidade / separação de processos:

    1. Crie um novo Guiddentro do código do aplicativo e passe-o como um parâmetro para o Stored Procedure. Armazene este Guid em uma variável, pois ele será usado várias vezes.
    2. No procedimento armazenado, crie uma tabela temporária global usando esse guia como parte do nome da tabela, algo como CREATE TABLE ##MyProcess_{GuidFromApp};. A tabela pode ter quaisquer colunas dos tipos de dados que você precisa.
    3. Sempre que você tiver os dados, insira-os na tabela Temp Global.

    4. No código do aplicativo, comece a tentar ler os dados, mas envolva o SELECTem um IF EXISTSmodo que não irá falhar se a tabela não foi criado ainda:

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

    Com String.Format(), você pode substituir {0}pelo valor na variável Guid. Teste se Reader.HasRows, e se for verdadeiro, leia os resultados; caso contrário, ligue Thread.Sleep()ou o que quer que seja para pesquisar novamente.

    Benefícios:

    • Esta tabela é isolada de outros processos, pois apenas o código do aplicativo conhece o valor específico do Guid, portanto, não é necessário se preocupar com outros processos. Outro processo terá sua própria tabela temporária global privada.
    • Por ser uma tabela, tudo é fortemente digitado.
    • Por ser uma tabela temporária, quando a sessão que executa o Procedimento Armazenado terminar, a tabela será limpa automaticamente.
    • Porque é uma tabela temporária global :
      • é acessível por outras sessões, assim como uma tabela permanente
      • sobreviverá ao final do subprocesso em que é criado (ou seja, a chamada EXEC/ sp_executesql)


    Eu testei isso e funciona como esperado. Você pode tentar por si mesmo com o seguinte código de exemplo.

    Em uma guia de consulta, execute o seguinte e destaque as 3 linhas no comentário do bloco e execute:

    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;
    */
    

    Vá para a guia "Mensagens" e copie o GUID que foi impresso. Em seguida, abra outra guia de consulta e execute o seguinte, colocando o GUID que você copiou da guia Mensagens da outra sessão na inicialização da variável na linha 1:

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

    Continue batendo F5. Você deverá ver 1 entrada pelos primeiros 10 segundos e depois 2 entradas pelos próximos 10 segundos.

  3. Você pode usar o SQLCLR para fazer uma chamada de volta ao seu aplicativo por meio de um serviço da Web ou de algum outro meio.

  4. Talvez você possa usar PRINT/ RAISERROR(..., 1, 10) WITH NOWAITpara transmitir seqüências de caracteres imediatamente, mas isso seria um pouco complicado devido aos seguintes problemas:

    • A saída "Mensagem" é restrita a um VARCHAR(8000)ou aNVARCHAR(4000)
    • As mensagens não são enviadas da mesma maneira que os resultados. Para capturá-los, você precisa configurar um manipulador de eventos. Nesse caso, você pode criar uma variável como uma coleção estática para obter as mensagens que estariam disponíveis para todas as partes do código. Ou talvez de outra maneira. Eu tenho um exemplo ou dois em outras respostas aqui mostrando como capturar as mensagens e vinculará a elas mais tarde quando as encontrar.
    • As mensagens, por padrão, também não são enviadas até que o processo seja concluído. Esse comportamento, no entanto, pode ser alterado definindo a propriedade SqlConnection.FireInfoMessageEventOnUserErrors como true. A documentação declara:

      Quando você define FireInfoMessageEventOnUserErrors como true , os erros que foram tratados anteriormente como exceções agora são tratados como eventos do InfoMessage. Todos os eventos são acionados imediatamente e são manipulados pelo manipulador de eventos. Se FireInfoMessageEventOnUserErrors estiver definido como false, os eventos do InfoMessage serão tratados no final do procedimento.

      A desvantagem aqui é que a maioria dos erros de SQL não aumentará mais a SqlException. Nesse caso, você precisa testar propriedades adicionais do evento que são passadas para o Message Event Handler. Isso vale para toda a conexão, o que torna as coisas um pouco mais complicadas, mas não incontroláveis.

    • Todas as mensagens aparecem no mesmo nível sem campo ou propriedade separados para distinguir uma da outra. A ordem em que eles são recebidos deve ser a mesma de como são enviados, mas não tem certeza se isso é confiável o suficiente. Pode ser necessário incluir uma tag ou algo que você possa analisar. Dessa forma, você pode pelo menos ter certeza de qual é qual.

Solomon Rutzky
fonte
2
Eu tento isso. Depois de calcular a string, eu a retorno como simples, selecione e continue o procedimento. O problema é que ele retorna todos os conjuntos ao mesmo tempo (suponho que após a RETURNinstrução). Portanto, não está funcionando.
Bogdan Bogdanov
2
@BogdanBogdanov Você está usando .NET e SqlConnection? Quantos dados você deseja transmitir? Quais tipos de dados? Você já tentou PRINTou RAISERROR WITH NOWAIT?
Solomon Rutzky 18/03/16
Vou tentar agora. Usamos o .NET Web Service.
Bogdan Bogdanov
"Como é uma tabela temporária global, você não precisa se preocupar com os níveis de isolamento de transações" - isso é realmente correto? As tabelas temporárias do IIRC, mesmo as globais, devem estar sujeitas às mesmas restrições ACID que qualquer outra tabela. Você poderia detalhar como testou o comportamento?
David Spillett 21/03
Agora que penso nisso, o nível de isolamento é realmente um problema, e o mesmo se aplica à sua sugestão. Contanto que a tabela não seja criada dentro de uma transação. Acabei de atualizar minha resposta com o código de exemplo.
Solomon Rutzky 22/03
0

Se o seu procedimento armazenado precisar ser executado em segundo plano (ou seja, de forma assíncrona), você deverá usar o Service Broker. É um pouco complicado de configurar, mas, uma vez feito, você poderá iniciar o procedimento armazenado (sem bloqueio) e ouvir as mensagens de progresso pelo tempo (ou tão pouco) quanto desejar.

Sarja
fonte