2015-11-27 102 views
9

Chcę przeczytać całą tabelę z pliku MS Access i próbuję zrobić to tak szybko, jak to możliwe. Podczas testowania dużej próbki odkryłem, że licznik pętli rośnie szybciej, gdy odczytuje najlepsze rekordy w porównaniu do ostatnich rekordów tabeli. Oto przykładowy kod, który demonstruje w ten sposób:Dlaczego przewijanie przez ADOTable działa wolniej i wolniej?

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; 

A wynik na moim komputerze: enter image description here

Stół posiada dwa pola ciągów, jedno podwójne i jedno całkowitą. Nie ma klucza głównego ani pola indeksu. Dlaczego tak się dzieje i jak mogę temu zapobiec?

+0

Nie. Programuję programowo, nie ma nic więcej niż to, co można zobaczyć w przykładowym kodzie. – saastn

+0

Czy Twoja pętla For nie jest wyłączona? W każdym razie, czy jesteś zaskoczony, że jeśli czytasz wiele rekordów, wiąże się to z dużą ilością alokacji pamięci, a to trwa dłużej, im więcej pamięci zostanie przydzielone? – MartynA

+0

@MartynA Masz rację co do pętli. Ale nie mogę powiedzieć, że alokacja pamięci sprawia, że ​​jest wolniejsza. Wygląda na to, że pobiera wszystkie rekordy w 'Table.Open', Menedżer zadań nie pokazuje przydziału pamięci po uruchomieniu tej linii. – saastn

Odpowiedz

17

Mogę odtworzyć wyniki za pomocą AdoQuery przy użyciu zestawu danych MS Sql Server podobnego rozmiaru do twojego.

Jednak po zrobieniu trochę profilowania linii, myślę, że znalazłem odpowiedź na to, i to jest trochę sprzeczne z intuicją. Jestem pewien, że każdy, kto wykonuje programowanie w Delphi w wersji , jest przyzwyczajony do tego, że przechodzenie przez zbiór danych jest znacznie szybsze, jeśli otoczysz pętlę wywołaniami funkcji Disable/EnableControls. Ale kto by to zawracał sobie głowę, gdyby do zestawu danych nie dołączono elementów sterujących db-aware?

Cóż, okazuje się, że w twojej sytuacji, nawet jeśli nie ma kontroli DB-aware, prędkość wzrasta ogromnie, jeśli użyjesz Disable/EnableControls niezależnie.

Powodem jest to, że TCustomADODataSet.InternalGetRecord w AdoDB.Pas zawiera to:

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

i zgodnie z moim linii profilera, póki nie AdoQuery1.Eof zrobić AdoQuery1.Next pętlę spędza 98,8% swojego czasu wykonywania zadanie

 RecordNumber := Recordset.AbsolutePosition; 

! Obliczanie Recordset.AbsolutePosition jest ukryte, oczywiście, po "złej stronie" interfejsu Recordset, ale fakt, że czas na wywołanie go wyraźnie zwiększa się, gdy idziesz dalej do zestawu rekordów, sprawia, że ​​rozsądnie jest spekulować, że jest on obliczony przez odliczanie od początku danych zestawu rekordów.

Oczywiście zwraca wartość true, jeśli została wywołana DisableControls i nie została cofnięta przez połączenie z EnableControls. Tak więc, powtórz test z pętlą otoczoną przez Disable/EnableControls i miejmy nadzieję, że uzyskasz podobny wynik do mojego. Wygląda na to, że masz rację, że spowolnienie nie ma związku z przydzielaniem pamięci.

Korzystanie poniższy kod:

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; 

uzyskać następujące wyniki (z DisableControls nie nazywa ile nie zaznaczono inaczej):

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 

Wywołanie AdoQuery1.Recordset.MoveNext połączenia bezpośrednio do warstwy MDAC/ADO z kursu , natomiast AdoQuery1.Next obejmuje cały narzut standardowego modelu TDataSet . Jak powiedział Serge Kraikov, zmiana CursorLocation z pewnością robi różnicę i nie wykazuje spowolnienia, które zauważyliśmy, chociaż oczywiście jest znacznie wolniejsza niż użycie clUseClient i wywoływanie DisableControls. Przypuszczam, że zależy to dokładnie od tego, co próbujesz zrobić, czy możesz skorzystać z dodatkowej szybkości używania clUseClient z RecordSet.MoveNext.

+0

Dziękuję bardzo, "DisableControls" pracował dla mnie. Ale w przeciwieństwie do twoich wyników 'clUseServer' nie jest tu wolniejszy niż' clUseClient'. Chociaż zestaw danych nie zwraca żadnych rekordów po ustawieniu 'CursorLocation' na' clUseServer', chyba że ustawię 'LockType' na' ltReadOnly'. – saastn

+0

@MartynA z ciekawości, jakiego profilera użyłeś? –

+0

@ ChristianHolmJørgensen: Użyłem profilera liniowego Nexus Quality Suite (www.nexusdb.com), który jest reinkarnacją starego produktu Turbopower o podobnej nazwie. – MartynA

1

Po otwarciu tabeli, zestaw danych ADO wewnętrznie tworzy specjalne struktury danych, aby nawigować w zestawie danych do przodu/do tyłu - "zestaw danych CURSOR". Podczas nawigacji ADO przechowuje listę już odwiedzonych rekordów, aby zapewnić dwukierunkową nawigację.
Wygląda na to, że kod kursora ADO używa algorytmu O (n2) w czasie kwadratu do zapisania tej listy.
Ale istnieje obejście - stosowanie po stronie serwera kursor:

Table.CursorLocation := clUseServer; 

testowałem kodu przy użyciu tej poprawki i uzyskać liniowy sprowadzić czas - pobieranie każdy następny kawałek zapisów trwa tyle samo czasu jak poprzednie.

PS Niektóre inne biblioteki dostępu do danych udostępniają specjalne "jednokierunkowe" zbiory danych - te zbiory danych mogą przechodzić tylko do przodu i nie przechowują już przechodzących rekordów - uzyskujesz stałe zużycie pamięci i liniowy czas pobierania.

1

DAO pochodzi z Access i (IMHO) jest zwykle szybszy. Niezależnie od tego, czy się przełączasz, użyj metody GetRows. Obsługują go zarówno DAO, jak i ADO. Nie ma pętli. Możesz zrzucić cały zestaw rekordów do tablicy z kilkoma liniami kodu. Kod powietrza: yourrecordset.MoveLast yourrecordset.MoveFirst yourarray = yourrecordset.GetRows(yourrecordset.RecordCount)

+0

Być może, ale OP pyta o kod Delphi, aw Delphi zwykle nie pracujesz z tablicami rekordów db. – MartynA

+0

Dzięki MartynA. Nic nie wiem o Delphi, ale pomyślałem, że może mieć podobne struktury do innych języków. – AVG

+0

Cóż, to * może * mieć je (po prostu deklarując tablicę odpowiedniego typu), ale to nie jest po prostu sposób na "Delphi" robienia rzeczy. Chodzi o to, że w Delphi wszystkie obsługiwane typy zbiorów danych są potomkami jednego przodka (TDataset), który zawiera uogólniony model zbioru danych z ruchomym logicznym kursorem. Wszystkie kontrolki db-aware są zaprojektowane do interakcji z tym modelem, a nie z tablicami. Konsekwencją jest to, że wszystkie kontrolki db-aware działają z każdym obsługiwanym potomkiem TDataset. – MartynA