Большой архив статей, книг, документации по программированию, вебдизайну, компьютерной графике, сетям, операционным системам и многому другому
 
<Добавить в Избранное>    <Сделать стартовой>    <Реклама на сайте>    <Контакты>
  Главная Документация Программы Обои   Экспорт RSS E-Books
 
 

   Базы данных -> FoxPro -> Быстродействие программ на VFP

Быстродействие программ на VFP

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

Здесь вы можете скачать тексты демонстрационных программ.

Важное замечание
Скорость загрузки программы
Rushmore
Навигационный и реляционный подходы
Оптимизация запросов
Нормализация БД
Алгоритм
Скорость выполнения отдельных команд
Пример
Как успокоить пользователя
Тестирование
Благодарности

Важное замечание

Наши пользователи хотят, чтобы программы выполнялись максимально быстро. Но часто бывает, что быстродействие может быть достигнуто в ущерб другим требованиям, например универсальности, модифицируемости или надежности кода. Далеко не всегда стоит добиваться максимальной производительности. Бывает, что код настолько усложняется, что вы рискуете потерять контроль над своей "супер-быстрой" программой. Так же не стоит отказываться от проверок входящих параметров. Сэкономленные секунды могут обернуться долгими часами, потраченными на ликвидацию последствий незначительной ошибки.

Скорость загрузки программы

Если пользователи жалуются на слишком длительную загрузку форм, вы можете предпринять следующие действия:

  • Загружайте элементы интерфейса только тогда, когда в них появится необходимость. Это могут быть элементы на неактивных закладках PageFrame, скрытые узлы Treeview, или невидимые формы в FormSet. Практически любой набор объектов можно поместить в контейнер, сохранить его, как класс, а в нужный момент создать экземпляр этого класса.

  • Можно поступить "с точностью до наоборот" - при загрузке программы создать самые тяжелые и часто используемые объекты, и сделать их невидимыми до определенного момента. Обращали внимание, как долго загружается программа 1С? Зато потом скорость работы вполне приемлемая. Это пример такого подхода.

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

Rushmore

Вы уже знаете про оптимизацию Rushmore. Вот наиболее частые ошибки, при которых оптимизация перестает работать:

  • Не совпадает Set Collate (в индексах таблицы используется Russian, а системная установка - Machine)

  • Заведен индекс с условием FOR ...

  • Выражение для поиска не совпадает с индексным. Часто допускают такую ошибку. У таблицы есть индекс по UPPER(cField1). Для поиска пустого значения используют условие EMPTY(cField1) = .T., что не совпадает с индексным выражением. Для того, чтобы поиск был оптимизированным, выражение должно выглядеть как UPPER(cField1) = SPACE(N), где N - длина поля cField1.

Навигационный и реляционный подходы

В FoxPro сосуществуют два подхода к обработке данных - навигационный и реляционный.Навигационные команды - это Locate, Seek, Set Relation, Set Order, GoTo. Основная реляционная команда - Select SQL. Скорость выполнения запросов SQL весьма высока, однако во многих случаях навигационный подход работает еще быстрее.

Пример поиска максимального значения поля различными способами:

*** поиск с помощью Select SQL
Select MAX(Field1) as MaxField From Table1 Into Cursor _Temp
? _Temp.MaxField

*** а так выполнится значительно быстрее 
(необходимо наличие индекса по полю Field1)
Select Table1
Set Order To Field1
Go Bottom
? Field1

Оптимизация запросов

Запросы на FoxPro могут выполняться долго, если в них участвует больше трех-четырех таблиц. Речь идет именно о запросах к родным БД на dbf, а не о запросах, отправляемых на сервер. Мне не удавалось увеличить скорость путем разбиения большого запроса на несколько последовательно выполняемых подзапросов. С другой стороны, эффект от разбиения может зависеть от количества и распределения информации в связанных таблицах. Поэтому результаты могут отличаться. Можно выйти из положения, исключая из запроса справочные таблицы (например, ФИО сотрудников), и привязывая их к результату через Set Relation, смешивая таким образом "реляционный" и "навигационный" подходы.

Использование Outer join снижает скорость выполнения запроса. Если нет прямой необходимости в этой опции, лучше переписать запрос без нее. Как вариант - если у вас есть строки, не имеющие соответствия в связанной таблице, и потому содержащие .NULL., добавьте в связанную таблицу строку с нулевым кодом, а в основной - замените пустые значения на этот нулевой код.

Подзапросы вида Where Field1 in (Select ... ) или Where Exists (Select ... ) могут замедлить скорость выполнения запроса (хотя это зависит от многих параметров). Если такой запрос выполняется слишком медленно - попробуйте переписать его с использованием простого объединения.

Для большего быстродействия располагайте первыми те условия, которые максимально сужают область дальнейшего поиска.

Для анализа оптимальности SQL-запроса воспользуйтесь функцией Sys(3054). Имейте в виду, что если у таблиц нет индексов по Deleted(), эта команда будет показывать частичную оптимизацию, даже если фактически они вносят отрицательный вклад в скорость выполнения запроса. Этот эффект подробно описан Владимиром Максимовым.

Нормализация БД

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

Например, в справочнике клиентов есть поле "Город". Исходя из принципов нормализации, необходимо завести таблицу - справочник городов. Так как названия городов меняются нечасто, в таблице клиентов вместо кода города можно записывать его название. Это позволит уменьшить количество таблиц в запросах, но потребует аккуратности, если название города все-таки изменилось, или в нем была допущена ошибка.

Другой вариант - предварительные расчеты. Например, с помощью триггеров можно заранее пересчитывать суммарные данные о продажах за день в момент добавления или корректировки нового счета. К примеру, пусть счета хранятся в таблице Invoices, а данные о продажах за день - в таблице DailySales. Тогда в триггеры таблицы Invoices нужно добавить примерно такой код:

*** для триггера Insert
Update DailySales Set TolalSum = TolalSum + Invoices.InvoiceSum 
Where DailySales.SalesDate = Invoices.InvoiceDate

*** для триггера Update
Update DailySales Set TolalSum = TolalSum + Invoices.InvoicesSum - 
OldVal(Invoices.InvoiceSum) Where DailySales.SalesDate = Invoices.InvoiceDate

*** для триггера Delete
Update DailySales Set TolalSum = TolalSum - Invoices.InvoiceSum 
Where DailySales.SalesDate = Invoices.InvoiceDate

Естественно, сам код триггеров будет намного сложнее. Кроме приведенных строк там необходимы проверки, обработка ошибок и т.д.

Если вы решите идти по такому пути, не забудьте написать процедуру синхронизации, если из-за сбоев предварительные расчеты станут отличаться от основных.

Алгоритм

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

Вот несколько общих советов:

  • Вынесите за пределы циклов однократно выполняемые установки типа Set deleted, Set exact, и т.д.

  • Поместите проверки в начало подпрограммы. Это позволит не выполнять лишние действия, если проверки окончатся неудачно.

  • Освобождайте память от объектов и таблиц, которые вам больше не понадобятся.

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

Скорость выполнения отдельных команд

Скорость выполнения отдельных команд приобретает критическое значение, когда они выполняются в цикле, и доли секунды, за которые выполняется команда, умножаются в тысячи, если не в сотни тысяч раз. Так же бывает важным максимально сократить время работы триггеров, чтобы не тормозить операции с таблицей. Вот некоторые приемы:

  • Поиск записей с помощью команды Seek выполняется примерно в 5-10 раз быстрее, чем с помощью Locate. Команда Locate гибче, так как не требует индекса (но может использовать его для оптимизации), и кроме того, позволяет указывать несколько условий, например ClientID = X AND InvoiceDate = Y. Для максимально быстрого поиска по нескольким полям вы можете завести сложный индекс типа STR(ClientID, 10, 0) + DTOS(InvoiceDate), и осуществлять поиск с помощью Seek STR(X, 10, 0) + DTOS(Y). Но учтите, что этот прием усложняет модификацию кода, т.к. требует существенных изменений из-за локальной проблемы.

  • Если нужно только проверить наличие строк, удовлетворяющих условию, но нет необходимости перемещать указатель на эти строки, воспользуйтесь функцией IndexSeek() (она появилась в VFP начиная с 6-й версии).

  • Стандартные функции VFP хорошо оптимизированы и выполняются достаточно быстро. Например, условиями задачи требуется найти цифру, входящую в текстовую строку. Можно перебирать текстовую строку в поисках цифр
    For I = 1 to LEN(cText)
    
    	If ISDIGIT(SUBSTR(cText, m.I,1))
    		? m.I
    		Exit
    	EndIf
    
    EndFor
    Более быстрый вариант с использованием стандартных функций:
    cText1 = STRTRAN(cText, "123456789", "0")
    ? AT(cText1, "0")
  • Команда Scan...EndScan оптимизирует перебор записей таблицы даже без индекса. Там где это возможно, используйте ее вместо конструкции Do While not EOF() ... Skip ... EndDo.

  • Выполнение команд Locate, Scan For.., Set Filter to.. замедлится, если у таблицы установлен активный индекс. Отключите индекс командой Set Order to без параметров, если нет необходимости в сортировке данных.

  • Вместо макроподстановки & используйте выражения имени ( ) и функцию EVALUATE(). Разница между ними в следующем. Выражения имени можно использовать там, где Fox ожидает увидеть имя переменной или поля. Функция EVALUATE() может быть использована в операциях присвоения и вычислениях.
    X = "Var1"
    Y = "Var2"
    
    Store EVALUATE(X) to (Y) && так быстрее
    
    &Y = &X && эта команда сделает то же, что и предыдущая, но существенно медленнее.
    Макроподстановка уместна, когда вам нужно вставить в код сложную команду, например сформированную строку запроса SQL.
    *** пример использования макроподстановки ***
    
    OldSettings = Set("TALK") && запоминаем текущую установку
    Set Talk OFF
    ...
    Set Talk &OldSettings && возвращаем прежнее состояние
  • Скорость обращения к переменным выше, чем к свойствам объектов. Если значение какого-то свойства используется многократно, присвойте это значение переменной.

  • Подавление системных сообщений с помощью команд Set Talk off, Set Notify off (в последних версиях еще и Set Notify Cursor off) может существенно ускорить процесс.

  • Команда Goto nRecordNo выполняется практически мгновенно. Если вам нужно вернуться к какой-либо записи в таблице, переход по номеру записи отработает намного быстрее, чем поиск по идентификатору с помощью Seek или Locate.

  • Если вам необходимо просканировать некоторый диапазон значений в таблице, и по этим значениям есть индекс, вы можете воспользоваться приемом, известным как "Set Order + Seek + Scan while".
    Set Order to Field1
    Seek StartValue && устанавливаем указатель на начало диапазона
    Scan While Field1 =< EndValue && перебираем строки до конца диапазона
    ...
    EndScan
    Этот код работает быстрее, чем Scan for Between( Field1, StartValue, EndValue ), но проигрывает ему в читабельности, и кроме того, требует установки активного индекса.

  • Сложение текстовых строк происходит быстрее, если к длинной строке добавлять короткую, а не наоборот. То есть
    *** этот код выполнится быстро
    x = ""
    For i = 1 to 10000
    	x = x + Sys(3)
    EndFor
    
    *** а этот - медленно
    x = ""
    For i = 1 to 10000
    	x = Sys(3) + x
    EndFor
    Другими словами, если вам потребуется сложить некоторые значения в обратном порядке, лучше сначала отсортировать их по убыванию, и затем складывать.

  • Для поиска "узких мест" воспользуйтесь командой Set Coverage to. Эта команда создаст текстовый файл, в котором будет записано время выполнения каждой строки. Вы можете сохранить этот файл в таблицу, и проанализировать ее. Либо воспользоваться Coverage profiler из дистрибутива Visual FoxPro.

Вы можете самостоятельно сравнить скорость работы различных команд с помощью тестовых примеров.

Пример

Предположим, вам необходимо всем клиентам, у которых есть заказ в предыдущем месяце, предоставить скидку 10% на будущие заказы. Данные о заказах хранятся в таблице Invoices, а информация о скидке - в таблице Clients.

New_Discount = 0.10 && скидка, которую мы хотим предоставить

PrevDate = GoMonth(Date(), -1)
PrevMonth = Month(PrevDate) && предыдущий месяц
PrevYear = Year(PrevDate) && год, в котором был предыдущий месяц


Go top in Clients
Do while not EOF("Clients") && перебор записей в таблице клиентов

	Go top in Invoices

	Do while not EOF("Invoices") && перебор записей в таблице счетов

		If Invoices.ClientID = Clients.ClientID AND ;
		Year(Invoices.InvoiceDate) = PrevYear AND ;
		Month(Invoices.InvoiceDate) = PrevMonth

			Replace Clients.Discount with New_Discount
			Exit

		EndIf

		Skip in Invoices

	EndDo

	Skip in Clients

EndDo

Приведенный код логически верен, но абсолютно не оптимизируем.

Попробуем переписать этот код:

Month_End = Date() - Day(Date()) && последний день предыдущего месяца
Month_Start = Month_End - Day(Month_End) + 1 && первый день предыдущего месяца

Select Clients

Scan
	Select Invoices
	Locate for ClientID = Clients.ClientID AND Between(InvoiceDate, Month_Start, Month_End)

	If Found("Invoices")
		Replace Clients.Discount with New_Discount
	Endif
EndScan

Перебор записей таблицы Clients осуществляется командой Scan. Поиск в таблице Invoices может быть оптимизирован с помощью индексов по полям ClientID и InvoiceDate. Этот вариант будет выполняться существенно быстрее.

Для реализации следующего варианта необходимо, чтобы у таблицы Invoices существовал особый индекс:

Index on STR(ClientID, 10,0) + DTOS(InvoiceDate) tag MIXED
...
...
...
Set Exact OFF

LastMonth  = LEFT(DTOS( GOMONTH(DATE(), -1) ), 6) && год и месяц в формате "YYYYMM"

Select Clients
Replace Clients.Discount with New_Discount For IndexSeek( STR(ClientID, 10,0) + m.LastMonth, .F., 
"Invoices", "MIXED" )

Поиск в таблице Invoices осуществляется по индексу, поэтому этот пример отработает еще быстрее. Но индекс весьма специфический, и не может быть применен для других вычислений. Кроме того, даже автору это кода этого потребуется немало времени, чтобы спустя полгода разобраться, что именно он делает :-)

Решение с использованием команд SQL:

Update Clients Set Discount = New_Discount Where Exists ;
(Select * From Invoices Where ;
Invoices.ClientID = Clients.ClientID AND ;
Invoices.InvoiceDate BETWEEN Month_Start AND Month_End)

Возможно, данный вариант не выиграет с точки зрения скорости по сравнению с предыдущим, но он гораздо понятнее. Если время выполнения запроса находится в разумных пределах, я бы остановился именно на нем. К сожалению, такой синтаксис команды SQL-Update возможен только в VFP9.

Здесь вы можете скачать исходные тексты примеров.

Как успокоить пользователя, пока выполняется программа

Если программа в течение минуты не будет подавать признаков жизни, у пользователей возникает жгучее желание нажать Ctrl+Alt+Del, и прервать процесс. Вам необходимо позаботиться об информировании пользователя о состоянии дел. Вот варианты.

Если расчеты многоступенчатые, перед каждым этапом выводите либо в статус-бар, либо в окно информацию

Wait window "Этап 1: Проверка данных" nowait
...
Wait window "Этап 5: Суммирование данных за год" nowait
...
Wait window "Этап 10: Окончательное форматирование" nowait
...
Wait clear

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

Wait window "Расчет продлится примерно до " + Transform(DateTime() + Duration) nowait
Сохранять эту информацию нужно локально, так как скорость вычислений зависит от возможностей данной рабочей станции. Если длительность расчета зависит от каких-то параметров, вы можете экстраполировать данные пропорционально им. Например, если пользователь указывает диапазон дат, разделите длительность процесса на количество дней в диапазоне, и получите длительность в расчете на один день. В следующий раз, когда пользователь введет другой диапазон, умножьте сохраненный результат на новое количество дней, и получите экстраполяцию, сколько будут длиться вычисления. Время, естественно, будет очень приблизительным, но это в любом случае поможет пользователю оценить, сколько чашечек кофе он успеет выпить :-)

Для полноты картины можете вывести прогресс-бар. Однако имейте в виду, что любые украшательства требуют системных ресурсов. Так что не переусердствуйте.

У многих программистов возникает желание написать свой прогресс-бар для запросов. Увы, единственный известный работающий пример - это стандартный "градусник", который выводится при выполнении долгого запроса, если перед этим была дана команда Set Talk on. Все другие решения приводили к многократной задержке в обработке данных.

Тестирование

Советы уважаемых гуру, и даже выдержки из HELP не дают гарантий, что именно ваш случай не будет редким, но исключением из общих правил. Скорость работы может зависеть от сервера и сети, от активности пользователей, от объема и частотного распределения данных. Самый хороший способ определить, насколько быстро будет выполняться программа - провести тесты, максимально приближенные к реальности. Использование тестов только кажется сложным. Нет ничего сверхъестественного в том, чтобы сгенерировать табличку, близкую по размерам к максимальному ожидаемому размеру реальной БД, и попробовать поработать. В генерации тестовых данных вам помогут функции RAND(), SYS(3) и SYS(2015), а так же любой осмысленный текст, например kladr - классификатор адресов России. Вы легко можете найти его в Интернете. Для большей достоверности постарайтесь, чтобы данные не были физически отсортированы по какому-либо полю.

*** Генерация тестовой таблицы ***

=RAND(-1) &&  HELP рекомендует перед началом работы вызвать RAND() с отрицательным параметром

For nCount = 1 to BigNumber && число добавляемых записей

	*** пример генерации случайного числа от 1 до 1000
	nRandomValue = CEILING(1000 * RAND())

	*** а так можно заполнять почтовые индексы и номера телефонов
	cZipCode = PADR(CEILING(999999 * RAND()), 6, "0")

	*** поиск случайной записи в связанной таблице
	Goto CEILING(RECCOUNT("ChildTable") * RAND() ) in ChildTable
	RelatedTableValue = ChildTable.Field1

	*** пример генерации случайного адреса
	Goto CEILING(RECCOUNT("Towns") * RAND() ) in Towns
	Goto CEILING(RECCOUNT("Streets") * RAND() ) in Streets

	cAddress = ALLTRIM(Towns.Name) + ", " + ALLTRIM(Streets.Name) + 
	", дом " + TRANSFORM( CEILING(200 * RAND()) )

	*** Вставляем полученные данные в таблицу
	Insert into BigTable (...) values (nRandomValue, cZipCode, cAddress, RelatedTableValue)

EndFor

Регулярное проведение тестов имеет еще одну приятную особенность. Пока выполняется тест, вы имеете полное право откинуться на спинку стула, и несколько минут наслаждаться полным бездельем ;-)

Благодарности

В работе над статьей принимали участие члены Фокс-Клуба
Владимир Максимов
Игорь Королев
Alex Shustikov
is

Автор: Игорь Ильин
Источник:www.foxclub.ru

Ссылки по теме
FAQ - Visual Foxpro Club
Microsoft FoxPro Краткое руководство по СУБД для Windows
Расширение возможностей VFP за счет WINAPI
Основы языка Visual FoxPro
FoxPro будет жить еще долго

Вся документация по FoxPro

 

Компьютерная документация от А до Я - Главная

 

 
Интересное в сети
 
10 новых программ
CodeLobster PHP Edition 3.7.2
WinToFlash 0.7.0008
Free Video to Flash Converter 4.7.24
Total Commander v7.55
aTunes 2.0.1
Process Explorer v12.04
Backup42 v3.0
Predator 2.0.1
FastStone Image Viewer 4.1
Process Lasso 3.70.4
FastStone Image Viewer 4.0
Xion Audio Player 1.0.125
Notepad GNU v.2.2.8.7.7
K-Lite Codec Pack 5.3.0 Full


Наши сервисы
Рассылка новостей. Подпишитесь на рассылку сейчас и вы всегда будете в курсе последних событий в мире информационных технологий.
Новостные информеры. Поставьте наши информеры к себе и у вас на сайте появится дополнительный постоянно обновляемый раздел.
Добавление статей. Если вы являетесь автором статьи или обзора на тему ИТ присылайте материал нам, мы с удовольствием опубликуем его у себя на сайте.
Реклама на сайте. Размещая рекламу у нас, вы получите новых посетителей, которые могут стать вашими клиентами.
 
Это интересно
 

Copyright © CompDoc.Ru
При цитировании и перепечатке ссылка на www.compdoc.ru обязательна. Карта сайта.