为什么在 ADOTable 中滚动会变得越来越慢?
Why does scrolling through ADOTable get slower and slower?
我想从一个 MS Access 文件中读取整个 table,并且我正在尝试尽快完成。在测试大样本时,我发现循环计数器在读取顶部记录时比 table 的最后记录增加得更快。下面是演示这一点的示例代码:
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;
我电脑上的结果是:
table 有两个字符串字段,一个双精度和一个整数。它没有主键也没有索引字段。为什么会发生,我该如何预防?
当您打开 table 时,ADO 数据集会在内部创建特殊的数据结构来导航数据集 forward/backward - "dataset CURSOR"。在导航期间,ADO 存储已访问记录的列表以提供双向导航。
似乎 ADO 游标代码使用二次时间 O(n2) 算法来存储此列表。
但有解决方法——使用服务器端游标:
Table.CursorLocation := clUseServer;
我使用此修复程序测试了您的代码并获得了线性提取时间 - 提取下一个记录块所需的时间与之前的相同。
PS 其他一些数据访问库提供了特殊的 "unidirectional" 数据集——这些数据集只能向前遍历,甚至不存储已经遍历的记录——你会得到恒定的内存消耗和线性获取时间。
我可以使用 AdoQuery 和 MS Sql 大小与您相似的服务器数据集重现您的结果。
然而,在做了一些线路分析之后,我想我已经找到了这个问题的答案,而且它有点违反直觉。我敢肯定每个这样做的人
Delphi 中的 DB 编程习惯于这样的想法,即如果通过调用 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 的计算是隐藏的,当然,在 Recordset 接口的 "wrong side" 上,但是调用它的时间显然随着你进入记录集的距离而增加这一事实使我有理由推测它是通过从记录集的数据开始计算的。
当然,ControlsDisabled
returns 如果 DisableControls
已被调用且未通过调用 EnableControls
撤消,则为真。所以,用 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 not 调用,除非另有说明):
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.
一起使用的额外速度
DAO 是 Access 原生的,(恕我直言)通常速度更快。
无论是否切换,都使用 GetRows 方法。 DAO 和 ADO 都支持它。
没有循环。您可以使用几行代码将整个记录集转储到一个数组中。空运代码:
yourrecordset.MoveLast
yourrecordset.MoveFirst
yourarray = yourrecordset.GetRows(yourrecordset.RecordCount)
我想从一个 MS Access 文件中读取整个 table,并且我正在尝试尽快完成。在测试大样本时,我发现循环计数器在读取顶部记录时比 table 的最后记录增加得更快。下面是演示这一点的示例代码:
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;
我电脑上的结果是:
table 有两个字符串字段,一个双精度和一个整数。它没有主键也没有索引字段。为什么会发生,我该如何预防?
当您打开 table 时,ADO 数据集会在内部创建特殊的数据结构来导航数据集 forward/backward - "dataset CURSOR"。在导航期间,ADO 存储已访问记录的列表以提供双向导航。
似乎 ADO 游标代码使用二次时间 O(n2) 算法来存储此列表。
但有解决方法——使用服务器端游标:
Table.CursorLocation := clUseServer;
我使用此修复程序测试了您的代码并获得了线性提取时间 - 提取下一个记录块所需的时间与之前的相同。
PS 其他一些数据访问库提供了特殊的 "unidirectional" 数据集——这些数据集只能向前遍历,甚至不存储已经遍历的记录——你会得到恒定的内存消耗和线性获取时间。
我可以使用 AdoQuery 和 MS Sql 大小与您相似的服务器数据集重现您的结果。
然而,在做了一些线路分析之后,我想我已经找到了这个问题的答案,而且它有点违反直觉。我敢肯定每个这样做的人 Delphi 中的 DB 编程习惯于这样的想法,即如果通过调用 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 的计算是隐藏的,当然,在 Recordset 接口的 "wrong side" 上,但是调用它的时间显然随着你进入记录集的距离而增加这一事实使我有理由推测它是通过从记录集的数据开始计算的。
当然,ControlsDisabled
returns 如果 DisableControls
已被调用且未通过调用 EnableControls
撤消,则为真。所以,用 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 not 调用,除非另有说明):
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.
DAO 是 Access 原生的,(恕我直言)通常速度更快。
无论是否切换,都使用 GetRows 方法。 DAO 和 ADO 都支持它。
没有循环。您可以使用几行代码将整个记录集转储到一个数组中。空运代码:
yourrecordset.MoveLast
yourrecordset.MoveFirst
yourarray = yourrecordset.GetRows(yourrecordset.RecordCount)