为啥 DataTable 比 DataReader 快

Posted

技术标签:

【中文标题】为啥 DataTable 比 DataReader 快【英文标题】:Why is DataTable faster than DataReader为什么 DataTable 比 DataReader 快 【发布时间】:2012-11-18 23:34:27 【问题描述】:

所以我们在工作中就采取哪种 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));
    

这是 DataReader 的 DAL:

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

这是 DataTable 的 DAL:

        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 次迭代

这些结果是第二次运行,以减轻由于创建连接而产生的差异。

【问题讨论】:

最终结果不同。一个给你一个DataTable,一个给你一个List。据我所知,DataTable 存储了所有未解析的内容,并且会在您阅读时对其进行解析(我实际上不知道 DataTables 如何在内部存储它们的数据,我一直怀疑它是 XML-ish)我知道 DataTables 会浪费读者的内存。 您是在调试器内部还是外部运行比较?调试器通常会减慢您自己的代码的速度,而对于已经编译的框架代码,即使在发布模式下也是如此。 令我感到非常惊讶的另一件事是没人提到 DataTable 使用 GetValues(object[]) 一次加载一整行,而您的代码分别加载每个字段。实际上,每个单独的调用都会产生一些开销,并且开销可能足够大以使 DataTable 加载更快。 【参考方案1】:

我不认为它会解释所有差异,但尝试这样的方法来消除一些额外的变量和函数调用:

using (SqlDataReader reader = command.ExecuteReader())

    while (reader.Read())
    
        artifactList.Add(new ArtifactString
        
            FormNumber = reader["FormNumber"].ToString(),
            //etc
        );
     

【讨论】:

【参考方案2】:

我看到三个问题:

    您使用 DataReader 的方式通过将其转换为列表来否定它在内存中的单一项目优势, 您正在以有利于 DataTable 的方式在与生产环境明显不同的环境中运行基准测试,并且 您花时间将 DataReader 记录转换为在 DataTable 代码中不重复的 Artifact 对象。

DataReader 的主要优点是您不必一次将所有内容加载到内存中。这对于 Web 应用程序中的 DataReader 来说应该是一个巨大的优势,其中内存而不是 cpu 通常是瓶颈,但是通过将每一行添加到通用列表中,您已经否定了这一点。这也意味着即使您将代码更改为一次只使用一条记录,差异可能不会显示在您的基准测试中,因为您在具有大量可用内存的系统上运行它们,这将有利于 DataTable。此外,DataReader 版本花时间将结果解析为 DataTable 尚未完成的 Artifact 对象。

要修复 DataReader 使用问题,请将 List&lt;ArtifactString&gt; 更改为 IEnumerable&lt;ArtifactString&gt;,然后在您的 DataReader DAL 中更改此行:

artifactList.Add(artifact);

到这里:

yield return artifact;

这意味着您还需要将迭代结果的代码添加到 DataReader 测试工具中以保持公平。

我不确定如何调整基准以创建对 DataTable 和 DataReader 都公平的更典型场景,除了构建您的页面的两个版本,并在类似的生产下为每个版本提供一个小时 -水平负载,以便我们有真正的内存压力......做一些真正的 A/B 测试。此外,请确保您涵盖将 DataTable 行转换为 Artifacts... 如果参数是您需要为 DataReader 执行此操作,而不是为 DataTable 执行此操作,那是完全错误的。

【讨论】:

我不同意这是“错误地使用 DataReader”。使用 DataReader 返回实体列表的 DAL 很常见。惰性枚举有它的位置 - 例如如果 BLL 正在计算聚合,但这不是剥猫皮的唯一方法。 @Joe - 也许这不是“错误的”,但如果这是“正确的”,那么您已经否定了大多数普通数据读取器的优势,并且有更好的方法来编写您的 DAL。但是,我已经改写了。 +1 非常有趣的点乔尔,谢谢。您能否详细介绍一下“普通数据读取器的优势”“编写 DAL 的更好方法” @ShaiCohen 都包含在答案中:最大的优势是内存压力,更好的方法是使用 IEnumerable 而不是 List 从 DAL 一直到演示。这与其说是关于惰性或即时枚举,不如说是避免将完整的结果集带到 RAM 中。 虽然这个答案中有一些很好的信息,但对于我参与的大多数应用程序来说,“您在与生产环境有很大不同的环境中运行基准测试”这一说法并不适用。如果您将数据读取器传递到应用程序的下一个级别以供使用,那么肯定您可能会获得良好的性能,但您将失去强类型和面向对象编程的好处。在大多数应用程序中,我已经看到从数据读取器读取到对象的效率很高,因为这样更不容易出错而消耗堆栈。【参考方案3】:

SqlDataAdapter.Fill 调用设置了CommandBehavior.SequentialAccess 的SqlCommand.ExecuteReader。也许这足以改变现状。

顺便说一句,我看到您的 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();

【讨论】:

+1 这并没有涵盖全部差异,但它在一定程度上减少了时间。谢谢。 我觉得这个foreach(IDataRecord in (DbDataReader)reader) 更具可读性,从某种意义上说意图更清晰。【参考方案4】:

2 件事可能会拖慢你的速度。

首先,如果您对性能感兴趣,我不会为每列执行“按名称查找序数”。 请注意,下面的“布局”类负责此查找。 并且布局提供者以后的可读性,而不是使用“0”、“1”、“2”等。 它允许我编写接口(IDataReader)而不是混凝土。

第二。您正在使用“.Value”属性。 (我认为这确实会有所作为)

如果你使用具体的数据类型“getters”,你会得到更好的结果(恕我直言)。

获取字符串, 获取日期时间, 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
                
                
            
        
    

【讨论】:

GetValue() 的 +1。我同意,但我一辈子都无法弄清楚我为什么这么做。 :)。虽然,我不完全同意你关于“按名称查找序数”的说法。由于每次调用仅执行一次,因此影响很小。事实上,我曾经做过一个测试,按顺序调用和按名称调用之间的区别充其量是疏忽。 是的,我确实看到你做了“只得到一次序数”,这很好。我只是想尽可能地调整最后一点。通过 "Layouts" ,我获得了可读性。如果职位发生变化,我只有一个地方可以更新它们。我猜是脚趾,脚趾垫脚趾。

以上是关于为啥 DataTable 比 DataReader 快的主要内容,如果未能解决你的问题,请参考以下文章

DataReader 或 DataTable 绑定Repeater?

用 DataTable 替换 DataReader

C# - 将 DataReader 转换为 DataTable

如何通过DataReader / DataTable进行查看?

.Net工具类--表达式目录树解析DataReader和DataTable

DataTable.Load 显示的行数少于源 DataReader