Производительность Delphi: считывание всех значений из поля в наборе данных

#performance #delphi #list #tadoquery

#Производительность #delphi #Список #tadoquery

Вопрос:

Мы пытаемся найти некоторые исправления производительности при чтении из TADOQuery. В настоящее время мы перебираем записи, используя ‘while not Q.eof do begin … Q.следующий метод. Для каждого мы считываем идентификатор и значение каждой записи и добавляем каждое в список со списком.

Есть ли способ преобразовать все значения указанного поля в список за один раз? Вместо того, чтобы перебирать набор данных? Было бы действительно удобно, если бы я мог сделать что-то вроде…

 TStrings(MyList).Assign(Q.ValuesOfField['Val']);
  

Я знаю, что это не настоящая команда, но это концепция, которую я ищу. Требуется быстрый ответ и решение (как всегда, но это для устранения действительно срочной проблемы с производительностью).

Комментарии:

1. Чтобы быть более конкретным, мы должны просмотреть каждую запись в наборе данных, прочитать 2 поля (ID: Integer и Val: String) и добавить его в поле со списком. Ну, когда имеется более 20 000 записей, это занимает очень много времени, и я ищу более быстрый способ.

2. 20 000 строк в выпадающем списке — это слишком много для обработки, не в последнюю очередь для некоторых бедных конечных пользователей. Возможно, вам следует пересмотреть свой дизайн?

3. Кто такие мы ? Как долго это очень долго ? 20 тысяч строк — это не так много данных. Какой код вы хотите, чтобы мы срочно оптимизировали? Отрицательный. @Mikael Eriksson, есть несколько приложений для очень больших простых выпадающих списков или списков, например: индекс в электронном словаре.

4. Вы можете извлечь весь набор строк в TStrings decendant, а затем назначить его для управления (с профилированием). Больше ничего нельзя было сказать без фактического кода.

5. @PrematureOptimization: просмотр TStrings. Назначить, это вряд ли будет быстрее, чем то, что он уже делает.

Ответ №1:

Глядя на ваш комментарий, вот несколько предложений:

Есть несколько вещей, которые, вероятно, будут узким местом в этой ситуации. Первый — это повторный поиск полей. Если вы вызываете FieldByName или FindField внутри своего цикла, вы тратите время процессора на повторное вычисление значения, которое не изменится. Вызовите FieldByName один раз для каждого поля, из которого вы читаете, и вместо этого назначьте их локальным переменным.

При извлечении значений из полей вызывайте AsString или AsInteger или другие методы, которые возвращают тип данных, который вы ищете. Если вы читаете из TField.Value свойства, вы тратите время на variant преобразования.

Если вы добавляете кучу элементов в поле со списком Delphi, вы, вероятно, имеете дело со строковым списком в форме Items свойства. Задайте Capacity свойство списка и обязательно вызовите BeginUpdate перед началом обновления, а также вызовите EndUpdate в конце. Это может включить некоторые внутренние оптимизации, которые ускоряют загрузку больших объемов данных.

В зависимости от используемого вами поля со списком могут возникнуть некоторые проблемы с большим количеством элементов во внутреннем списке. Посмотрите, есть ли у него «виртуальный» режим, где вместо того, чтобы загружать все заранее, вы просто сообщаете ему, сколько элементов ему нужно, и когда он выпадает, он вызывает обработчик событий для каждого элемента, который должен отображаться на экране, и вы даете ему правильный текст для отображения. Это действительно может ускорить некоторые элементы управления пользовательского интерфейса.

Кроме того, вы должны убедиться, что ваш запрос к базе данных выполняется быстро, конечно, но оптимизация SQL выходит за рамки этого вопроса.

И, наконец, комментарий Микаэля Эрикссона определенно заслуживает внимания!

Комментарии:

1. Спасибо, отличные комментарии, но я уже подтвердил все это. Я вызываю FieldByName(‘Val’). В качестве строки только один раз я использую BeginUpdate / EndUpdate , это наш стандартный унаследованный компонент TCustomComboBox, и для возврата инструкции SQL буквально требуется 0 Мс. Что касается «виртуального» режима, можете ли вы объяснить подробнее? Не уверен, смогу ли я использовать это или нет…

2. Почему-то подозреваю, что наш стандартный унаследованный компонент TCustomComboBox но лучше использовать профилировщик.

3. Хорошая идея установить Capacity заранее!

Ответ №2:

Вы можете использовать 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;
  

Если вам нужно больше одного столбца в массиве, вы должны использовать variant array в качестве третьего параметра.

 V := ADOQuery1.Recordset.GetRows(adGetRowsRest, EmptyParam, 
       VarArrayOf(['ColumnName1', 'ColumnName2']);
  

Ответ №3:

Есть несколько отличных предложений по производительности, сделанных другими людьми, которые вы должны реализовать в Delphi. Вы должны их учитывать. Я сосредоточусь на ADO.

Вы не указали, что такое внутренний сервер базы данных, поэтому я не могу быть слишком конкретным, но есть некоторые вещи, которые вы должны знать об ADO.

Набор записей ADO

В ADO есть объект набора записей. В данном случае этот объект набора записей в основном является вашим результирующим набором. Интересная особенность итерации по набору записей заключается в том, что он все еще связан с поставщиком.

Тип курсора

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

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

В случае, если вы не знаете, что такое курсор, при вызове функции, подобной Next, вы перемещаете курсор, который представляет текущую запись.

Не каждый поставщик поддерживает все типы курсоров.

Кэширование

Размер кэша Delphi и ADO по умолчанию для набора записей равен 1. Это 1 запись. Это работает в сочетании с типом курсора. Размер кэша указывает набору записей, сколько записей нужно извлекать и сохранять одновременно.

Когда вы выполняете команду типа Next (действительно MoveNext в ADO) с размером кэша 1, набор записей содержит только текущую запись в памяти, поэтому, когда он извлекает эту следующую запись, он должен снова запросить ее у поставщика. Если курсор не является статическим, это дает поставщику возможность получить последние данные, прежде чем возвращать следующую запись. Итак, размер 1 имеет смысл для набора ключей или динамического, потому что вы хотите, чтобы поставщик мог получать вам обновленные данные.

Очевидно, что при значении 1 существует связь между поставщиком и набором записей при каждом перемещении курсора. Ну, это накладные расходы, которые нам не нужны, если тип курсора статический. Таким образом, увеличение размера вашего кэша уменьшит количество циклов между набором записей и поставщиком. Это также увеличивает ваши требования к памяти, но это должно быть быстрее.

Также обратите внимание, что при размере кэша больше 1 для курсоров набора ключей, если нужная запись находится в кэше, она не будет запрашивать ее у поставщика снова, что означает, что вы не увидите обновления.

Ответ №4:

Вы не можете избежать зацикливания. «Очень долгое время» относительно, но если получение 20000 записей занимает слишком много времени, что-то не так.

  • Проверьте свой запрос; возможно, SQL можно улучшить (отсутствует индекс?)
  • Покажите код вашего цикла, в котором вы добавляете элементы в поле со списком. Возможно, это можно оптимизировать. (повторный вызов FieldByName в цикле? использование вариантов для извлечения значений полей?)
  • Обязательно вызывайте ComboBox.Items.BeginUpdate; перед циклом и ComboBox.Items.EndUpdate после.
  • Используйте профилировщик, чтобы найти узкое место.

Комментарии:

1. Запрос не занимает буквально никакого времени, 0 Мс. Это цикл, который требует времени. Я использую FieldByName для чтения значений. Я попробовал BeginUpdate / EndUpdate, и хотя это немного помогло, этого недостаточно.

2. 1 для оптимизации FieldByName в цикле — FieldByName действительно занимает некоторое время, если вы делаете это снова и снова

3. FieldByName может замедлить цикл, особенно при большом количестве полей в результирующем наборе. Если это проблема, лучше использовать локальные TField переменные, назначьте их один раз перед циклом и используйте их в цикле.

4. Также не используйте вариант Field.Value , вместо этого используйте Field.As* свойство в зависимости от типа данных столбца.

Ответ №5:

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

То, что я однажды сделал, это объединил значения на сервере, передал их клиенту одним пакетом и снова разделил. На самом деле это сделало систему быстрее, поскольку сократило необходимое взаимодействие между клиентом и сервером.

Вы должны внимательно смотреть, где находится узкое место в производительности. Это также может быть поле со списком, если вы не блокируете обновления графического интерфейса при добавлении значений (особенно когда мы говорим о значениях 20K — это много для прокрутки).

Редактировать: если вы не можете изменить связь, возможно, вы могли бы сделать ее асинхронной. Запрашивайте новые данные в потоке, поддерживайте адаптивный графический интерфейс, заполняйте поле со списком, когда данные есть. Это означает, что пользователь видит пустое поле со списком в течение 5 секунд, но, по крайней мере, он может сделать что-то еще за это время. Однако необходимое время не изменяется.

Комментарии:

1. Спасибо за ответ. Блокировка обновлений графического интерфейса — это фактически то, что я уже пробовал (потратив 20 минут на то, чтобы убедить ведущего разработчика сделать это), и это сократило 1 секунду из 6 секунд, которые это занимает. Это было просто с помощью TStrings. BeginUpdate и EndUpdate в блоке try .. finally. однако 1 секунды недостаточно. Работа на стороне сервера — хорошая идея, к сожалению, это огромный программный пакет 16-летней давности со строгим взаимодействием только с SQL, поэтому мы не можем ввести новую форму подключения для загрузки списка 1.

Ответ №6:

Ваш запрос также привязан к некоторым элементам управления с поддержкой данных или к TDataSource? Если это так, выполните цикл внутри блока DisableControls и EnableControls, чтобы ваши визуальные элементы управления не обновлялись каждый раз, когда вы переходите к новой записи.

Является ли список элементов достаточно статичным? Если это так, рассмотрите возможность создания невизуального экземпляра combobox при запуске приложения, возможно, внутри отдельного потока, а затем назначьте свой невизуальный combobox визуальному combobox при создании вашей формы.

Комментарии:

1. если это набор данных ADO, это основное узкое место

2. Это хорошая идея попробовать, не уверен, подключен ли TADOQuery к каким-либо другим источникам данных / элементам управления, но я вижу, как это может вызвать эту проблему.

3. 1 за упоминание Disable/EnableControls . Но «невизуальный список» — плохая идея; вы можете более легко использовать TStringList , и накладных расходов намного меньше, чем создание визуального элемента управления, который никогда не будет виден. в конце концов, TComboBox.Items это TStrings потомок; вы можете назначить TStringList им напрямую.

4. Ты прав, Кен. Я использую коммерческий набор компонентов, в котором есть репозиторий редактора, поэтому я немного избалован в этом отношении. В этом случае TStringList был бы намного эффективнее.

Ответ №7:

попробуйте использовать 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;
  

Ответ №8:

Не уверен, поможет ли это, но мое предложение было бы не добавлять непосредственно в 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;
  

Комментарии:

1. 1. SL.BeginUpdate / EndUpdate не повлияет на скорость, поскольку не зарегистрировано никаких событий уведомления. 2. Метод AddStrings() уже имеет вызовы BeginUpdate / EndUpdate. 3. AddStrings() уже добавляет отдельные строки по одной — Вывод: использование временного TStringList не будет быстрее.

2. 1. Не сейчас; что, если события уведомлений будут добавлены в будущем? Добавление Begin / EndUpdate ничего не повредит и при необходимости добавит защиту. 2. Begin / EndUpdate могут быть вложенными, и их добавление (как я уже сказал) ничего не повредит. 3. Использование TStringList имеет меньше накладных расходов, чем TComboBox , и если у вас нет доказательства профилирования, я не уверен, что согласен. Вывод: Вы здесь ничего не сказали, и я, по крайней мере, предложил попытку помочь (и указал, что я не уверен, что это сработает). Но спасибо, что прочитали мой ответ. 🙂 Кроме того, вы могли бы указать, что try..finally блоки, вероятно, здесь не нужны.