Wie erhalte ich eine Antwort von der gespeicherten Prozedur, bevor sie abgeschlossen ist?

8

Ich muss ein Teilergebnis (als einfache Auswahl) von einer gespeicherten Prozedur zurückgeben, bevor es abgeschlossen ist.

Ist das möglich?

Wenn ja, wie geht das?

Wenn nicht, eine Problemumgehung?

EDIT: Ich habe mehrere Teile des Verfahrens. Im ersten Teil berechne ich mehrere Strings. Ich benutze sie später in der Prozedur, um zusätzliche Operationen durchzuführen. Das Problem ist, dass der Anrufer so schnell wie möglich eine Zeichenfolge benötigt. Also muss ich diesen String berechnen und zurückgeben (irgendwie aus einer Auswahl zum Beispiel) und dann weiterarbeiten. Der Anrufer erhält seine wertvolle Zeichenfolge viel schneller.

Anrufer ist ein Webdienst.

Bogdan Bogdanov
quelle
Angenommen, eine vollständige Tabellensperre ist nicht aufgetreten oder eine explizite Transaktion wurde nicht deklariert, sollten Sie SELECT in einer separaten Sitzung ohne Probleme ausführen können.
Steve Mangiameli
Im Allgemeinen ist dies nur so, wie ich es jetzt sehe, aber ich denke nicht, dass es viel schneller sein wird (es gibt auch andere Probleme), @SteveMangiameli
Bogdan Bogdanov
In zwei SP teilen? Übergeben Sie die Ausgabe von der ersten an die zweite.
Paparazzo
Keine sehr schnelle Lösung, deshalb haben wir sie verschoben, @Paparazzi
Bogdan Bogdanov

Antworten:

11

Sie suchen wahrscheinlich nach dem RAISERRORBefehl mit der NOWAITOption.

Gemäß den Bemerkungen :

RAISERROR kann als Alternative zu PRINT verwendet werden, um Nachrichten an aufrufende Anwendungen zurückzugeben.

Dadurch werden die Ergebnisse einer SELECTAnweisung nicht zurückgegeben, aber Sie können Nachrichten / Zeichenfolgen an den Client zurückgeben. Wenn Sie eine kurze Teilmenge der von Ihnen ausgewählten Daten zurückgeben möchten, sollten Sie den FASTAbfragehinweis berücksichtigen .

Gibt an, dass die Abfrage für das schnelle Abrufen der ersten number_rows optimiert ist. Dies ist eine nichtnegative Ganzzahl. Nachdem die ersten number_rows zurückgegeben wurden, setzt die Abfrage die Ausführung fort und erzeugt ihre vollständige Ergebnismenge.

Hinzugefügt von Shannon Severance in einem Kommentar:

Aus der Fehler- und Transaktionsbehandlung in SQL Server von Erland Sommarskog:

Beachten Sie jedoch, dass einige APIs und Tools möglicherweise auf ihrer Seite gepuffert werden, wodurch die Auswirkung von aufgehoben wird WITH NOWAIT.

Den vollständigen Kontext finden Sie im Quellartikel.

Erik
quelle
FASTDas Problem wurde für mich in einem Problem behoben, bei dem ich die Ausführung einer gespeicherten Prozedur und des C # -Codes synchronisieren musste, um eine Racebedingung zu verschärfen und zu reproduzieren. Es ist einfacher, Ergebnismengen programmgesteuert zu verwenden, als so etwas zu verwenden RAISERROR(). Als ich anfing, Ihre Antwort zu lesen, schien es, als würden Sie sagen, dass dies nicht möglich SELECTist. Vielleicht könnte das geklärt werden?
Binki
5

UPDATE: Siehe Strutzkys Antwort ( oben ) und die Kommentare für mindestens ein Beispiel, bei dem sich dies nicht so verhält, wie ich es hier erwarte und beschreibe. Ich muss weiter experimentieren / lesen, um mein Verständnis zu aktualisieren, wenn es die Zeit erlaubt ...

Wenn Ihr Anrufer asynchron mit der Datenbank interagiert oder über einen Thread / Multiprozess verfügt, sodass Sie eine zweite Sitzung öffnen können, während die erste noch ausgeführt wird, können Sie eine Tabelle erstellen, in der die Teildaten gespeichert sind, und diese im Verlauf der Prozedur aktualisieren. Dies kann dann von einer zweiten Sitzung mit der Transaktionsisolationsstufe 1 gelesen werden, damit nicht festgeschriebene Änderungen gelesen werden können:

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT * FROM progress_table

1: Gemäß den Kommentaren und der anschließenden Aktualisierung in der Antwort von srutzky ist das Festlegen der Isolationsstufe nicht erforderlich, wenn der überwachte Prozess nicht in eine Transaktion eingebunden ist, obwohl ich ihn unter Umständen, die er nicht verursacht, aus Gewohnheit festlege Schaden, wenn in diesen Fällen nicht benötigt

Wenn auf diese Weise mehrere Prozesse ausgeführt werden könnten (was wahrscheinlich ist, wenn Ihr Webserver gleichzeitig Benutzer akzeptiert und dies sehr selten nicht der Fall ist), müssen Sie die Fortschrittsinformationen für diesen Prozess auf irgendeine Weise identifizieren . Übergeben Sie der Prozedur möglicherweise eine frisch geprägte UUID als Schlüssel, fügen Sie diese der Fortschrittstabelle hinzu und lesen Sie mit:

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

Ich habe diese Methode verwendet, um lang laufende manuelle Prozesse in SSMS zu überwachen. Ich kann mich nicht entscheiden, ob es zu viel "riecht", als dass ich es in der Produktion verwenden könnte ...

David Spillett
quelle
1
Dies ist eine Option, aber ich mag es im Moment nicht. Ich hoffe, dass einige andere Optionen auftauchen.
Bogdan Bogdanov
5

Das OP hat bereits versucht, mehrere Ergebnismengen (nicht MARS) zu senden, und hat festgestellt, dass es tatsächlich auf den Abschluss der gespeicherten Prozedur wartet, bevor es Ergebnismengen zurückgibt. In Anbetracht dieser Situation gibt es hier einige Optionen:

  1. Wenn Ihre Daten klein genug sind, um in 128 Bytes zu passen, können Sie höchstwahrscheinlich verwenden SET CONTEXT_INFO, um diesen Wert über sichtbar zu machen SELECT [context_info] FROM [sys].[dm_exec_requests] WHERE [session_id] = @SessionID;. Sie müssten nur eine kurze Abfrage ausführen, bevor Sie die gespeicherte Prozedur ausführen SELECT @@SPID;und diese über abrufen SqlCommand.ExecuteScalar.

    Ich habe das gerade getestet und es funktioniert.

  2. Ähnlich wie bei @ Davids Vorschlag, die Daten in eine "Fortschritts" -Tabelle zu stellen, ohne sich jedoch mit Aufräum- oder Parallelitäts- / Prozesstrennungsproblemen herumschlagen zu müssen:

    1. Erstellen Sie einen neuen GuidCode im App-Code und übergeben Sie ihn als Parameter an die gespeicherte Prozedur. Speichern Sie diese Guid in einer Variablen, da sie mehrmals verwendet wird.
    2. Erstellen Sie in der gespeicherten Prozedur eine globale temporäre Tabelle, indem Sie diese Guid als Teil des Tabellennamens verwenden CREATE TABLE ##MyProcess_{GuidFromApp};. Die Tabelle kann beliebige Spalten mit beliebigen Datentypen enthalten.
    3. Wenn Sie die Daten haben, fügen Sie sie in diese globale Temp-Tabelle ein.

    4. Versuchen Sie im App-Code, die Daten zu lesen, schließen Sie sie jedoch SELECTein, IF EXISTSdamit sie nicht fehlschlagen, wenn die Tabelle noch nicht erstellt wurde:

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

    Mit String.Format()können Sie durch {0}den Wert in der Guid-Variablen ersetzen . Testen Sie, ob Reader.HasRowsund ob true, lesen Sie die Ergebnisse, rufen Sie an Thread.Sleep()oder was auch immer, um dann erneut abzufragen.

    Leistungen:

    • Diese Tabelle ist von anderen Prozessen isoliert, da nur der App-Code den spezifischen Guid-Wert kennt und Sie sich daher keine Gedanken über andere Prozesse machen müssen. Ein anderer Prozess verfügt über eine eigene private globale temporäre Tabelle.
    • Da es sich um eine Tabelle handelt, ist alles stark typisiert.
    • Da es sich um eine temporäre Tabelle handelt, wird die Tabelle beim Bereinigen der Sitzung, in der die gespeicherte Prozedur ausgeführt wird, automatisch bereinigt.
    • Weil es sich um eine globale temporäre Tabelle handelt:
      • Es ist für andere Sitzungen zugänglich, genau wie eine permanente Tabelle
      • Es überlebt das Ende des Unterprozesses, in dem es erstellt wurde (dh der EXEC/ sp_executesql-Anruf).


    Ich habe dies getestet und es funktioniert wie erwartet. Sie können es mit dem folgenden Beispielcode selbst ausprobieren.

    Führen Sie auf einer Abfrage-Registerkarte Folgendes aus, markieren Sie dann die 3 Zeilen im Blockkommentar und führen Sie Folgendes aus:

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

    Gehen Sie zur Registerkarte "Nachrichten" und kopieren Sie die gedruckte GUID. Öffnen Sie dann eine andere Abfrage-Registerkarte und führen Sie die folgenden Schritte aus. Platzieren Sie die GUID, die Sie von der Registerkarte Nachrichten der anderen Sitzung kopiert haben, in die Variableninitialisierung in Zeile 1:

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

    Schlagen Sie weiter F5. Sie sollten 1 Eintrag für die ersten 10 Sekunden und dann 2 Einträge für die nächsten 10 Sekunden sehen.

  3. Sie können SQLCLR verwenden, um über einen Webdienst oder auf andere Weise einen Rückruf zu Ihrer App zu tätigen.

  4. Sie könnten / möglicherweise verwenden , um Zeichenfolgen sofort zurückzugeben, dies wäre jedoch aufgrund der folgenden Probleme etwas schwierig:PRINTRAISERROR(..., 1, 10) WITH NOWAIT

    • Die Ausgabe von "Nachricht" ist auf entweder VARCHAR(8000)oder beschränktNVARCHAR(4000)
    • Nachrichten werden nicht auf die gleiche Weise wie Ergebnisse gesendet. Um sie zu erfassen, müssen Sie einen Ereignishandler einrichten. In diesem Fall können Sie eine Variable als statische Auflistung erstellen, um die Nachrichten abzurufen, die für alle Teile des Codes verfügbar sind. Oder vielleicht auf andere Weise. Ich habe ein oder zwei Beispiele in anderen Antworten hier, die zeigen, wie die Nachrichten erfasst werden, und werde später darauf verlinken, wenn ich sie finde.
    • Standardmäßig werden auch keine Nachrichten gesendet, bis der Vorgang abgeschlossen ist. Dieses Verhalten kann jedoch geändert werden, indem die Eigenschaft SqlConnection.FireInfoMessageEventOnUserErrors auf gesetzt wird true. In der Dokumentation heißt es:

      Wenn Sie FireInfoMessageEventOnUserErrors auf true setzen , werden Fehler, die zuvor als Ausnahmen behandelt wurden, jetzt als InfoMessage-Ereignisse behandelt. Alle Ereignisse werden sofort ausgelöst und vom Ereignishandler behandelt. Wenn FireInfoMessageEventOnUserErrors auf gesetzt ist false, werden InfoMessage-Ereignisse am Ende des Vorgangs behandelt.

      Der Nachteil hierbei ist, dass die meisten SQL-Fehler a nicht mehr auslösen SqlException. In diesem Fall müssen Sie zusätzliche Ereigniseigenschaften testen, die an den Nachrichtenereignishandler übergeben werden. Dies gilt für die gesamte Verbindung, was die Dinge etwas schwieriger, aber nicht unüberschaubar macht.

    • Alle Nachrichten werden auf derselben Ebene ohne separates Feld oder Eigenschaft angezeigt, um sie voneinander zu unterscheiden. Die Reihenfolge, in der sie empfangen werden, sollte der Reihenfolge entsprechen, in der sie gesendet werden, aber nicht sicher, ob dies zuverlässig genug ist. Möglicherweise müssen Sie ein Tag oder etwas hinzufügen, das Sie dann analysieren können. Auf diese Weise könnten Sie zumindest sicher sein, welches welches ist.

Solomon Rutzky
quelle
2
Ich versuche das. Nach der Berechnung der Zeichenfolge gebe ich sie als einfache Auswahl zurück und setze die Prozedur fort. Das Problem ist, dass alle Mengen gleichzeitig zurückgegeben werden (ich nehme an, nach der RETURNAnweisung). Es funktioniert also nicht.
Bogdan Bogdanov
2
@BogdanBogdanov Verwenden Sie .NET und SqlConnection? Wie viele Daten möchten Sie zurückgeben? Welche Datentypen? Hast du es entweder versucht PRINToder RAISERROR WITH NOWAIT?
Solomon Rutzky
Ich werde es jetzt versuchen. Wir verwenden den .NET Web Service.
Bogdan Bogdanov
"Da es sich um eine globale temporäre Tabelle handelt, müssen Sie sich keine Gedanken über die Isolationsstufen von Transaktionen machen" - ist das wirklich richtig? Temporäre IIRC-Tabellen, auch globale, sollten denselben ACID-Einschränkungen unterliegen wie jede andere Tabelle. Können Sie genau beschreiben, wie Sie das Verhalten getestet haben?
David Spillett
@DavidSpillett Nun, da ich darüber nachdenke, ist die Isolationsstufe wirklich kein Problem, und das Gleiche gilt für Ihren Vorschlag. Solange die Tabelle nicht innerhalb einer Transaktion erstellt wird. Ich habe gerade meine Antwort mit dem Beispielcode aktualisiert.
Solomon Rutzky
0

Wenn Ihre gespeicherte Prozedur im Hintergrund ausgeführt werden muss (dh asynchron), sollten Sie Service Broker verwenden. Das Einrichten ist etwas mühsam, aber wenn Sie fertig sind, können Sie die gespeicherte Prozedur (nicht blockierend) starten und Fortschrittsmeldungen so lange (oder so wenig) abhören, wie Sie möchten.

Serge
quelle