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

时间:2015-11-27 21:16:38

标签: delphi ms-access ado delphi-xe5

我想从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

该表有两个字符串字段,一个double和一个整数。它没有主键也没有索引字段。为什么会发生这种情况?如何防止它?

3 个答案:

答案 0 :(得分:18)

我可以使用AdoQuery重现您的结果,其中MS Sql Server数据集的大小与您的相似。

然而,在做了一些线条剖析之后,我想我已经找到了答案,这有点违反直觉。我确信每个人都这样做 Delphi中的数据库编程用于这样的想法,即如果通过调用Disable / EnableControls来环绕循环,则循环数据集的速度会快得多。但是,如果没有数据集附加数据集感知控件,那谁还会这么做呢?

嗯,事实证明,在您的情况下,即使没有DB-aware控件,如果您使用Disable / EnableControls,速度也会大大增加。

原因是AdoDB.Pas中的TCustomADODataSet.InternalGetRecord包含:

      if ControlsDisabled then
        RecordNumber := -2 else
        RecordNumber := Recordset.AbsolutePosition;

根据我的行分析器,而不是AdoQuery1.Eof做AdoQuery1.Next循环花费98.8%的时间执行赋值

        RecordNumber := Recordset.AbsolutePosition;

! Recordset.AbsolutePosition的计算当然隐藏在"错误的一面"对于Recordset界面,但事实上,调用它的时间显然会增加你进入记录集的距离,这使得通过从记录集的数据开始计算它来计算它是合理的。

当然,ControlsDisabled如果已调用DisableControls而未通过调用EnableControls撤消,则返回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要慢得多。我想这取决于你是否可以利用在RecordSet.MoveNext中使用clUseClient的额外速度来确切地尝试做什么。

答案 1 :(得分:1)

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

Table.CursorLocation := clUseServer;  

我使用此修复程序测试了您的代码并获得线性提取时间 - 获取每个下一个记录块的时间与之前相同。

PS一些其他数据访问库提供特殊的"单向"数据集 - 此数据集只能向前遍历,甚至不存储已经遍历的记录 - 您将获得恒定的内存消耗和线性提取时间。

答案 2 :(得分:1)

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