Delphi programming blog
Источник: http://teran.karelia.pro/articles/item_4430.html
 

DisableControls экономит ваше время?

Опубликовано 30.06.2010 г. 00:26

Часто ли вы работаете с базами данных? У меня из этого состоит треть моей рабочей деятельности, вот и подумал я, а мб написать что нибудь про работу с БД?

Вообще на самом деле я не считаю себя достаточно опытным разработчиком приложений для баз данных, поскольку мои приложения обычно не представляют эти самые данные пользователю, и не позволяют их редактировать. В основном задачи этих приложений в получении некоторых наборов данных, иногда достаточно больших, их обработка, использование этих данных в каких либо математических расчетах, и в конечном счете представление пользователю результата. В обработке больших наборов данных (на самом деле любых, но на больших это более заметно (:) кроется небольшая загвоздка, которая начинающего разработчика может весьма озадачить. Вероятно все знают, что к набору данных можно подключить визуальные элементы, т.н. DBControls, с помощью которых можно отображать значения элементов текущей строки набора данных. Такими элементами могут быть, как просто TDBEdit так, например, и целая таблица TDBGrid. Допустим, мы подключили наш набор данных к таблице и теперь все данные в этой таблице отображаются. Теперь представим, что нам для каких то целей требуется перебрать все строки таблицы, и провести какие то вычисления. Но окажется, что при переходе к новой записи с помощью метода TDataSet.Next() так же вызывает смену текущей строки в нашей таблице. Либо если мы используем простые элементы управления, то например изменения текста в TDBEdit. Смена текущей позиции это еще пол беды, зачастую при этом форма начинает "подтормаживать", а элементы управления призванные отображать данные "мерцают". Здесь к нам на помощь приходят методы disableControls & enableControls, названия которых говорят сами за себя. Используя данные методы можно отключить изменение данных в элементах управления. После чего итерации по набору данных будут проходить безболезненно. Однако остается не забыть восстановить исходную позицию курсора на прежнее место. Тут возможны варианты, например, установить ее в начало, воспользоваться закладками TBookMark, либо запомнить значение ключевого поля, и перейти к нему после обработки используя метод Locate(). Я не использую отображение данных, у меня нет связанных элементов управления, мне не потребуются подобные методы,- скажете вы. Но так ли это? действительно ли данные методы будут бесполезны если даже вы просто извлекаете данные, и потом делаете итерации по набору данных, с целью провести какой либо расчет? Естественно изменение данных в элементах управления требует дополнительных временных затрат, что увеличивает время итераций. Но по скольку у нас нет подобных элементов, то и затрат дополнительных нет, следовательно все работает быстро. На самом деле это не совсем так. Возможно на небольших наборах данных это не заметно. Но что будет если в запросе вы получите 100 тысяч строк, которые затем вам потребуется перебрать? Давайте проверим. Имеется форма с кнопкой testButton, logMemo:TMemo для отображения некоторой статистики; dbConnect: TADOConnection для подключения к БД; Для извлечения данных будет использовать query: TADOQuery. пусть наш запрос будет возвращать нам 100 тысяч записей. Наша задача - засечь время итерации по набору данных, с первой до последней записи, при чем при итерациях не будет проводится никаких действий. Для замера времени воспользуемся структурой TStopWatch из модуля Diagnostics (d2010). Итак выполняем запрос (~700мс), засекаем время, проводим итерации по набору данных, записываем время проведения итераций в лог.
var t : TStopwatch;
begin
    query.SQL.Text := 'SELECT TOP 100000 * FROM my_big_test_table';

    t := TStopWatch.StartNew;
    logMemo.Lines.Add('-------------');

    query.Open();
    logMemo.Lines.Add( Format( 'open: %d ms',  [t.ElapsedMilliseconds] ));

    t := TStopWatch.StartNew;
    while not query.Eof do begin
        query.Next();
    end;
    logMemo.Lines.Add( Format('quit: %d ms', [t.ElapsedMilliseconds]) );
    query.Close;
Возможно некоторые скажут, что надо открыть набор данных в режиме навигации только вперед, либо в режиме "только чтение", и это ускорит итерации, но это не так. Итак среднее время итераций (мс):
  1. default (кинули TADQuery на форме и запустили) - 96600
  2. openForward - 96500
  3. openForward + readOnly - 96550
"90 секунд на перебор данных?! выкинь свою машину парень!", нет скажу я вам. машина тут совершенно ни при чем. Давайте добавим в наш код, перед проведением цикла вызов метода disableControls(). каков же будет результат? -
  1. DisableControls() - 390
  2. DisableControls + OpenForward + readOnly - 312
Отмечу, что единицы измерения времени в обоих тестах одинаковы, и время приведено в миллисекундах. Так что не забывайте использовать данные методы при переборе больших наборов данных, это может сэкономить вам минуты ожидания. Что же тогда отключает DisableControl, и чем вызвана такая большая задержка времени? На этот вопрос я вам не дам ответ. Но вы всегда сможете его поискать, включив Debug режим компиляции вашего проекта, и установив в true опцию Use Debug .dcus в настройках компиляции, после чего погрузиться в изучения структуры класса TDataSet.
Метки:  db 

Комментарии

Вячеслав
30.06.2010 в 10:16
Отрисовка контролов - очень долгое занятие. Скорее всего вывод "logMemo.Lines.Add( Format( 'open: %d ms', [t.ElapsedMilliseconds] ))" дали разницу во времени. Поэтому лучше всего подсчёт времени делать через переменные, а уже после открытия и закрытия БД лог из переменных выводить через визуальные компоненты. Да и это не VCL тормозной, это просто вся система так работает, так как отрисовка контролов это очень сложная процедура в системе.
ter
30.06.2010 в 11:36
замечу, что добавление времени в лог проводится один раз по окончанию итераций по набору данных, так что я думаю можно утверждать, что оно не сказывается на итоговых результатах.
Вячеслав
30.06.2010 в 16:03
Всё возможно :), просто растаскивался с тем что когда выводил большую выборку из БД в Memo, быстрее в раз было вывести в переменную, а потом всё в Memo. Можно попробовать по изменять в разы объём выборки. Если разность будет постоянной то это вывод текста, если нет, то скорее всего что то ещё дополнительное производится в визуальных контролах при переборе. Думаю так как то :)
ter
30.06.2010 в 17:23
чтобы memo при выводе данных большой таблицы не перерисовывалось постоянно, можно данные выводить не через третью переменную. Должно быть достаточно использования методов TMemo.Lines.BeginUpdate() и endUpdate(), а между ними добавлять данные в мемо. Использование данных методов предполагает, что визуальное отображение мемо не будет обновляться при добавлении строк в мемо между этими вызовами (:

что касается опять же моего поста, то у меня там нет никакого вывода (т.е нет вывода текста и связанных контролов (: ), запрос вообще ни к чему не привязан (: просто пустой цикл перебора.
такой же эффект будет получен например подобным кодом.
with  TADOQuery.Create(nil) do begin
connection := dbConnect;
sql.text := 'select top 100000 * from test_table';
open();
while not eof do next();
end;
т.е абсолютно ни к чему не привязанный запрос с пустым циклом итераций.
Lak
30.06.2010 в 11:19
Сколько раз проводились замеры? После повторения будет ли такая же разница в результатах?
Какая база данных использовалась? Вполне возможно, что запрос был просто прокеширован
ter
30.06.2010 в 11:07
замеры проводились 3-4 раза. БД - MS SQL Server 2008.
Время выполнения запроса базой данных и возвратом результата составляет почти постоянные 700мс.

Значения времени приведенные в тексте - время итерации по набору данных на стороне клиента, и от БД не зависят.
ter
30.06.2010 в 12:31
вообще еще забыл написать что вызов данных методов, особенно при использовании отображения данных на форме настоятельно рекомендуется оформлять в виде блока try-finally, чтобы в случае возникновения исключительной ситуации было восстановлено исходное состояние
try
   query.DisableControls();
   query.open();
   .....
finally
   query.enableControls();
Uyri
01.07.2010 в 13:40
Очень странные результаты, я проходил по набору данных ADOTable на 15000 записях:

без DisableControls - 30 сек
с DisableControls - 28 сек

всего на 2 секунды быстрее, но никак не в разы??

Причем если использовать для навигации курсор:

ADOTable1.Recordset.MoveNext;

то DisableControls вообще некакого влияния не оказывает!
Uyri
01.07.2010 в 13:44
Забыл добавить, для тех, кто использует АДО, если использовать ADOTable1.Recordset.MoveNext;
то время прохода по записям уменьшилось в 2 раза - составило 13 секунд против 30-ти!!
ter
01.07.2010 в 14:46
Для интересу взял TADOTable c 10 тыс. записей. (maxRecordCount = 10000)

1. disableControls - 30мс
2. enableControls - 1170 мс

маленькая разница в ваших результатах и большое время выполнения на 15к записей натолкнула на мысль что использовался сursorLocation = useServer
1. useServer + disableControls = 3450 мс
2. useServer + EnableControls = 6700 ms

далее проверил с ADOTable1.RecordSet.MoveNext() + :
1. useServer + MoveNext() + disableControls = 3350 мс
2. useServer + MoveNext() + enableControls = 3400 мс


и последний вариант:
1. useClient + MoveNext() + disableControls = 15 ms
2. useClient + MoveNext() + enableControls = 16 ms


какая у вас кстати СУБД использовалась?
Uyri
01.07.2010 в 18:36
MS Excel :)
Uyri
01.07.2010 в 18:20
Большое время выполнения связано с тем, что у меня в цикле имитация преобразований полей таблицы.

Я говорил о дельте при применении DisableControls. И еще - у меня D7
Uyri
01.07.2010 в 19:44
Все, вроде бы, сходиться:
1. disableControls - 30мс
2. enableControls - 1170 мс

разница 1-2 секунды

1. useClient + MoveNext() + disableControls = 15 ms
2. useClient + MoveNext() + enableControls = 16 ms

здесь разницы нет, как я и писал не зависит от disableControls

а вот замена MoveNext ускорила в два было 30 мс стало 15.
ter
01.07.2010 в 22:59
боюсь некорректно сравнивать 30мс и 1170 говоря что разница в 1 секунду (: разница в 30 раз скорее. с ростом объема выборки разница будет только увеличиваться. как показывает практика со 100к записей то время итераций увеличивается линейно, что в логично, и если для 10к это 30/1100 то для 100к это 300/10000, где разница уже далека от 1-2 секунд
Uyri
01.07.2010 в 13:22
Ну и раз уж пощла такая пьянка - можно дать несколько советов для н.д. с большим количеством записей и большим количеством полей:

1. Внутри циклов страйтесь не использовать любые циклы, в т.ч. скрытые, например свойства FiendField, FieldByName...
2. Применяйте Fields[i] или до цикла стройте свои списки. И уже их используйте внутри цикла по записям.
3. Если уж пришлось применять FieldByName, не используйте его несколько раз для одного поля, выносите в переменную.
4. Внутри циклов никогда не используйте FieldValues - там тот же FieldByName , причем применяется для каждого поля из параметра.
5. Ну и еще, уж не знаю почему, но у меня VarToStr(ADOTable1.Fields[j].Value) оказался быстрее ADOTable1.Fields[j].AsString;
Александр Божко
06.07.2010 в 02:05
Вообще, тут надо понимать определенные моменты. А именно, как устроен TDataSet и как устроены Data Control'ы. А главное, как они взаимодействуют. В одной из книг Марко Кэнту есть примеры разработки собственного датасета и контролов (датасет мне доводилось писать, крайне кропотливая задача). Рекомендую пустить его пример в режиме отладки и посмотреть сколько же раз контрол дернет методы из Датасета. И сразу становится понятно, что дело тут не только в перерисовке контролов.
ter
06.07.2010 в 10:51
дак контролов то нет.
но в том что надо глубоко знать устройство датасетов в этом согласен (:
Uyri
06.07.2010 в 16:49
Если контролов нет, закладки не используются, а нужен проход по записям, то логично использовать не DataSet а компоненты с однонаправленным курсором.
Еще больше экономим Ваше время :) и память...
- Имя
- e-mail*
- Сайт
вы можете использовать теги [i],[b],[code],[quote]
Дополнительно