2015-11-27 14 views
9

Tôi muốn đọc toàn bộ bảng từ một tệp MS Access và tôi đang cố gắng làm điều đó càng nhanh càng tốt. Khi thử nghiệm một mẫu lớn tôi thấy rằng bộ đếm vòng lặp tăng nhanh hơn khi nó đọc các bản ghi hàng đầu so với các bản ghi cuối cùng của bảng. Dưới đây là một mẫu mã mà chứng tỏ điều này:Tại sao việc di chuyển qua ADOTable trở nên chậm hơn và chậm hơn?

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; 

Và kết quả trên máy tính của tôi: enter image description here

Bảng này có hai lĩnh vực chuỗi, một đôi và một số nguyên. Nó không có khóa chính và trường chỉ mục. Tại sao nó xảy ra và làm thế nào tôi có thể ngăn chặn nó?

+0

Không. Tôi đang tạo điều khiển theo chương trình, không có gì nhiều hơn những gì bạn có thể thấy trong mã mẫu. – saastn

+0

Không phải là vòng lặp For của bạn không? Dù sao, bạn có ngạc nhiên rằng nếu bạn đọc rất nhiều hồ sơ, nó liên quan đến rất nhiều phân bổ bộ nhớ, và rằng những mất nhiều thời gian hơn bộ nhớ được phân bổ? – MartynA

+0

@MartynA Bạn nói đúng về vòng lặp. Nhưng tôi không thể nói rằng đó là phân bổ bộ nhớ mà làm cho nó chậm hơn. Có vẻ như nó nạp tất cả các bản ghi trong 'Table.Open', Trình quản lý Tác vụ sẽ không hiển thị cấp phát bộ nhớ sau khi chạy dòng đó. – saastn

Trả lời

17

Tôi có thể sao chép kết quả của bạn bằng cách sử dụng AdoQuery với tập dữ liệu MS Sql Server có kích thước tương tự như của bạn.

Tuy nhiên, sau khi thực hiện một chút dòng định dạng, tôi nghĩ rằng tôi đã tìm thấy câu trả lời cho điều này và nó hơi phản trực giác. Tôi chắc rằng tất cả những ai thực hiện lập trình DB trong Delphi được sử dụng cho ý tưởng rằng việc lặp qua một tập dữ liệu có xu hướng nhanh hơn nhiều nếu bạn bao quanh vòng lặp bằng các cuộc gọi tới Disable/EnableControls. Nhưng ai sẽ làm phiền điều đó nếu không có các điều khiển nhận biết db gắn liền với tập dữ liệu?

Vâng, nó chỉ ra rằng trong tình huống của bạn, mặc dù không có điều khiển nhận thức DB, tốc độ tăng lên rất nhiều nếu bạn sử dụng Disable/EnableControls bất kể.

Lý do là TCustomADODataSet.InternalGetRecord trong AdoDB.Pas chứa này:

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

và theo hồ sơ dòng của tôi, trong khi không AdoQuery1.Eof làm AdoQuery1.Next loop dành 98,8% thời gian của mình thực hiện bài tập

 RecordNumber := Recordset.AbsolutePosition; 

! Các tính toán của Recordset.AbsolutePosition được ẩn, tất nhiên, trên "bên sai" của giao diện Recordset, nhưng thực tế là thời gian để gọi nó dường như tăng hơn nữa bạn đi vào recordset làm cho nó hợp lý imo để suy đoán rằng nó được tính toán bằng cách đếm từ đầu dữ liệu của recordset.

Tất nhiên, ControlsDisabled trả về true nếu DisableControls đã được gọi và không được hoàn tác bằng một cuộc gọi đến EnableControls. Vì vậy, hãy thử lại với vòng lặp được bao quanh bởi Disable/EnableControls và hy vọng bạn sẽ nhận được một kết quả tương tự như của tôi. Có vẻ như bạn đã đúng rằng sự chậm lại không liên quan đến phân bổ bộ nhớ.

Sử dụng đoạn mã sau:

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; 

tôi nhận được kết quả như sau (với DisableControls không gọi là trừ khi có ghi chú):

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 

Calling AdoQuery1.Recordset.MoveNext cuộc gọi trực tiếp vào lớp MDAC/ADO , trong số khóa học, trong khi AdoQuery1.Next liên quan đến tất cả chi phí của mô hình TDataSet tiêu chuẩn. Như Serge Kraikov đã nói, việc thay đổi CursorLocation chắc chắn tạo ra sự khác biệt và không thể hiện sự chậm lại mà chúng ta nhận thấy, mặc dù rõ ràng nó chậm hơn đáng kể so với việc sử dụng clUseClient và gọi DisableControls. Tôi cho rằng nó phụ thuộc vào chính xác những gì bạn đang cố gắng để làm cho dù bạn có thể tận dụng lợi thế của tốc độ thêm của việc sử dụng clUseClient với RecordSet.MoveNext.

+0

Cảm ơn bạn rất nhiều, 'DisableControls' đã làm việc cho tôi. Tuy nhiên, không giống như kết quả của bạn, 'clUseServer' không chậm hơn' clUseClient' ở đây. Mặc dù, tập dữ liệu không trả về bất kỳ bản ghi nào sau khi đặt 'CursorLocation' thành' clUseServer', trừ khi tôi đặt 'LockType' thành' ltReadOnly'. – saastn

+0

@MartynA vì tò mò, bạn đã sử dụng profiler nào? –

+0

@ ChristianHolmJørgensen: Tôi đã sử dụng trình biên dạng dòng của Suite Chất lượng Nexus (www.nexusdb.com), đây là một sự tái sinh của sản phẩm Turbopower cũ có tên tương tự. – MartynA

1

Khi bạn mở bảng, bộ dữ liệu ADO sẽ tạo cấu trúc dữ liệu đặc biệt trong nội bộ để điều hướng tập dữ liệu về phía trước/phía sau - "tập dữ liệu CURSOR". Trong khi điều hướng, ADO lưu trữ danh sách các bản ghi đã truy cập để cung cấp điều hướng hai hướng.
Dường như mã con trỏ ADO sử dụng thuật toán O (n2) bậc hai để lưu trữ danh sách này.
Nhưng có những cách giải quyết khác - sử dụng server-side con trỏ:

Table.CursorLocation := clUseServer; 

Tôi đã thử nghiệm mã của bạn sử dụng sửa chữa này và nhận được tuyến tính lấy thời gian - lấy mỗi đoạn tiếp theo của hồ sơ mất thời gian tương tự như trước.

PS Một số thư viện truy cập dữ liệu khác cung cấp bộ dữ liệu "một chiều" đặc biệt - bộ dữ liệu này chỉ có thể chuyển tiếp và thậm chí không lưu trữ các bản ghi đã được duyệt qua - bạn nhận được mức tiêu thụ bộ nhớ liên tục và thời gian tìm nạp tuyến tính.

1

DAO có nguồn gốc là Truy cập và (IMHO) thường nhanh hơn. Bạn có chuyển đổi hay không, sử dụng phương thức GetRows. Cả DAO và ADO đều hỗ trợ nó. Không có vòng lặp. Bạn có thể đổ toàn bộ recordset vào một mảng với một vài dòng mã. Mã không khí: yourrecordset.MoveLast yourrecordset.MoveFirst yourarray = yourrecordset.GetRows(yourrecordset.RecordCount)

+0

Có thể, nhưng OP hỏi về mã Delphi và trong Delphi, bạn không thường làm việc với các mảng bản ghi db. – MartynA

+0

Cảm ơn MartynA. Tôi không biết gì về Delphi, nhưng chỉ nghĩ rằng nó có thể có cấu trúc tương tự với các ngôn ngữ khác. – AVG

+0

Vâng, nó * có thể * có chúng (chỉ bằng cách khai báo một mảng kiểu phù hợp) nhưng nó không chỉ là cách "Delphi" làm việc. Vấn đề là ở Delphi, tất cả các kiểu tập dữ liệu được hỗ trợ đều là hậu duệ của một tổ tiên (TDataset) có chứa một mô hình tổng quát của tập dữ liệu với con trỏ logic có thể di chuyển được. Và tất cả các điều khiển nhận dạng db của nó được thiết kế để tương tác với mô hình này, chứ không phải là các mảng. Kết quả là tất cả các điều khiển nhận biết db của nó hoạt động với bất kỳ hậu duệ TDataset nào được hỗ trợ. – MartynA

Các vấn đề liên quan