Оптимизация приложений для работы с СУБД InterBase 2

Оптимизация приложений для работы с СУБД InterBase

Продолжение. Начало в КГ №51 (2003)

Режимы транзакций Read-Write и Read-Only. Установка транзакции в режим read-only (как, впрочем, и в любой другой из режимов) является способом указания цели использования этой транзакции. В некоторых случаях это позволяет серверу провести оптимизацию, которая уменьшает объем требуемой от него работы для обслуживания транзакции. Так, например, применение опции read-only полезно в случае использования только SELECT-запросов, когда вам нет необходимости добавлять, изменять и удалять записи из набора данных. Опция read-write применяется, как уже понятно, в случае необходимости изменения набора данных, полученного в контексте рассматриваемой транзакции.

Завершение транзакций. Существует два способа завершения транзакций: подтверждение (COMMIT) и отмена (ROLLBACK). По причинам, которые будут обсуждаться ниже, очень важно закрывать (или, правильнее будет сказать, завершать) транзакции, когда в них больше нет необходимости, даже если вам придется их сразу же перезапустить (переоткрыть). Заметьте, что команды COMMIT RETAINING и ROLLBACK RETAINING реально не завершают транзакцию — они только делают изменения, внесенные транзакцией, видимыми для других транзакций. Везде же, где видится возможным, рекомендуется использовать команды COMMIT и ROLLBACK вместо COMMIT RETAINING/ROLLBACK RETAINING. До выхода InterBase версии 7 единственным преимуществом COMMIT RETAINING являлась возможность использовать изменения, сделанные вашей транзакцией, в контексте этой самой транзакции. Впрочем, если необходимо, вы можете использовать и две транзакции. В седьмой версии InterBase добавлены новые возможности для COMMIT RETAINING, требующие отдельного рассмотрения.

В отличие от многих других серверов баз данных, InterBase может исполнять откат транзакций немедленно благодаря многоверсионной структуре данных. Однако использовать откат транзакций следует только тогда, когда это действительно необходимо, так как количество откатов учитывается сервером для определения времени очистки базы данных (SWEEP INTERVAL of DATABASE). Разработчики часто задаются вопросом, как завершать транзакцию в read-only-запросах. Правильнее, наверное, будет использовать COMMIT ввиду того, что InterBase все равно для read-only-запроса выполнит команду COMMIT вместо ROLLBACK, даже если была послана последняя команда. Фактически же каждый четный откат в транзакциях с параметром read-write преобразуется в подтверждение старой версии редактируемых данных, так как это повышает производительность сервера. Так что следует разумно пользоваться операцией отката (ROLLBACK), когда изменения нужно отменить, и операцией подтверждения, если изменения надо сохранить. Конечно, с учетом контекста текущей транзакции.

Множественные транзакции в контексте одного подключения. В отличие от некоторых других СУБД, серверы InterBase и Firebird (а с выходом InterBase версии 7 можно говорить о них как о родственных, но все-таки разных СУБД) позволяют иметь практически неограниченное количество одновременно активных транзакций внутри одного подключения к базе данных. Это дает программисту очень большую свободу в проектировании способа использования транзакций. Можно каждому набору данных сопоставить свою отдельную транзакцию или множество наборов данных могут использовать одну и ту же транзакцию. Нижеследующее описание может помочь в планировании использования транзакций.
Атомарность/Изоляция. Если группа запросов должна выполниться или отклониться вся сразу или один запрос должен видеть неподтвержденные данные, произведенные другим запросом, они просто должны использовать одну и ту же транзакцию. Причем другого пути достичь нужного результата в данном случае нет.

Производительность. Один из ключевых факторов, позволяющих повысить производительность программы, — это сохранение времени активности (еще говорят "жизни") транзакции как можно меньшим. Кроме того, наличие относительно большого количества одновременно активных транзакций не является хорошим тоном в программировании. Если и существует предел количества одновременно открытых транзакций, то, естественно, разработчик вовсе не желает его достигнуть. Позволяя многим транзакциям оставаться активными одновременно, даже если они не используются на текущий момент, разработчик может столкнуться с проблемой достижения предела количества активных транзакций сервера — "Too many open transactions" (слишком много открытых транзакций).

Простота. Выберите схему работы, наиболее удобную для вас. Некоторые находят для себя более легкой работу с одной транзакцией; другие предпочитают связку "один запрос — одна транзакция"; третьи используют группировку запросов на основании их уровня изоляции — так, запросы на выборку могут использовать одну транзакцию с опцией read-only, а запросы на изменение — другую, с опцией read-write.

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

Многоверсионная архитектура базы данных.
Важно знать, что сервер InterBase/FireBird всегда создает множество копий записи, когда она со временем изменяется. В этом плане СУБД работает как Система Контроля Версий CVS (Concurrent Versioning System), создавая новую копию записи каждый раз, когда различные транзакции ее (запись) изменяют. Данный механизм необходим серверу для того, чтобы транзакции могли определять, какую версию записи им необходимо видеть.

Работает этот механизм следующим образом. Когда стартует каждая из транзакций, они по порядку получают свой внутренний уникальный номер TRANSACTION_ID. Также транзакция обладает копией внутренней структуры, называемой Transaction Inventory Page (TIP), по которой она получает информацию о состоянии других транзакций (committed, rolled back и т.д.) на момент ее старта. Когда транзакция воздействует на строку в таблице (изменяя, добавляя или удаляя и т.д.), создается новая версия строки и помечается уникальным номером породившей ее транзакции. Теперь, когда другая транзакция выполняет запрос на выборку (SELECT), который возвращает эту строку данных, сервер сравнит режим изоляции транзакции — исполнителя запроса с состоянием транзакций, затронувших каждую версию строки, захватит и выдаст наиболее новую версию строки из всех видимых версий для транзакции — исполнителя запроса на выборку (если таковые имеются).

Очистка базы данных (Sweeping). Понятие sweep — очистка — означает, что сервер должен провести работу по сбору старых версий строк данных, появившихся за время работы с программой — клиентом базы данных. Функции сервера, ответственные за сборку мусора и очистку, будут удалять из базы данных более ненужные устаревшие версии записей. Версия записи считается устаревшей в случае, если нет в принципе возможности существования транзакции, которой данная версия может потребоваться. Так что, если вы начали транзакцию в режиме snapshot, к примеру, то сервер будет не в состоянии очистить ни одну из старых версий и ни одну из записей с момента начала транзакции до тех пор, пока транзакция не завершится. Ведь сервер не имеет ни малейшего понятия, какие таблицы и какие записи могут понадобиться транзакции. Это, конечно, подразумевает использование дополнительного дискового пространства, но в большинстве случаев больших проблем в производительности это не вызывает.

Еще одной причиной, по которой может проявиться понижение производительности в старых версиях InterBase, является собственно процесс очистки — sweep process. В СУБД InterBase 7 многопоточная модель работы ядра сервера баз данных была значительно улучшена, и на текущий момент процесс очистки не вызывает замедления или блокирования выполнения запросов. В старых версиях InterBase скорость выполнения пользовательских запросов может быть заметно ниже обычной, если база данных в этот момент времени очищается. Но несмотря на все трудности, существует несколько обходных путей, позволяющих решить эту проблему полностью или частично.

Самый простой — перейдите на СУБД InterBase версии 7.1. Запретите автоматическую очистку базы данных и запускайте ее очистку в ручном режиме во время наименьшей загруженности сервера. Уменьшите объем работы, который необходимо будет выполнить процессу, и увеличьте период между повторными запусками очистки. Этого можно достигнуть путем использования транзакций в режиме read committed вместо snapshot, а также путем минимизации откатов изменений везде, где это возможно.

Перегрузка транзакций.
Как было сказано выше, при старте каждая транзакция получает свою копию TIP (Transaction Inventory Page — Страница Состояния Транзакций). Структура TIP описывает состояние всех транзакций, активных или, наоборот, завершившихся, откатом к ранее стартовавшей транзакции, которая была активна на момент старта текущей транзакции (Oldest Active Transaction, OAT — Старейшая Активная Транзакция). Это означает, что, если какая-либо транзакция остается активной достаточно долгое время, структура TIP может иметь очень большой размер. Так как каждая новая транзакция получает копию страницы состояния и должна консультироваться с ней при чтении каждой записи, производительность сервера также падает. Для диагностирования подобной проблемы можно использовать любой монитор производительности запросов.
Тем не менее, в некоторых обстоятельствах длительные транзакции режима read committed не производят подобного замедления работы сервера, однако это требует проведения практических тестов по использованию транзакций в разных режимах работы сервера. Для снижения влияния длительно активных транзакций были внесены соответствующие изменения в версию СУБД InterBase 7.0.

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

Стандартные узкие места и способы их устранения. Ниже приведены наиболее общие и часто встречающиеся ошибки, существующие в клиентских приложениях, работающих с СУБД InterBase/FireBird, и способы их устранения.

Избыточная передача данных.
Самый простой способ диагностирования этой проблемы — использование монитора SQL-запросов. Заметьте, что большинство мониторов SQL не показывают объем переданных данных — эту опцию, как правило, необходимо включить вручную. Если избыточная передача данных замедляет работу приложения, это можно легко выявить с помощью монитора SQL.
Для нормальной работы приложения необходимо, чтобы размер результирующего набора данных был небольшим. Разбиение на страницы результирующего набора данных взамен передачи всех данных (fetch all) иногда действительно может помочь. Также предоставляйте вашим пользователям средства для быстрого и эффективного поиска необходимых данных вместо бесконечных строк с долгим перемещением для позиционирования на необходимой записи. В качестве примера, чтобы не показывать тысячи записей с именами работников компании, добавьте индексные закладки с алфавитными указателями — тогда результирующий набор данных будет уже меньше как минимум раз в 20-25. Если даже такой вариант составляет большие наборы данных для каждой закладки, предоставьте пользователю возможность поиска необходимой записи — это лучше, чем выбирать информацию из длинного списка.
Если у вас есть много наборов данных, передача их клиентскому приложению также может сильно сказаться на производительности, даже если размер этих наборов данных мал. Причины появления таких узких мест могут быть разными, поэтому ограничимся наиболее распространенными и приведем краткое их описание.

Причина: Использование BDE. Решение: Так как BDE может использовать только одну транзакцию в контексте одного подключения (несмотря на то, что сам InterBase такого ограничения не имеет), и подтверждение транзакции закрывает все без исключения активные курсоры, механизм BDE выполняет полную передачу записей (FetchAll) по всем открытым наборам данных, когда транзакция подтверждается (commit). Практически это выражается в том, что вызов метода TDataSet.Post в приложении, использующем BDE, вызывает выполнение метода FetchAll каждого набора данных внутри приложения. Самым лучшим решением в таком случае будет отказ от использования BDE и переход на работу со специализированными компонентами для InterBase, поддерживающими работу с несколькими транзакциями внутри одного подключения к базе данных. Если нет возможности обойти проблему, сократите до минимального количество одновременно открытых наборов данных и вызывайте FetchAll по их открытию (таким образом FetchAll для каждого набора данных не будут выполняться одновременно, когда пользователь отредактировал некоторую запись).

Причина: Использование lookup-полей. Решение: Когда вы используете lookup-поля вместо оператора JOIN, тогда lookup-таблицы (или их значительные части) должны быть переданы клиенту вместе с "основным" набором данных, когда тот активируется. Проблема состоит в том, что процесс поиска в lookup-полях происходит без использования индексной информации, ведь поиск производится на стороне клиента. Следует всегда пользоваться JOIN в SQL-запросах и отказаться от использования lookup-полей. Если все-таки необходимо использовать lookup-поля, кэшируйте необходимую информацию из статических таблиц путем сохранения в TClientDataSet.

Причина: Длительно активные транзакции. Решение: Решение лежит прямо на поверхности — просто не надо иметь в программе транзакций с большим временем жизни. Но существуют случаи, когда такие транзакции действительно необходимы. Будем считать, что мы используем InterBase 7.0 или выше (FireBird 1.5), и поэтому потеря производительности нас не затрагивает. Случается иногда, что даже такой выход не помогает, хотя подобные случаи крайне редки. Например, такое может случиться при длительной обработке данных сложной структуры, но даже в этом случае многое зависит от косвенных нюансов проектирования базы данных и специфики работы приложения. Также разработчики сталкиваются, но не часто, с проблемами использования длительно активных транзакций при пакетной обработке данных (так называемые "внутрикодовые" транзакции, которые несколько отличаются от транзакций, обслуживающих отображение данных). Решение проблемы вполне стандартно и самодостаточно и заключается в использовании в коде блока try … finally. Ведь, как правило, проблемы с длительно активными транзакциями проявляются сугубо при отображении информации в пользовательском интерфейсе.

Конечно, управление транзакциями осуществляется радикально отличающимися путями в зависимости от того, какой набор компонент вы используете для доступа к данным базы InterBase. Наборы компонент IBX, dbExpress, IBO и BDE, например, работают с транзакциями по своим схемам. Каждый из наборов работает с транзакциями по-своему, поэтому самым лучшим советом здесь будет тщательное изучение механизма работы с транзакциями тех компонентов, которые вы используете.
Нельзя не упомянуть технологию DataSnap, которая предоставляет нейтральную, независимую от компонент методику управления транзакциями, работающую очень и очень хорошо и использующую очень короткие по времени активности транзакции. При использовании DataSnap появляется возможность даже радикальной смены компонент доступа без капитальной перестройки приложения.

Ведение подсчета записей.
Важно понимать, что каждая из идей подсчета записей неуместна в случае многопользовательской архитектуры программы, за исключением случая с использованием транзакций в режиме snapshot. Без такого уровня изоляции невозможно точно подсчитать количество записей и выбрать их в контексте одной операции. Так, если на момент выборки существуют другие пользователи, работающие с таблицей (таблицами), "действительное" количество записей может измениться с момента подсчета записей до момента, когда они выбраны. В мире Delphi и C++ Builder все еще сложнее: свойство TDataSet.RecordCount слабо отражает действительное положение вещей, то есть не отображает реального количества информации в наборе данных. Более того, каждый из наборов компонент, будь то IBX, IBO, dbExpress или BDE, имеет свои реализации подсчета записей. И ни одна из реализаций на другую не похожа.

И, наконец, не забывайте, что многоверсионная структура данных в СУБД InterBase, хоть и имеет массу преимуществ, делает процесс подсчета записей в больших таблицах крайне затратной операцией — это особенно заметно влияет на производительность, если очистка базы данных (sweep process) производилась давно. Серверу просто приходится проверять каждую запись на предмет попадания в область видимости текущей транзакции. По моему мнению, существует, если вообще есть, буквально пара-тройка случаев, когда использование свойства RecordCount или оператора SQL COUNT может быть обоснованным. Такие ошибки, как правило, следует искать в программах, показывающих очень слабую производительность, которые вы сами не писали, но вас попросили улучшить их скорость работы.
Для подобных случаев существует также несколько альтернативных вариантов реализации. Один из них — перед вами. Так, чтобы определить, не пуста ли таблица, лучше использовать свойство IsEmpty:

if not MyTable.IsEmpty then begin
DoSomething;
end
else begin
raise Exception.Create (NORECORDS);
end;
Далее — если необходимо определить в хранимой процедуре или триггере, пуста ли таблица. Пример ниже возвращает "таблица не пуста", если в ней (таблице) существует хотя бы одна запись:

SELECT "таблица не пуста"
FROM RDB$DATABASE
WHERE EXISTS (SELECT 1 FROM MY_TABLE);

При написании этого запроса используется несколько полезных (или бесполезных) уловок. Таблица RDB$DATABASE является системной таблицей, которая всегда имеет хотя бы одну запись.
Благодаря этому в вышеописанном запросе мы получим в качестве результата только одну запись, содержащую строку "таблица не пуста" в случае, если в таблице есть одна или более записей, что обуславливается SQL-оператором EXISTS.
Выбор константы обусловлен тем фактом, что для нас на этом этапе совершенно безразлично, что же именно содержится в таблице. Нам главное — знать, есть там записи или их нет.
Итерации по набору данных следует проводить с использованием свойства IsEmpty набора данных, а не путем перебора записей по какому-либо из счетчиков (об этом уже говорилось выше):

while not MyTable.Eof do begin
DoSomething;
MyTable.Next;
end;

Создайте в своем приложении также некий ProgressBar для отображения обработки больших объемов данных, чтобы пользователь не гадал, работает программа или уже повисла из-за ошибки; чтобы он мог знать, сколько еще времени займет та или иная операция.
Конечно, это не является альтернативой подсчету записей, но позволяет сделать процесс их обработки более эффективным путем разбиения на логические части меньшего объема. Как это можно сделать, будет показано на примере далее.

Продолжение следует.

Денис "Denver" Мигачев, dtm@tut.by



Компьютерная газета. Статья была опубликована в номере 09 за 2004 год в рубрике программирование :: разное

©1997-2024 Компьютерная газета