2015-11-27 35 views
9

Voglio leggere l'intera tabella da un file MS Access e sto cercando di farlo il più velocemente possibile. Durante il test di un grande campione ho scoperto che il contatore di loop aumenta più velocemente quando legge i record migliori rispetto agli ultimi record della tabella. Ecco un esempio di codice che illustra questo:Perché lo scorrimento su ADOTable rallenta e rallenta?

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; 

E il risultato sul mio PC: enter image description here

La tabella ha due campi di stringa, una matrimoniale e un intero. Non ha chiave primaria né campo indice. Perché succede e come posso prevenirlo?

+0

No. Sto creando il controllo a livello di codice, non c'è niente di più di quello che puoi vedere nel codice di esempio. – saastn

+0

Il tuo ciclo For non è di uno? Ad ogni modo, sei sorpreso che se leggi molti record, questo comporta un sacco di allocazioni di memoria, e che richiedono più tempo e più memoria viene allocata? – MartynA

+0

@MartynA Hai ragione riguardo al ciclo. Ma non posso dire che sia l'allocazione di memoria che lo rende più lento. Sembra che recuperi tutti i record in "Table.Open', Task Manager non mostra allocazione di memoria dopo aver eseguito quella riga. – saastn

risposta

17

Posso riprodurre i risultati utilizzando un AdoQuery con un set di dati MS Sql Server di dimensioni simili al tuo.

Tuttavia, dopo aver eseguito un po 'di profilazione di linea, penso di aver trovato la risposta a questo, ed è leggermente contro-intuitivo. Sono sicuro che tutti coloro che eseguono la programmazione dei DB in Delphi sono abituati all'idea che il looping di un set di dati tende ad essere molto più veloce se si circonda il loop tramite chiamate a Disable/EnableControls. Ma chi si preoccuperebbe di farlo se non ci sono controlli db-aware collegati al set di dati?

Bene, si scopre che nella tua situazione, anche se non ci sono controlli compatibili con DB, la velocità aumenta notevolmente se si utilizza Disabilita/Abilita controlli indipendentemente.

La ragione è che in TCustomADODataSet.InternalGetRecord AdoDB.Pas contiene questo:

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

e secondo la mia linea di profiler, il pur non AdoQuery1.Eof fare AdoQuery1.Next ciclo spende il 98,8% del suo tempo in esecuzione l'incarico

 RecordNumber := Recordset.AbsolutePosition; 

! Il calcolo di Recordset.AbsolutePosition è nascosto, ovviamente, sul "lato sbagliato" dell'interfaccia Recordset, ma il fatto che il tempo di chiamarlo apparentemente aumenta man mano che si va nel recordset è ragionevole ipotizzare che sia calcolato contando dall'inizio dei dati del recordset.

Ovviamente, ControlsDisabled restituisce true se DisableControls è stato chiamato e non annullato da una chiamata a EnableControls. Quindi, ripeti il ​​ciclo circondato da Disable/EnableControls e speriamo che otterrai un risultato simile al mio. Sembra che tu avessi ragione che il rallentamento non è legato alle allocazioni di memoria.

utilizzando il seguente codice:

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; 

io ottenere i seguenti risultati (con DisableControls nonchiamato tranne dove indicato):

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 chiamate direttamente nello strato di MDAC/ADO , del corso , mentre AdoQuery1.Next riguarda tutto il sovraccarico del modello standard TDataSet . Come ha detto Serge Kraikov, cambiare CursorLocation fa certamente la differenza e non mostra il rallentamento che abbiamo notato, anche se ovviamente è molto più lento dell'utilizzo di clUseClient e chiama DisableControls. Suppongo che dipenda esattamente da cosa stai cercando di fare se puoi sfruttare la maggiore velocità di utilizzo di clUseClient con RecordSet.MoveNext.

+0

Grazie mille, 'DisableControls' ha funzionato per me. Ma, diversamente dai risultati, 'clUseServer' non è più lento di' clUseClient' qui. Sebbene, il set di dati non restituisca alcun record dopo aver impostato 'CursorLocation' su' clUseServer', a meno che non abbia impostato 'LockType' su' ltReadOnly'. – saastn

+0

@ MartynA per curiosità, quale profiler hai usato? –

+0

@ ChristianHolmJørgensen: Ho usato il line profiler di Nexus Quality Suite (www.nexusdb.com) che è una reincarnazione del vecchio prodotto Turbopower con un nome simile. – MartynA

1

Quando si apre una tabella, il set di dati ADO crea internamente strutture dati speciali per navigare il set di dati in avanti/indietro - "set di dati CURSOR". Durante la navigazione, ADO memorizza l'elenco dei record già visitati per fornire la navigazione bidirezionale.
Sembra che il codice del cursore ADO utilizza l'algoritmo O (n2) quadratico-tempo per memorizzare questo elenco.
Ma ci sono soluzione - cursore di utilizzo sul lato server:

Table.CursorLocation := clUseServer; 

Ho provato il codice utilizzando questa correzione e ottenere lineare prendere tempo - andare a prendere ogni successivo pezzo di record impiega lo stesso tempo come precedente.

PS Alcune altre librerie di accesso ai dati forniscono speciali insiemi di dati "unidirezionali" - questi set di dati possono attraversare solo l'inoltro e non memorizzare nemmeno i record già attraversati - si ottengono consumi di memoria costanti e tempo di recupero lineare.

1

DAO è nativo per Access e (IMHO) è in genere più veloce. Se si cambia o meno, utilizzare il metodo GetRows. Sia DAO che ADO lo supportano. Non ci sono cicli. È possibile scaricare l'intero recordset in una matrice con un paio di righe di codice. Codice aereo: yourrecordset.MoveLast yourrecordset.MoveFirst yourarray = yourrecordset.GetRows(yourrecordset.RecordCount)

+0

Forse, ma l'OP sta chiedendo del codice Delphi, e in Delphi, in genere non si lavora di array di record db. – MartynA

+0

Grazie MartynA. Non so nulla di Delphi, ma ho pensato che potrebbe avere strutture simili ad altre lingue. – AVG

+0

Beh, * può * averli (solo dichiarando una matrice di tipo adatto) ma non è solo il modo "Delphi" di fare le cose. Il punto è, in Delphi, tutti i tipi di set di dati supportati sono i discendenti di un antenato (TDataset) che contiene un modello generalizzato di un set di dati con cursore logico mobile. E tutti i suoi controlli db-aware sono progettati per interagire con questo modello, piuttosto che con gli array. Una conseguenza è che tutti i suoi controlli db-aware funzionano con qualsiasi discendente TDataset supportato. – MartynA