Cómo obtener el número de filas usando SqlDataReader en C #

98

Mi pregunta es cómo obtener la cantidad de filas devueltas por una consulta usando SqlDataReaderen C #. He visto algunas respuestas sobre esto, pero ninguna se definió claramente, excepto una que dice hacer un ciclo while con el Read()método e incrementar un contador.

Mi problema es que estoy tratando de llenar una matriz multidimensional con la primera fila con los nombres de los encabezados de las columnas y todas las filas posteriores a los datos de la fila.

Sé que puedo simplemente volcar las cosas en un control de lista y no preocuparme por ello, pero para mi propia edificación personal y también me gustaría extraer los datos dentro y fuera de la matriz según lo elija y mostrarlos en diferentes formatos.

Así que creo que no puedo hacer la forma Read()y luego incrementar ++ porque eso significa que tendría que abrir Read()y luego abrir de Read()nuevo para obtener la cantidad de filas y luego los datos de la columna.

Solo un pequeño ejemplo de lo que estoy hablando:

int counter = 0;    

while (sqlRead.Read())
{
    //get rows
    counter++
}

y luego un bucle for para recorrer las columnas y hacer estallar

something.Read();

int dbFields = sqlRead.FieldCount;

for (int i = 0; i < dbFields; i++)
{
   // do stuff to array
}
Tomasz Iniewicz
fuente

Respuestas:

96

Solo hay dos opciones:

  • Descúbralo leyendo todas las filas (y luego también podría almacenarlas)

  • ejecute una consulta SELECT COUNT (*) especializada de antemano.

Pasar dos veces por el bucle DataReader es realmente caro, tendría que volver a ejecutar la consulta.

Y (gracias a Pete OHanlon) la segunda opción solo es segura para la concurrencia cuando usa una transacción con un nivel de aislamiento de instantánea.

Dado que desea terminar almacenando todas las filas en la memoria de todos modos, la única opción sensata es leer todas las filas en un almacenamiento flexible ( List<>o DataTable) y luego copiar los datos en el formato que desee. La operación en memoria siempre será mucho más eficiente.

Henk Holterman
fuente
5
Henk tiene razón: no hay ningún miembro del DataReader que le permita obtener el número de filas porque es un lector solo hacia adelante. Es mejor que primero obtenga el recuento y luego ejecute la consulta, tal vez en una consulta de resultados múltiples para que solo acceda a la base de datos una vez.
flipdoubt
14
El problema con el recuento especializado es que existe la posibilidad de que el recuento sea diferente del número de filas devueltas porque alguien más ha cambiado los datos de una manera que conduce a que se devuelva el número de filas.
Pete OHanlon
1
Pete, tienes razón, requeriría un nivel de aislamiento caro.
Henk Holterman
1
¡Gracias a todos! Esto se está volviendo más claro. Entonces, ¿es mejor volcar toda la información al DataSet o ejecutar un SQL COUNT (*), almacenarlo y luego ejecutar la consulta requerida? ¿O estamos hablando de ejecutar el recuento y almacenar todo en el DataSet?
Tomasz Iniewicz
4
Un RepeatableReadnivel de aislamiento no realiza el bloqueo de rango, por lo que aún permite insertar registros; debe usar un nivel de aislamiento de Snapshoto Serializable.
Lukazoid
10

Si no necesita recuperar toda la fila y desea evitar hacer una consulta doble, probablemente pueda intentar algo como eso:

using (var sqlCon = new SqlConnection("Server=127.0.0.1;Database=MyDb;User Id=Me;Password=glop;"))
      {
        sqlCon.Open();

        var com = sqlCon.CreateCommand();
        com.CommandText = "select * from BigTable";
        using (var reader = com.ExecuteReader())
        {
            //here you retrieve what you need
        }

        com.CommandText = "select @@ROWCOUNT";
        var totalRow = com.ExecuteScalar();

        sqlCon.Close();
      }

Es posible que deba agregar una transacción y no esté seguro de si la reutilización del mismo comando agregará automáticamente una transacción en él ...

Pit Ming
fuente
1
¿Alguien puede decir si @@ ROWCOUNT siempre se basa en la última consulta que se ejecuta arriba? ¿Problemas si muchas conexiones ejecutan consultas en paralelo?
YvesR
1
¿Es necesario hacerlo sqlCon.Close();? Pensé que usingdebería hacerlo por ti.
azulado
1
no funcionará en caso de que necesitemos un
recuento de filas
8

Según lo anterior, un conjunto de datos o un conjunto de datos escrito puede ser una buena estructura temporal que podría usar para hacer su filtrado. Un SqlDataReader está destinado a leer los datos muy rápidamente. Mientras está en el bucle while (), todavía está conectado a la base de datos y está esperando que haga lo que esté haciendo para leer / procesar el siguiente resultado antes de que continúe. En este caso, puede obtener un mejor rendimiento si extrae todos los datos, cierra la conexión a la base de datos y procesa los resultados "sin conexión".

La gente parece odiar los conjuntos de datos, por lo que lo anterior también se podría hacer con una colección de objetos fuertemente tipados.

Daniel Segan
fuente
2
Yo mismo amo los DataSets, ya que son una representación genérica bien escrita y extremadamente útil de datos basados ​​en tablas. Curiosamente, me he dado cuenta de que la mayoría de las personas que evitan el DataSet para ORM son las mismas personas que intentan escribir su propio código para que sea lo más genérico posible (generalmente sin sentido).
MusiGenesis
5
Daniel, "arriba" no es una buena manera de hacer referencia a otra respuesta.
Henk Holterman
6

No puede obtener un recuento de filas directamente desde un lector de datos porque es lo que se conoce como cursor de manguera de incendios, lo que significa que los datos se leen fila por fila en función de la lectura que se realiza. Aconsejaría no hacer 2 lecturas de los datos porque existe la posibilidad de que los datos hayan cambiado entre las 2 lecturas y, por lo tanto, obtendría resultados diferentes.

Lo que podría hacer es leer los datos en una estructura temporal y usarla en lugar de la segunda lectura. Alternativamente, deberá cambiar el mecanismo mediante el cual recupera los datos y usar algo como un DataTable en su lugar.

Pete OHanlon
fuente
5

para completar la respuesta de Pit y para un mejor rendimiento: obtenga todo en una consulta y use el método NextResult.

using (var sqlCon = new SqlConnection("Server=127.0.0.1;Database=MyDb;User Id=Me;Password=glop;"))
{
    sqlCon.Open();
    var com = sqlCon.CreateCommand();
    com.CommandText = "select * from BigTable;select @@ROWCOUNT;";
    using (var reader = com.ExecuteReader())
    {
        while(reader.read()){
            //iterate code
        }
        int totalRow = 0 ;
        reader.NextResult(); // 
        if(reader.read()){
            totalRow = (int)reader[0];
        }
    }
    sqlCon.Close();
}
mehdi
fuente
1

También me enfrento a una situación en la que necesitaba devolver un resultado superior, pero también quería obtener el total de filas que coincidían con la consulta. finalmente llego a esta solución:

   public string Format(SelectQuery selectQuery)
    {
      string result;

      if (string.IsNullOrWhiteSpace(selectQuery.WherePart))
      {
        result = string.Format(
@"
declare @maxResult  int;
set @maxResult = {0};

WITH Total AS
(
SELECT count(*) as [Count] FROM {2}
)
SELECT top (@maxResult) Total.[Count], {1} FROM Total, {2}", m_limit.To, selectQuery.SelectPart, selectQuery.FromPart);
      }
      else
      {
        result = string.Format(
@"
declare @maxResult  int;
set @maxResult = {0};

WITH Total AS
(
SELECT count(*) as [Count] FROM {2} WHERE {3}
)
SELECT top (@maxResult) Total.[Count], {1} FROM Total, {2} WHERE {3}", m_limit.To, selectQuery.SelectPart, selectQuery.FromPart, selectQuery.WherePart);
      }

      if (!string.IsNullOrWhiteSpace(selectQuery.OrderPart))
        result = string.Format("{0} ORDER BY {1}", result, selectQuery.OrderPart);

      return result;
    }
Pit Ming
fuente