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

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

отказ от ответственности Я на стороне DataReader и эти результаты потрясли мой мир.

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

результаты удивили нас. DataTable был последовательно быстрее, чем объект DataReader. Иногда приближается вдвое быстрее.

поэтому я обращаюсь к вам, членам так. Почему, когда большая часть документации и даже 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 итераций в тестовом жгуте

For 10 iterations within the test harness

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

enter image description here

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

4 ответов


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

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

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

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

artifactList.Add(artifact);

для этого:

yield return artifact;

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

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


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

в стороне, я вижу ваш 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();

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

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

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

вы получите лучшие результаты (IMHO), если вы используете конкретный тип данных "геттеры".

метода 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
                {
                }
            }
        }
    }

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

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