Come ottenere risposta dalla procedura memorizzata prima che termini?


8

Devo restituire un risultato parziale (come semplice selezione) da una procedura memorizzata prima che sia terminata.

è possibile farlo?

Se sì, come farlo?

In caso contrario, qualche soluzione alternativa?

EDIT: ho diverse parti della procedura. Nella prima parte ho calcolato diverse stringhe. Li utilizzo più avanti nella procedura per effettuare operazioni aggiuntive. Il problema è che la stringa è necessaria al chiamante il più presto possibile. Quindi ho bisogno di calcolare quella stringa e passarla indietro (in qualche modo, ad esempio da una selezione) e quindi continuare a funzionare. Il chiamante ottiene la sua preziosa stringa molto più rapidamente.

Caller è un servizio Web.


Supponendo che non si sia verificato un blocco completo della tabella o che non sia stata dichiarata una transazione esplicita, si dovrebbe essere in grado di eseguire SELECT in una sessione separata senza problemi.
Steve Mangiameli,

In generale questo è solo il modo in cui lo vedo ora, ma non penso che sarà molto più veloce (ci sono anche altri problemi), @SteveMangiameli
Bogdan Bogdanov

Dividerlo in due SP? Passa l'output dal primo al secondo.
paparazzo,

Soluzione non molto rapida, ecco perché l'abbiamo annullata, @Pararazzi
Bogdan Bogdanov

Risposte:


11

Probabilmente stai cercando il RAISERRORcomando con l' NOWAITopzione.

Secondo le osservazioni :

RAISERROR può essere utilizzato in alternativa a PRINT per restituire messaggi alle applicazioni chiamanti.

Ciò non restituisce i risultati da SELECTun'istruzione, ma consente di restituire messaggi / stringhe al client. Se si desidera restituire un sottoinsieme rapido dei dati che si stanno selezionando, è possibile prendere in considerazione il FASTsuggerimento per la query.

Specifica che la query è ottimizzata per il recupero rapido dei primi numeri. Questo è un numero intero non negativo. Dopo che sono stati restituiti i primi number_rows, la query continua l'esecuzione e produce il set di risultati completo.

Aggiunto da Shannon Severance in un commento:

Dalla gestione degli errori e delle transazioni in SQL Server di Erland Sommarskog:

Attenzione, tuttavia, che alcune API e strumenti potrebbero bufferizzarsi dalla loro parte, annullando così l'effetto di WITH NOWAIT.

Vedi l'articolo di origine per l'intero contesto.


FASTrisolto il problema per me in un problema in cui avevo bisogno di sincronizzare l'esecuzione di una procedura memorizzata e del codice C # per esacerbare e riprodurre una condizione di competizione. È più facile consumare i set di risultati a livello di codice piuttosto che usare qualcosa di simile RAISERROR(). Quando ho iniziato a leggere la tua risposta, mi sembrava che stavi dicendo che non si può fare SELECT, quindi forse questo potrebbe essere chiarito?
binki,

5

AGGIORNAMENTO: vedi la risposta di strutzky ( sopra ) e i commenti per almeno un esempio in cui questo non si comporta come mi aspetto e descrivo qui. Dovrò sperimentare / leggere ulteriormente per aggiornare la mia comprensione quando il tempo lo consente ...

Se il chiamante interagisce con il database in modo asincrono o è thread / multi-processo, quindi è possibile aprire una seconda sessione mentre la prima è ancora in esecuzione, è possibile creare una tabella per contenere i dati parziali e aggiornarli mentre la procedura procede. Questo può quindi essere letto da una seconda sessione con il livello di isolamento della transazione 1 impostato per consentire la lettura delle modifiche senza commit:

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT * FROM progress_table

1: secondo i commenti e il successivo aggiornamento nella risposta di srutzky, l'impostazione del livello di isolamento non è richiesta se il processo monitorato non è incluso in una transazione, anche se tendo a metterlo fuori abitudine in circostanze che non causano danno quando non necessario in questi casi

Ovviamente se potessi avere più processi che funzionano in questo modo (il che è probabile che il tuo server web accetti utenti simultanei ed è molto raro che non sia così) dovrai identificare in qualche modo le informazioni sullo stato di avanzamento di questo processo . Forse passare la procedura a un UUID appena coniato come chiave, aggiungerlo alla tabella di avanzamento e leggere con:

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

Ho usato questo metodo per monitorare processi manuali di lunga durata in SSMS. Non riesco a decidere se "puzza" troppo per me per prendere in considerazione l'utilizzo in produzione però ...


1
Questa è un'opzione ma non mi piace in questo momento. Spero che vengano visualizzate altre opzioni.
Bogdan Bogdanov,

5

L'OP ha già provato a inviare più set di risultati (non MARS) e ha visto che attende effettivamente il completamento della Stored Procedure prima di restituire qualsiasi set di risultati. Con questa situazione in mente, ecco alcune opzioni:

  1. Se i tuoi dati sono abbastanza piccoli da contenere entro 128 byte, molto probabilmente potresti usarli SET CONTEXT_INFOche dovrebbero rendere visibile quel valore tramite SELECT [context_info] FROM [sys].[dm_exec_requests] WHERE [session_id] = @SessionID;. Avresti solo bisogno di eseguire una query veloce prima di eseguire la Stored Procedure SELECT @@SPID;e afferrarla tramite SqlCommand.ExecuteScalar.

    Ho appena provato questo e funziona.

  2. Simile al suggerimento di @ David di inserire i dati in una tabella "progress", ma senza la necessità di pasticciare con problemi di pulizia o di concorrenza / separazione dei processi:

    1. Creane uno nuovo Guidnel codice dell'app e passalo come parametro alla Stored Procedure. Conservare questo Guid in una variabile in quanto verrà utilizzato più volte.
    2. Nella Stored procedure, crea una tabella temporanea globale usando quella guida come parte del nome della tabella, qualcosa del genere CREATE TABLE ##MyProcess_{GuidFromApp};. La tabella può avere qualunque colonna di qualunque tipo di dato sia necessario.
    3. Ogni volta che si dispone dei dati, inserirli nella tabella delle temp globali.

    4. Nel codice dell'app, iniziare a provare a leggere i dati, ma racchiuderlo SELECTin in IF EXISTSmodo che non fallisca se la tabella non è stata ancora creata:

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

    Con String.Format(), è possibile sostituire {0}con il valore nella variabile Guid. Verifica se Reader.HasRows, e se vero, leggi i risultati, altrimenti chiama Thread.Sleep()o qualsiasi altra cosa per eseguire nuovamente il polling.

    Benefici:

    • Questa tabella è isolata da altri processi poiché solo il codice dell'app conosce il valore Guid specifico, quindi non è necessario preoccuparsi di altri processi. Un altro processo avrà una propria tabella temporanea globale privata.
    • Perché è una tabella, tutto è fortemente tipizzato.
    • Poiché si tratta di una tabella temporanea, al termine della sessione che esegue la Stored Procedure, la tabella verrà ripulita automaticamente.
    • Perché è una tabella temporanea globale :
      • è accessibile da altre sessioni, proprio come una tabella permanente
      • sopravviverà alla fine del sottoprocesso in cui è stato creato (ovvero il EXEC/ sp_executesqlcall)


    Ho provato questo e funziona come previsto. Puoi provarlo tu stesso con il seguente codice di esempio.

    In una scheda della query, eseguire quanto segue, quindi evidenziare le 3 righe nel commento a blocchi ed eseguirlo:

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

    Vai alla scheda "Messaggi" e copia il GUID che è stato stampato. Quindi, apri un'altra scheda della query ed esegui quanto segue, posizionando il GUID che hai copiato dalla scheda Messaggi dell'altra sessione nell'inizializzazione della variabile alla riga 1:

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

    Continua a colpire F5. Dovresti vedere 1 voce per i primi 10 secondi, quindi 2 voci per i successivi 10 secondi.

  3. È possibile utilizzare SQLCLR per richiamare l'app tramite un servizio Web o altri mezzi.

  4. Potresti forse usare PRINT/ RAISERROR(..., 1, 10) WITH NOWAITper restituire immediatamente le stringhe, ma questo sarebbe un po 'complicato a causa dei seguenti problemi:

    • L'output "Messaggio" è limitato a VARCHAR(8000)oNVARCHAR(4000)
    • I messaggi non vengono inviati allo stesso modo dei risultati. Per acquisirli è necessario impostare un gestore eventi. In tal caso, è possibile creare una variabile come raccolta statica per ottenere i messaggi che sarebbero disponibili per tutte le parti del codice. O forse in un altro modo. Ho un esempio o due in altre risposte che mostrano come catturare i messaggi e li collegherò più tardi quando li trovo.
    • I messaggi, per impostazione predefinita, non vengono inviati fino al completamento del processo. Questo comportamento, tuttavia, può essere modificato impostando la proprietà SqlConnection.FireInfoMessageEventOnUserErrors su true. La documentazione afferma:

      Quando si imposta FireInfoMessageEventOnUserErrors su true , gli errori che erano stati precedentemente trattati come eccezioni vengono ora gestiti come eventi InfoMessage. Tutti gli eventi vengono attivati ​​immediatamente e vengono gestiti dal gestore eventi. Se FireInfoMessageEventOnUserErrors è impostato su false, gli eventi di InfoMessage vengono gestiti al termine della procedura.

      Il rovescio della medaglia qui è che la maggior parte degli errori SQL non genererà più a SqlException. In questo caso, è necessario testare ulteriori proprietà dell'evento che vengono passate nel gestore eventi del messaggio. Questo vale per l'intera connessione, il che rende le cose un po 'più complicate, ma non ingestibili.

    • Tutti i messaggi vengono visualizzati allo stesso livello senza campo o proprietà separati per distinguere l'uno dall'altro. L'ordine in cui vengono ricevuti dovrebbe essere lo stesso di come vengono inviati, ma non è sicuro che sia abbastanza affidabile. Potrebbe essere necessario includere un tag o qualcosa su cui è possibile analizzare. In questo modo potresti almeno essere sicuro di quale sia quale.


2
Ci provo. Dopo aver calcolato la stringa, la restituisco semplicemente selezionando e proseguendo la procedura. Il problema è che restituisce tutti i set contemporaneamente (suppongo dopo la RETURNdichiarazione). Quindi non funziona.
Bogdan Bogdanov,

2
@BogdanBogdanov Stai usando .NET e SqlConnection? Quanti dati vuoi restituire? Quali tipi di dati? Hai provato PRINTo RAISERROR WITH NOWAIT?
Solomon Rutzky,

Ci proverò adesso. Utilizziamo il servizio Web .NET.
Bogdan Bogdanov,

"Poiché si tratta di una tabella temporanea globale, non è necessario preoccuparsi dei livelli di isolamento delle transazioni" - è davvero corretto? Le tabelle temporanee IIRC, anche globali, dovrebbero essere soggette alle stesse restrizioni ACID di qualsiasi altra tabella. Potresti descrivere in dettaglio come hai testato il comportamento?
David Spillett,

@DavidSpillett Ora che ci penso, il livello di isolamento è davvero un problema e lo stesso vale per il tuo suggerimento. Finché la tabella non viene creata all'interno di una Transazione. Ho appena aggiornato la mia risposta con il codice di esempio.
Solomon Rutzky,

0

Se la procedura memorizzata deve essere eseguita in background (ovvero in modo asincrono), è necessario utilizzare Service Broker. È un po 'una seccatura da configurare, ma una volta fatto, sarai in grado di dare il via alla procedura memorizzata (non bloccante) e ascoltare i messaggi di avanzamento per tutto il tempo (o il minimo) che desideri.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.