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

Оптимизация приложений для работы с СУБД 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-запросы, как и все остальные, следует использовать в контексте транзакций, и вот почему нельзя получить часть данных в транзакции, завершить ее, а затем продолжить получение информации из набора данных. Причем нет никакого способа предписывать уровни изоляции без механизма транзакций. Теперь о том, зачем вообще заботиться об изоляции транзакций. Когда к таблицам могут одновременно обращаться сотни пользователей, должен быть способ точно и ясно определить, что видит каждая из транзакций. Вы нуждаетесь в возможности связывания одного набора данных с другим, а этого нельзя сделать, если набор данных может изменяться в какое-то время между собственно выборкой и передачей его клиенту.

Продолжение следует.
Денис "Denver" Мигачев,
dtm@tut.by



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

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