为什么在 ADOTable 中滚动会变得越来越慢?

11
我希望从一个MS Access文件中读取整个表,并尽可能快地实现。在测试大样本时,我发现循环计数器在读取表的顶部记录时增加得比读取表的最后记录快。以下是演示此情况的示例代码:
procedure TForm1.Button1Click(Sender: TObject);
const
  MaxRecords = 40000;
  Step = 5000;
var
  I, J: Integer;
  Table: TADOTable;
  T: Cardinal;
  Ts: TCardinalDynArray;
begin
  Table := TADOTable.Create(nil);
  Table.ConnectionString :=
    'Provider=Microsoft.ACE.OLEDB.12.0;'+
    'Data Source=BigMDB.accdb;'+
    'Mode=Read|Share Deny Read|Share Deny Write;'+
    'Persist Security Info=False';
  Table.TableName := 'Table1';
  Table.Open;

  J := 0;
  SetLength(Ts, MaxRecords div Step);
  T := GetTickCount;
  for I := 1 to MaxRecords do
  begin
    Table.Next;
    if ((I mod Step) = 0) then
    begin
      T := GetTickCount - T;
      Ts[J] := T;
      Inc(J);
      T := GetTickCount;
    end;
  end;
  Table.Free;

//  Chart1.SeriesList[0].Clear;
//  for I := 0 to Length(Ts) - 1 do
//  begin
//    Chart1.SeriesList[0].Add(Ts[I]/1000, Format(
//      'Records: %s %d-%d %s Duration:%f s',
//      [#13, I * Step, (I + 1)*Step, #13, Ts[I]/1000]));
//  end;
end;

以下是我的电脑上的结果:enter image description here

这个表有两个字符串字段,一个双精度浮点数和一个整数。它没有主键也没有索引字段。为什么会发生这种情况,我该如何避免?


不,我是通过编程方式创建控件的,除了示例代码中所看到的内容外,没有其他东西。 - saastn
你的 For 循环不是少了一个吗?无论如何,如果你读取了大量记录,难道你会感到惊讶吗?这将涉及大量内存分配,并且随着分配的内存越多,所需时间也会越长。 - MartynA
@MartynA 你说的循环没错。但我不能说是内存分配使它变慢。似乎在Table.Open中获取了所有记录,任务管理器在运行该行后没有显示任何内存分配。 - saastn
你尝试过使用 while not Table.EOF... 进行迭代吗? - Jerry Dodge
@JerryDodge 我刚刚检查了一下,但结果相同。 - saastn
3个回答

20
我可以使用一个带有类似大小的MS Sql Server数据集的AdoQuery来复现您的结果。
然而,在进行了一些行性能分析之后,我认为我已经找到了答案,而且这是有点违反直觉的。我相信在Delphi中进行数据库编程的每个人都习惯于这样一个想法:如果您将循环包围在Disable/EnableControls的调用之间,那么循环会更快。但是如果没有与数据集相关联的db-aware控件,谁会费心去做呢?
好吧,事实证明,在您的情况下,即使没有DB-aware控件,如果您仍然使用Disable/EnableControls,速度也会大幅提高。
原因是AdoDB.Pas中的TCustomADODataSet.InternalGetRecord包含以下内容:
      if ControlsDisabled then
        RecordNumber := -2 else
        RecordNumber := Recordset.AbsolutePosition;

根据我的行性能分析器,while not AdoQuery1.Eof do AdoQuery1.Next 循环的执行时间中有 98.8% 花费在赋值操作上。

        RecordNumber := Recordset.AbsolutePosition;

在记录集接口的"错误侧"上隐藏了Recordset.AbsolutePosition的计算,但据我观察,调用它的时间显然会随着你进入记录集而增加,这使得合理推测它是通过从记录集数据的开头进行计数来计算的。

当然,如果已经调用了 DisableControls 并且未通过调用 EnableControls 撤销,则 ControlsDisabled 返回true。因此,请使用被 Disable/EnableControls 包围的循环重新测试,希望您能获得与我类似的结果。看起来您是正确的,减速与内存分配无关。

使用以下代码:

procedure TForm1.btnLoopClick(Sender: TObject);
var
  I: Integer;
  T: Integer;
  Step : Integer;
begin
  Memo1.Lines.BeginUpdate;
  I := 0;
  Step := 4000;
  if cbDisableControls.Checked then
    AdoQuery1.DisableControls;
  T := GetTickCount;
{.$define UseRecordSet}
{$ifdef UseRecordSet}
  while not AdoQuery1.Recordset.Eof do begin
    AdoQuery1.Recordset.MoveNext;
    Inc(I);
    if I mod Step = 0 then begin
      T := GetTickCount - T;
      Memo1.Lines.Add(IntToStr(I) + ':' + IntToStr(T));
      T := GetTickCount;
    end;
  end;
{$else}
  while not AdoQuery1.Eof do begin
    AdoQuery1.Next;
    Inc(I);
    if I mod Step = 0 then begin
      T := GetTickCount - T;
      Memo1.Lines.Add(IntToStr(I) + ':' + IntToStr(T));
      T := GetTickCount;
    end;
  end;
{$endif}
  if cbDisableControls.Checked then
    AdoQuery1.EnableControls;
  Memo1.Lines.EndUpdate;
end;

我得到了以下结果(除非特别指定,否则没有调用 DisableControls):

Using CursorLocation = clUseClient

AdoQuery.Next   AdoQuery.RecordSet    AdoQuery.Next 
                .MoveNext             + DisableControls

4000:157            4000:16             4000:15
8000:453            8000:16             8000:15
12000:687           12000:0             12000:32
16000:969           16000:15            16000:31
20000:1250          20000:16            20000:31
24000:1500          24000:0             24000:16
28000:1703          28000:15            28000:31
32000:1891          32000:16            32000:31
36000:2187          36000:16            36000:16
40000:2438          40000:0             40000:15
44000:2703          44000:15            44000:31
48000:3203          48000:16            48000:32

=======================================

Using CursorLocation = clUseServer

AdoQuery.Next   AdoQuery.RecordSet    AdoQuery.Next 
                .MoveNext             + DisableControls

4000:1031           4000:454            4000:563
8000:1016           8000:468            8000:562
12000:1047          12000:469           12000:500
16000:1234          16000:484           16000:532
20000:1047          20000:454           20000:546
24000:1063          24000:484           24000:547
28000:984           28000:531           28000:563
32000:906           32000:485           32000:500
36000:1016          36000:531           36000:578
40000:1000          40000:547           40000:500
44000:968           44000:406           44000:562
48000:1016          48000:375           48000:547

调用 AdoQuery1.Recordset.MoveNext 直接调用 MDac/ADO 层,而 AdoQuery1.Next 则涉及标准 TDataSet 模型的所有开销。正如 Serge Kraikov 所说,更改 CursorLocation 肯定会产生差异,并且不会出现我们注意到的减速,尽管显然它比使用 clUseClient 并调用 DisableControls 慢得多。我想这取决于你要做什么,是否可以利用使用 clUseClient 与 RecordSet.MoveNext 的额外速度优势。

非常感谢,DisableControls 对我很有用。但是与你的结果不同,在这里 clUseServer 并不比 clUseClient 慢。尽管将 CursorLocation 设置为 clUseServer 后数据集并没有返回任何记录,除非我将 LockType 设置为 ltReadOnly - saastn
@MartynA 出于好奇,你用的是哪个分析器? - Christian Holm Jørgensen
@ChristianHolmJørgensen:我使用了Nexus Quality Suite(www.nexusdb.com)的行分析器,它是旧的Turbopower产品的重生版本。 - MartynA

1

DAO是Access原生支持的,而且(在我看来)通常更快。 无论您是否切换,都可以使用GetRows方法。DAO和ADO都支持它。 没有循环。您可以使用几行代码将整个记录集转储到数组中。代码如下: yourrecordset.MoveLast yourrecordset.MoveFirst yourarray = yourrecordset.GetRows(yourrecordset.RecordCount)


也许吧,但 OP 正在询问 Delphi 代码,而在 Delphi 中,你通常不会使用 db 记录数组。 - MartynA
谢谢MartynA。我对Delphi一无所知,但是认为它可能与其他语言具有类似的结构。 - AVG
好的,它可以拥有它们(只需声明适当类型的数组),但这不是“Delphi”的做事方式。关键在于,在Delphi中,所有支持的数据集类型都是一个祖先(TDataset)的后代,其中包含一个具有可移动逻辑光标的数据集的概括模型。而且,所有其db-aware控件都是设计与此模型交互,而不是数组。其结果是,所有其db-aware控件都可以与任何支持的TDataset后代一起使用。 - MartynA

1
当您打开一个表时,ADO数据集在内部创建特殊的数据结构来前后导航数据集 - “数据集游标”。在导航期间,ADO存储已访问记录的列表以提供双向导航。似乎ADO游标代码使用二次时间O(n2)算法来存储此列表。但是有解决方法 - 使用服务器端游标:
Table.CursorLocation := clUseServer;  

我使用此修复程序测试了您的代码,并获得了线性获取时间 - 获取每个下一个记录块所需的时间与先前相同。
PS:其他一些数据访问库提供了特殊的“单向”数据集 - 这些数据集只能向前遍历,甚至不会存储已遍历的记录 - 您可以获得恒定的内存消耗和线性的获取时间。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接