2015-11-27 3 views
9

Я хочу прочитать всю таблицу из файла MS Access, и я стараюсь сделать это как можно быстрее. При тестировании большой выборки я обнаружил, что счетчик циклов увеличивается быстрее, когда он считывает верхние записи по сравнению с последними записями таблицы. Вот пример кода, который демонстрирует это:Почему прокрутка через ADOTable замедляется и замедляется?

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

В таблице есть две строковые поля, один двойной и одно целое. Он не имеет первичного ключа и поля индекса. Почему это происходит и как я могу это предотвратить?

+0

Нет. Я создаю управление программно, нет ничего более того, что вы можете увидеть в примере кода. – saastn

+0

Разве это не для вас? В любом случае, вы удивлены тем, что если вы читаете много записей, это связано с большим количеством распределений памяти, и что это занимает больше времени, чем больше выделяется память? – MartynA

+0

@MartynA Вы правы насчет цикла. Но я не могу сказать, что это распределение памяти, которое делает его медленнее. Похоже, что он извлекает все записи в «Table.Open», диспетчер задач не отображает выделение памяти после запуска этой строки. – saastn

ответ

17

Я могу воспроизвести ваши результаты с помощью AdoQuery с набором данных MS Sql Server такого же размера, как ваш.

Однако, сделав немного профилирования строк, я думаю, что нашел ответ на этот вопрос, и он немного противоречит интуиции. Я уверен, что все, кто делает DB-программирование в Delphi, используют идею о том, что цикл через набор данных имеет тенденцию быть намного быстрее, если вы окружите цикл вызовами Disable/EnableControls. Но кто бы это сделал, если нет элементов управления db, прикрепленных к набору данных?

Хорошо, оказывается, что в вашей ситуации, даже если нет элементов управления, поддерживающих DB, скорость увеличивается очень сильно, если вы используете Disable/EnableControls независимо.

Причина заключается в том, что TCustomADODataSet.InternalGetRecord в AdoDB.Pas содержит следующее:

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

и по моей линии профилировщика пока не AdoQuery1.Eof делать AdoQuery1.Next цикл тратит 98,8% своего времени выполнения присвоение

 RecordNumber := Recordset.AbsolutePosition; 

! Расчет Recordset.AbsolutePosition скрыт, конечно, на «неправильной стороне» интерфейса Recordset, но тот факт, что время его вызова, по-видимому, увеличивается, чем дальше вы переходите в набор записей, поэтому разумно предположить, что он рассчитан путем подсчета с начала данных набора записей.

Конечно, ControlsDisabled возвращает true, если 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 не называется, за исключением особо оговоренных случаев):

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 . Как сказал Сергей Крайев, изменение CursorLocation, безусловно, имеет значение и не показывает замедление, которое мы заметили, хотя, очевидно, это значительно медленнее, чем использование clUseClient и вызов DisableControls. Я полагаю, это зависит от того, что вы пытаетесь сделать, можете ли вы использовать дополнительную скорость использования clUseClient с помощью RecordSet.MoveNext.

+0

Большое спасибо, 'DisableControls' работал для меня. Но, в отличие от ваших результатов, 'clUseServer' не медленнее, чем' clUseClient' здесь. Хотя, набор данных не возвращает никаких записей после установки 'CursorLocation' в' clUseServer', если я не установил 'LockType' в' ltReadOnly'. – saastn

+0

@MartynA из любопытства, какой профайлер вы использовали? –

+0

@ ChristianHolmJørgensen: Я использовал профилировщик Nexus Quality Suite (www.nexusdb.com), который является перевоплощением старого продукта Turbopower с аналогичным названием. – MartynA

1

Когда вы открываете таблицу, набор данных ADO внутренне создает специальные структуры данных для навигации по набору данных вперед/назад - «набор данных CURSOR». Во время навигации ADO сохраняет список уже посещенных записей, чтобы обеспечить двунаправленную навигацию.
Кажется, что код курсора ADO использует алгоритм O (n2) с квадратичным временем для хранения этого списка.
Но есть обходной путь - использование серверного курсора:

Table.CursorLocation := clUseServer; 

Я тестировал код с помощью исправления и получить линейное время выборки - выборка каждый следующий фрагмент записей занимает столько же времени, как и предыдущие.

PS Некоторые другие библиотеки доступа к данным предоставляют специальные «однонаправленные» наборы данных - эти наборы данных могут перемещаться только вперед и даже не хранить уже пройденные записи - вы получаете постоянное потребление памяти и время линейной выборки.

1

DAO является родным для доступа и (IMHO), как правило, быстрее. Независимо от того, переключитесь или нет, используйте метод GetRows. Оба DAO и ADO поддерживают его. Нет циклов. Вы можете сбросить весь набор записей в массив с несколькими строками кода. Воздушный код: yourrecordset.MoveLast yourrecordset.MoveFirst yourarray = yourrecordset.GetRows(yourrecordset.RecordCount)

+0

Возможно, но OP задает вопрос о коде Delphi, а в Delphi обычно не работает массив массивов db-записей. – MartynA

+0

Спасибо MartynA. Я ничего не знаю о Delphi, но подумал, что он может иметь сходные структуры с другими языками. – AVG

+0

Ну, это * может * иметь их (просто объявив массив подходящего типа), но это просто не "Delphi" способ делать вещи. Дело в том, что в Delphi все поддерживаемые типы наборов данных являются потомками одного предка (TDataset), который содержит обобщенную модель набора данных с подвижным логическим курсором. И все его элементы управления db предназначены для взаимодействия с этой моделью, а не с массивами. Следствием этого является то, что все его элементы управления, поддерживающие db, работают с любым поддерживаемым потомком TDataset. – MartynA