¿Cómo puedo obtener recuentos de filas individuales como SSMS?

8

Tengo un programa cliente C # que está ejecutando procedimientos almacenados a través de ExectueNonQuery, incluida la captura PRINTy la salida de error con eventos de InfoMessage. Funciona bien, pero he notado algo extraño.

Cuando ejecuto un procedimiento almacenado desde SSMS, muestra recuentos de filas para cada instrucción SQL individual que se ejecuta en la pestaña Mensajes (como si viniera de InfoMessages). Sin embargo, mi programa nunca ve estos mensajes, aunque capta todos los mismos resultados. En cambio, solo devuelve las filas afectadas en el resultado de la función ExecuteNonQuery que es la suma de todos los recuentos de filas individuales (lo cual es un poco inútil).

Por ejemplo, este procedimiento:

use [tempdb]
go

SELECT  * 
INTO    MyCols
FROM    sys.columns
go

CREATE PROC foo As

    UPDATE  MyCols
    SET     name = name + N''
-- SSMS shows (662 row(s) affected)

    UPDATE  MyCols
    SET     name = name + N''
    WHERE   name like '%x%'
-- SSMS shows (59 row(s) affected)

PRINT 'bar'
-- both SSMS and ExecuteNonQuery get this

-- ExecuteNonQuery returns 721 rows affected
GO

Cuando foose ejecuta el proceso, SSMS muestra recuentos de filas de 662 y 59, pero ExecuteNonQuerysolo devuelve el total de 721.

Entonces, ¿cómo puedo obtener la misma información que SSMS está recibiendo?


Solo para que quede claro aquí: no estoy interesado en cómo cambiar los procedimientos almacenados para agregar PRINT @@ROWCOUNTs después de cada instrucción SQL. Sé cómo hacerlo y no es una opción la mayoría de las veces por una variedad de razones.

Estoy preguntando cómo hacer lo que SSMS está haciendo aquí. Puedo cambiar el código del cliente todo lo que quiero en este momento (por ahora, de todos modos) y me gustaría hacerlo bien.

RBarryYoung
fuente

Respuestas:

6

El SqlCommand.StatementCompletedevento se activará después de cada declaración en un lote, y una de las propiedades del evento (bueno, prácticamente la única propiedad) es el número de filas afectadas por la declaración que activó el evento.

Algunas notas:

  • Un requisito para obtener esta información es que no especificó SET NOCOUNT ON;o, por el contrario, especificó SET NOCOUNT OFF;.
  • Todos los eventos se disparan al finalizar cada uno Execute___(), no durante la ejecución.
  • La StatementCompletedEventArgs.RecordCountincluye cuenta de las filas de SELECTlos estados, mientras que el SqlDataReader.RecordsAffected propiedad sólo informa de cuenta de las filas de las sentencias DML ( INSERT, UPDATE, DELETE, etc.).
  • El StatementCompletedevento no incluye la instrucción SQL individual del lote que activó el evento. Sin embargo, el controlador de eventos se envía el sendercomo parámetro de entrada y este es el SqlCommandde la hornada de la pregunta, y se puede ver que los lotes por colada sendera SqlCommandy luego mirando a la CommandTextpropiedad (esto se muestra en el ejemplo siguiente).

La documentación es muy escasa sobre esto, así que he elaborado un ejemplo que muestra la activación de este evento para ambos ExecuteNonQueryy ExecuteScalar, así como para consultas ad hoc y procedimientos almacenados (es decir, SqlCommand.CommandTypede Textvs StoredProcedure):

using System;
using System.Data;
using System.Data.SqlClient;

namespace StatementCompletedFiring
{
    class Program
    {
        static void Main(string[] args)
        {
            using (SqlConnection _Connection =
                          new SqlConnection("Integrated Security = True;"))
            {
                using (SqlCommand _Command = new SqlCommand(@"
SET NOCOUNT OFF; --  ensures that the 'StatementCompleted' event fires

EXEC('
CREATE PROCEDURE #TestProc
AS
SELECT * FROM sys.objects;

SELECT * FROM sys.tables;
');

SELECT * FROM sys.objects;
", _Connection))
                {

                    _Command.StatementCompleted += _Command_StatementCompleted;

                    try
                    {
                        _Connection.Open();

                        _Command.ExecuteNonQuery();

                        _Command.CommandText = @"
SELECT 123 AS [Bob];

WAITFOR DELAY '00:00:05.000'; --5 second pause to shows when the events fire

SELECT 2 AS [Sally]
UNION ALL
SELECT 5;
";
                        Console.WriteLine("\n\t");
                        Console.WriteLine(_Command.ExecuteScalar().ToString());
                        Console.WriteLine("\n");


                        _Command.CommandType = CommandType.StoredProcedure;
                        _Command.CommandText = "#TestProc";
                        _Command.ExecuteNonQuery();
                    }
                    catch (Exception _Exception)
                    {
                        throw new Exception(_Exception.Message);
                    }
                }
            }
        }

        static void _Command_StatementCompleted(object sender,
                                                StatementCompletedEventArgs e)
        {
            Console.ForegroundColor = ConsoleColor.Red;
            Console.Write("\nQuery Batch: ");
            Console.ForegroundColor = ConsoleColor.White;
            Console.WriteLine(((SqlCommand)sender).CommandText);

            Console.ForegroundColor = ConsoleColor.Red;
            Console.Write("Row(s) affected: ");
            Console.ForegroundColor = ConsoleColor.White;
            Console.WriteLine(e.RecordCount.ToString() + "\n");

            Console.ResetColor();
        }
    }
}

SALIDA:

Lote de consultas:
APAGAR NOCOUNT; - asegura que se active el evento 'StatementCompleted'

EXEC ('CREATE PROCEDURE #TestProc AS SELECT * FROM sys.objects;

SELECCIONAR * DESDE sys.tables; ');

SELECCIONAR * DESDE sys.objects;

Fila (s) afectada (s): 453

Lote de consultas:
SELECCIONE 123 AS [Bob];

RETARDO DE ESPERA '00: 00: 05.000 '; --5 segundos de pausa

SELECCIONE 2 COMO [Sally] UNIÓN TODOS SELECCIONE 5;

Fila (s) afectada (s): 1

Lote de consultas:
SELECCIONE 123 AS [Bob];

RETARDO DE ESPERA '00: 00: 05.000 '; --5 segundos de pausa

SELECCIONE 2 COMO [Sally] UNIÓN TODOS SELECCIONE 5;

Fila (s) afectada (s): 2

123

Lote de consultas: #TestProc
Fila (s) afectada (s): 453

Lote de consultas: #TestProc
Fila (s) afectadas: 17

Solomon Rutzky
fuente
1
He intentado esto y funciona para mí. Curiosamente, los StatementCompletions y los InfoMessages de las declaraciones PRINT en los procedimientos almacenados no parecen estar sincronizados entre sí (recibo un montón de StatementCompletions, luego un montón de salidas de la declaración PRINT, aunque se supone que están intercaladas) Pero yo supongo que es un truco de SSMS para otro día ...
RBarryYoung
1
@RBarryYoung Sí, ese comportamiento, creo que es de esperar, incluso si es molesto. Tiene que ver con el orden de los elementos en el TDS (flujo de datos tabular): msdn.microsoft.com/en-us/library/dd304523.aspx . Sé que todos los mensajes PRINTy RAISERROR(..., 10, 1)vienen después de los conjuntos de resultados. Estoy tratando de encontrar el mensaje que ordena en esa documentación pero hasta ahora no lo he encontrado.
Solomon Rutzky
El misterio para mí es cómo SSMS lo ordena correctamente.
RBarryYoung
1
@RBarryYoung ¿Quizás esta debería ser una pregunta por separado ya que se trataba solo de conteos de filas de consultas individuales? Es una buena pregunta, y lo he descubierto :). Lo publicaré como una pregunta si tengo la oportunidad de hacerlo antes de que lo hagas.
Solomon Rutzky
1
@RBarryYoung Yikes. Lamento escuchar eso. Espero que estés reparando. Trataré de llegar en los próximos días. Solo me quedan una o dos variaciones para probar que pensé después de publicar ese último mensaje. Publicaré el enlace aquí.
Solomon Rutzky
-1

El resultado de ejecución de consulta simplemente no va a hacer lo que quiere aquí. Pero aún puede llegar allí, solo depende de para qué desea usar la información.

Puede agregar esta línea después de cada inserción "PRINT @@ ROWCOUNT" y debe obtener el número de filas afectadas por la operación anterior como parte de la salida (donde obtiene "bar".

Alternativamente, puede agregar un parámetro "OUTPUT" a su procedimiento almacenado para guardar los resultados y luego simplemente capturarlo cuando ejecute la consulta ejecutada.

EDITAR:

Me las arreglé para modificar la muestra que Jonathan Kehasias reunió para incluir el manejo de eventos con declaración completa. Solo agrega estas dos líneas.

#Add handler for StatementCompleted
$statementhandler = {param($sender, [System.Data.StatementCompletedEventArgs]$event) Write-Host $event.RecordCount };

#Attach handler...
$cmd.add_StatementCompleted($statementhandler)
Jonathan Fite
fuente
No puedo modificar estos procedimientos. Puedo modificar el código del cliente, incluido el uso de algo que no sea ExecuteNonQuery.
RBarryYoung
Podría intentar adjuntar un controlador de eventos al evento sqlcommand infomessage. Este artículo muestra cómo hacerlo usando powershell. sqlskills.com/blogs/jonathan/…
Jonathan Fite
Si hubieras leído mi pregunta, habrías visto que ya lo estoy haciendo. Es no allí.
RBarryYoung
1
Esta pregunta en el área C # dice que agregar un oyente al evento SQLCommand.StatementCompleted les dio lo que estaban buscando. stackoverflow.com/questions/27993049/…
Jonathan Fite