2012-11-30 35 views
17

Tak więc mieliśmy gorącą debatę w trakcie pracy nad wyborem trasy DataAccess: DataTable lub DataReader.Dlaczego DataTable jest szybsze niż DataReader

ZASTRZEŻENIE Jestem po stronie DataReader i te wyniki wstrząsnęły moim światem.

Skończyło się na napisaniu kilku testów porównawczych, aby przetestować różnice prędkości. Ogólnie ustalono, że DataReader jest szybszy, ale chcieliśmy zobaczyć, o ile szybciej.

Wyniki nas zaskoczyły. DataTable był konsekwentnie szybszy niż DataReader. Zbliża się dwukrotnie szybciej.

Więc zwracam się do was, członkowie SO. Dlaczego, kiedy większość dokumentacji, a nawet Microsoft, stwierdza, że ​​DataReader jest szybszy, nasz test pokazuje inaczej.

A teraz dla kodu:

Uprząż Test:

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<ArtifactBusinessModel.Entities.ArtifactString> aList = ArtifactBusinessModel.BusinessLogic.ArtifactBL.RetrieveModified(date); 

     } 

     sw.Stop(); 

     long listTotalSeconds = sw.ElapsedMilliseconds; 

     MessageBox.Show(String.Format("list:{0}, table:{1}", listTotalSeconds, dataTableTotalSeconds)); 
    } 

To DAL dla DataReader:

 internal static List<ArtifactString> RetrieveByModifiedDate(DateTime modifiedLast) 
     { 
      List<ArtifactString> artifactList = new List<ArtifactString>(); 

      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(); 
    } 

To DAL dla 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; 
     } 

Wyniki:

dla 10 powtórzeń w Test wiązki

For 10 iterations within the test harness

na 1000 iteracji w testu wiązki

enter image description here

Te wyniki są drugi bieg, aby złagodzić różnice d do tworzenia połączenia.

+3

Wyniki końcowe są różne. Jedna daje tabelę danych, a druga daje listę .Z tego, co wiem, DataTable zapisywał wszystko, co nie zostało sprawdzone i przeanalizuje je podczas czytania (nie mam pojęcia, jak DataTables przechowuje ich dane wewnętrznie, zawsze podejrzewałem, że jest to XML-owski). Wiem, że DataTables marnuje pamięć, gdzie Czytelnicy tego nie robią. – MatthewMartin

+0

Czy przeprowadziłeś porównanie wewnątrz debuggera lub poza nim? Debugger często spowalnia twój własny kod w sposób, w jaki nie działa on dla skompilowanego kodu, nawet w trybie wydania. –

+0

Inną rzeczą, która mnie naprawdę zaskoczyła, nikt nie wspomniał, że DataTable ładuje cały wiersz na raz, używając GetValues ​​(object []), podczas gdy twój kod ładuje każde pole osobno. Z każdym pojedynczym połączeniem wynika pewne obciążenie i możliwe, że narzut jest wystarczająco duży, aby ładowanie DataTable było szybsze. –

Odpowiedz

25

widzę trzy kwestie:

  1. sposób, w jaki używasz obiektu DataReader, neguje jego dużą przewagę nad jednym elementem w pamięci, konwertując go na listę,
  2. używasz testu porównawczego w środowisku, które różni się znacznie od produkcji w sposób, który sprzyja DataTable, oraz
  3. poświęcasz czas na konwersję rekordu DataReader na obiekty Artifact, które nie są duplikowane w kodzie DataTable.

Główną zaletą modułu DataReader jest to, że nie trzeba jednocześnie ładować wszystkiego do pamięci. To powinno być wielką zaletą dla DataReader w aplikacjach internetowych, gdzie często problemem jest pamięć, a nie cpu, ale poprzez dodanie każdego wiersza do ogólnej listy negowałeś to.Oznacza to również, że nawet po zmianie kodu, aby użyć tylko jednego rekordu na raz, różnica może nie być widoczna w testach porównawczych, ponieważ uruchamiasz je w systemie z dużą ilością wolnej pamięci, co faworyzuje DataTable. Ponadto wersja DataReader poświęca czas na analizowanie wyników w obiektach Artefaktu, których DataTable jeszcze nie wykonała.

Aby rozwiązać ten DataReader użytkowaniu problem, należy zmienić List<ArtifactString> do IEnumerable<ArtifactString> wszędzie, aw swojej zmiany DAL DataReader tej linii:

artifactList.Add(artifact); 

do tego:

yield return artifact; 

Oznacza to trzeba także dodać kod, który iteruje nad wynikami do wiązki testowej DataReader, aby zachować uczciwość.

Nie jestem pewien, jak dostosować benchmark, aby stworzyć bardziej typowy scenariusz, który jest sprawiedliwy zarówno dla DataTable, jak i DataReader, z wyjątkiem budowania dwóch wersji twojej strony i serwowania każdej wersji przez godzinę pod podobną produkcją -poziom obciążenia, abyśmy mieli prawdziwe ciśnienie pamięci ... wykonaj prawdziwe testy A/B. Ponadto upewnij się, że pokrywasz konwersję wierszy DataTable do artefaktów ... i jeśli argumentem jest to, że musisz to zrobić dla obiektu DataReader, ale nie dla elementu DataTable, to jest po prostu błędne.

+0

Nie zgadzam się, że jest to "używanie niewłaściwego DataReadera". Jest dość powszechny w przypadku DAL, który używa DataReadera w celu zwrócenia listy elementów. Leniwe wyliczenie ma swoje miejsce - np. jeśli BLL oblicza agregaty, ale nie jest to jedyny sposób na skórze kota. – Joe

+1

@Joe - może to nie jest "złe", ale jeśli jest to "właściwe", zanegowałeś większość normalnych zalet przetwarzania danych i istnieją lepsze sposoby na zapisanie DAL. Jednak przeformułowałem to. –

+0

+1 Bardzo interesujące punkty Joel, dziękuję. Czy mógłbyś rozszerzyć nieco bardziej na "normalne zalety datareaderek" _ i _ "lepsze sposoby na zapisanie DAL" _? –

0

nie sądzę, będzie to stanowić ogromną różnicę, ale spróbuj coś takiego, aby wyeliminować niektóre z dodatkowych zmiennych i wywołania funkcji:

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

SqlDataAdapter.Fill dzwoni SqlCommand.ExecuteReader z zestawem CommandBehavior.SequentialAccess. Może to wystarczy, by zrobić różnicę.

Na marginesie widzę, że twoja implementacja IDbReader buforuje wartości porządkowe każdego pola ze względu na wydajność. Alternatywą dla tego podejścia jest użycie klasy DbEnumerator.

DbEnumerator buforuje nazwy pola -> porządkowa słowniku wewnętrznie, więc daje wiele korzyści wydajności przy użyciu liczb porządkowych z prostotą używania nazwy pól:

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

lub nawet:

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

+1 To nie pokryło całej różnicy, ale zmniejszyło nieco czas. Dzięki. –

+0

Znajduję to bardziej czytelny 'foreach (IDataRecord w czytniku (DbDataReader)) w sensie zamiar jest wyraźniejszy. – nawfal

2

2 rzeczy mogą cię spowalniać.

Po pierwsze, nie robiłbym "znaleźć porządek po imieniu" dla każdej kolumny, jeśli interesuje Cię wydajność. Uwaga, klasa "layout" poniżej zajmuje się tym sprawdzeniem. A dostawcy układu później czytelność, zamiast używać "0", "1", "2", itp. I pozwala mi kod do interfejsu (IDataReader) zamiast betonu.

Po drugie. Używasz właściwości ".Value". (i myślę, że to robi różnicę)

Otrzymasz lepsze wyniki (IMHO), jeśli użyjesz konkretnego typu danych "getters".

GetString, GetDateTime, GetInt32, itp.

Oto mój typowy IDataReader do kodu 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<Employee> 
{ 
} 

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 
       { 
       } 
      } 
     } 
    } 
+0

+1 dla GetValue(). Zgadzam się i nie mogę, dla mojego życia, dowiedzieć się, dlaczego tak zrobiłem. :). Chociaż nie zgadzam się w pełni z twoją statuetką "znajdź porządek po imieniu". Ponieważ jest to wykonywane tylko raz na połączenie, wpływ jest minimalny. W rzeczywistości kiedyś zrobiłem test, a różnica między wywołaniem przez porządek i nazwę była w najlepszym przypadku zaniedbana. –

+0

Tak, widziałem, że zrobiłeś "dostań ordinals tylko raz", co było dobre. Po prostu lubię podkręcać wszystko, co tylko mogę. A dzięki "układom" otrzymuję czytelność. A jeśli zmieniają się pozycje, mam tylko jedno miejsce, aby je zaktualizować. Toe-may-toes, toe-mat-palce, jak sądzę. – granadaCoder