Почему DataTable быстрее, чем DataReader
Таким образом, у нас были ожесточенные споры по поводу того, какой маршрут 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 итераций в тестовой жгуте
![For 10 iterations within the test harness]()
Для 1000 итераций в тестовой жгуте
![enter image description here]()
Эти результаты являются вторым прогоном, чтобы смягчить различия из-за создания соединения.
Ответы
Ответ 1
Я вижу три проблемы:
- то, как вы используете DataReader, отрицает его большое преимущество в одном элементе в памяти, преобразовывая его в список,
- вы используете эталон в среде, которая значительно отличается от производства таким образом, который поддерживает DataTable, и
- вы тратите время на преобразование записи 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, это просто неправильно.
Ответ 2
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();
Ответ 3
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
{
}
}
}
}
Ответ 4
Я не думаю, что это будет учитывать все различия, но попробуйте что-то вроде этого, чтобы устранить некоторые дополнительные переменные и вызовы функций:
using (SqlDataReader reader = command.ExecuteReader())
{
while (reader.Read())
{
artifactList.Add(new ArtifactString
{
FormNumber = reader["FormNumber"].ToString(),
//etc
});
}
}