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

ОГЛАВЛЕНИЕ

Золотое правило оптимизации

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


Всегда разрабатывайте программы для баз данных с использованием реальных данных

 

Много проблем с производительностью возникает только тогда, когда база начинает заполняться, иногда даже переполняться, реальными данными при повседневной работе. Практически каждое приложение будет работать очень быстро с пустой базой данных. Разработчики затрачивают на формальное тестирование системы практически столько же времени, сколько тратится ими на активную разработку проекта, если не еще больше — это зависит от сложности системы. При использовании настоящих, так называемых "жизненных", данных в разработке вы можете столкнуться с проблемами производительности приложения уже на ранних этапах. И если для устранения узких мест понадобятся радикальные изменения в логике программы или в метаданных, это будет сделать намного проще именно на раннем этапе проектирования программы и базы данных.

Более того, убедитесь, что работаете с программой именно так, как делал бы это конечный пользователь. В проектах часто делается логическое разделение функций на функции пользователя и функции администратора. Кроме того, при работе простого пользователя на данные накладывается ряд существенных ограничений. На этапе тестирования программы разработчики и тестировщики пользуются правами администратора, чтобы можно было без лишнего труда проверить все функции программы как для администратора, так и для простого пользователя. Некоторые же проблемы производительности проявляются только тогда, когда пользователь входит в систему не как администратор, ведь тогда это требует выполнения некоторых дополнительных ограничений на доступ к данным, зачастую очень существенных. И только используя программу так, как будет с ней работать конечный пользователь (то есть проверяя работоспособность программы и как пользователь, и как администратор, строго разделяя сессии их работы), можно найти участки кода, ответственные за снижение производительности, и устранить их до того, как на них наткнется покупатель программы.

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

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


Бутылочное горлышко

 

Результативная производительность программ, работающих с СУБД InterBase/Firebird, может быть достигнута с учетом некоторых требований как к аппаратной, так и к программной части. Аппаратные требования — это отдельная тема, которая достойна более полного описания в отдельной статье. Если же рассматривать проблему с точки зрения программных требований, то все программы для работы с InterBase/Firebird используют соответствующий сервер плюс клиентское приложение и иногда какие-либо middleware-программы или программы сторонних производителей, например, Crystal Reports.

Бутылочным горлышком называются те части системы, которые приводят к снижению производительности работающего приложения. Это могут быть как фигуральные "бутылочные горлышки", когда запрос выполняется слишком долго, так и "реальные", когда приложению необходимо через dial-up-соединение перекачать достаточно большой объем данных. В соответствии с золотым правилом первым шагом на пути оптимизации должно быть определение таких узких мест в системе и поиск среди них тех, с которыми связаны наибольшие проблемы. Хотя, конечно, было бы идеально, если бы получалось избегать наиболее часто встречающихся ошибок еще на этапе проектирования. И, прежде чем обсуждать поиск проблемных мест в существующей программе, остановимся на аспектах проектирования приложений, которые имеют критически важное влияние на производительность системы.

Другая важная деталь, которая также не будет обсуждаться в этой статье, — как создавать эффективные SQL-запросы к базам данных. Этой теме будет посвящена другая статья с названием "Оптимизация SQL-запросов и метаданных".


Выбор типа доступа к данным

 

Эта часть статьи будет наиболее полезна тем, кто начинает проектирование нового проекта. Хотя она может пригодиться и тем, кто собирается мигрировать с уже устаревшего и морально, и физически движка BDE.


Клиент-сервер против многозвенной архитектуры

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

Замечу, что "псевдомногозвенная" архитектура, когда исполняемый файл содержит слои как сервера приложений, так и тонкого клиента, сейчас очень популярна. В такой модели существует четкое разделение в коде на сервер приложений (бизнес-правила) и тонкого клиента (представление данных). Но рассмотрение преимуществ той или иной модели проектирования выходит за рамки этой статьи. Поэтому, когда говорится о "многозвенных приложениях", имеются в виду как действительно многозвенные, с выделенным сервером приложений, программные комплексы, так и "псевдомногозвенные", в которых два слоя объединяются в одном исполняемом файле. С точки зрения оптимизации они очень похожи.

Существует, кроме всего вышеописанного, несколько различий между архитектурой клиент-сервер и многозвенной архитектурой с точки зрения оптимизации. Я не буду описывать оптимизацию работы DCOM-приложений, но одно из очень важных отличий проистекает из того, что сервер приложений должен быть "бессостоятельным" (от англ. stateless).

В приложении, основанном на архитектуре "клиент-сервер", считается вполне допустимым открытие запроса и поддержание его в таком состоянии довольно долго. СУБД семейства InterBase/Firebird имеют некоторые определенные проблемы в работе при наличии транзакции, активной достаточно длительное время, которые будут обсуждаться чуть ниже. Но наличие запроса, открытого в течение нескольких минут, не вызовет, скорее всего, никаких проблем в приложении клиент-сервер. Многозвенные же приложения, со своей стороны, требуют возврата результатов запроса, посланного тонким клиентом серверу приложений, в вызове одного и того же метода.

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

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

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


Компоненты для доступа и работы с InterBase/Firebird

Первая задача, которую вы должны решить, — что лучше использовать: специализированные компоненты для InterBase/Firebird или компоненты, независимые от типа СУБД.

Специализированные компоненты, такие, как IB Objects, IBX, FIB Plus, показывают лучшую производительность, часто значительно лучшую, чем независимые компоненты. Они, кроме всего прочего, поддерживают специальные возможности InterBase/Firebird, которые не реализованы в независимых компонентах. Некоторые из этих возможностей важны. Например, на момент написания этой статьи dbExpress не поддерживает создание новой базы данных InterBase. Если использовать специализированные компоненты оптимально, эффективность их работы сопоставима с эффективностью прямого использования API InterBase.

Проблема может возникнуть тогда, когда вас просят о том, чтобы приложение могло работать с другими базами данных, отличными от семейства InterBase. Это ограничение легко преодолеть, если вы остановились на многозвенной или псевдомногозвенной архитектуре приложения, тогда интерфейс пользователя не будет жестко привязан к специфическим для InterBase/Firebird компонентам. Но все же это будет труднее сделать, если вы будете использовать независимые от типа СУБД компоненты доступа к данным.


Независимые от СУБД компоненты

Практически очевидно, что независимые от типа СУБД компоненты работают медленнее своих специализированных собратьев. В то время как dbExpress работает значительно лучше, чем BDE, первые версии имели проблемы с выборкой метаданных, решением которых стали заниматься только в последующих выпусках. Проблема получения метаданных решается выставлением свойства NoMetadata, но тогда возникает несовместимость с некоторыми наборами данных. Таким образом, в то время как dbExpress все-таки работает быстрее, чем работал BDE, эта технология по-прежнему уступает по своей эффективности и скорости "родным" для InterBase/Firebird компонентам.


Использование InterBase API

Как было упомянуто выше, правильное использование специализированных компонент ставит их по производительности практически на одну ступень с вызовами API выбранной СУБД. На мой взгляд, использование API оправданно в том редком случае, когда возможностей даже специфических компонент для разработки недостаточно, хотя это и крайне маловероятно, или если для платформы, под которую ведется разработка, такие компоненты отсутствуют (Sun Solaris).


Создание запросов к базе данных

Выбрав стратегию доступа к данным и определившись с архитектурой приложения, можно обратить внимание на то, каким образом мы собираемся их использовать. Главное правило состоит в том, что чем меньше вы запрашиваете данных у сервера, тем быстрее будет работать ваше приложение. Конечно, запрашивать у сервера меньше данных, чем пользователь хочет увидеть за один раз, нерационально, поэтому первым вопросом должен быть "какие данные необходимы для каждого модуля системы?" Разработчикам, переходящим с настольных баз данных, требуется перебороть в себе таблично ориентированное представление о базах данных. База InterBase, несомненно, содержит таблицы. Но при проектировании программы вы их не видите, вы видите только результат выполнения запроса SQL. Можно, конечно, написать запрос, который возвращает все записи из таблицы (по крайней мере, видимые для данной транзакции):

SELECT * FROM SOME_TABLE

Но в большинстве случаев такой запрос вернет значительно больше данных, чем это требуется для оптимальной работы пользовательского интерфейса и обработки бизнес-процессов. Подобный запрос, кстати, не использует такие полезные особенности InterBase/ Firebird, как возможность объединения (JOIN) и сортировки (ORDER BY) результирующего набора данных.


Запрашиваете меньше данных — получаете большую скорость

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

К чему все это сводится? Вот к чему: чем меньше вы запрашиваете и пересылаете данных, тем быстрее ваше приложение будет работать, даже на не очень скоростных сетях. Вот несколько прикладных методов, которые вы можете использовать для уменьшения количества выбираемых (SE-LECT) данных.


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

Если список слишком велик, чтобы отображать его в единственном неразрывном виде, разбейте его на логические страницы с табуляцией по первым буквам от "А" до "Я". Если и в этом случае списки получаются слишком длинными, предоставьте пользователю мощные средства фильтрации данных для сужения полученного в результате применения фильтра множества записей. Для реализации поиска данных в приложении вы можете взять на вооружение методы, используемые для поиска web-страниц. Когда пользователю выдается набор записей, даже если он сравнительно небольшой, достаточно использовать одно-два ключевых поля для формирования фильтра запроса. Пусть в приложении будет отдельное окно или часть окна, где пользователь может увидеть все данные по записи, если он обнаружил то, что искал. Старайтесь также использовать объединения таблиц (JOIN) в запросах вместо lookup-полей на формах всюду, где это будет возможно. Хотя и возможно оптимизировать выполнение метода TDataset. Lookup, даже этот улучшенный метод не будет работать быстрее объединения таблиц (JOIN) — про работу немодифицированного метода вообще можно не упоминать.


Избегайте итераций по наборам данных

Уменьшение трафика между клиентом и сервером очень важно для неинтерактивных процессов и приложений. При написании неинтерактивных приложений старайтесь избегать употребления конструкций, подобных приведенной ниже:

Begin
While not someQuery.Eof do begin
do something here
someQuery.Next;
end;

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


Понимание механизма транзакций

Создается впечатление, что большинство разработчиков желали бы, чтобы их не существовало в принципе (в MySQL на MyISAM вам всем тогда дорога, хотя и там есть свое понятие транзакций, очень специфическое). Действительно, если программа делает всего лишь выборку данных запросом (SELECT), то зачем в этом случае использовать транзакции? В действительности же транзакции так же важны для чтения данных, как и для их записи, и надлежащее использование транзакций может уменьшить объем выполняемой клиентом и сервером работы. Надлежащее использование транзакций — критический аспект хорошо разработанного приложения базы данных. Если вы не знаете, как использовать транзакции себе на пользу, ваш проект не полон. СУБД InterBase/Firebird дают разработчикам значительно большее количество вариантов для настройки поведения транзакций, чем это когда-либо может понадобиться любому из разработчиков. Здесь же будут описаны наиболее часто употребляемые опции. Классическая модель транзакций описывается аббревиатурой ACID (кислота) — Atomicity, Consis-tency, Isolation and Durability (атомарность, последовательность, изоляция и длительность). Рабочий аспект транзакций — их атомарность. Но мы должны также учесть аспект изоляции, который определяет поведение транзакций.


Изоляция транзакций

Изоляция означает, что параллельно выполняющиеся транзакции не могут пересекаться друг с другом. Кроме всего прочего, это означает, что ни одна транзакция не может видеть неподтвержденные другой транзакцией данные (иногда это также называется dirty read). Не все СУБД это предписывают, но InterBase/Firebird так делает, причем по умолчанию — всегда. Одно из важнейших последствий такого принципа работы состоит в том, что клиент должен запустить запрос, получить строки данных и завершить выполнение инструкций в контексте одной транзакции. Без указания транзакции запрос выполнить невозможно, так как сервер тогда просто не будет знать, какую версию записи транзакция должна видеть.

Существует несколько уровней изоляции транзакций, но наиболее часто используемый уровень — read committed (читать только подтвержденные) и snapshot (снимок). В первом случае запрос возвращает записи именно в таком виде, в каком они существуют на момент выполнения запроса, даже если они будут переданы клиенту значительно позже. Во втором случае запрос возвращает состояние записей на момент старта транзакции, даже если сам запрос был выполнен значительно позже. То есть, если Вася стартует транзакцию с уровнем read committed и выбирает все записи из таблицы, но не все записи передаются ей в клиентское приложение, а Петя в этот момент удаляет одну из записей и завершает транзакцию, запрос Васи должен возвратить удаленную Петей запись, как только запрос Васи наконец-то передает ее в приложение, даже если эта запись была передана уже после того, как Петя ее удалил. Если бы набор данных Васи не включал удаленной Петей записи, получилось бы, что транзакции Пети и Васи пересеклись, и изоляция была нарушена. Последнее приводит к тому, что транзакция Васи транзакцией, грубо говоря, называться не может. Вот почему SELECT-запросы, как и все остальные, следует использовать в контексте транзакций, и вот почему нельзя получить часть данных в транзакции, завершить ее, а затем продолжить получение информации из набора данных. Причем нет никакого способа предписывать уровни изоляции без механизма транзакций.

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


Режимы транзакций 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 для отображения обработки больших объемов данных, чтобы пользователь не гадал, работает программа или уже повисла из-за ошибки; чтобы он мог знать, сколько еще времени займет та или иная операция.

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