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

Итак, у нас на работе были жаркие споры о том, какой маршрут доступа к данным выбрать: 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 итераций в Test Harness

Для 10 итераций в тестовом наборе

За 1000 итераций в Test Harness

введите здесь описание изображения

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


person Shai Cohen    schedule 30.11.2012    source источник
comment
Конечные результаты разные. Один дает вам DataTable, а другой — List‹ArtifactString›. Насколько я знаю, DataTable хранит все неразобранные и будет анализировать их по мере чтения (на самом деле я понятия не имею, как DataTables хранят свои данные внутри, я всегда подозревал, что это XML-ish).   -  person MatthewMartin    schedule 30.11.2012
comment
Вы проводили сравнение в отладчике или вне его? Отладчик часто замедляет ваш собственный код так, как он не делает для уже скомпилированного кода фреймворка, даже в режиме выпуска.   -  person Bryce Wagner    schedule 20.06.2016
comment
Еще одна вещь, которую я действительно удивил, никто не упомянул, это то, что DataTable загружает всю строку за раз, используя GetValues ​​(object []), тогда как ваш код загружает каждое поле отдельно. На самом деле при каждом отдельном вызове возникают некоторые накладные расходы, и вполне возможно, что накладные расходы достаточно велики, чтобы ускорить загрузку DataTable.   -  person Bryce Wagner    schedule 20.06.2016


Ответы (4)


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

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

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

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

artifactList.Add(artifact);

к этому:

yield return artifact;

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

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

person Joel Coehoorn    schedule 30.11.2012
comment
Я не согласен с тем, что это неправильное использование DataReader. Довольно часто DAL использует DataReader для возврата списка сущностей. Ленивое перечисление имеет свое место - например. если BLL вычисляет агрегаты, но это не единственный способ содрать шкуру с кота. - person Joe; 30.11.2012
comment
@Joe - может быть, это не так, но если это так, вы правы, вы свели на нет большинство обычных преимуществ чтения данных, и есть лучшие способы написать свой DAL. Однако я перефразировал это. - person Joel Coehoorn; 30.11.2012
comment
+1 Очень интересные моменты, Джоэл, спасибо. Не могли бы вы подробнее рассказать об преимуществах обычного чтения данных и улучшенных способах написания DAL? - person Shai Cohen; 30.11.2012
comment
@ShaiCohen оба описаны в ответе: большим преимуществом является нехватка памяти, и лучший способ - использовать IEnumerable, а не List на всем пути от DAL до презентации. Речь идет не столько о ленивом или своевременном перечислении, сколько о том, чтобы вообще не переносить полный набор результатов в ОЗУ. - person Joel Coehoorn; 30.11.2012
comment
Хотя в этом ответе есть некоторая полезная информация, утверждение, что вы запускаете тест в среде, которая значительно отличается от рабочей, не соответствует действительности для большинства приложений, в которых я участвовал. Если вы передаете устройства чтения данных до следующего уровень вашего приложения для потребления, тогда вы можете получить хорошую производительность, но вы теряете преимущества строгой типизации и объектно-ориентированного программирования. В большинстве приложений, которые я видел, чтение из устройства чтения данных в объекты так же эффективно, как это менее подвержено ошибкам, чтобы потреблять стек. - person Alan Macdonald; 30.01.2015
comment
@AlanMacdonald Среда, о которой я говорю, - это не код ... это нагрузка на сервер. Тестовая нагрузка обычно имеет много свободной памяти по сравнению с продуктом. Что касается создания строго типизированных объектов... да, делайте это при использовании DataReader. Но вам не нужно считывать записи в список для создания этих объектов. По-прежнему возможно сохранить функцию чтения данных по одному объекту в памяти за раз с помощью блоков Iterator и IEnumerable. - person Joel Coehoorn; 15.04.2015

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

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

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

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();
person Joe    schedule 30.11.2012
comment
+1 Это не покрыло всей разницы, но несколько сократило время. Спасибо. - person Shai Cohen; 30.11.2012
comment
Я нахожу это более читаемым foreach(IDataRecord in (DbDataReader)reader), в том смысле, что намерение яснее. - person nawfal; 31.07.2015

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

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

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

Вы получите лучшие результаты (ИМХО), если будете использовать «геттеры» конкретного типа данных.

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
                {
                }
            }
        }
    }
person granadaCoder    schedule 09.04.2013
comment
+1 для GetValue(). Я согласен, и я не могу, хоть убей, понять, почему я это сделал. :). Хотя я не совсем согласен с вашим утверждением, что найти порядковый номер по имени. Поскольку это делается только один раз за вызов, влияние минимально. На самом деле, я однажды сделал тест, и разница между вызовом по порядковому номеру и по имени была в лучшем случае небрежной. - person Shai Cohen; 09.04.2013
comment
Да, я видел, что ты только один раз получил порядковые номера, и это было хорошо. Я просто люблю доделывать каждую мелочь, когда могу. А с Layouts я получаю удобочитаемость. И если позиции меняются, у меня есть только одно место для их обновления. To-may-toes, Toe-mat-toes, я думаю. - person granadaCoder; 09.04.2013

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

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