¿Por qué DataTable es más rápido que DataReader?

Así que hemos tenido un acalorado debate sobre qué ruta de DataAccess tomar: DataTable o DataReader.

DESCARGO DE RESPONSABILIDAD Estoy en el lado de DataReader y estos resultados han sacudido mi mundo.

Terminamos escribiendo algunos puntos de referencia para probar las diferencias de velocidad. En general, se estuvo de acuerdo en que un DataReader es más rápido, pero queríamos ver cuánto más rápido.

Los resultados nos sorprendieron. El DataTable fue consistentemente más rápido que el DataReader. Acercarse dos veces más rápido a veces.

Entonces me dirijo a ustedes, miembros de SO. Por qué, cuando la mayoría de la documentación e incluso Microsoft, afirman que un DataReader es más rápido, nuestra prueba muestra lo contrario.

Y ahora para el código:

El arnés de prueba:

private void button1_Click(object sender, EventArgs e) { System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); DateTime date = DateTime.Parse("01/01/1900"); for (int i = 1; i < 1000; i++) { using (DataTable aDataTable = ArtifactBusinessModel.BusinessLogic.ArtifactBL.RetrieveDTModified(date)) { } } sw.Stop(); long dataTableTotalSeconds = sw.ElapsedMilliseconds; sw.Restart(); for (int i = 1; i < 1000; i++) { List aList = ArtifactBusinessModel.BusinessLogic.ArtifactBL.RetrieveModified(date); } sw.Stop(); long listTotalSeconds = sw.ElapsedMilliseconds; MessageBox.Show(String.Format("list:{0}, table:{1}", listTotalSeconds, dataTableTotalSeconds)); } 

Este es el DAL para el DataReader:

  internal static List RetrieveByModifiedDate(DateTime modifiedLast) { List artifactList = new List(); try { using (SqlConnection conn = SecuredResource.GetSqlConnection("Artifacts")) { using (SqlCommand command = new SqlCommand("[cache].[Artifacts_SEL_ByModifiedDate]", conn)) { command.CommandType = CommandType.StoredProcedure; command.Parameters.Add(new SqlParameter("@LastModifiedDate", modifiedLast)); using (SqlDataReader reader = command.ExecuteReader()) { int formNumberOrdinal = reader.GetOrdinal("FormNumber"); int formOwnerOrdinal = reader.GetOrdinal("FormOwner"); int descriptionOrdinal = reader.GetOrdinal("Description"); int descriptionLongOrdinal = reader.GetOrdinal("DescriptionLong"); int thumbnailURLOrdinal = reader.GetOrdinal("ThumbnailURL"); int onlineSampleURLOrdinal = reader.GetOrdinal("OnlineSampleURL"); int lastModifiedMetaDataOrdinal = reader.GetOrdinal("LastModifiedMetaData"); int lastModifiedArtifactFileOrdinal = reader.GetOrdinal("LastModifiedArtifactFile"); int lastModifiedThumbnailOrdinal = reader.GetOrdinal("LastModifiedThumbnail"); int effectiveDateOrdinal = reader.GetOrdinal("EffectiveDate"); int viewabilityOrdinal = reader.GetOrdinal("Viewability"); int formTypeOrdinal = reader.GetOrdinal("FormType"); int inventoryTypeOrdinal = reader.GetOrdinal("InventoryType"); int createDateOrdinal = reader.GetOrdinal("CreateDate"); while (reader.Read()) { ArtifactString artifact = new ArtifactString(); ArtifactDAL.Map(formNumberOrdinal, formOwnerOrdinal, descriptionOrdinal, descriptionLongOrdinal, formTypeOrdinal, inventoryTypeOrdinal, createDateOrdinal, thumbnailURLOrdinal, onlineSampleURLOrdinal, lastModifiedMetaDataOrdinal, lastModifiedArtifactFileOrdinal, lastModifiedThumbnailOrdinal, effectiveDateOrdinal, viewabilityOrdinal, reader, artifact); artifactList.Add(artifact); } } } } } catch (ApplicationException) { throw; } catch (Exception e) { string errMsg = String.Format("Error in ArtifactDAL.RetrieveByModifiedDate. Date: {0}", modifiedLast); Logging.Log(Severity.Error, errMsg, e); throw new ApplicationException(errMsg, e); } return artifactList; } internal static void Map(int? formNumberOrdinal, int? formOwnerOrdinal, int? descriptionOrdinal, int? descriptionLongOrdinal, int? formTypeOrdinal, int? inventoryTypeOrdinal, int? createDateOrdinal, int? thumbnailURLOrdinal, int? onlineSampleURLOrdinal, int? lastModifiedMetaDataOrdinal, int? lastModifiedArtifactFileOrdinal, int? lastModifiedThumbnailOrdinal, int? effectiveDateOrdinal, int? viewabilityOrdinal, IDataReader dr, ArtifactString entity) { entity.FormNumber = dr[formNumberOrdinal.Value].ToString(); entity.FormOwner = dr[formOwnerOrdinal.Value].ToString(); entity.Description = dr[descriptionOrdinal.Value].ToString(); entity.DescriptionLong = dr[descriptionLongOrdinal.Value].ToString(); entity.FormType = dr[formTypeOrdinal.Value].ToString(); entity.InventoryType = dr[inventoryTypeOrdinal.Value].ToString(); entity.CreateDate = DateTime.Parse(dr[createDateOrdinal.Value].ToString()); entity.ThumbnailURL = dr[thumbnailURLOrdinal.Value].ToString(); entity.OnlineSampleURL = dr[onlineSampleURLOrdinal.Value].ToString(); entity.LastModifiedMetaData = dr[lastModifiedMetaDataOrdinal.Value].ToString(); entity.LastModifiedArtifactFile = dr[lastModifiedArtifactFileOrdinal.Value].ToString(); entity.LastModifiedThumbnail = dr[lastModifiedThumbnailOrdinal.Value].ToString(); entity.EffectiveDate = dr[effectiveDateOrdinal.Value].ToString(); entity.Viewability = dr[viewabilityOrdinal.Value].ToString(); } 

Este es el DAL para la DataTable:

  internal static DataTable RetrieveDTByModifiedDate(DateTime modifiedLast) { DataTable dt= new DataTable("Artifacts"); try { using (SqlConnection conn = SecuredResource.GetSqlConnection("Artifacts")) { using (SqlCommand command = new SqlCommand("[cache].[Artifacts_SEL_ByModifiedDate]", conn)) { command.CommandType = CommandType.StoredProcedure; command.Parameters.Add(new SqlParameter("@LastModifiedDate", modifiedLast)); using (SqlDataAdapter da = new SqlDataAdapter(command)) { da.Fill(dt); } } } } catch (ApplicationException) { throw; } catch (Exception e) { string errMsg = String.Format("Error in ArtifactDAL.RetrieveByModifiedDate. Date: {0}", modifiedLast); Logging.Log(Severity.Error, errMsg, e); throw new ApplicationException(errMsg, e); } return dt; } 

Los resultados:

Para 10 iteraciones dentro del arnés de prueba

Para 10 iteraciones dentro del arnés de prueba

Para 1000 iteraciones dentro del arnés de prueba

enter image description here

Estos resultados son la segunda ejecución, para mitigar las diferencias debidas a la creación de la conexión.

Veo tres problemas:

  1. la forma en que usas un DataReader niega su gran ventaja de un único elemento en la memoria al convertirlo en una lista,
  2. está ejecutando el punto de referencia en un entorno que difiere significativamente de la producción de una manera que favorece a la DataTable, y
  3. está pasando mucho tiempo convirtiendo el registro de DataReader en objetos Artifact que no están duplicados en el código de DataTable.

La principal ventaja de un DataReader es que no tiene que cargar todo en la memoria a la vez. Esto debería ser una gran ventaja para DataReader en aplicaciones web, donde la memoria, en lugar de la CPU, es a menudo el cuello de botella, pero al agregar cada fila a una lista genérica, usted ha negado esto. Eso también significa que incluso después de cambiar el código para que solo use un registro a la vez, la diferencia podría no aparecer en sus puntos de referencia porque los está ejecutando en un sistema con mucha memoria libre, lo que favorecerá a DataTable. Además, la versión de DataReader está pasando tiempo analizando los resultados en objetos Artifact que DataTable aún no ha hecho.

Para solucionar el problema del uso de DataReader, cambie List a IEnumerable todas partes, y en su DataReader DAL cambie esta línea:

 artifactList.Add(artifact); 

a esto:

 yield return artifact; 

Esto significa que también necesita agregar código que itere sobre los resultados a su arnés de prueba de DataReader para mantener las cosas en orden.

No estoy seguro de cómo ajustar el punto de referencia para crear un escenario más típico que sea justo para DataTable y DataReader, excepto para construir dos versiones de su página y publicar cada versión durante una hora con una carga de nivel de producción similar. que tenemos presión de la memoria real … hacer algunas pruebas A / B reales. Además, asegúrese de cubrir la conversión de las filas de DataTable a Artifacts … y si el argumento es que necesita hacer esto para un DataReader, pero no para una DataTable, es simplemente incorrecto.

SqlDataAdapter.Fill llama a SqlCommand.ExecuteReader con CommandBehavior.SequentialAccess establecido. Tal vez eso sea suficiente para marcar la diferencia.

Como IDbReader , veo que su implementación de IDbReader almacena en caché los ordinales de cada campo por motivos de rendimiento. Una alternativa a este enfoque es usar la clase DbEnumerator .

DbEnumerator almacena en caché un nombre de campo -> ordinal dictionary internamente, por lo que le brinda gran beneficio de rendimiento al usar ordinales con la simplicidad de usar nombres de campo:

 foreach(IDataRecord record in new DbEnumerator(reader)) { artifactList.Add(new ArtifactString() { FormNumber = (int) record["FormNumber"], FormOwner = (int) record["FormOwner"], ... }); } 

o incluso:

 return new DbEnumerator(reader) .Select(record => new ArtifactString() { FormNumber = (int) record["FormNumber"], FormOwner = (int) record["FormOwner"], ... }) .ToList(); 

2 cosas podrían ralentizarte.

Primero, no haría un “buscar ordinal por nombre” para cada columna, si le interesa el rendimiento. Tenga en cuenta la clase de “diseño” a continuación para encargarse de esta búsqueda. Y los proveedores de diseño más tarde legibilidad, en lugar de utilizar “0”, “1”, “2”, etc. Y me permite codificar a una interfaz (IDataReader) en lugar de la Concreto.

Segundo. Está utilizando la propiedad “.Value”. (y creo que esto hace la diferencia)

Obtendrá mejores resultados (en mi humilde opinión) si utiliza el tipo de datos concreto “getters”.

GetString, GetDateTime, GetInt32, etc., etc.

Aquí está mi código típico de IDataReader a DTO / POCO.

 [Serializable] public partial class Employee { public int EmployeeKey { get; set; } public string LastName { get; set; } public string FirstName { get; set; } public DateTime HireDate { get; set; } } [Serializable] public class EmployeeCollection : List { } internal static class EmployeeSearchResultsLayouts { public static readonly int EMPLOYEE_KEY = 0; public static readonly int LAST_NAME = 1; public static readonly int FIRST_NAME = 2; public static readonly int HIRE_DATE = 3; } public EmployeeCollection SerializeEmployeeSearchForCollection(IDataReader dataReader) { Employee item = new Employee(); EmployeeCollection returnCollection = new EmployeeCollection(); try { int fc = dataReader.FieldCount;//just an FYI value int counter = 0;//just an fyi of the number of rows while (dataReader.Read()) { if (!(dataReader.IsDBNull(EmployeeSearchResultsLayouts.EMPLOYEE_KEY))) { item = new Employee() { EmployeeKey = dataReader.GetInt32(EmployeeSearchResultsLayouts.EMPLOYEE_KEY) }; if (!(dataReader.IsDBNull(EmployeeSearchResultsLayouts.LAST_NAME))) { item.LastName = dataReader.GetString(EmployeeSearchResultsLayouts.LAST_NAME); } if (!(dataReader.IsDBNull(EmployeeSearchResultsLayouts.FIRST_NAME))) { item.FirstName = dataReader.GetString(EmployeeSearchResultsLayouts.FIRST_NAME); } if (!(dataReader.IsDBNull(EmployeeSearchResultsLayouts.HIRE_DATE))) { item.HireDate = dataReader.GetDateTime(EmployeeSearchResultsLayouts.HIRE_DATE); } returnCollection.Add(item); } counter++; } return returnCollection; } //no catch here... see http://blogs.msdn.com/brada/archive/2004/12/03/274718.aspx finally { if (!((dataReader == null))) { try { dataReader.Close(); } catch { } } } } 

No creo que represente toda la diferencia, pero pruebe algo como esto para eliminar algunas de las variables adicionales y llamadas a funciones:

 using (SqlDataReader reader = command.ExecuteReader()) { while (reader.Read()) { artifactList.Add(new ArtifactString { FormNumber = reader["FormNumber"].ToString(), //etc }); } }