Почему DataTable быстрее, чем DataReader

17

Таким образом, у нас были ожесточенные споры по поводу того, какой маршрут DataAccess нужно взять: DataTable или DataReader.

ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ Я на стороне DataReader, и эти результаты потрясли мой мир.

Мы закончили тем, что написали несколько тестов, чтобы проверить разницу в скорости. В целом было решено, что DataReader работает быстрее, но мы хотели бы видеть, насколько быстрее.

Результаты удивили нас. DataTable был последовательно быстрее, чем DataReader. Приблизительно в два раза быстрее.

Итак, я обращаюсь к вам, членам SO. Почему, когда большая часть документации и даже Microsoft заявляют, что DataReader быстрее, наш тест показывает иначе.

И теперь для кода:

Испытательный жгут:

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

Это DAL для 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();
    }

Это значение DAL для 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;
        }

Результаты:

Для 10 итераций в тестовой жгуте

Для 1000 итераций в тестовой жгуте

Эти результаты являются вторым прогоном, чтобы смягчить различия из-за создания соединения.

    
задан Shai Cohen 30.11.2012 в 18:48
источник
  • Конечные результаты разные. Один дает вам DataTable и дает вам List <ArtifactString>. Для всех, кого я знаю, DataTable хранит все непараметрированное и будет анализировать его при чтении (на самом деле я не знаю, как DataTables хранит свои данные внутри себя, я всегда подозревал, что это XML-ish). Я знаю, что DataTables тратит память, в которой нет читателей. –  MatthewMartin 30.11.2012 в 18:54
  • Проводили ли вы сравнение внутри или вне отладчика? Отладчик часто замедляет ваш собственный код таким образом, что он не для уже скомпилированного кода фреймворка, даже в режиме выпуска. –  Bryce Wagner 20.06.2016 в 15:35
  • Другое, что я действительно удивляюсь, никто не упомянул, что DataTable загружает всю строку за раз, используя GetValues ​​(object []), тогда как ваш код загружает каждое поле отдельно. На каждом отдельном вызове есть некоторые накладные расходы, и возможно, что накладные расходы достаточно велики, чтобы ускорить загрузку DataTable. –  Bryce Wagner 20.06.2016 в 15:37

4 ответа

25

Я вижу три проблемы:

  1. то, как вы используете DataReader, отрицает, что это большое преимущество в одном элементе в памяти, преобразовывая его в список,
  2. вы используете эталон в среде, которая значительно отличается от производства таким образом, который поддерживает DataTable, и
  3. вы тратите время на преобразование записи DataReader в объекты Artifact, которые не дублируются в коде DataTable.

Основным преимуществом DataReader является то, что вам не нужно сразу загружать все в память. Это должно быть огромным преимуществом для DataReader в веб-приложениях, где память, а не процессор, часто является узким местом, но, добавляя каждую строку в общий список, вы это отрицаете. Это также означает, что даже после того, как вы изменили свой код, чтобы использовать только одну запись за раз, разница может не отображаться в ваших тестах, потому что вы запускаете их в системе с большим количеством свободной памяти, что будет способствовать использованию DataTable. Кроме того, версия DataReader проводит время, анализируя результаты в объектах Artifact, которые еще не сделали DataTable.

Чтобы устранить проблему использования DataReader, измените List<ArtifactString> на IEnumerable<ArtifactString> везде, а в DataReader DAL измените эту строку:

artifactList.Add(artifact);

:

yield return artifact;

Это означает, что вам также необходимо добавить код, который выполняет итерации по результатам на тестовом жгуте DataReader, чтобы все было в порядке.

Я не уверен, как настроить эталон для создания более типичного сценария, который справедлив как для DataTable, так и для DataReader, за исключением того, чтобы создавать две версии вашей страницы и обслуживать каждую версию в течение часа под аналогичным производством, так что у нас есть реальное давление в памяти ... выполните некоторые реальные тесты A / B. Кроме того, убедитесь, что вы закрываете преобразование строк DataTable в Artifacts ... и если аргумент состоит в том, что вам нужно сделать это для DataReader, но не для DataTable, это просто неправильно.

    
ответ дан Joel Coehoorn 30.11.2012 в 19:13
  • Я не согласен, что это «неправильно использует DataReader». Для DAL довольно часто используется DataReader для возврата списка объектов. Ленивое перечисление имеет свое место - например, если BLL вычисляет агрегаты, но это не единственный способ скинуть кошку. –  Joe 30.11.2012 в 19:20
  • @Joe - может быть, это не «неправильно», но если это вы «правы», вы отрицаете большую часть обычных преимуществ datareader, и есть лучшие способы написать свой DAL. Однако я это перефразировал. –  Joel Coehoorn 30.11.2012 в 19:21
  • +1 Очень интересные моменты Джоэл, спасибо. Не могли бы вы немного подробнее рассказать о «нормальных преимуществах datareader» и «лучших способах написания вашего DAL»? –  Shai Cohen 30.11.2012 в 19:27
  • @ShaiCohen оба включены в ответ: большое преимущество - это давление памяти, и лучшим способом является использование IEnumerable, а не списка всего от DAL до презентации. Это не столько о ленивом, сколько простом перечислении, поскольку он заключается в том, чтобы не допустить полного набора результатов в ОЗУ вообще. –  Joel Coehoorn 30.11.2012 в 19:32
  • Получил это. Еще раз спасибо. –  Shai Cohen 30.11.2012 в 19:33
Показать остальные комментарии
2

SqlDataAdapter.Fill вызывает SqlCommand.ExecuteReader с CommandBehavior.SequentialAccess . Возможно, этого достаточно, чтобы изменить ситуацию.

В стороне, я вижу, что ваша реализация IDbReader кэширует ординалы каждого поля по соображениям производительности. Альтернативой этому подходу является использование класса DbEnumerator .

DbEnumerator кэширует имя поля - & gt; ординарный словарь внутренне, поэтому дает вам большую выгоду от использования ординалов с простотой использования имен полей:

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

или даже:

return new DbEnumerator(reader)
    .Select(record => new ArtifactString() {
        FormNumber = (int) record["FormNumber"],
        FormOwner = (int) record["FormOwner"],
        ...
      })
    .ToList();
    
ответ дан Joe 30.11.2012 в 19:14
  • +1 Это не покрывало всей разницы, но это немного сократило время. Благодарю. –  Shai Cohen 30.11.2012 в 19:40
  • Я нахожу это более читаемым foreach (IDataRecord in (DbDataReader) reader), в смысле намерения более ясны. –  nawfal 31.07.2015 в 15:52
2

2 вещи могут замедлить вас.

Во-первых, я бы не стал «определять порядковый номер по имени» для каждого столбца, если вы заинтересованы в производительности. Обратите внимание, что класс «layout» ниже, чтобы позаботиться об этом поиске. А макет провайдеров более поздней читаемости, вместо использования «0», «1», «2» и т. Д. И это позволяет мне кодировать интерфейс (IDataReader) вместо Concrete.

Во-вторых. Вы используете свойство «.Value». (и я думаю, что это действительно имеет значение)

Вы получите лучшие результаты (IMHO), если используете конкретный тип данных «getters».

GetString, GetDateTime, GetInt32, и т.д., и т.д..

Вот мой типичный код IDataReader для 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
                {
                }
            }
        }
    }
    
ответ дан granadaCoder 09.04.2013 в 19:15
  • +1 для GetValue (). Я согласен, и я не могу, ради жизни, понять, почему я это сделал. :). Хотя, я не полностью согласен с вашим утверждением «найти порядковый номер по имени». Поскольку это делается только один раз за вызов, воздействие минимально. Фактически, я когда-то проводил тест, и разница между звонками по порядку и по имени в лучшем случае была небрежной. –  Shai Cohen 09.04.2013 в 20:39
  • Да, я видел, что вы «получили ординалы только один раз», что было хорошо. Я просто хочу настроить каждый последний бит, когда смогу. И с помощью «Макетов» я получаю читаемость. И если позиции меняются, у меня есть только одно место для их обновления. Думаю, To-may-toes, Toe-mat-toes. –  granadaCoder 09.04.2013 в 20:58
0

Я не думаю, что это будет учитывать все различия, но попробуйте что-то вроде этого, чтобы устранить некоторые дополнительные переменные и вызовы функций:

using (SqlDataReader reader = command.ExecuteReader())
{
    while (reader.Read())
    {
        artifactList.Add(new ArtifactString
        {
            FormNumber = reader["FormNumber"].ToString(),
            //etc
        });
     }
}
    
ответ дан Geoff 30.11.2012 в 18:59