Производительность Delphi: чтение всех значений в поле набора данных
Мы пытаемся найти некоторые исправления производительности, читающие из TADOQuery. В настоящее время мы перебираем записи с помощью метода "while not Q.eof do begin ... Q.next". Для каждого мы читаем ID и значение каждой записи и добавляем каждый в список выпадающих списков.
Есть ли способ преобразовать все значения указанного поля в список за один снимок? Вместо того, чтобы перебирать набор данных? Было бы очень удобно, если бы я мог сделать что-то вроде...
TStrings(MyList).Assign(Q.ValuesOfField['Val']);
Я знаю, что это не настоящая команда, но это концепция, которую я ищу. Ищем быстрый ответ и решение (как всегда, но это для того, чтобы решить действительно актуальную проблему производительности).
8 ответов
Есть некоторые отличные предложения по производительности, сделанные другими людьми, которые вы должны реализовать в Delphi. Вы должны рассмотреть их. Я сосредоточусь на ADO.
Вы не указали, что такое сервер баз данных, поэтому я не могу быть слишком конкретным, но есть некоторые вещи, которые вы должны знать об ADO.
ADO RecordSet
В ADO есть объект RecordSet. В данном случае этот объект RecordSet является в основном вашим ResultSet. Интересная вещь в переборе RecordSet заключается в том, что он все еще связан с провайдером.
Тип курсора
Если ваш тип курсора - динамический или набор ключей Delphi по умолчанию, то каждый раз, когда RecordSet запрашивает новую строку у провайдера, провайдер будет проверять наличие изменений до того, как вернет запись.
Итак, для TADOQuery, где все, что вы делаете, - это чтение результирующего набора для заполнения комбинированного списка, и он вряд ли изменился, вы должны использовать тип статического курсора, чтобы избежать проверки на наличие обновленных записей.
Если вы не знаете, что такое курсор, при вызове функции, такой как Next, вы перемещаете курсор, который представляет текущую запись.
Не каждый провайдер поддерживает все типы курсоров.
Размер кэша
Размер кэша Delphi и ADO по умолчанию для RecordSet равен 1. Это 1 запись. Это работает в сочетании с типом курсора. Размер кэша сообщает RecordSet, сколько записей нужно извлекать и хранить одновременно.
Когда вы запускаете команду типа Next (на самом деле MoveNext в ADO) с размером кэша 1, RecordSet имеет только текущую запись в памяти, поэтому, когда он выбирает эту следующую запись, он должен снова запросить ее у провайдера. Если курсор не является статическим, это дает провайдеру возможность получить последние данные перед возвратом следующей записи. Таким образом, размер 1 имеет смысл для Keyset или Dynamic, потому что вы хотите, чтобы поставщик мог получать обновленные данные.
Очевидно, что со значением 1 существует связь между провайдером и RecordSet при каждом перемещении курсора. Ну, это накладные расходы, которые нам не нужны, если тип курсора статический. Таким образом, увеличение размера кэша приведет к уменьшению количества циклов между RecordSet и провайдером. Это также увеличивает ваши требования к памяти, но это должно быть быстрее.
Также обратите внимание, что при размере кеша, превышающем 1 для курсоров Keyset, если нужная запись находится в кеше, она больше не будет запрашивать ее у провайдера, что означает, что вы не увидите обновления.
Глядя на ваш комментарий, вот несколько предложений:
Есть несколько вещей, которые могут стать узким местом в этой ситуации. Первый - это многократный поиск полей. Если ты звонишь FieldByName
или же FindField
внутри вашего цикла вы тратите время процессора, пересчитывая значение, которое не изменится. Вызовите FieldByName один раз для каждого поля, из которого вы читаете, и вместо этого назначьте их локальным переменным.
При получении значений из полей звоните AsString
или же AsInteger
или другие методы, которые возвращают тип данных, который вы ищете. Если вы читаете из TField.Value
собственность, ты тратишь время на variant
преобразования.
Если вы добавляете несколько элементов в поле со списком Delphi, вы, вероятно, имеете дело со списком строк в форме Items
имущество. Установить список Capacity
собственность и обязательно позвоните BeginUpdate
прежде чем начать обновление, и позвоните EndUpdate
в конце. Это может включить некоторые внутренние оптимизации, которые ускоряют загрузку больших объемов данных.
В зависимости от используемого поля со списком, у него могут возникнуть проблемы с большим количеством элементов в его внутреннем списке. Посмотрите, есть ли у него "виртуальный" режим, в котором вместо того, чтобы загружать все заранее, вы просто сообщаете ему, сколько элементов ему нужно, а когда он выпадает, он вызывает обработчик событий для каждого элемента, который должен быть показан на экране, и вы даете ему правильный текст для отображения. Это действительно может ускорить определенные элементы управления пользовательским интерфейсом.
Кроме того, вы должны убедиться, что сам запрос к базе данных выполняется быстро, но оптимизация SQL выходит за рамки этого вопроса.
И наконец, комментарий Микаэля Эрикссона определенно заслуживает внимания!
Вы можете использовать Getrows. Вы указываете интересующий вас столбец (столбцы), и он возвращает массив со значениями. Время, необходимое для добавления 22 000 строк в поле со списком, начинается с 7 секунд с while not ADOQuery1.Eof ...
цикл до 1,3 секунд в моих тестах.
Образец кода:
var
V: Variant;
I: Integer;
begin
V := ADOQuery1.Recordset.GetRows(adGetRowsRest, EmptyParam, 'ColumnName');
for I:= VarArrayLowBound(V, 2) to VarArrayHighBound(V, 2) do
ComboBox1.Items.Add(V[0, I]));
end;
Если вам нужно более одного столбца в массиве, вы должны использовать вариантный массив в качестве третьего параметра.
V := ADOQuery1.Recordset.GetRows(adGetRowsRest, EmptyParam,
VarArrayOf(['ColumnName1', 'ColumnName2']);
Ваш запрос также привязан к некоторым элементам управления данными или TDataSource? Если это так, делайте зацикливание внутри блока DisableControls и EnableControls, чтобы визуальные элементы управления не обновлялись каждый раз, когда вы переходите к новой записи.
Список пунктов довольно статичен? Если это так, рассмотрите возможность создания невизуального экземпляра комбинированного списка при запуске приложения, возможно, внутри отдельного потока, а затем назначьте свой невизуальный комбинированный список визуальному комбинированному списку при создании формы.
Вы можете попробовать вставить все данные в ClientDataSet и выполнить итерации, но я думаю, что копирование данных в CDS делает именно то, что вы делаете в настоящее время - циклы и назначения.
То, что я когда-то делал, - это конкатенация значений на сервере, их передача в один пакет клиенту и повторное разбиение. Это на самом деле сделало систему быстрее, потому что это уменьшило коммуникацию, необходимую между клиентом и сервером.
Вы должны внимательно следить за узким местом в производительности. Это также может быть выпадающий список, если вы не блокируете обновления графического интерфейса при добавлении значений (особенно, когда мы говорим о 20К-значениях - это много для прокрутки).
Изменить: Если вы не можете изменить связь, то, возможно, вы можете сделать ее асинхронной. Запрашивать новые данные в потоке, поддерживать отзывчивость графического интерфейса, заполнять поле со списком, когда данные там есть. Это означает, что пользователь видит пустой комбинированный список в течение 5 секунд, но, по крайней мере, он может сделать что-то еще в это время. Не меняет количество времени, необходимое.
Вы не можете избежать зацикливания. "Очень долгое время" является относительным, но если получение 20000 записей занимает слишком много времени, значит что-то не так.
- Проверьте ваш запрос; возможно, SQL можно улучшить (отсутствует индекс?)
- Покажите код вашего цикла, где вы добавляете элементы в выпадающий список. Может быть, это можно оптимизировать. (вызов
FieldByName
неоднократно в цикле? использовать варианты для получения значений полей?) - Обязательно позвони
ComboBox.Items.BeginUpdate;
до цикла иComboBox.Items.EndUpdate
после. - Используйте профилировщик, чтобы найти узкое место.
Попробуйте использовать DisableControls и EnableControls для увеличения производительности линейного процесса в наборе данных.
var
SL: TStringList;
Fld: TField;
begin
SL := TStringList.Create;
AdoQuery1.DisableControls;
Fld := AdoQuery1.FieldByName('ListFieldName');
try
SL.Sorted := False; // Sort in the query itself first
SL.Capacity := 25000; // Some amount estimate + fudge factor
SL.BeginUpdate;
try
while not AdoQuery1.Eof do
begin
SL.Append(Fld.AsString);
AdoQuery1.Next;
end;
finally
SL.EndUpdate;
end;
YourComboBox.Items.AddStrings(SL);
finally
SL.Free;
AdoQuery1.EnableControls;
end;
end;
Не уверен, что это поможет, но мое предложение было бы не добавлять непосредственно в ComboBox
, Загрузить в местный TStringList
вместо этого сделайте это как можно быстрее, а затем используйте TComboBox.Items.AddStrings
добавить их все сразу:
var
SL: TStringList;
Fld: TField;
begin
SL := TStringList.Create;
Fld := AdoQuery1.FieldByName('ListFieldName');
try
SL.Sorted := False; // Sort in the query itself first
SL.Capacity := 25000; // Some amount estimate + fudge factor
SL.BeginUpdate;
try
while not AdoQuery1.Eof do
begin
SL.Append(Fld.AsString);
AdoQuery1.Next;
end;
finally
SL.EndUpdate;
end;
YourComboBox.Items.BeginUpdate;
try
YourComboBox.Items.AddStrings(SL);
finally
YourComboBox.Items.EndUpdate;
end;
finally
SL.Free;
end;
end;