¿Cómo obtener la respuesta del procedimiento almacenado antes de que finalice?

8

Necesito devolver un resultado parcial (como selección simple) de un procedimiento almacenado antes de que finalice.

¿Es posible hacer eso?

Si es así, ¿cómo hacer eso?

Si no, ¿alguna solución?

EDITAR: Tengo varias partes del procedimiento. En la primera parte calculo varias cuerdas. Los uso más adelante en el procedimiento para realizar operaciones adicionales. El problema es que la persona que llama necesita la cadena lo antes posible. Por lo tanto, necesito calcular esa cadena y volver a pasarla (de alguna manera, desde una selección, por ejemplo) y luego continuar trabajando. La persona que llama obtiene su valiosa cadena mucho más rápido.

La persona que llama es un servicio web.

Bogdan Bogdanov
fuente
Si no se ha producido un bloqueo completo de la tabla o no se ha declarado una transacción explícita, debería poder ejecutar SELECT en una sesión separada sin problemas.
Steve Mangiameli
En general, esta es la única forma en que lo veo ahora, pero no creo que sea mucho más rápido (también hay otros problemas), @SteveMangiameli
Bogdan Bogdanov
Dividirlo en dos SP? Pase la salida de la primera a la segunda.
paparazzo
No es una solución muy rápida, por eso lo descartamos, @Paparazzi
Bogdan Bogdanov

Respuestas:

11

Probablemente esté buscando el RAISERRORcomando con la NOWAITopción.

Por las observaciones :

RAISERROR se puede usar como una alternativa a PRINT para devolver mensajes a las aplicaciones que llaman.

Esto no devuelve los resultados de una SELECTdeclaración, pero le permitirá pasar mensajes / cadenas al cliente. Si desea devolver un subconjunto rápido de los datos que está seleccionando, puede considerar la FASTsugerencia de la consulta.

Especifica que la consulta está optimizada para la recuperación rápida de los primeros número_camas. Este es un entero no negativo. Después de que se devuelven los primeros números_cadena, la consulta continúa la ejecución y produce su conjunto de resultados completo.

Agregado por Shannon Severance en un comentario:

Del error y manejo de transacciones en SQL Server por Erland Sommarskog:

Sin embargo, tenga en cuenta que algunas API y herramientas pueden almacenarse en su lado, anulando así el efecto de WITH NOWAIT.

Vea el artículo fuente para el contexto completo.

Erik
fuente
FASTresolvió el problema para mí en un problema en el que necesitaba sincronizar la ejecución de un procedimiento almacenado y un código C # para exacerbar y reproducir una condición de carrera. Es más fácil consumir conjuntos de resultados mediante programación que usar algo como esto RAISERROR(). Cuando comencé a leer tu respuesta, parecía que estabas diciendo que no se puede hacer SELECTasí que, ¿tal vez eso podría aclararse?
binki
5

ACTUALIZACIÓN: Vea la respuesta de strutzky ( arriba ) y los comentarios para al menos un ejemplo donde esto no se comporta como esperaba y describo aquí. Tendré que experimentar / leer más para actualizar mi comprensión cuando el tiempo lo permita ...

Si su interlocutor interactúa con la base de datos de forma asincrónica o está enhebrado / multiproceso, por lo que puede abrir una segunda sesión mientras la primera aún se está ejecutando, puede crear una tabla para contener los datos parciales y actualizarla a medida que avanza el procedimiento. Esto se puede leer en una segunda sesión con el nivel de aislamiento de transacción 1 configurado para permitirle leer los cambios no confirmados:

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT * FROM progress_table

1: según los comentarios y la actualización posterior en la respuesta de srutzky, no se requiere establecer el nivel de aislamiento si el proceso que se está monitoreando no está envuelto en una transacción, aunque tiendo a dejarlo fuera de la costumbre en tales circunstancias, ya que no causa daño cuando no es necesario en estos casos

Por supuesto, si pudiera tener múltiples procesos operando de esta manera (lo que es probable si su servidor web acepta usuarios concurrentes y es muy raro que ese no sea el caso) deberá identificar la información de progreso de este proceso de alguna manera . Quizás pase el procedimiento con un UUID recién acuñado como clave, agréguelo a la tabla de progreso y lea con:

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

He usado este método para monitorear procesos manuales de larga ejecución en SSMS. Sin embargo, no puedo decidir si "huele" demasiado para que considere usarlo en la producción ...

David Spillett
fuente
1
Esta es una opción, pero no me gusta en este momento. Espero que aparezcan otras opciones.
Bogdan Bogdanov
5

El OP ya ha intentado enviar múltiples conjuntos de resultados (no MARS) y ha visto que realmente espera a que se complete el Procedimiento almacenado antes de devolver cualquier conjunto de resultados. Con esa situación en mente, aquí hay algunas opciones:

  1. Si sus datos son lo suficientemente pequeños como para caber dentro de 128 bytes, lo más probable es SET CONTEXT_INFOque pueda usar lo que debería hacer que ese valor sea visible a través de SELECT [context_info] FROM [sys].[dm_exec_requests] WHERE [session_id] = @SessionID;. Sólo se necesita para ejecutar una consulta rápida antes de ejecutar el procedimiento almacenado a SELECT @@SPID;y agarrar esa vía SqlCommand.ExecuteScalar.

    Acabo de probar esto y funciona.

  2. Similar a la sugerencia de @ David de poner los datos en una tabla de "progreso", pero sin necesidad de meterse con problemas de limpieza o concurrencia / separación de procesos:

    1. Cree uno nuevo Guiddentro del código de la aplicación y páselo como parámetro al Procedimiento almacenado. Almacene este Guid en una variable, ya que se usará varias veces.
    2. En el Procedimiento almacenado, cree una Tabla temporal global utilizando esa Guía como parte del nombre de la tabla, algo así CREATE TABLE ##MyProcess_{GuidFromApp};. La tabla puede tener las columnas de cualquier tipo de datos que necesite.
    3. Siempre que tenga los datos, insértelos en esa tabla de temperatura global.

    4. En el código de la aplicación, iniciar intentar leer los datos, pero envolver el SELECTde una IF EXISTSmanera que no se producirá un error si la tabla no se ha creado todavía:

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

    Con String.Format(), puede reemplazar {0}con el valor en la variable Guid. Verifique si Reader.HasRowses verdadero, luego lea los resultados, de lo contrario llame Thread.Sleep()o lo que sea para luego sondear nuevamente.

    Beneficios:

    • Esta tabla está aislada de otros procesos, ya que solo el código de la aplicación conoce el valor Guid específico, por lo tanto, no es necesario preocuparse por otros procesos. Otro proceso tendrá su propia tabla temporal global privada.
    • Debido a que es una tabla, todo está fuertemente tipado.
    • Debido a que es una tabla temporal, cuando finaliza la sesión que ejecuta el Procedimiento almacenado, la tabla se limpiará automáticamente.
    • Porque es una tabla temporal global :
      • es accesible por otras sesiones, como una mesa permanente
      • sobrevivirá al final del subproceso en el que se crea (es decir, la llamada EXEC/ sp_executesql)


    He probado esto y funciona como se esperaba. Puede probarlo usted mismo con el siguiente código de ejemplo.

    En una pestaña de consulta, ejecute lo siguiente y luego resalte las 3 líneas en el comentario de bloque y ejecútelo:

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

    Vaya a la pestaña "Mensajes" y copie el GUID que se imprimió. Luego, abra otra pestaña de consulta y ejecute lo siguiente, colocando el GUID que copió de la pestaña Mensajes de la otra sesión en la inicialización de variables en la línea 1:

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

    Sigue golpeando F5. Debería ver 1 entrada durante los primeros 10 segundos y luego 2 entradas durante los próximos 10 segundos.

  3. Puede usar SQLCLR para devolver una llamada a su aplicación a través de un servicio web o por algún otro medio.

  4. Tal vez podría usar PRINT/ RAISERROR(..., 1, 10) WITH NOWAITpara pasar las cadenas de inmediato, pero esto sería un poco complicado debido a los siguientes problemas:

    • "Mensaje" de salida se limita a cualquiera de los dos VARCHAR(8000)oNVARCHAR(4000)
    • Los mensajes no se envían de la misma manera que los resultados. Para capturarlos, debe configurar un controlador de eventos. En ese caso, podría crear una variable como una colección estática para obtener los mensajes que estarían disponibles para todas las partes del código. O tal vez de otra manera. Tengo un ejemplo o dos en otras respuestas aquí que muestran cómo capturar los mensajes y los vincularé más tarde cuando los encuentre.
    • Los mensajes, por defecto, tampoco se envían hasta que se completa el proceso. Sin embargo, este comportamiento puede modificarse estableciendo la propiedad SqlConnection.FireInfoMessageEventOnUserErrors en true. La documentación dice:

      Cuando configura FireInfoMessageEventOnUserErrors en verdadero , los errores que anteriormente se trataban como excepciones ahora se manejan como eventos de InfoMessage. Todos los eventos se disparan inmediatamente y son manejados por el controlador de eventos. Si se establece falseFireInfoMessageEventOnUserErrors en, los eventos de InfoMessage se manejan al final del procedimiento.

      La desventaja aquí es que la mayoría de los errores de SQL ya no generarán a SqlException. En este caso, debe probar propiedades de eventos adicionales que se pasan al controlador de eventos de mensajes. Esto es válido para toda la conexión, lo que hace las cosas un poco más complicadas, pero no inmanejables.

    • Todos los mensajes aparecen en el mismo nivel sin campo o propiedad separados para distinguir uno del otro. El orden en que se reciben debe ser el mismo que el de su envío, pero no estoy seguro de si eso es lo suficientemente confiable. Es posible que deba incluir una etiqueta o algo que luego pueda analizar. De esa manera, al menos podría estar seguro de cuál es cuál.

Solomon Rutzky
fuente
2
Lo intento Después de calcular la cadena, la devuelvo como simple selección y continúo el procedimiento. El problema es que devuelve todos los conjuntos al mismo tiempo (supongo que después de la RETURNdeclaración). Entonces no está funcionando.
Bogdan Bogdanov
2
@BogdanBogdanov ¿Está utilizando .NET y SqlConnection? ¿Cuántos datos quieres devolver? ¿Qué tipos de datos? ¿Has probado alguna PRINTo RAISERROR WITH NOWAIT?
Solomon Rutzky
Lo intentaré ahora. Utilizamos .NET Web Service.
Bogdan Bogdanov
"Debido a que es una tabla temporal global, no necesita preocuparse por los niveles de aislamiento de transacciones", ¿es eso realmente correcto? Las tablas temporales del IIRC, incluso las globales, deben estar sujetas a las mismas restricciones ACID que cualquier otra tabla. ¿Podría detallar cómo probó el comportamiento?
David Spillett
@DavidSpillett Ahora que lo pienso, el nivel de aislamiento no es realmente un problema, y ​​lo mismo ocurre con respecto a su sugerencia. Siempre que la tabla no se cree dentro de una transacción. Acabo de actualizar mi respuesta con el código de ejemplo.
Solomon Rutzky
0

Si su procedimiento almacenado necesita ejecutarse en segundo plano (es decir, de forma asíncrona), entonces debe usar Service Broker. Es un poco complicado configurarlo, pero una vez hecho esto, podrá iniciar el procedimiento almacenado (sin bloqueo) y escuchar los mensajes de progreso durante el tiempo que desee.

Sarga
fuente