【问题标题】:Delphi Performance: Reading all values under a field in a datasetDelphi Performance:读取数据集中某个字段下的所有值
【发布时间】:2011-11-04 22:47:35
【问题描述】:

我们正在尝试找出一些从 TADOQuery 读取的性能修复。目前,我们使用 'while not Q.eof do begin ... Q.next 方法遍历记录。对于每条记录,我们读取每条记录的 ID 和值,并将每条记录添加到组合框列表中。

有没有办法一次性将指定字段的所有值转换为列表?而不是遍历数据集?如果我可以做类似的事情,那将非常方便......

TStrings(MyList).Assign(Q.ValuesOfField['Val']);

我知道这不是一个真正的命令,但这是我正在寻找的概念。寻找快速响应和解决方案(一如既往,但这是为了解决一个非常紧迫的性能问题)。

【问题讨论】:

  • 更具体地说,我们必须遍历数据集中的每条记录,读取 2 个字段(ID:Integer 和 Val:String)并将其添加到组合框中。那么当有超过 20,000 条记录时,这需要很长时间,我正在寻找一种更快的方法。
  • 组合框中的 20,000 行需要处理很多,尤其是对于一些可怜的最终用户而言。也许你应该重新考虑你的设计?
  • 谁是我们很长的时间有多长? 20k 行数据并不多。您希望我们紧急优化哪些代码?消极的。 @Mikael Eriksson,有一些非常大的简单组合框或列表框的应用程序,例如:电子词典中的索引。
  • 您可以将整个行集提取到TStrings decendant,然后将其分配给控制(使用分析)。如果没有实际代码,就不能再说什么了。
  • @PrematureOptimization:看看 TStrings.Assign,这不太可能比他已经在做的更快。

标签: performance delphi list tadoquery


【解决方案1】:

查看您的评论,这里有一些建议:

在这种情况下,有几件事可能会成为瓶颈。首先是反复查找字段。如果你在循环中调用FieldByNameFindField,你就是在浪费CPU 时间来重新计算一个不会改变的值。为您正在读取的每个字段调用一次 FieldByName 并将它们分配给局部变量。

从字段中检索值时,调用AsStringAsInteger,或其他返回您要查找的数据类型的方法。如果您从 TField.Value 属性中读取数据,那么您就是在浪费时间进行 variant 转换。

如果您要向 Delphi 组合框添加一堆项目,您可能正在处理Items 属性形式的字符串列表。设置列表的Capacity 属性并确保在开始更新之前调用BeginUpdate,最后调用EndUpdate。这可以实现一些内部优化,从而更快地加载大量数据。

根据您使用的组合框,它在处理其内部列表中的大量项目时可能会遇到一些问题。查看它是否具有“虚拟”模式,而不是预先加载所有内容,您只需告诉它需要多少项目,当它被下拉时,它会为每个应该显示的项目调用事件处理程序在屏幕上,你给它正确的文本来显示。这确实可以加快某些 UI 控件的速度。

当然,你应该确保你的数据库查询本身是快速的,但是 SQL 优化超出了这个问题的范围。

最后,Mikael Eriksson 的评论绝对值得关注!

【讨论】:

  • 谢谢你们,伟大的 cmets,但我已经确认了这一切。我只调用了一次 FieldByName('Val').AsString,我使用的是 BeginUpdate/EndUpdate,这是我们的标准继承的 TCustomComboBox 组件,SQL 语句实际上需要 0 毫秒才能返回。关于“虚拟”模式,你能解释一下吗?不确定我是否可以使用它...
  • 不知何故怀疑 我们的标准继承的 TCustomComboBox 组件,但最好使用分析器。
  • 最好提前设置Capacity
【解决方案2】:

您可以使用Getrows。您指定您感兴趣的列,它将返回一个包含值的数组。在我的测试中,将 22.000 行添加到组合框所需的时间从使用 while not ADOQuery1.Eof ... 循环的 7 秒变为 1.3 秒。

示例代码:

var
  V: Variant;
  I: Integer;
begin
  V := ADOQuery1.Recordset.GetRows(adGetRowsRest, EmptyParam, 'ColumnName');

  for I:= VarArrayLowBound(V, 2) to VarArrayHighBound(V, 2) do
    ComboBox1.Items.Add(V[0, I]));
end;

如果您希望数组中有多个列,则应使用变体数组作为第三个参数。

V := ADOQuery1.Recordset.GetRows(adGetRowsRest, EmptyParam, 
       VarArrayOf(['ColumnName1', 'ColumnName2']);

【讨论】:

    【解决方案3】:

    其他人提出了一些很好的性能建议,您应该在 Delphi 中实施。你应该考虑他们。我将专注于 ADO。

    您还没有指定后端数据库服务器是什么,所以我不能太具体,但是关于 ADO,您应该了解一些事情。

    ADO 记录集

    在 ADO 中,有一个 RecordSet 对象。在这种情况下,该 RecordSet 对象基本上就是您的 ResultSet。遍历 RecordSet 的有趣之处在于它仍然与提供者耦合。

    光标类型

    如果您的游标类型是 Dynamic 或 Delphi 的默认 Keyset,那么每次 RecordSet 向提供者请求新行时,提供者都会在返回记录之前检查是否有任何更改。

    因此,对于您所做的只是读取结果集以填充组合框的 TADOQuery,并且它不太可能发生更改,您应该使用静态游标类型来避免检查更新的记录。

    如果你不知道游标是什么,当你调用像Next这样的函数时,你正在移动代表当前记录的游标。

    并非每个提供者都支持所有游标类型。

    缓存大小

    Delphi 和 ADO 对 RecordSet 的默认缓存大小是 1。这是 1 条记录。这与游标类型结合使用。 cachesize 告诉 RecordSet 一次要获取和存储多少条记录。

    当您发出像 Next(在 ADO 中实际上是 MoveNext)这样的命令时,缓存大小为 1,RecordSet 仅在内存中拥有当前记录,因此当它获取下一条记录时,它必须再次向提供者请求它。如果游标不是静态的,则提供者有机会在返回下一条记录之前获取最新数据。因此,对于 Keyset 或 Dynamic,大小 1 是有意义的,因为您希望提供者能够为您获取更新的数据。

    显然,值为 1 时,每次移动光标时提供程序和 RecordSet 之间都会进行通信。好吧,如果游标类型是静态的,这就是我们不想要的开销。因此,增加缓存大小将减少 RecordSet 和提供者之间的往返次数。这也会增加您的内存需求,但应该更快。

    还要注意,对于 Keyset 游标,如果缓存大小大于 1,如果您想要的记录在缓存中,它将不会再次向提供者请求它,这意味着您将看不到更新。

    【讨论】:

      【解决方案4】:

      你无法避免循环。 “很长时间”是相对的,但是如果检索 20000 条记录花费的时间太长,那就有问题了。

      • 检查您的查询;也许 SQL 可以改进(缺少索引?)
      • 显示循环代码,您可以在其中将项目添加到组合框。也许它可以被优化。 (在循环中重复调用FieldByName?使用变体检索字段值?)
      • 确保在循环之前调用ComboBox.Items.BeginUpdate;,之后调用ComboBox.Items.EndUpdate
      • 使用分析器查找瓶颈。

      【讨论】:

      • 查询几乎不需要任何时间,0 毫秒。这是需要时间的循环。我正在使用 FieldByName 来读取值。我尝试了 BeginUpdate/EndUpdate,虽然有点帮助,但还不够。
      • +1 用于在循环中优化 FieldByName - 如果您一遍又一遍地这样做,FieldByName 确实需要一些时间
      • FieldByName 会减慢您的循环速度,尤其是在结果集中有许多字段的情况下。如果这是问题,最好使用本地 TField 变量,在循环之前分配一次并在循环中使用它们。
      • 也不要使用 Variant Field.Value,而是使用 Field.As* 属性,具体取决于列数据类型。
      【解决方案5】:

      您可以尝试将所有数据推入 ClientDataSet 并对其进行迭代,但我认为将数据复制到 CDS 正是您当前正在做的事情 - 循环和分配。

      我曾经做的是连接服务器上的值,将其批量传输到客户端并再次拆分。这实际上使系统更快,因为它减少了客户端和服务器之间必要的通信。

      您必须小心性能瓶颈在哪里。如果您在添加值时不阻止 GUI 更新,它也可以是组合框(特别是当我们谈论 20K 值时 - 这需要滚动很多)。

      编辑:当您无法更改通信时,您也许可以使其异步。在线程中请求新数据,保持 GUI 响应,当数据存在时填充组合框。这意味着用户看到一个空的组合框 5 秒钟,但至少他可以在此期间做其他事情。但不会改变所需的时间。

      【讨论】:

      • 感谢您的回复。阻止 GUI 更新实际上是我已经尝试过的一件事(在花了 20 分钟试图说服首席开发人员这样做之后),它确实从 6 秒中缩短了 1 秒。这只是通过在 try..finally 块中使用 TStrings.BeginUpdate 和 EndUpdate。不过1秒还不够。服务器端的工作是个好主意,不幸的是这是一个巨大的 16 年历史的软件包,只有严格的 SQL 通信,所以我们不能引入一种新的连接形式来加载 1 个列表。
      【解决方案6】:

      您的查询是否也与某些数据感知控件或 TDataSource 相关联?如果是这样,请在 DisableControls 和 EnableControls 块内进行循环,这样您的视觉控件就不会在每次移动到新记录时更新。

      项目列表是否相当静态?如果是这样,请考虑在应用程序启动时创建组合框的非可视实例,可能在单独的线程中,然后在创建表单时将非可视组合框分配给可视组合框。

      【讨论】:

      • 如果是 ADO 数据集,这是一个主要瓶颈
      • 这是一个很好的尝试,不确定 TADOQuery 是否与任何其他数据源/控件挂钩,但我可以看到这是如何导致此问题的。
      • +1 用于提及Disable/EnableControls。但是“非视觉组合框”是个坏主意。您可以更轻松地使用TStringList,并且比创建永远不会看到的视觉控件的开销要少得多。毕竟TComboBox.ItemsTStrings 的后代;您可以直接将TStringList 分配给他们。
      • 你说得对,肯。我使用具有编辑器存储库的商业组件集,所以在这方面我有点被宠坏了。在这种情况下,TStringList 会更有效率。
      【解决方案7】:

      尝试使用 DisableControls 和 EnableControls 来提高数据集中线性过程的性能。

         var
        SL: TStringList;
        Fld: TField;
      begin
        SL := TStringList.Create;
        AdoQuery1.DisableControls;
        Fld := AdoQuery1.FieldByName('ListFieldName'); 
        try
          SL.Sorted := False; // Sort in the query itself first
          SL.Capacity := 25000; // Some amount estimate + fudge factor
      
          SL.BeginUpdate;  
          try
            while not AdoQuery1.Eof do
            begin
              SL.Append(Fld.AsString);
              AdoQuery1.Next;
            end;
          finally
            SL.EndUpdate;
          end;
      
          YourComboBox.Items.AddStrings(SL);
      
        finally
          SL.Free;
          AdoQuery1.EnableControls;
        end;
      end;
      

      【讨论】:

        【解决方案8】:

        不确定这是否会有所帮助,但我的建议是不要直接添加到ComboBox。而是加载到本地 TStringList,使其尽可能快,然后使用 TComboBox.Items.AddStrings 一次性添加它们:

        var
          SL: TStringList;
          Fld: TField;
        begin
          SL := TStringList.Create;
          Fld := AdoQuery1.FieldByName('ListFieldName'); 
          try
            SL.Sorted := False; // Sort in the query itself first
            SL.Capacity := 25000; // Some amount estimate + fudge factor
            SL.BeginUpdate;  
            try
              while not AdoQuery1.Eof do
              begin
                SL.Append(Fld.AsString);
                AdoQuery1.Next;
              end;
            finally
              SL.EndUpdate;
            end;
            YourComboBox.Items.BeginUpdate;
            try
              YourComboBox.Items.AddStrings(SL);
            finally
              YourComboBox.Items.EndUpdate;
            end;
          finally
            SL.Free;
          end;
        end;
        

        【讨论】:

        • 1. SL.BeginUpdate/EndUpdate 对速度没有任何影响,因为没有注册通知事件。 2. AddStrings() 方法已经有 BeginUpdate/EndUpdate 调用。 3. AddStrings() 已经逐个添加单个字符串 - 结论:使用临时 TStringList 不会更快。
        • 1.现在不要;如果将来添加通知事件怎么办?添加 Begin/EndUpdate 不会造成任何伤害,并在需要时添加保护。 2. Begin/EndUpdate 可以嵌套,添加它们(如我所说)不会造成任何伤害。 3. 使用TStringList 的开销比TComboBox 少,除非你有分析证明,否则我不确定我是否同意。结论:你没有在这里提出一个观点,我至少提供了帮助的尝试(并指出我不确定它会)。但感谢您阅读我的回答。 :) 另外,您可以指出这里可能不需要 try..finally 块。
        猜你喜欢
        • 2020-03-13
        • 1970-01-01
        • 2014-01-19
        • 2016-03-31
        • 2014-06-06
        • 2011-01-29
        • 1970-01-01
        • 2014-06-09
        • 1970-01-01
        相关资源
        最近更新 更多