Comment obtenir la réponse d'une procédure stockée avant qu'elle ne se termine?

8

J'ai besoin de renvoyer le résultat partiel (comme simple sélection) d'une procédure stockée avant qu'elle ne soit terminée.

Est-il possible de faire ça?

Si oui, comment faire?

Sinon, une solution?

EDIT: J'ai plusieurs parties de la procédure. Dans la première partie, je calcule plusieurs chaînes. Je les utilise plus tard dans la procédure pour effectuer des opérations supplémentaires. Le problème est que la chaîne est requise par l'appelant dès que possible. J'ai donc besoin de calculer cette chaîne et de la transmettre (en quelque sorte, à partir d'une sélection par exemple), puis de continuer à travailler. L'appelant obtient sa chaîne de valeur beaucoup plus rapidement.

L'appelant est un service Web.

Bogdan Bogdanov
la source
En supposant qu'un verrou de table complet ne s'est pas produit ou qu'une transaction explicite n'a pas été déclarée, vous devriez pouvoir exécuter SELECT dans une session distincte sans problème.
Steve Mangiameli
En général, c'est seulement ainsi que je le vois maintenant, mais je ne pense pas que ce sera beaucoup plus rapide (il y a aussi d'autres problèmes), @SteveMangiameli
Bogdan Bogdanov
Le diviser en deux SP? Passez la sortie du premier au second.
paparazzo
Solution pas très rapide, c'est pourquoi nous l'avons supprimée, @Paparazzi
Bogdan Bogdanov

Réponses:

11

Vous recherchez probablement la RAISERRORcommande avec l' NOWAIToption.

Par les remarques :

RAISERROR peut être utilisé comme alternative à PRINT pour renvoyer des messages aux applications appelantes.

Cela ne renvoie pas les résultats d'une SELECTinstruction, mais cela vous permettra de transmettre des messages / chaînes au client. Si vous souhaitez renvoyer un sous-ensemble rapide des données que vous sélectionnez, vous souhaiterez peut-être prendre en compte l' FASTindice de requête.

Spécifie que la requête est optimisée pour une récupération rapide des premières number_rows. Il s'agit d'un entier non négatif. Une fois les premières number_rows renvoyées, la requête continue son exécution et produit son jeu de résultats complet.

Ajouté par Shannon Severance dans un commentaire:

De la gestion des erreurs et des transactions dans SQL Server par Erland Sommarskog:

Attention, cependant, certaines API et outils peuvent tamponner de leur côté, annulant ainsi l'effet de WITH NOWAIT.

Voir l'article source pour le contexte complet.

Erik
la source
FASTrésolu le problème pour moi dans un problème où je devais synchroniser l'exécution d'une procédure stockée et du code C # pour exacerber et reproduire une condition de concurrence critique. Il est plus facile de consommer des jeux de résultats par programme que d'utiliser quelque chose comme RAISERROR(). Quand j'ai commencé à lire votre réponse, il semblait que vous disiez que cela ne pouvait pas être fait SELECT, alors peut-être que cela pourrait être clarifié?
binki
5

MISE À JOUR: Voir la réponse de strutzky ( ci-dessus ) et les commentaires pour au moins un exemple où cela ne se comporte pas comme je m'attends et le décris ici. Je devrai expérimenter / lire plus loin pour mettre à jour ma compréhension lorsque le temps le permettra ...

Si votre appelant interagit avec la base de données de manière asynchrone ou est fileté / multi-processus, vous pouvez donc ouvrir une deuxième session pendant que la première est toujours en cours d'exécution, vous pouvez créer une table pour contenir les données partielles et la mettre à jour au fur et à mesure de la progression de la procédure. Cela peut ensuite être lu par une deuxième session avec le niveau d'isolement de transaction 1 défini pour lui permettre de lire les modifications non validées:

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT * FROM progress_table

1: selon les commentaires et la mise à jour subséquente dans la réponse de srutzky, la définition du niveau d'isolement n'est pas requise si le processus surveillé n'est pas encapsulé dans une transaction, bien que j'ai tendance à le mettre hors d'usage dans les circonstances car il ne provoque pas nuire lorsqu'il n'est pas nécessaire dans ces cas

Bien sûr, si plusieurs processus peuvent fonctionner de cette façon (ce qui est probable si votre serveur Web accepte des utilisateurs simultanés et il est très rare que ce ne soit pas le cas), vous devrez identifier les informations de progression de ce processus d'une manière ou d'une autre. . Peut-être passer la procédure un UUID fraîchement créé comme clé, l'ajouter au tableau de progression et lire avec:

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

J'ai utilisé cette méthode pour surveiller les processus manuels de longue durée dans SSMS. Je ne peux pas décider si ça "sent" trop pour moi d'envisager de l'utiliser en production ...

David Spillett
la source
1
C'est une option mais je ne l'aime pas pour le moment. J'espère que d'autres options apparaîtront.
Bogdan Bogdanov
5

L'OP a déjà essayé d'envoyer plusieurs jeux de résultats (pas MARS) et a vu qu'il attend en effet que la procédure stockée se termine avant de retourner des jeux de résultats. Avec cette situation à l'esprit, voici quelques options:

  1. Si vos données sont suffisamment petites pour tenir dans les 128 octets, vous pourriez très probablement utiliser SET CONTEXT_INFOce qui devrait rendre cette valeur visible via SELECT [context_info] FROM [sys].[dm_exec_requests] WHERE [session_id] = @SessionID;. Vous auriez juste besoin d'exécuter une requête rapide avant d'exécuter la procédure stockée SELECT @@SPID;et de la saisir via SqlCommand.ExecuteScalar.

    Je viens de tester cela et cela fonctionne.

  2. Semblable à la suggestion de @ David de mettre les données dans un tableau de "progression", mais sans avoir à s'occuper des problèmes de nettoyage ou de concurrence / séparation des processus:

    1. Créez-en un nouveau Guiddans le code d'application et transmettez-le en tant que paramètre à la procédure stockée. Stockez ce Guid dans une variable car il sera utilisé plusieurs fois.
    2. Dans la procédure stockée, créez une table temporaire globale en utilisant ce Guid dans le nom de la table, quelque chose comme CREATE TABLE ##MyProcess_{GuidFromApp};. La table peut avoir toutes les colonnes de tous les types de données dont vous avez besoin.
    3. Chaque fois que vous avez les données, insérez-les dans cette table de températures globales.

    4. Dans le code de l'application, commencez à essayer de lire les données, mais encapsulez-le SELECTdans un IF EXISTSafin qu'il n'échoue pas si la table n'a pas encore été créée:

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

    Avec String.Format(), vous pouvez remplacer {0}par la valeur de la variable Guid. Testez si Reader.HasRowset si vrai, lisez les résultats, sinon appelez Thread.Sleep()ou quoi que ce soit pour interroger à nouveau.

    Avantages:

    • Cette table est isolée des autres processus car seul le code de l'application connaît la valeur Guid spécifique, donc pas besoin de se soucier des autres processus. Un autre processus aura sa propre table temporaire globale privée.
    • Parce que c'est une table, tout est fortement typé.
    • Puisqu'il s'agit d'une table temporaire, à la fin de la session exécutant la procédure stockée, la table sera nettoyée automatiquement.
    • Parce que c'est une table temporaire globale :
      • il est accessible par d'autres Sessions, tout comme une table permanente
      • il survivra à la fin du sous-processus dans lequel il est créé (c'est-à-dire l' appel EXEC/sp_executesql


    J'ai testé cela et cela fonctionne comme prévu. Vous pouvez l'essayer par vous-même avec l'exemple de code suivant.

    Dans un onglet de requête, exécutez ce qui suit, puis mettez en surbrillance les 3 lignes dans le bloc-commentaire et exécutez cela:

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

    Accédez à l'onglet "Messages" et copiez le GUID qui a été imprimé. Ensuite, ouvrez un autre onglet de requête et exécutez ce qui suit, en plaçant le GUID que vous avez copié à partir de l'onglet Messages de l'autre session dans l'initialisation variable sur la ligne 1:

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

    Continuez à frapper F5. Vous devriez voir 1 entrée pour les 10 premières secondes, puis 2 entrées pour les 10 secondes suivantes.

  3. Vous pouvez utiliser SQLCLR pour rappeler votre application via un service Web ou tout autre moyen.

  4. Vous pouvez peut - être utiliser PRINT/ RAISERROR(..., 1, 10) WITH NOWAITpour renvoyer des chaînes immédiatement, mais ce serait un peu délicat en raison des problèmes suivants:

    • La sortie "Message" est limitée à VARCHAR(8000)ouNVARCHAR(4000)
    • Les messages ne sont pas envoyés de la même manière que les résultats. Pour les capturer, vous devez configurer un gestionnaire d'événements. Dans ce cas, vous pouvez créer une variable en tant que collection statique pour obtenir les messages qui seraient disponibles pour toutes les parties du code. Ou peut-être d'une autre manière. J'ai un exemple ou deux dans d'autres réponses ici montrant comment capturer les messages et les lierai plus tard lorsque je les trouverai.
    • Par défaut, les messages ne sont également pas envoyés avant la fin du processus. Ce comportement, cependant, peut être modifiée en réglant la SqlConnection.FireInfoMessageEventOnUserErrors propriété à true. La documentation indique:

      Lorsque vous définissez FireInfoMessageEventOnUserErrors sur true , les erreurs qui étaient précédemment traitées comme des exceptions sont désormais traitées comme des événements InfoMessage. Tous les événements se déclenchent immédiatement et sont gérés par le gestionnaire d'événements. Si FireInfoMessageEventOnUserErrors est défini sur false, les événements InfoMessage sont traités à la fin de la procédure.

      L'inconvénient ici est que la plupart des erreurs SQL ne soulèveront plus a SqlException. Dans ce cas, vous devez tester des propriétés d'événement supplémentaires qui sont transmises au gestionnaire d'événements de message. Cela vaut pour toute la connexion, ce qui rend les choses un peu plus délicates, mais pas ingérables.

    • Tous les messages apparaissent au même niveau, sans champ ni propriété distincts pour les distinguer les uns des autres. L'ordre dans lequel ils sont reçus doit être le même que la façon dont ils sont envoyés, mais ne savez pas si cela est suffisamment fiable. Vous devrez peut-être inclure une balise ou quelque chose que vous pourrez ensuite analyser. De cette façon, vous pourriez au moins être certain lequel est lequel.

Solomon Rutzky
la source
2
J'essaye. Après avoir calculé la chaîne, je la renvoie comme simple sélection et continue la procédure. Le problème est qu'il renvoie tous les ensembles en même temps (je suppose après l' RETURNinstruction). Donc ça ne marche pas.
Bogdan Bogdanov
2
@BogdanBogdanov Utilisez-vous .NET et SqlConnection? Combien de données souhaitez-vous transmettre? Quels types de données? Avez-vous essayé l'un PRINTou l' autre RAISERROR WITH NOWAIT?
Solomon Rutzky
Je vais essayer maintenant. Nous utilisons le service Web .NET.
Bogdan Bogdanov
"Parce qu'il s'agit d'une table temporaire globale, vous n'avez pas à vous soucier des niveaux d'isolement des transactions" - est-ce vraiment correct? Les tableaux temporaires de l'IIRC, même mondiaux, devraient être soumis aux mêmes restrictions ACID que tout autre tableau. Pourriez-vous détailler comment vous avez testé le comportement?
David Spillett
@DavidSpillett Maintenant que j'y pense, le niveau d'isolement n'est vraiment pas un problème, et il en va de même en ce qui concerne votre suggestion. Tant que la table n'est pas créée dans une transaction. Je viens de mettre à jour ma réponse avec l'exemple de code.
Solomon Rutzky
0

Si votre procédure stockée doit s'exécuter en arrière-plan (c'est-à-dire de manière asynchrone), vous devez utiliser Service Broker. C'est un peu pénible à configurer, mais une fois terminé, vous pourrez lancer la procédure stockée (non bloquante) et écouter les messages de progression aussi longtemps (ou aussi peu) que vous le souhaitez.

Serge
la source