Используйте связываемые переменные
Если бы мне пришлось писать книгу о том, как создавать немасштабируемые приложения Oracle, первая и единственная ее глава называлась бы "Не используйте связываемые переменные". Это — основная причина проблем, связанных с производительностью, и основная помеха масштабируемости. Особенности использования разделяемого пула Oracle (очень важной структуры данных разделяемой памяти) требуют от разработчиков использовать разделяемые переменные. Если надо замедлить работу приложения Oracle, вплоть до полного останова, — откажитесь от их использования.
Связываемая переменная — это подставляемый параметр запроса. Например, для получения записи доя сотрудника с номером 123, можно выполнить запрос:
select * from emp where empno = 123;
Но можно задать и другой запрос:
select * from emp where empno = :empno;
В обычной системе информацию о сотруднике с номером 123 могут запрашивать всего один раз. В дальнейшем будут запрашивать информацию о сотрудниках с номерами 456, 789 и т.д. При использовании в запросе литералов (констант) каждый запрос является для СУБД абсолютно новым, никогда ранее не выполнявшимся. Его надо разбирать, уточнять (определять объекты, соответствующие именам), проверять права доступа, оптимизировать и т.д. — короче, каждый выполняемый уникальный оператор придется компилировать при каждом выполнении.
Во втором запросе используется связываемая переменная, :empno, значение которой подставляется в запрос при выполнении. Этот запрос компилируется один раз, а затем план его выполнения запоминается в разделяемом пуле (в библиотечном кеше), из которого его можно выбрать для повторного выполнения. Различие между этими двумя вариантами в плане производительности и масштабируемости — огромное, даже принципиальное.
Из представленного выше описания вполне понятно, что разбор оператора с явными, жестко заданными константами (так называемый полный разбор) выполняется дольше и требует намного больше ресурсов, чем повторное использование уже сгенерированного плана запроса (его называют частичным разбором). Менее очевидным может оказаться, насколько постоянный полный разбор сокращает количество пользователей, поддерживаемых системой. Отчасти это связано с повышенным потреблением ресурсов, но в гораздо большей степени — с механизм защелок, используемых в библиотечном кеше. При полном разборе запроса СУБД будет дольше удерживать определенные низкоуровневые средства обеспечения последовательного доступа, которые называются защелками (подробнее о них см. в главе 3). Защелки защищают структуры данных в разделяемой памяти сервера Oracle от одновременного изменения двумя сеансами (иначе эти структуры данных Oracle в конечном итоге были бы повреждены) и от чтения этой структуры данных по ходу изменения другим сеансом. Чем чаще и на более продолжительное время на эти структуры данных устанавливаются защелки, тем длиннее становится очередь для установки этих защелок. Точно так же происходит при использовании длинных транзакций в среде MTS, — монополизируются критические ресурсы. Временами машина может казаться минимально загруженной, а СУБД работает очень медленно. Вполне вероятно, что один из сеансов удерживает защелку и формируется очередь в ожидании ее освобождения. В результате работа с максимальной скоростью невозможна. Достаточно одного неверно работающего приложения для существенного снижения производительности всех остальных приложений. Одно небольшое приложение, не использующее связываемые переменные, приводит со временем к удалению из разделяемого пула необходимых SQL-операторов других хорошо настроенных приложений. Достаточно ложки дегтя, чтобы испортить бочку меда.
При использовании связываемых переменных любой сеанс, выдающий тот же самый запрос, будет использовать уже скомпилированный план выполнения из
библиотечного пула. Подпрограмма компилируется один раз, а используется многократно. Это очень эффективно, и именно такую работу пользователей предполагает СУБД. При этом не только используется меньше ресурсов (частичный разбор требует намного меньше ресурсов), но и защелки удерживаются значительно меньше времени, и нужны гораздо реже. Это повышает производительность и масштабируемость.
Чтобы хоть примерно понять, насколько существенно это может сказаться на производительности, достаточно выполнить очень простой тест:
tkyte@TKYTE816> alter system flush shared_pool;
System altered.
Здесь я начинаю с "пустого" разделяемого пула. Если потребуется выполнять этот тест многократно, придется очищать разделяемый пул каждый раз, иначе представленный ниже оператор SQL, в котором не используются связываемые переменные, окажется в кеше и будет выполняться очень быстро.
tkyte@TKYTE816> set timing on tkyte@TKYTE816> declare 2 type rc is ref cursor; 3 l_rc rc; 4 l_dummy all_objects.object_name%type; 5 l_start number default dbms_utility.get_time; 6 begin 7 for i in 1 .. 1000 8 loop 9 open l_rc for 10 'select object_name 11 from all_objects 12 where object_id = ' i; 13 fetch l_rc into l_dummy; 14 close l_rc; 15 end loop; 16 dbms_output.put_line 17 ( round( (dbms_utility.get_time-l_start)/100, 2 ) 18 ' seconds...' ); 19 end; 20 / 14.86 seconds...
PL/SQL procedure successfully completed.
В этом коде используется динамический SQL для запроса одной строки из представления ALL_OBJECTS. Он генерирует 1000 уникальных запросов со значениями 1, 2, 3, ... и так далее, жестко заданными в конструкции WHERE. На моем ноутбуке с процессором Pentium 300 Мгц для его выполнения потребовалось около 15 секунд (скорость выполнения на разных машинах может быть различной).
Теперь сделаем то же самое с использованием связываемых переменных:
tkyte@TKYTE816> declare 2 type rc is ref cursor; 3 l_rc rc; 4 l_dummy all_objects.object_name%type; 5 l_start number default dbms_utility.get_time; 6 begin 7 for i in 1 .. 1000 8 loop 9 open l_rc for 10 'select object_name 11 from all_objects 12 where object_id = :x' 13 using i; 14 fetch l_rc into l_dummy; 15 close l_rc; 16 end loop; 17 dbms_output.put_line 18 (round( (dbms_utility.get_time-l_start)/100, 2 ) 19 ' seconds...'); 20 end; 21 / 1.27 seconds...
PL/SQL procedure successfully completed.
В этом коде использован точно такой же алгоритм. Единственное изменение — вместо жестко заданных значений 1, 2, 3... и так далее в запросе используется связываемая переменная. Результаты весьма впечатляющи. Код не только выполняется намного быстрее (разбор запросов требовал больше времени, чем их реальное выполнение!), но и позволяет большему количеству пользователей одновременно работать с системой.
Выполнение операторов SQL без связываемых переменных во многом подобно перекомпиляции подпрограммы перед каждым вызовом. Представьте себе передачу клиентам такого исходного кода на языке Java, что перед любым вызовом метода класса им необходимо вызывать компилятор Java, компилировать класс и выбрасывать сгенерированный байт-код сразу после выполнения метода. При необходимости вызова того же метода в дальнейшем им пришлось бы делать то же самое — компилировать, запускать и выбрасывать байт-код. В своих приложениях никто так не поступает — не делайте этого и в СУБД.
В главе 10 мы рассмотрим способы определить, используются ли связываемые переменные, различные варианты их применения, поддерживаемую СУБД возможность автоматической подстановки связываемых переменных и т.д. Мы также рассмотрим особый случай, когда использование связываемых переменных нежелательно.
Часто, как и в рассматриваемом проекте, — переписать существующий код так, чтобы использовались связываемые переменные, является единственно возможным выходом. Получаемый в результате код работает на несколько порядков быстрее и во много раз увеличивается количество поддерживаемых системой одновременно работающих пользователей. Для этого, однако, требуется много времени и усилий. Дело не в том, что использовать связываемые переменные сложно или при этом часто делают ошибки, — проблема в том, что с самого начала этого не делали, и поэтому пришлось пересмотреть и изменить практически весь код. Разработчикам не пришлось бы платить такую цену, если бы они с первого дня понимали принципиальную важность использования в приложении связываемых переменных.
Избегайте длительных транзакций в среде MTS
Решение использовать транзакции продолжительностью более 45 секунд в среде MTS выдало недостаточное понимание назначения режима MTS и особенностей его работы в Oracle. Если коротко, в режиме MTS используется общий пул серверных процессов, обслуживающий намного больший пул конечных пользователей. Это похоже на пул подключений. Поскольку создание и управление процессом — наиболее дорогостоящие операции, выполняемые операционной системой, режим MTS дает большие преимущества для крупномасштабной системы. Можно обслуживать 100 пользователей всего пятью или десятью разделяемыми серверными процессами.
Когда разделяемый серверный процесс получает запрос на изменение данных или выполнение хранимой процедуры, он привязывается к этой задаче до ее завершения. Ни одна другая задача не будет использовать разделяемый серверный процесс, пока не будет закончено изменение или не завершится выполнение хранимой процедуры. Поэтому при использовании режима MTS надо использовать как можно быстрее выполняющиеся операторы. Режим MTS создан для обеспечения масштабируемости систем оперативной обработки транзакций (OLTP), для которых характерны операторы, выполняющиеся за доли секунды. Речь идет об изменениях отдельных строк, вставке нескольких строк и запросах записей по первичному ключу. Не стоит в этом режиме выполнять пакетные процессы, для завершения которых требуются десятки секунд или минуты.
Если все операторы выполняются быстро, архитектура MTS работает отлично. Можно эффективно обслуживать небольшим количеством процессов большое сообщество пользователей. Если же имеются сеансы, монополизирующие разделяемый сервер надолго, то кажется, что СУБД "зависает". Пусть сконфигурировано десять разделяемых серверов для поддержки 100 пользователей. Если в некоторый момент времени десять пользователей одновременно введут оператор, выполняющийся более 45 секунд, то всем остальным транзакциям (и новым подключениям) придется ждать. Если некоторым из ожидающих в очереди сеансов необходимо выполнять оператор такой же продолжительности, возникает большая проблема — "зависание" будет продолжаться не 45 секунд, а намного дольше. Даже если желающих выполнить подобный оператор одновременно будет не десять, а лишь несколько, все равно будет наблюдаться существенное падение производительности сервера. Мы отберем на длительное время совместно используемый ресурс, и это плохо. Вместо десяти серверных процессов, выполняющих быстрые запросы в очереди, остается пять или шесть (или еще меньше). Со временем система станет работать с производительностью, заметно меньше предполагаемой, исключительно из-за нехватки этого ресурса.
Простое решение "в лоб" состоит в запуске большего количества разделяемых серверов, но в конечном итоге придется запускать разделяемый сервер для каждого пользователя, а это неприемлемо для системы с тысячами пользователей (как та, что создавалась в рассматриваемом проекте). Это не только создает узкие места в самой системе (чем большим количеством процессов приходится управлять, тем больше процессорного времени на это уходит), но и просто не соответствует целям создания режима MTS.
Реальное решение этой проблемы оказалось простым: не выполнять продолжительные транзакции на сервере, работающем в режиме MTS. А вот реализация этого решения оказалась сложнее. Это можно было сделать несколькими способами, но все они требовали существенных изменений архитектуры. Самым подходящим способом, требующим минимальных изменений, оказалось использование средств расширенной поддержки очередей (Advanced Queues — AQ).
AQ — это промежуточное программное обеспечения для обмена сообщениями, реализованное на базе СУБД Oracle и позволяющее клиентскому сеансу добавлять сообщения в таблицу очереди базы данных. Это сообщение в дальнейшем (обычно сразу после фиксации транзакции) выбирается из очереди другим сеансом, проверяющим содержимое сообщения. Сообщение содержит информацию для обработки другим сеансом. Оно может использоваться для эмуляции мгновенного выполнения за счет вынесения продолжительного процесса за пределы интерактивного клиента.
Итак, вместо выполнения 45-секундного процесса, компонент должен помещать запрос со всеми необходимыми входными данными в очередь и выполнять его асинхронно, а не синхронно. В этом случае пользователю не придется ждать ответа 45 секунд, то есть система становится более динамичной.
Хотя, судя по описанию, этот подход прост (подключение механизма AQ полностью решает проблему), потребовалось сделать намного больше. Этот 45-секундный процесс генерировал идентификатор транзакции, необходимый на следующем шаге в интерфейсе для соединения таблиц — по проекту интерфейс без этого не работал. Используя механизм AQ, мы не ждем генерации идентификатора транзакции, — мы обращаемся к системе с просьбой сделать это когда-нибудь. Поэтому приложение опять оказалось в тупике. С одной стороны, мы не можем ждать завершения процесса 45 секунд, но, с другой стороны, для перехода к следующему экрану необходим сгенерированный идентификатор, а получить его можно только спустя 45 секунд. Для того чтобы решить эту проблему, пришлось синтезировать собственный поддельный идентификатор транзакции, изменить продолжительный процесс так, чтобы он принимал этот сгенерированный поддельный идентификатор и обновлял таблицу, записывая его по завершении работы, благодаря чему реальный идентификатор транзакции связывался с поддельным. То есть, вместо получения реального идентификатора в результате длительного процесса, этот идентификатор становится для процесса входными данными. Во всех "подчиненных" таблицах использовался этот поддельный идентификатор транзакции, а не реальный (поскольку генерации реального надо ждать определенное время). Нам также пришлось пересмотреть использование этого идентификатора транзакции, чтобы понять, как это изменение повлияет на другие модули, и так далее.
Еще одна проблема состояла в том, что при синхронной работе, если 45-секундный процесс завершался неудачно, пользователь узнавал об этом сразу. Он мог устранить причину ошибки (обычно путем изменения входных данных) и повторно выполнить запрос. Теперь, когда транзакции выполняются асинхронно с помощью механизма AQ, сделать это невозможно. Для поддержки отсроченного уведомления об ошибке пришлось добавить новые средства. В частности, понадобилось реализовать механизм потоков заданий для отправки информации о неудавшихся транзакциях соответствующему лицу.
В результате пришлось существенно пересмотреть структуру базы данных. Пришлось добавить новое программное обеспечение (AQ). Пришлось также создать новые процессы (управление потоками заданий и другие служебные процессы). К положительным последствиям этих изменений можно отнести не только решение проблемы с архитектурой MTS, но и удобство для пользователя (создавалась видимость более быстрой реакции системы). С другой стороны, все эти изменения существенно задержали завершение проекта, поскольку проблемы были выявлены лишь непосредственно перед внедрением, на этапе тестирования масштабируемости. Очень жаль, что приложение сразу не был правильно спроектировано. Если бы разработчики знали, как физически реализован механизм MTS, было бы ясно, что исходный проект не обеспечивает требуемой масштабируемости.
Как надо (и как не надо) разрабатывать приложения баз данных
Однако хватит теоретизировать, по крайней мере пока. В оставшейся части главы я использую более эмпирический подход, описывая, почему знание СУБД и особенностей ее работы существенно повышает шансы успешной реализации приложения (без необходимости переписывать его дважды!). Некоторые проблемы решить легко, если известно, как их найти. Для решения других потребуются существенные переделки. Одна из целей этой книги — помочь вам вообще избежать проблем.
В следующих разделах я опишу ряд важнейших особенностей СУБД Oracle, не вдаваясь в подробности их реализации и использования. Например, я опишу только одно из последствий использования архитектуры многопотокового сервера (Multi-Threaded Server — MTS) — режима, в котором можно (а иногда и нужно) конфигурировать сервер Oracle для поддержки множества пользовательских сеансов. Я, однако, не буду детально описывать архитектуру MTS, особенности ее работы и т.п. Все это подробно описано в руководстве Oracle Server Concepts Manual (дополнительную информацию можно найти также в руководстве Net8 Administrators Guide).
Как ускорить работу?
Вынесенный в название раздела вопрос мне задают постоянно. Все ищут, где бы сделать установку fast = true, предполагая, что настройка производительности базы данных выполняется на уровне СУБД. Мой опыт показывает, что более 80 процентов (часто — намного больше, до 100 процентов) всего повышения производительности достигается на уровне приложения, а не базы данных. Нельзя заниматься настройкой СУБД, пока не настроено приложение, использующее данные.
Со временем появился ряд установок, включая которые на уровне СУБД, можно снизить влияние грубых ошибок программирования. Например, в Oracle 8.1.6 добавлен новый параметр — CURSOR_SHARING=FORCE. Он позволяет включить автоматическое использование связываемых переменных. В результате запрос SELECT * FROM EMP WHERE EMPNO = 1234 автоматически переписывается в виде SELECT * FROM EMP WHERE EMPNO = :x. Это может существенно сократить количество полных разборов и уменьшить ожидание защелок в библиотечном кеше, которые описаны в главе об архитектуре, но (всегда есть но) может также иметь ряд побочных эффектов. Можно нарваться на проблему (или ошибку) при использовании этой возможности, как, например, в первоначальной версии:
ops$tkyte@ORA8I.WORLD> alter session set cursor_sharing=force; Session altered.
ops$tkyte@ORA8I.WORLD> select * from dual where dummy='X'and 1=0; select * from dual where dummy='X'and 1=0 * ERROR at line 1: ORA-00933: SQL command not properly ended
ops$tkyte@ORA8I.WORLD> alter session set cursor_sharing=exact; Session altered.
ops$tkyte@ORA8I.WORLD> select * from dual where dummy='X'and 1=0; no rows selected
Принятый способ переписывания запроса дает некорректный результат в версии 8.1.6 (из-за отсутствия пробела между 'X' и ключевым словом AND). В итоге запрос приобретает вид:
select * from dual where dummy=:SYS_B_0and :SYS_B_1=:SYS_B_2;
Ключевое слово AND стало частью имени связываемой переменной :SYS_B_0. В версии 8.1.7, однако, этот запрос переписывается так:
select * from dual where dummy=:"SYS_B_0"and :"SYS_B_1"=:"SYS_B_2";
Теперь на уровне синтаксиса все работает, но переписывание может отрицательно сказаться на производительности приложения. Например, обратите внимание, что в рассмотренном ранее коде условие 1=0 (всегда ложное) переписано как :"SYS_B_1" = :"SYS_B_2". Теперь на этапе анализа у оптимизатора нет полной информации чтобы определить, вернет ли этот запрос ноль строк (еще до его выполнения). Я понимаю, что запросов с конструкциями типа 1=0 у вас немного, но подозреваю, что в некоторых запросах литералы используются умышленно. В таблице может быть столбец с весьма неравномерным распределением значений (например, 90 процентов значений в столбце — больше 100, а 10 процентов — меньше 100). Причем лишь 1 процент значений меньше 50. Хотелось бы, чтобы при выполнении запроса:
select * from t where x < 50;
индекс использовался, а при выполнении запроса:
select * from t where x > 100;
не использовался. Если установить параметр CURSOR_SHARING = FORCE, оптимизатор не сможет учесть значения 50 или 100, поэтому будет выбирать план для общего случая, когда индекс скорее всего не будет использоваться (даже если 99,9 процентов запросов будут содержать конструкцию WHERE x < 50).
Кроме того, я обнаружил, что, хотя установка CURSOR_SHARING = FORCE
обеспечивает намного большую скорость работы, чем повторный анализ и оптимизация множества одинаковых запросов как уникальных, это все равно медленнее, чем выполнение запросов, где связываемые переменные используются изначально. Это происходит не из-за неэффективности механизма совместного использования кода курсора, а из-за неэффективности самой программы. В главе 10 мы рассмотрим, как анализ операторов SQL может влиять на производительность в целом. Во многих случаях приложение, не использующее связываемые переменные, также не обеспечивает эффективного анализа и повторного использования курсоров. Поскольку в приложении предполагается уникальность каждого запроса (так как для каждого из них создается уникальный оператор), то и курсор в нем не будет использоваться более одного раза. Факт в том, что если программист использует связываемые переменные, то он зачастую также анализирует курсор один раз и затем использует многократно. Именно затраты ресурсов на повторный анализ приводят к наблюдаемому снижению производительности.
Итак, важно помнить, что просто добавление параметра инициализации CURSOR_SHARING = FORCE не всегда позволяет решить проблемы. Могут даже возникнуть новые. Во многих случаях параметр CURSOR_SHARING — действительно полезное средство, но это не панацея. Для хорошо продуманного приложения он не нужен. В долгосрочной перспективе обоснованное использование связываемых переменных (и при необходимости — констант) — наиболее правильно.
Даже если есть соответствующие параметры, которые можно установить на уровне базы данных, а их пока немного, проблемы одновременного доступа и неэффективных запросов (неудачно сформулированных или вызванных неудачной организацией данных) нельзя решить только установкой параметров сервера. Для решения этих проблем необходимо переписать приложение (а зачастую и изменить его архитектуру). Перенос файлов данных с одного диска на другой, изменение количества блоков, читаемых подряд одной операцией ввода, и другие настройки "на уровне базы данных" часто мало влияют на общую производительность приложения. Они никак не дадут ускорения в 2, 3, ... N раз, необходимого для достижения приемлемой скорости работы приложения. Как часто требуется ускорить работу приложения на 10 процентов? Если надо ускорить работу на 10 процентов, обычно никто вообще не поднимает вопрос об этом. Пользователи начинают жаловаться, когда, по их мнению, скорость надо увеличить раз в пять. Однако повторяю: вы не увеличите скорость работы в пять раз за счет переноса файлов данных на другие диски. Это можно сделать только путем изменения приложения, например, сократив объем ввода-вывода.
О производительности необходимо думать уже на уровне проекта, а затем непрерывно проверять в процессе разработки. Это нельзя откладывать на потом. Я удивляюсь, сталкиваясь со случаями, когда разработчики передают приложение заказчику, устанавливают и только после этого начинают настраивать. Я видел приложения, которые поставлялись клиентам только с первичными ключами, вообще без дополнительных индексов. Запросы никто не настраивал и вообще не тестировал их производительность. С приложением никогда не работало более десятка пользователей. Настройка считается частью процесса установки и внедрения программного продукта. Для меня такой подход неприемлем. Пользователи должны получать быстро работающую, хорошо настроенную систему. Проблем с продуктом у них будет достаточно и без производительности. Пользователи готовы к тому, что в приложении будут ошибки, но не заставляйте их бесконечно ждать появления сообщений об этих ошибках на экране.
Многоверсионность
Эта тема очень тесно связана с управлением одновременным доступом, поскольку создает основу для механизмов управления одновременным доступом в СУБД Oracle — Oracle использует модель многовариантной согласованности по чтению при одновременном доступе. В главе 3 мы более детально рассмотрим технические аспекты многоверсионности, но по сути это механизм, с помощью которого СУБД Oracle обеспечивает:
согласованность по чтению для запросов: запросы выдают согласованные результаты на момент начала их выполнения;
неблокируемые запросы: запросы не блокируются сеансами, в которых изменяются данные, как это бывает в других СУБД.
Это две очень важные концепции СУБД Oracle. Термин многоверсионность
произошел от того, что фактически СУБД Oracle может одновременно поддерживать множество версий данных в базе данных. Понимая сущность многоверсионности, всегда можно понять результаты, получаемые из базы данных. Наиболее простой из известных мне способов продемонстрировать многоверсионность в Oracle:
tkyte@TKYTE816> create table t 2 as 3 select * from all_users;
Table created.
tkyte@TKYTE816> variable x refcursor
tkyte@TKYTE816> begin 2 open :x for select * from t; 3 end; 4 /
PL/SQL procedure successfully completed.
tkyte@TKYTE816> delete from t;
18 rows deleted.
tkyte@TKYTE816> commit;
Commit complete.
tkyte@TKYTE816> print x
USERNAME USER_ID CREATED ------------------------------ ---------- --------- SYS 0 04-NOV-00 SYSTEM 5 04-NOV-00 DBSNMP 16 04-NOV-00 AURORA$ORB$UNAUTHENTICATED 24 04-NOV-00 ORDSYS 25 04-NOV-00 ORDPLUGINS 26 04-NOV-00 MDSYS 27 04-NOV-00 CTXSYS 30 04-NOV-00 ... DEMO 57 07-FEB-01
18 rows selected.
В этом примере мы создали тестовую таблицу T и заполнили ее данными из представления ALL_USERS. Мы открыли курсор для этой таблицы. Мы не выбирали данные с помощью этого курсора, просто открыли его.
Помните, что при открытии курсора сервер Oracle не "отвечает" на запрос; он никуда не копирует данные при открытии курсора (представьте, сколько времени потребовало бы открытие курсора для таблицы с миллиардом строк в противном случае). Курсор просто открывается и дает результаты запроса по ходу обращения к данным. Другими словами, он будет читать данные из таблицы при извлечении их через курсор.
В том же (или в другом) сеансе мы затем удаляем все данные из таблицы. Более того, мы даже фиксируем (
COMMIT) это удаление. Строк больше нет — не так ли? На самом деле их можно извлечь с помощью курсора. Фактически, результирующее множество, возвращаемое командой
OPEN, было предопределено в момент открытия курсора. Мы не прочитали при открытии курсора ни одного блока данных таблицы, но результат оказался жестко зафиксированным. Мы не сможем узнать этот результат, пока не извлечем данные, но с точки зрения нашего курсора результат этот неизменен. Дело не в том, что СУБД Oracle скопировала все эти данные в другое место при открытии курсора; данные сохранил оператор
delete, поместив их в область данных под названием сегмент отката.
Именно в этом и состоит согласованность по чтению, и если не понимать, как работает схема многоверсионности в Oracle и каковы ее последствия, вы не только не сможете воспользоваться всеми преимуществами СУБД Oracle, но и не создадите корректных приложений для Oracle, гарантирующих целостность данных.
Давайте рассмотрим последствия использования многоверсионной согласованности запросов по чтению и неблокируемых чтений. Для тех, кто не знаком с многоверсионностью, представленные ниже результаты могут показаться удивительными. Для простоты предположим, что в каждом блоке данных (это минимальная единица хранения в СУБД) считываемой таблицы хранится всего одна строка и что выполняется полный просмотр таблицы.
В запрашиваемой таблице хранятся балансы банковских счетов. Она имеет очень простую структуру:
create table accounts ( account_number number primary key, account_balance number );
В реальной таблице счетов будут сотни тысяч строк, но для простоты мы будем рассматривать таблицу всего с четырьмя строками (более детально мы рассмотрим этот пример в главе 3):
Строка |
Номер счета |
Баланс счета |
1 |
123 |
500,00 $ |
2 |
234 |
250,00 $ |
3 |
345 |
400,00 $ |
4 |
456 |
100,00 $ |
Требуется создать отчет, который в конце банковского дня позволяет определить количество денег в банке. Это делается с помощью предельно простого запроса:
select sum(account_balance) from accounts;
Конечно, в данном примере ответ очевиден - 1250 $. Однако что произойдет, если мы прочитаем строку 1, а при считывании строк 2 и 3 с одного из банкоматов будет выполнена транзакция, переводящая 400 $ со счета 123 на счет 456? Наш запрос прочтет 500 $ в строке 4 и выдаст результат 1650 $, не так ли? Конечно, этого надо избежать, так как подобный результат ошибочен — никогда такого баланса по счетам в базе данных не было. Нужно понять, как СУБД Oracle избегает подобных ситуаций и чем отличаются используемые при этом методы от используемых во всех остальных СУБД.
Практически в любой другой СУБД для получения "согласованного" и "корректного" ответа на этот запрос необходимо блокировать либо всю таблицу, по которой идет суммирование, либо строки по мере их чтения. Это предотвратит изменение результата другими сеансами в ходе его получения. Если заблокировать всю таблицу, будет получен результат, соответствующий состоянию базы данных в момент начала выполнения запроса. Если блокировать данные по мере чтения (такая разделяемая блокировка чтения предотвращает изменения, но не чтение данных другими сеансами), будет получен результат, соответствующий состоянию базы данных в момент завершения выполнения запроса. Оба эти метода существенно снижают возможности одновременного доступа. Блокировка таблицы предотвращает любые изменения таблицы во время выполнения запроса (для таблицы из четырех строк этот период очень короток, но для таблиц с сотнями тысяч строк запрос может выполняться несколько минут). Метод "блокирования по ходу чтения" предотвращает изменение уже прочитанных и обработанных данных и потенциально может приводить к взаимным блокировкам выполнения вашего запроса и других изменений.
Как уже было сказано, вы не сможете в полном объеме использовать преимущества СУБД Oracle, если не понимаете концепцию многоверсионности. В СУБД Oracle многоверсионность используется для получения результатов, соответствующих моменту начала выполнения запроса, при этом не блокируется ни единой строки (пока транзакция по переводу денег изменяет строки 1 и 4, они будут заблокированы от других изменений, но не от чтения, выполняемого, например, нашим запросом
SELECT SUM...). Фактически в СУБД Oracle нет "разделяемых блокировок чтения", типичных для других СУБД, — они в ней просто не нужны. Все устранимые препятствия для одновременного доступа были устранены.
Итак, как же СУБД Oracle получает корректный, согласованный результат (1250 $) при чтении, не блокируя данных, другими словами, не мешая одновременному доступу? Секрет — в механизме выполнения транзакций, используемом в СУБД Oracle. При любом изменении данных Oracle создает записи в двух разных местах. Одна запись попадает в журнал повторного выполнения, где Oracle хранит информацию, достаточную для повторного выполнения, или "наката", транзакции. Для оператора вставки это будет вставляемая строка. Для оператора удаления это будет запрос на удаление строки в слоте X блока Y файла Z. И так далее. Другая запись — это запись отмены, помещаемая в сегмент отката. Если транзакция завершается неудачно и должна быть отменена, СУБД Oracle будет читать "предварительный" образ из сегмента отката, восстанавливая необходимые данные. Помимо отмены транзакций, СУБД Oracle использует сегменты отката для отмены изменений в блоках при их чтении, то есть для восстановления данных блока на момент начала выполнения запроса. Это позволяет читать данные несмотря на блокировку и получать корректные, согласованные результаты, не блокируя данные.
Итак, в нашем примере Oracle получает результат следующим образом:
Время |
Запрос |
Транзакция по переводу со счета на счет |
T1 |
Читает строку 1, sum получает значение 500 $ |
|
T2 |
|
Изменяет строку 1, устанавливает исключительную блокировку на строку 1, предотвращая другие изменения. В строке 1 теперь хранится значение 100 $ |
T3 |
Читает строку 2, sum получает значение 750 $ |
|
T4 |
Читает строку 3, sum получает значение 1150 $ |
|
T5 |
|
Изменяет строку 4, устанавливает исключительную блокировку на строку 4, предотвращая другие изменения (но не чтение). В строке 4 теперь хранится значение 500 $. |
T6 |
Читает строку 4, определяет, что она была изменена. Выполняется откат блока до того состояния, которое он имел в момент времени T1. Запрос затем прочитает значение 100 $ из этого блока |
|
T7 |
|
Транзакция фиксируется |
T8 |
Выдает 1250 $ в качестве результата суммирования |
|
<
В момент времени
T6 СУБД Oracle фактически " читает поверх" блокировки, установленной транзакцией на строке 4. Именно так реализуется неблокируемое чтение: СУБД Oracle просто проверяет, изменились ли данные, игнорируя тот факт, что они в настоящий момент заблокированы (т.е. определенно изменены). Она извлечет старое значение из сегмента отката и перейдет к следующему блоку данных.
Это еще одна убедительная демонстрация многоверсионности: в базе данных имеется несколько версий одной и той же информации, по состоянию на различные моменты времени. СУБД Oracle использует эти сделанные в разное время "моментальные снимки" данных для поддержки согласованности по чтению и неблокируемости запросов.
Это согласованное по чтению представление данных всегда выполняется на уровне оператора SQL, — результаты выполнения любого оператора SQL всегда согласованы на момент его начала. Именно это свойство позволяет получать предсказуемый набор данных в результате, например, следующих вставок:
for x in (select * from t) loop insert into t values (x.username, x.user_id, x.created); end loop;
Результат выполнения оператора
SELECT * FROM T предопределен в момент начала выполнения запроса. Оператор
SELECT не будет "видеть" новых данных, генерируемых операторами
INSERT. Представьте себе, что было бы в противном случае: оператор превратился бы в бесконечный цикл. Если бы по мере генерации оператором
INSERT дополнительных строк в таблице
T, оператор
SELECT мог "видеть" эти вставляемые строки, представленный выше фрагмент кода создал бы неизвестное количество строк. Если бы в таблице
T первоначально было 10 строк, в результате могло бы получиться 20, 21, 23 или бесконечное количество строк. Точно предсказать результат было бы невозможно. Согласованность по чтению обеспечивается для всех операторов, так что операторы
INSERT, вроде представленного ниже, тоже работают предсказуемо:
insert into t select * from t;
Оператор
INSERT получит согласованное по чтению представление таблицы
T — он не "увидит" строки, которые сам же только что вставил, и будет вставлять только строки, существовавшие на момент начала его выполнения. Во многих СУБД подобные рекурсивные операторы просто не разрешены, поскольку они не могут определить, сколько строк вообще будет вставлено.
Поэтому если вы привыкли к реализации согласованности и одновременности запросов в других СУБД или просто никогда не сталкивались с такими понятиями (не имеете реального опыта работы с СУБД), то теперь понимаете, насколько важно для вашей работы их понимание. Чтобы максимально использовать потенциальные возможности СУБД Oracle, необходимо понимать эти проблемы и способы их решения именно в Oracle, а не в других СУБД.
Мой подход
Прежде чем начать, хотелось бы объяснить вам мой подход к разработке. Я предпочитаю решать большинство проблем на уровне СУБД. Если что-то можно сделать в СУБД, я так и сделаю. Для этого есть две причины. Первая и главная состоит в том, что если встроить функциональность в СУБД, то ее можно будет применять где угодно. Я не знаю серверной операционной системы, для которой нет реализации СУБД Oracle. Одна и та же СУБД Oracle со всеми опциями работает везде — от Windows до десятков версий ОС UNIX и больших ЭВМ типа OS/390. Я часто разрабатываю и тестирую программы на моем портативном компьютере, где работает СУБД Oracle8i для Windows NT. А применяются эти программы на различных серверах с ОС UNIX, на которых работает та же версия СУБД. Если приходится реализовать функциональность за пределами СУБД, ее очень сложно переносить на любую другую платформу. Одна из основных особенностей, делающих язык Java привлекательным для многих разработчиков, состоит в том, что программы всегда компилируются в одной и той же виртуальной среде, виртуальной машине Java Virtual Machine (JVM), и поэтому максимально переносимы. Именно эта особенность привлекает меня в СУБД. СУБД Oracle — это моя виртуальная машина, моя "виртуальная операционная система".
Мой подход состоит в том, чтобы делать в СУБД все, что возможно. Если требования выходят за пределы возможностей СУБД, я реализую соответствующие функции на языке Java вне СУБД. В этом случае особенности практически любой операционной системы скрываются. Мне все равно надо понимать, как работают мои "виртуальные машины" (Oracle или JVM) — надо знать используемые инструментальные средства, — но наиболее эффективная реализация соответствующих функций в конкретной ОС остается прерогативой создателей этих виртуальных машин.
Таким образом, зная лишь особенности работы одной "виртуальной ОС", можно создавать приложения, демонстрирующие отличную производительность и масштабируемость во многих операционных системах. Я не утверждаю, что можно полностью игнорировать базовую ОС, — просто разработчик приложений баз данных достаточно хорошо от нее изолирован, и ему не придется учитывать многие ее нюансы. Ваш АБД, отвечающий за поддержку СУБД Oracle, должен знать намного больше об особенностях базовой ОС (если не знает — найдите нового АБД!). При разработке клиент-серверного программного обеспечения, если основная часть кода вынесена из СУБД и виртуальной машины (наиболее популярной виртуальной машиной, вероятно, является Java Virtual Machine), разработчику придется учитывать особенности ОС сервера.
При разработке приложений баз данных я использую очень простую мантру:
если можно, сделай это с помощью одного оператора SQL;
если это нельзя сделать с помощью одного оператора SQL, сделай это в PL/SQL;
если это нельзя сделать в PL/SQL, попытайся использовать хранимую процедуру на языке Java;
если это нельзя сделать в Java, сделай это в виде внешней процедуры на языке C;
если это нельзя реализовать в виде внешней процедуры на языке C, надо серьезно подумать, зачем это вообще делать...
В книге вы увидите применение этого подхода. Мы будем использовать язык PL/SQL и его объектные типы для реализации того, что нельзя сделать в SQL. Язык PL/SQL существует давно, за ним стоит более тринадцати лет настройки, и нет другого языка, настолько тесно интегрированного с языком SQL и настолько оптимизированного для взаимодействия с SQL. Когда возможностей PL/SQL оказывается недостаточно, например, при доступе к сети, отправке сообщений электронной почты и т.п., мы будем использовать язык Java. Иногда мы будем решать определенные задачи с помощью языка C, но обычно лишь в тех случаях, когда программирование на C — единственно возможный вариант или когда обеспечиваемая компилятором C скорость работы программы действительно необходима. Во многих случаях сейчас последняя причина отпадает при использовании компиляции в машинные коды программ на языке Java (возможности преобразовать байт-код Java в специфический объектный код операционной системы для данной платформы). Это обеспечивает программам на Java такую же скорость работы, как и у программ на языке C.
Независимость от СУБД?
Вы, наверное, уже поняли направление моей мысли. Я ссылался на другие СУБД и описывал различия реализации одних и тех же возможностей в каждой из них. Я убежден: за исключением некоторых приложений, исключительно читающих из базы данных, создать полностью независимое от СУБД и при этом масштабируемое приложение крайне сложно и даже практически невозможно, не зная особенностей работы всех СУБД.
Например, давайте вернемся к первому примеру планировщика ресурсов (до добавления конструкции FOR UPDATE). Предположим, это приложение было разработано на СУБД с моделью блокирования/обеспечения одновременного доступа, полностью отличающейся от принятой в Oracle. Я собираюсь продемонстрировать, что при переводе приложения с одной СУБД на другую необходимо проверять, работает ли оно корректно в новой среде.
Предположим, что первоначально приложение по планированию ресурсов работало в СУБД, использующей блокирование на уровне страниц и блокировку чтения (чтение блокируется при изменении считываемых данных), и для таблицы SCHEDULES был создан индекс:
create index schedules_idx on schedules(resource_name, start_time);
Предположим также, что бизнес-правило было реализовано с помощью триггера (после выполнения оператора INSERT, но перед фиксацией транзакции мы проверяем, что для указанного временного интервала в базе данных имеется только наша, только что вставленная строка). В системе с блокированием на уровне страниц, из-за изменения страницы индекса по столбцам RESOURCE_NAME
и START_TIME, очень вероятно, что транзакции будут выполняться строго последовательно. Система будет выполнять вставки поочередно, поскольку страница индекса блокируется (все близкие значения по полю START_TIME для одного ресурса RESOURCE_NAME будут находиться на той же странице). В такой СУБД с блокированием на уровне страниц наше приложение, вероятно, будет работать нормально, так как перекрытие выделяемых ресурсов будет проверяться последовательно, а не одновременно.
Если просто перенести это приложение в СУБД Oracle, исходя из предположения, что она работает точно так же, можно получить шок. В СУБД Oracle, выполняющей блокирование на уровне строк и не блокирующей чтения, оно окажется некорректным. Как уже было показано, необходимо использовать конструкцию FOR UPDATE для упорядочения доступа. Без этой конструкции два пользователя могут зарезервировать ресурс на одно и то же время. Это будет прямым следствием непонимания особенностей работы используемой СУБД в многопользовательской среде.
С подобными проблемами я сталкивался многократно при переносе приложений из СУБД А в СУБД Б. Когда приложение, без проблем работавшее в СУБД А, не работает или работает весьма странно в СУБД Б, сразу же возникает мысль, что "СУБД Б — плохая". Правда, однако, в том, что СУБД Б работает иначе. Ни одна из СУБД не ошибается и не является "плохой" — они просто разные. Знание и понимание особенностей их работы поможет успешно решить подобные проблемы.
Совсем недавно я помогал перевести код с языка Transact SQL (язык создания хранимых процедур для СУБД SQL Server) на PL/SQL. Разработчик, занимавшийся переводом, жаловался, что SQL-запросы в Oracle возвращают "неправильный" ответ. Запросы выглядели следующим образом:
declare l_some_variable varchar2(25); begin if ( some_condition ) then l_some_variable := f( ... ); end if;
for x in ( select * from T where x = l_some_variable ) loop ...
Целью является получение всех строк таблицы
T, которые в столбце
X имеют пустое значение, если некоторое условие не выполнено, или определенное значение, если это условие выполнено.
Суть жалобы состояла в том, что, в Oracle этот запрос не возвращал данных, если переменная
L_SOME_VARIABLE не получала значения явно (когда у нее оставалось значение
NULL). В СУБД Sybase или SQL Server все было не так — запрос находил строки с неопределенным (
NULL) значением в столбце
X. Я встречался с этим практически при любом переводе приложения с СУБД Sybase или SQL Server на Oracle. Язык SQL предполагает использование трехзначной логики, и СУБД Oracle реализует неопределенные значения так, как того требует стандарт ANSI SQL. По этим правилам сравнение столбца
X со значением
NULL не дает ни
True, ни
False — результат фактически неизвестен. Следующий пример показывает, что я имею в виду:
ops$tkyte@ORA8I.WORLD> select * from dual;
D - X
ops$tkyte@ORA8I.WORLD> select * from dual where null=null;
no rows selected
ops$tkyte@ORA8I.WORLD> select * from dual where null<>null;
no rows selected
В первый раз это может показаться странным: в Oracle
NULL не равен и не не равен
NULL. СУБД SQL Server по умолчанию ведет себя не так: в SQL Server и Sybase
NULL равен
NULL. Ни Oracle, ни Sybase, ни SQL Server не выполняет операторы SQL неправильно — они просто делают это по-разному. Все эти СУБД якобы соответствуют стандарту ANSI, но все равно работают по-разному. Есть неоднозначности, проблемы совместимости с прежними версиями и так далее, которые необходимо решать. Например, СУБД SQL Server поддерживает метод сравнения со значением
NULL, диктуемый стандартом ANSI, но не по умолчанию (это нарушило бы работу тысяч уже существующих приложений, созданных для этой СУБД).
Одним из решений проблемы могло быть переформулирование запроса следующим образом:
select * from t where ( x = l_some_variable OR (x is null and l_some_variable is NULL ))
Однако это привело бы к еще одной проблеме. В СУБД SQL Server при выполнении этого запроса использовался бы индекс по столбцу
X. В СУБД Oracle индекс на основе B*-дерева (подробнее о методах индексирования читайте в главе 7) не позволяет индексировать значения ключа
NULL. Поэтому, если необходимо найти неопределенные значения, индексы на основе B*-деревьев не сильно помогут.
В рассматриваемом случае, чтобы свести к минимуму изменения в коде, столбцу
X присваивалось значение, которого не могло быть в реальных данных. Так,
X, по определению, был числом положительным, поэтому было выбрано значение -1. Запрос приобрел следующий вид:
select * from t where nvl(x,-1) = nvl(l_some_variable,-1)
Мы создали индекс по функции:
create index t_idx on t( nvl(x,-1) );
С минимальными изменениями мы добились того же результата. Отсюда можно сделать следующие важные выводы.
СУБД — различны. Опыт работы с одной может оказаться полезен в другой, но нужно быть готовым к ряду принципиальных отличий и многим очень мелким.
Мелкие различия (вроде обработки
NULL-значений) могут иметь такое же влияние, как и принципиальные (например, механизм управления одновременным доступом).
Единственный способ справиться с этими проблемами — знать особенности работы СУБД и уметь реализовать предоставляемые ею возможности.
Разработчики часто спрашивают меня, как сделать в СУБД что-то конкретное. Например, меня спрашивают: "Как создать временную таблицу в хранимой процедуре?". На такие вопросы я не даю прямого ответа — я всегда отвечаю вопросом: "А для чего вам это нужно?". Неоднократно в ответ я слышал: "Мы создавали временные таблицы в хранимых процедурах в SQL Server, и теперь нам надо это сделать в Oracle". Именно это я и предполагал услышать. В таком случае мой ответ прост: "Вы ошибаетесь, думая, что надо создавать временные таблицы в хранимой процедуре в Oracle". На самом деле в СУБД Oracle это будет крайне неудачным решением. При создании таблиц в хранимых процедурах в Oracle вскоре обнаружится, что:
выполнение операторов DDL в этом контексте снижает масштабируемость;
постоянное выполнение операторов DDL снижает производительность;
выполнение операторов DDL приводит к фиксации транзакции;
для доступа к этой таблице во всех хранимых процедурах придется использовать динамический SQL, т.к. статический SQL использовать невозможно;
динамический SQL в PL/SQL оптимизируется хуже и работает медленнее статического.
Итак, не надо делать в точности так, как в SQL Server (если временная таблица в Oracle вообще понадобится). Делать следует то, что является наиболее оптимальным для Oracle. При обратном переходе из Oracle в SQL Server тоже не стоит создавать одну большую таблицу с временными данными для всех пользователей (как это делается в Oracle). Это приведет к снижению масштабируемости и возможностей одновременного доступа в данной СУБД. Каждая СУБД имеет существенные отличия.
Особенности управления одновременным доступом
Управление одновременным доступом — это то, чем отличаются различные СУБД. Именно это отличает СУБД от файловой системы и одну СУБД от другой. Для программиста важно, чтобы его приложение базы данных корректно работало в условиях одновременного доступа, и именно это постоянно забывают проверять. Приемы, прекрасно работающие в условиях последовательного доступа, работают гораздо хуже при одновременном их применении несколькими сеансами. Если не знать досконально, как в конкретной СУБД реализованы механизмы управления одновременным доступом, то:
будет нарушена целостность данных;
приложение будет работать медленнее, чем предусмотрено, даже при небольшом количестве пользователей;
будет потеряна возможность масштабирования до большого числа пользователей.
Обратите внимание: я не пишу "возможно, будет..." или "вы рискуете..." — все эти проблемы точно будут. Все это вы получите, даже не представляя, что именно происходит. При неправильном управлении одновременным доступом будет нарушена целостность данных, поскольку то, что работает отдельно, будет работать не так, как предполагалось, в многопользовательской среде. Приложение будет работать медленнее, поскольку придется ждать доступа к данным. Возможность масштабирования будет потеряна из-за проблем с блокированием и конфликтов блокировок. По мере усложнения запросов к ресурсу ждать придется все дольше и дольше. Можно провести аналогию с таможенным переходом на границе. Если машины приезжают по одной, равномерно, с предсказуемой частотой, никаких очередей нет. Если же одновременно приедет много машин, начинают формироваться очереди. Причем время ожидания растет нелинейно по отношению к длине очереди. С определенного момента больше времени сотрудников уходит на "наведение порядка" в очереди, чем на таможенный досмотр машин (в случае СУБД мы говорим о планировании процессов и переключении контекста).
Проблемы одновременного доступа выявлять сложнее всего — трудности сопоставимы с отладкой многопотоковой программы. Программа может отлично работать в управляемой, искусственной среде отладчика, но постоянно "слетать" в "реальном мире". Например, в условиях интенсивных обращений может оказаться, что два потока одновременно изменяют одну и ту же структуру данных. Такого рода ошибки очень сложно выявлять и исправлять. Если приложение тестировалось только в однопользовательском режиме, а затем внедряется в среде с десятками одновременно обращающихся пользователей, вполне вероятно проявление болезненных проблем с одновременным доступом.
В следующих двух разделах будет представлено два небольших примера того, как непонимание особенностей управления одновременным доступом может разрушить данные или снизить производительность и масштабируемость приложения.
Открытость
Есть еще одна причина, почему при разработке часто выбирается сложный способ решения проблемы, — сложившееся представление, что надо жертвовать всем ради "открытости" и "независимости от СУБД". Разработчики хотят избежать использования "закрытых", "специфических" возможностей СУБД — иногда даже таких простых, как хранимые процедуры или последовательности, поскольку это привяжет их к определенной СУБД. Я настаиваю на том, что если создается приложение, читающее и изменяющее данные, оно уже в некоторой степени привязано к СУБД. Когда запросы начнут выполняться одновременно с изменениями, вы сразу обнаружите небольшие (а иногда — и большие) отличия в работе СУБД. Например, в одной СУБД может оказаться, что оператор SELECT COUNT(*) FROM T вступает во взаимную блокировку с простым изменением двух строк. В Oracle же запрос SELECT COUNT(*) никогда не блокирует другие сеансы. Мы уже рассматривали пример, когда в одной СУБД бизнес-правило работало как побочный эффект используемой модели блокирования, а в другой СУБД — нет. Было показано, что при одном и том же порядке выполнения транзакций в различных СУБД приложение может давать разные результаты. Причина — принципиальные различия в реализациях. Вы со временем поймете, что лишь очень немногие приложения можно непосредственно перенести из одной в другую СУБД. Различия в интерпретации (например, выражения NULL=NULL) и обработке операторов SQL будут всегда.
В одном из недавних проектов разработчики создавали Web-приложение с использованием Visual Basic, управляющих элементов ActiveX, Web-сервера IIS и СУБД Oracle 8i. Разработчики выразили опасение по поводу реализации бизнес-логики на языке PL/SQL - приложение становится зависимым от СУБД — и спрашивали, можно ли это исправить.
Меня этот вопрос несколько удивил. Просматривая список выбранных технологий, я не мог понять, чем им "не понравилась" зависимость от СУБД:
они выбрали язык программирования, привязанный к определенной операционной системе и поддерживаемый единственным производителем (можно было выбрать язык Java);
они выбрали технологию создания компонентов, привязывающую к одной операционной системе и производителю (они могли выбрать технологию EJB или CORBA);
они выбрали Web-сервер, работающий на единственной платформе того же производителя (почему не Apache?).
Все остальные технологии они выбрали так, что оказались привязанными к конкретной операционной системе — фактически свобода выбора оставалась только в отношении СУБД.
Независимо от того, что у них, видимо, были веские причины выбрать именно эти технологии, разработчики почему-то решили не использовать в полном объеме возможности критического компонента своей архитектуры и сделали это во имя "открытости". Мне кажется, что нужно сначала вдумчиво выбрать технологии, а затем максимально использовать предоставляемые ими возможности. За все эти технологии заплачены немалые деньги — не в ваших ли интересах максимально их использовать? Причем, создавалось впечатление, что они собирались воспользоваться преимуществами остальных технологий, так почему же для СУБД сделано исключение? На этот вопрос особенно сложно ответить, если учесть, что для эффективности приложения успешная работа с СУБД имеет первостепенное значение.
Можно рассмотреть это с точки зрения "открытости". Все данные помещаются в базу данных. СУБД, поддерживающая эту базу данных, — очень открытое средство. Она обеспечивает доступ к данным через SQL, с помощью компонентов EJB по протоколам HTTP, FTP, SMB и множества других протоколов и механизмов доступа. Пока все отлично: что может быть более открытым?
Затем вне базы данных добавляются алгоритмы и, что важнее, механизмы защиты. Например, в компоненты, обеспечивающие доступ к данным, или в код на Visual Basic, работающий на сервере Microsoft Transaction Server (MTS). В результате с открытостью базы данных покончено — она уже "закрыта". Пользователи теперь не могут использовать эти данные с помощью существующих технологий — они должны использовать предложенные методы доступа (или обращаться к данным в обход защиты). Сегодня это не кажется проблемой, но помните: то, что сегодня является "самой современной" технологией, например компоненты EJB, вчера было идеей, а завтра будет устаревшей, неэффективной технологией. Что осталось неизменным за последние 20 с лишним лет в мире реляционного программирования (да, собственно, и объектно-ориентированного) — это базы данных. Средства работы для пользователей меняются практически ежегодно, и по мере этого все приложения, самостоятельно, а не с помощью СУБД, реализующие защиту, становятся препятствиями на пути дальнейшего прогресса.
СУБД Oracle предлагает возможность детального контроля доступа (Fine Grained Access Control, FGAC, — ему посвящена глава 21). Если коротко, эта технология позволяет разработчику встраивать в базу данных процедуры, которые изменяют поступающие в базу данных запросы. Это изменение запросов используется для ограничения количества строк, которые клиент может получать или изменять. Процедура может определять, кто выполняет запрос, когда этот запрос выполняется, с какого терминала и т.д., и ограничивать соответствующим образом доступ к данным. С помощью FGAC можно организовать такую защиту, когда:
запросы, выполняемые в нерабочее время определенным классом пользователей, не возвращают никаких записей;
данные могут читаться с терминала в охраняемом офисе, но на терминал "удаленного" клиента конфиденциальная информация не выдается.
Эта возможность позволяет организовать контроль доступа в СУБД, непосредственно выдающей данные. Теперь уже неважно, получает ли пользователь данные через компоненты, страницы JSP, из приложения на VB с помощью ODBC или через SQL*Plus, — будут применяться одинаковые правила защиты. Вы готовы воспринять любую новую технологию.
Теперь я спрошу: какая технология более "открытая"? Та, что позволяет обращаться к данным только из кода VB и управляющих элементов ActiveX (замените язык VB языком Java, а компоненты ActiveX — компонентами EJB, если хотите; я говорю не о конкретной технологии, а о подходе)? Или та, что обеспечивает доступ из любой среды, способной взаимодействовать с СУБД, по столь отличающимся протоколам, как SSL, HTTP и Net8, или с помощью функциональных интерфейсов ODBC, JDBC, OCI и т. д.? Покажите мне средство создания отчетов, способное выполнять запросы к коду на VB. Я же назову десятки таких средств, выполняющих SQL-запросы.
Решение идти на жертвы ради независимости от СУБД и полной "открытости" волен принять каждый, и многие так и поступают, но я считаю такое решение ошибочным. Независимо от СУБД, ее функциональные возможности необходимо использовать в полной мере. Именно это, как правило, и делается на этапе настройки производительности (этим приходится заниматься сразу же после внедрения). Удивительно, как быстро отказываются от требования независимости, если использование специфических возможностей СУБД позволяет ускорить работу приложения в пять раз.
Подход с использованием принципа черного ящика
У меня есть предположение, основанное на личном опыте, почему так часто разработка приложений баз данных заканчивается неудачно. Позвольте уточнить, что к разряду неудавшихся разработок я отношу также проекты, официально не признанные неудавшимися, но потребовавшие на разработку и внедрение намного больше времени, чем планировалось первоначально, поскольку пришлось их существенно "переписывать", "перепроектировать" или "настраивать". Лично я такие не завершенные в срок проекты считаю неудавшимися: очень часто их вполне можно было завершить вовремя (и даже досрочно).
Наиболее типичной причиной неудачи является нехватка практических знаний по используемой СУБД — элементарное непонимание основ работы используемого инструментального средства. Подход по принципу "черного ящика" требует осознанного решения: оградить разработчиков от СУБД. Их заставляют не вникать ни в какие особенности ее функционирования. Причины использования этого подхода связаны с опасениями, незнанием и неуверенностью. Разработчики слышали, что СУБД — это "сложно", язык SQL, транзакции и целостность данных — не менее "сложно". Решение: не заставлять никого делать что-либо "сложное". Будем относиться к СУБД, как к черному ящику, и найдем инструментальное средство, которое сгенерирует необходимый код. Изолируем себя несколькими промежуточными уровнями, чтобы не пришлось сталкиваться непосредственно с этой "сложной" СУБД.
Такой подход к разработке приложений баз данных я не мог понять никогда. Одна из причин, почему мне трудно это понять, состоит в том, что для меня изучение языков Java и C оказалось намного сложнее, чем изучение основ работы СУБД. Я сейчас очень хорошо знаю языки Java и C, но для их освоения мне понадобилось намного больше практического опыта, чем для достижения соответствующего уровня компетентности при использовании СУБД. В случае СУБД необходимо знать, как она работает, но детали знать необязательно. При программировании на языке C или Java, необходимо, например, знать все особенности используемых компонентов; кроме того, это очень большие по объему языки.
Еще одна причина — то, что при создании приложений базы данных самым важным компонентом программного обеспечения является СУБД. Для успешной разработки необходимо учитывать это и доводить до сведения разработчиков, постоянно обращая на это их внимание. Много раз я сталкивался с проектами, где придерживались прямо противоположных воззрений.
Вот типичный сценарий такого рода разработки.
Разработчики были полностью обучены графической среде разработки или соответствующему языку программирования (например, Java), использованных для создания клиентской части приложения. Во многих случаях они обучались несколько недель, если не месяцев.
Команда разработчиков ни одного часа не изучала СУБД Oracle и не имела никакого опыта работы с ней. Многие разработчики вообще впервые сталкивались с СУБД.
В результате разработчики столкнулись с огромными проблемами, связанными с производительностью, обеспечением целостности данных, зависанием приложений и т.д. (но пользовательский интерфейс выглядел отлично).
Не сумев обеспечить нужную производительность, разработчики обращались за помощью ко мне. Особенно показателен один случай. Я не мог вспомнить точный синтаксис новой команды, которую надо было использовать, и попросил руководство SQL Reference. Мне принесли экземпляр из документации по СУБД Oracle версии 6.0, хотя разработка велась на версии 7.3, через пять лет после выхода версии 6.0! Ничего другого для работы у них не было, но это вообще никого не беспокоило. Хотя необходимое им для трассировки и настройки инструментальное средство в то время вообще не существовало. Хотя за пять лет, прошедших после написания имевшейся у них документации, были добавлены такие средства, как триггеры, хранимые процедуры, и многие сотни других. Несложно понять, почему им потребовалась помощь, гораздо труднее было решить их проблемы.
Странная идея о том, что разработчик приложения баз данных должен быть огражден от СУБД, чрезвычайно живуча. Многие почему-то считают, что разработчикам не следует тратить время на изучение СУБД. Неоднократно приходилось слышать: "СУБД Oracle — самая масштабируемая в мире, моим сотрудникам не нужно ее изучать, потому что СУБД со всеми проблемами справится сама". Действительно, СУБД Oracle — самая масштабируемая. Однако написать плохой код, который масштабироваться не будет, в Oracle намного проще, чем написать хороший, масштабируемый код. Можно заменить СУБД Oracle любой другой СУБД — это утверждение останется верным. Это факт: проще писать приложения с низкой производительностью, чем высокопроизводительные приложения. Иногда очень легко создать однопользовательскую систему на базе самой масштабируемой СУБД в мире, если не знать, что делаешь. СУБД — это инструмент, а неправильное применение любого инструмента может привести к катастрофе. Вы будете щипцами колоть орехи так же, как молотком? Можно, конечно, и так, но это неправильное использование инструмента, и результат вас не порадует. Аналогичные результаты будут и при игнорировании особенностей используемой СУБД.
Я недавно работал над проектом, в котором проектировщики придумали очень элегантную архитектуру. Клиент с помощью Web-браузера взаимодействовал по протоколу HTTP с сервером приложений, обеспечивающим поддержку Java Server Pages (JSP). Алгоритмы работы приложения целиком генерировались инструментальными средствами и реализовывались в виде компонентов EJB (с использованием постоянного хранения на базе контейнеров), причем физически они выполнялись другим сервером приложений. В базе данных хранились только таблицы и индексы.
Итак, мы начали с технически сложной архитектуры. Для решения задачи должны взаимодействовать друг с другом четыре компонента. Web-браузер получает страницы JSP от сервера приложений, который обращается к компонентам EJB, а те, в свою очередь, — к СУБД. Для разработки, тестирования, настройки и внедрения этого приложения необходимы были технически компетентные специалисты. После завершения разработки меня попросили оценить производительность приложения. Прежде всего я хотел узнать подход разработчиков к СУБД:
где, по их мнению, у приложения могут быть узкие места, точки потенциальных конфликтов?
каковы, по их мнению, основные препятствия для достижения требуемой производительности?
Они не имели ни малейшего представления об этом. На вопрос о том, кто поможет мне переписать код компонента EJB для настройки сгенерированного запроса, ответ был следующий: "О, этот код нельзя изменять, все надо делать в базе данных". То есть, приложение должно оставаться неизменным. В этот момент я был готов отказаться от работы над проектом — ясно, что заставить это приложение нормально работать невозможно:
приложение было создано без учета масштабирования на уровне базы данных;
приложение нельзя настраивать и вообще изменять;
по моему опыту, от 80 до 90 процентов всей настройки выполняется на уровне приложения, а не на уровне базы данных;
разработчики не представляли себе, что делают их компоненты с базой данных и где искать потенциальные проблемы.
Все это стало понятно уже через час тестирования. Как оказалось, в приложении сначала выполнялся оператор:
select * from t for update;
Это приводило к строго последовательной работе всех клиентов. В базе данных была реализована такая модель, что перед выполнением любых существенных действий приходилось блокировать весьма большой ресурс. Это моментально превращало приложение в очень большую однопользовательскую систему. Разработчики не верили мне (в другой СУБД, использующей разделяемую блокировку чтения, наблюдалась другая ситуация). После десяти минут работы с инструментальным средством TKPROF (о нем подробно написано в главе 10) я смог продемонстрировать, что именно этот оператор SQL выполнялся приложением (они об этом не знали — просто никогда не видели генерируемых операторов SQL). Я не просто показал, какие операторы SQL выполняются приложением, но и с помощью пары сеансов SQL*Plus продемонстрировал, что второй сеанс не начинается до полного завершения работы первым сеансом.
Итак, вместо того, чтобы неделю тестировать производительность приложения, я употребил это время на обучение разработчиков настройке, особенностям блокирования в базах данных, механизмам управления одновременным доступом, сравнение их реализаций в СУБД Oracle, Informix, SQL Server, DB2 и так далее (во всех этих СУБД они различны). Но сначала мне пришлось понять, однако, почему использовался оператор
SELECT FOR UPDATE. Оказалось, что разработчики хотели добиться повторяемости при чтении.
Повторяемость при чтении — это такой режим работы СУБД, когда повторное чтение в транзакции строки, которая уже один раз прочитана в этой транзакции, дает тот же результат.
Зачем им это было нужно? Они слышали, что "это хорошо". Ладно, предположим, повторяемость при чтении действительно необходима. В СУБД Oracle это делается путем установки уровня изолированности транзакции
SERIALIZABLE (что дает не только повторяемость при чтении строки, но и повторяемость при выполнении любого запроса — если два раза выполнить один и тот же запрос в пределах транзакции с таким уровнем изолированности, будут получены одинаковые результаты). Для обеспечения повторяемости при чтении в Oracle не нужно использовать
SELECT FOR UPDATE — это делается только для обеспечения последовательного доступа к данным. К сожалению, использованное разработчиками инструментальное средство это не учитывало — оно было создано для использования с другой СУБД, где именно так повторяемость при чтении и достигалась.
Итак, в данном случае для установки уровня изолированности транзакций
SERIALIZABLE пришлось создать триггер на регистрацию в базе данных, изменяющий параметры сеанса (уровень изолированности транзакций) для данного приложения. Затем мы отключили все установки повторяемости при чтении в использовавшемся инструментальном средстве и повторно запустили приложение. Теперь, без конструкции
FOR UPDATE, в базе данных определенные действия стали выполняться одновременно.
Это была далеко не последняя проблема данного проекта. Нам пришлось разобраться:
как настраивать операторы SQL, не изменяя их кода (это сложно — некоторые методы мы рассмотрим в главе 11);
как измерять производительность;
как находить узкие места;
что и как индексировать, и так далее.
В конце недели разработчики, никогда ранее не работавшие с СУБД, были удивлены тем, что в действительности она дает возможность сделать, как легко получить указанную выше информацию и, что наиболее важно, как существенно все это может сказаться на производительности приложения. Тестированием производительности в течение этой недели мы не занимались (им кое-что пришлось переделывать!), но в конечном итоге проект завершился успешно — просто на пару недель позже запланированного срока.
Это не критика инструментальных средств или современных технологий, таких как компоненты EJB и поддержка постоянного существования на базе контейнеров. Это — критика намеренного игнорирования особенностей СУБД, принципов ее работы и использования. Технологии, выбранные в этом проекте, работали отлично, но лишь после того, как разработчики немного разобрались в самой СУБД.
Подводя итоги: СУБД — это краеугольный камень приложения. Если она не работает как следует, все остальное не имеет значения. Если плохо работает черный ящик, что с ним делать? Его нельзя исправить, нельзя настроить (поскольку непонятно, как он устроен), и такую позицию вы выбрали сами. Но есть и другой подход, который я отстаиваю: разберитесь в используемой СУБД и принципах ее работы, поймите, что она может делать, и используйте весь ее потенциал.
Понимание архитектуры СУБД Oracle
Недавно я участвовал в проекте, создатели которого решили использовать только новейшие, самые совершенные технологии: все программное обеспечение было написано на Java в виде компонентов EJB. Клиентское приложение взаимодействовало с СУБД через эти компоненты — никакого протокола Net8. Между клиентом и сервером не предавались операторы SQL — только обращения к компонентам EJB с помощью вызовов удаленных методов (Remote Method Invocation — RMI) по протоколу Internet Inter-Orb Protocol (IIOP).
Об организации RMI по протоколу IIOP можно узнать на сайте .
Это вполне допустимый подход. Такой способ взаимодействия работает и может быть весьма масштабируемым. Те, кто разрабатывал архитектуру, хорошо понимали язык Java, технологию компонентов EJB, знали используемые протоколы, в общем — всю "кухню". Им казалось, что имеются все основания для успешной реализации подобного проекта. Когда же выяснилось, что приложение может поддерживать лишь нескольких пользователей, они решили, что проблема — в СУБД, и усомнились в декларируемой корпорацией Oracle "рекордной масштабируемости СУБД".
Проблема, однако, была не в СУБД, а в незнании особенностей ее работы, и некоторые решения, принятые на этапе проектирования, привели к краху приложения в целом. Чтобы использовать компоненты EJB в базе данных, сервер Oracle должен быть сконфигурирован для работы в режиме многопотокового (MTS), а не выделенного сервера. Чего не понимала команда разработчиков в принципе, так это последствий использования режима MTS в сочетании с компонентами EJB для их приложения. При отсутствии этого понимания, как и знания основ работы СУБД Oracle вообще, были приняты два ключевых решения.
В компонентах будут запускаться хранимые процедуры, работающие по 45 и более секунд (иногда — намного дольше).
Связываемые переменные использоваться не будут. В условиях всех запросов будут использоваться литеральные константы. Все входные данные для процедур будут передаваться в виде строк. Это "проще", чем использовать связываемые переменные.
Эти, казалось бы, непринципиальные решения обусловили неизбежный провал проекта. Все было сделано так, что предельно масштабируемая СУБД не справлялась с нагрузкой даже при небольшом количестве пользователей. Нехватка знаний об особенностях работы СУБД свела на нет все глубокие знания разработчиков по созданию компонентов на Java и распределенной обработке. Если бы они нашли время на минимальное изучение особенностей работы СУБД Oracle и затем применили два представленных далее простых принципа, шансы на успех проекта уже в первой версии существенно бы повысились.
любезно разрешило мне опубликовать переводы
Издательство "ДиаСофт" любезно разрешило мне опубликовать переводы нескольких глав знаменитой книги Тома Кайта "Expert one-on-one Oracle" в формате HTML. Я с удовольствием пользуюсь предоставленной возможностью, и предлагаю вашему вниманию перевод первой главы.
Учтите, что в этом тексте используется терминология, соответствующая третьему, исправленному изданию, которое должно выйти в свет в ближайшее время.
С наилучшими пожеланиями,
В.К.
Разработка успешных приложений для Oracle
Значительную часть времени я провожу, работая с программным обеспечением СУБД Oracle или, точнее, с людьми, которые это программное обеспечение используют. В течение последних двенадцати лет я работал над многими проектами, как успешными, так и закончившимися неудачно, и если бы потребовалось обобщить приобретенный при этом опыт несколькими фразами, я бы сказал следующее:
успех или неудача разработки приложения базы данных (приложения, зависящего от базы данных) определяется тем, как оно использует базу данных;
в команде разработчиков должно быть ядро "программистов базы данных", обеспечивающих согласованность логики работы с базой данных и настройку производительности системы.
Эти утверждения могут показаться очевидными, но опыт показывает, что слишком многие используют СУБД как "черный ящик", о деталях устройства которого знать необязательно. Они могут использовать генератор SQL, позволяющий не затруднять себя изучением языка SQL. Возможно, они решат использовать ее как обычный файл с возможностью чтения записей по ключу. Как бы то ни было, я могу вам сказать, что подобного рода соображения почти наверняка приводят к неправильным выводам — работать, не понимая устройства СУБД, просто нельзя. В этой главе описано, почему необходимо знать устройство СУБД, в частности, почему необходимо понимать:
архитектуру СУБД, ее компоненты и алгоритмы работы;
что такое средства управления одновременным доступом и каково их значение для разработчиков;
как настраивать приложение с момента введения его в эксплуатацию;
как реализованы определенные компоненты СУБД и чем эта реализация отличается от обычно предполагаемой;
какие возможности реализованы в самой СУБД и почему, как правило, лучше использовать предоставляемые СУБД функции, а не реализовать их самостоятельно;
зачем может понадобиться более глубокое знание языка SQL.
Этот список тем для начального изучения может показаться слишком длинным, но давайте рассмотрим следующую аналогию: если бы вы разрабатывали масштабируемое, высокопроизводительное приложение для абсолютно новой операционной системы (ОС), с чего бы вы начали? Надеюсь, ваш ответ: "С изучения особенностей функционирования этой новой ОС, работы приложений в ней и т.п.". Если ответ принципиально другой, ваша разработка обречена на неудачу.
Рассмотрим, например, одну из ранних версий Windows (скажем, Windows 3.x). Она, как и ОС UNIX, была "многозадачной" операционной системой. Однако эта многозадачность была не такой, как в ОС UNIX, — использовалась модель невытесняющей многозадачности (т.е., если работающее приложение не возвращает управление, ничто другое работать не может, включая операционную систему). Фактически, по сравнению с UNIX, Windows 3.x вообще не была многозадачной ОС. Для создания эффективных приложений разработчики должны были точно знать, как реализована возможность "многозадачности" Windows. Если необходимо разрабатывать приложение, работающее непосредственно в среде определенной ОС, понимание особенностей этой ОС очень важно.
То, что верно в отношении приложений, непосредственно работающих в среде операционной системы, верно и для приложений, работающих в среде СУБД: понимание особенностей СУБД является определяющим фактором успеха. Если вы не понимаете, что делает используемая СУБД или как она это делает, создаваемое приложение не будет работать успешно. Предположение о том, что успешно работающее в среде SQL Server приложение так же успешно будет работать и в среде Oracle, скорей всего не оправдается.
Реализация блокирования
СУБД использует блокировки, чтобы в каждый момент времени те или иные данные могли изменяться только одной транзакцией. Говоря проще, блокировки — это механизм обеспечения одновременного доступа. При отсутствии определенной модели блокирования, предотвращающей одновременное изменение, например, одной строки, многопользовательский доступ к базе данных попросту невозможен. Однако при избыточном или неправильном блокировании одновременный доступ тоже может оказаться невозможным. Если пользователь или сама СУБД блокирует данные без необходимости, то работать одновременно сможет меньшее количество пользователей. Поэтому понимание назначения блокирования и способов его реализации в используемой СУБД принципиально важно для создания корректных и масштабируемых приложений.
Принципиально важно также понимать, что в различных СУБД блокирование реализовано по-своему. В одних — используется блокирование на уровне страниц, в других — на уровне строк; в некоторых реализациях выполняется эскалация блокировок со строчного на страничный уровень, в других — не выполняется; в некоторых СУБД используются блокировки чтения, в других — нет; в одних СУБД реализуется уровень изолированности транзакций SERIALIZABLE с помощью блокирования, а в других — через согласованные по чтению представления данных (без установки блокировок). Эти небольшие отличия могут перерасти в огромные проблемы, связанные с производительностью, или даже привести к возникновению ошибок в приложениях, если не понимать их особенностей.
Ниже приведены принципы блокирования в СУБД Oracle.
Oracle блокирует данные на уровне строк и только при изменении. Эскалация блокировок до уровня блока или таблицы никогда не выполняется.
Oracle никогда не блокирует данные с целью считывания. При обычном чтении блокировки на строки не устанавливаются.
Сеанс, записывающий данные, не блокирует сеансы, читающие данные. Повторю: операции чтения не блокируются операциями записи. Это принципиально отличается от практически всех остальных СУБД, в которых операции чтения блокируются операциями записи.
Сеанс записи данных блокируется, только если другой сеанс записи уже заблокировал строку, которую предполагается изменять. Сеанс считывания данных никогда не блокирует сеанс записи.
Эти факты необходимо учитывать при разработке приложений, однако следует помнить, что эти принципы используются только в Oracle. Разработчик, не понимающий, как используемая СУБД обеспечивает одновременный доступ, неизбежно столкнется с проблемами целостности данных (особенно часто это происходит, когда разработчик переходит с другой СУБД на Oracle, или наоборот, и не учитывает в приложении различия механизмов обеспечения одновременного доступа).
Один из побочных эффектов принятого в СУБД Oracle "неблокирующего" подхода состоит в том, что если действительно необходимо обеспечить доступ к строке не более чем одного пользователя в каждый момент времени, то именно разработчику необходимо предпринять для этого определенные усилия. Рассмотрим следующий пример. Один разработчик показывал мне только что завершенную им программу планирования ресурсов (учебных классов, проекторов и т.д.), находящуюся в стадии внедрения. Это приложение реализовало бизнес-правило, предотвращающее выделение ресурса более чем одному лицу на любой период времени. То есть, приложение содержало специальный код, который проверял, что никто из пользователей не затребовал ресурс на тот же период времени (по крайней мере разработчик думал, что его код это проверяет). Код обращался к таблице планов и, если в ней не было строк с перекрывающимся временным интервалом, вставлял в нее новую строку. Итак, разработчик просто работал с парой таблиц:
create table resources ( resource_name varchar2(25) primary key, ... ); create table schedules ( resource_name varchar2(25) references resources, start_time date, end_time date );
И прежде чем зарезервировать на определенный период, скажем, учебный класс, приложение выполняло запрос вида:
select count(*) from schedules where resource_name = :room_name and (start_time between :new_start_time and :new_end_time or end_time between :new_start_time and :new_end_time)
Он казался разработчику простым и надежным: если возвращено значение 0, учебный класс можно занимать; если возвращено ненулевое значение, значит, учебный класс на этот период уже кем-то занят. Ознакомившись с используемым алгоритмом, я подготовил простой тест, показывающий, какая ошибка будет возникать при реальной эксплуатации приложения. Эту ошибку будет крайне сложно обнаружить и тем более установить ее причину, — кому-то может даже показаться, что это ошибка СУБД.
Я предложил его коллеге сесть за соседний компьютер, перейти на тот же экран и попросил на счет три обоих нажать на кнопку
Go и попытаться зарезервировать класс на одно то же время. Оба смогли это сделать — то, что прекрасно работало в изолированной среде, не сработало в среде многопользовательской. Проблема в этом случае была вызвана неблокирующим чтением в Oracle. Ни один из сеансов не блокировал другой. Оба сеанса просто выполняли представленный выше запрос и применяли алгоритм резервирования ресурса. Они оба могли выполнять запрос, проверяющий занятость ресурса, даже если другой сеанс уже начал изменять таблицу планов (это изменение невидимо для других сеансов до фиксации, то есть до тех пор, когда уже слишком поздно). Поскольку сеансы никогда не пытались изменить одну и ту же строку в таблице планов, они никогда и не блокировали друг друга, вследствие чего бизнес-правило не срабатывало так, как ожидалось.
Разработчику необходим метод реализации данного бизнес-правила в многопользовательской среде, способ, гарантирующий что в каждый момент времени только один сеанс резервирует данный ресурс. В данном случае решение состояло в программном упорядочении доступа — кроме представленного выше запроса
count(*), необходимо было сначала выполнить:
select * from resources where resource_name = :room_name FOR UPDATE;
Ранее в этой главе рассматривался пример, когда использование конструкции
FOR UPDATE приводило к проблемам, но в этом случае она обеспечивает корректную работу бизнес-правила. Мы просто блокируем ресурс (учебный класс), использование которого планируется, непосредственно перед его резервированием, до выполнения запроса к таблице планов, выбирающего строки для данного ресурса. Блокируя ресурс, который мы пытаемся зарезервировать, мы гарантируем, что никакой другой сеанс в это же время не изменяет план использования ресурса. Ему придется ждать, пока наша транзакция не будет зафиксирована — после этого он сможет увидеть сделанное в ней резервирование. Возможность перекрытия планов, таким образом, устранена. Разработчик должен понимать, что в многопользовательской среде иногда необходимо использовать те же приемы, что и при многопотоковом программировании. В данном случае конструкция
FOR UPDATE работает как семафор. Она обеспечивает последовательный доступ к конкретной строке в таблице ресурсов, гарантируя, что два сеанса одновременно не резервируют ресурс.
Этот подход обеспечивает высокую степень параллелизма, поскольку резервируемых ресурсов могут быть тысячи, а мы всего лишь гарантируем, что сеансы изменяют конкретный ресурс поочередно. Это один из немногих случаев, когда необходимо блокирование вручную данных, которые не должны изменяться. Требуется уметь распознавать ситуации, когда это необходимо, и, что не менее важно, когда этого делать не нужно (пример, когда не нужно, приведен далее). Кроме того, такой прием не блокирует чтение ресурса другими сеансами, как это могло бы произойти в других СУБД, благодаря чему обеспечивается высокая масштабируемость.
Подобные проблемы приводят к масштабным последствиям при переносе приложения с одной СУБД на другую (к этой теме я вернусь чуть позже), которых разработчикам сложно избежать. Например, при наличии опыта разработки для другой СУБД, в которой пишущие сеансы блокируют читающих, и наоборот, разработчик может полагаться на это блокирование как защищающее от подобного рода проблем — именно так все и работает во многих СУБД, отличных от Oracle. В Oracle приоритет отдан одновременности доступа, и необходимо учитывать, что в результате все может работать по-другому.
В 99 процентах случаев блокирование выполняется незаметно, и о нем можно не заботиться. Но оставшийся 1 процент надо научиться распознавать. Для решения этой проблемы нет простого списка критериев типа "в таком-то случае надо сделать то-то". Нужно понимать, как приложение будет работать в многопользовательской среде и что оно будет делать в базе данных.
Решайте проблемы просто
Всегда есть два способа решения любой проблемы: простой и сложный. Но люди почему-то всегда выбирают сложный. Это не всегда делается намеренно, чаще — по незнанию. Разработчики просто не предполагают, что СУБД может делать "это". Я же предполагаю, что СУБД может делать все, и пишу что-то собственноручно, только если оказывается, что этого она не делает.
Например, меня часто спрашивают: "Как сделать, чтобы пользователь мог подключиться к базе данных только один раз?". (Есть еще сотня примеров, которые я мог бы здесь привести в качестве иллюстрации.) Наверное, это требование многих приложений; правда, в моей практике разработки такие приложения не встречались — я не вижу веской причины для того, чтобы ограничивать пользователей подобным образом. Однако другим разработчикам это нужно, и они обычно придумывают сложное решение. Например, создают пакетное задание, выполняемое операционной системой и просматривающее представление V$SESSION, а затем произвольно прекращающее сеансы пользователей, подключившихся к базе данных более одного раза. Или создают собственные таблицы, в которые приложение вставляет строку при регистрации пользователя и удаляет ее по завершении работы. Подобная реализация неизбежно приводит к многочисленным обращениям в службу поддержки, поскольку если приложение завершает работу нештатно, строка из этой таблицы не удаляется. Я видел еще много "творческих" способов добиться этого, но ни один из них не был таким простым:
ops$tkyte@ORA8I.WORLD> create profile one_session limit sessions_per_user 1; Profile created.
ops$tkyte@ORA8I.WORLD> alter user scott profile one_session; User altered.
ops$tkyte@ORA8I.WORLD> alter system set resource_limit=true; System altered.
Вот и все. Теперь любой пользователь с профилем ONE_SESSION может подключиться только один раз. Простота этого решения обычно приводит разработчиков в восторг и запоздалым сожалениям. Потратьте время на ознакомление с имеющимися средствами и их возможностями — это позволит сэкономить много времени и сил при разработке.
Тот же принцип "делай проще" применяется и на более высоком, архитектурном уровне. Я рекомендую подумать дважды, прежде чем браться за сложные реализации. Чем больше "движущихся частей" в системе, тем больше компонентов, которые могут работать неверно, а при использовании сложной архитектуры определить, что именно является причиной ошибки, будет непросто. Может быть, использование "надцатиуровневой" архитектуры — это действительно "круто", но лишено смысла, если в простой хранимой процедуре можно сделать то же самое, но лучше, быстрее и с использованием меньших ресурсов.
Я участвовал в разработке приложения, продолжающейся более года. Это было Web-приложение, используемое в масштабе компании. Клиент на базе HTML и с использованием технологии JSP динамически получал страницы с сервера промежуточного уровня, который взаимодействовал с CORBA-объектами, в свою очередь, обращавшимися к СУБД. CORBA-объекты должны были поддерживать "состояние" и подключаться к СУБД для организации сеанса. В ходе тестирования этой системы оказалось, что потребуется много серверов приложений и очень мощная машина для работы СУБД, чтобы поддерживать10000, как предполагалось, одновременно работающих пользователей. Более того, иногда возникала проблема нестабильности, связанная со сложностью взаимодействия компонентов (ответить на вопрос, где именно и почему произошла ошибка в этой сложной системе, было трудно). Система масштабировалась, но требовала при этом огромных ресурсов. Кроме того, поскольку для реализации использовалось много сложных технологий, для разработки и сопровождения системы требовалось много опытных программистов. Мы разобрались в этой системе и ее предполагаемых функциях и поняли, что архитектура ее несколько сложнее, чем необходимо для решения поставленных задач. Мы увидели, что с помощью модуля PL/SQL сервера приложений Oracle iAS и ряда хранимых процедур можно было сделать такую же систему, работающую на существенно менее мощном оборудовании, причем усилиями менее опытных разработчиков. Никаких компонентов EJB, никаких сложных взаимодействий между страницами JSP и компонентами EJB — обычное преобразование указанного адреса URL в вызов хранимой процедуры. Эта новая система работает и используется до сих пор, поддерживает больше пользователей, чем предполагалось, и работает так быстро, что порой не верится. Она использует самую простую архитектуру, минимум компонентов, работает на дешевом 4-процессорном сервере уровня рабочих групп и никогда не дает сбоев (ну, один раз табличное пространство переполнилось, но это уже другая проблема).
Для решения задачи я всегда предпочитаю наиболее простую архитектуру. Результат часто получается потрясающий. Для каждой технологии есть соответствующие инструменты — не всегда надо просто гвоздь забить, так что может понадобиться что-то кроме молотка...
в общих чертах рассмотрели, почему
Мы в общих чертах рассмотрели, почему необходимо знать используемую СУБД. Приведенные примеры — не уникальны, подобное происходит на практике каждый день. Давайте кратко повторим ключевые моменты. Если вы разрабатываете ПО для СУБД Oracle:
Вы должны понимать архитектуру Oracle. Не требуется знать ее настолько, чтобы переписать сервер, но достаточно хорошо, чтобы понимать последствия использования тех или иных возможностей.
Необходимо понимать, как выполняется блокирование и управление одновременным доступом, и учитывать, что в каждой СУБД это реализуетcя по-разному. Без этого понимания СУБД будет давать "неверные" ответы и у вас будут большие проблемы с конфликтами доступа и, как следствие, — низкая производительность.
Не воспринимайте СУБД как черный ящик, устройство которого понимать не обязательно. СУБД — самая важная часть большинства приложений. Ее игнорирование приводит к фатальным последствиям.
Не изобретайте велосипед. Я встречал разработчиков, попавших в трудное положение не только технически, но и на личном уровне из-за незнания возможностей СУБД Oracle. Это происходило, когда оказывалось, что реализуемые ими в течение нескольких месяцев функции на самом деле давно встроены в СУБД.
Решайте проблемы как можно проще, максимально используя встроенные возможности СУБД Oracle. Вы немало заплатили за это.
Программные проекты начинаются и заканчиваются, языки и среды программирования появляются и исчезают. От нас, разработчиков, ждут создания работающих систем в течение недель, может быть, месяцев, а затем мы переходим к следующей задаче. Если мы будем каждый раз изобретать велосипед, то никогда не перейдем к сути разработки. Никто ведь не создает класс, реализующий хеш-таблицу в Java, — он входит в набор стандартных компонентов. Вот и используйте имеющиеся функциональные возможности СУБД. Первый шаг к этому — узнать их. Читайте дальше.
Влияние стандартов
Если все СУБД соответствуют стандарту SQL92, они должны быть одинаковы. Так считают многие. Сейчас я развею этот миф.
SQL92 — это стандарт ANSI/ISO для СУБД. Он является развитием стандарта ANSI/ISO SQL89. Этот стандарт задает язык (SQL) и поведение (транзакции, уровни изоляции и т.д.) для СУБД. Знаете ли вы, что многие коммерческие СУБД соответствуют стандарту SQL92? А знаете ли, как немного это значит для переносимости запросов и приложений?
Начиная читать стандарт SQL92, обнаруживаешь, что он имеет четыре уровня.
Начальный уровень. Именно этому уровню соответствует большинство предлагаемых СУБД. Этот уровень является незначительным развитием предыдущего стандарта, SQL89. Ни одна СУБД не сертифицирована по более высокому уровню. Более того, фактически Национальный институт стандартов и технологий (National Institute of Standards and Technology — NIST), агентство, сертифицировавшее соответствие стандартам SQL, сертификацией больше не занимается. Я входил в состав команды, сертифицировавшей Oracle 7.0 в NIST как соответствующий начальному уровню стандарта SQL92 в 1993 году. СУБД, соответствующая начальному уровню этого стандарта, поддерживает набор возможностей Oracle 7.0.
Переходный. С точки зрения поддерживаемых возможностей это что-то среднее между начальным и промежуточным уровнем.
Промежуточный. Этот уровень добавляет много возможностей, в том числе (этот список далеко не исчерпывающий):
динамический SQL;
каскадное удаление для обеспечения целостности ссылок;
типы данных DATE и TIME;
домены;
символьные строки переменной длины;
выражения CASE;
функции CAST для преобразования типов данных.
Полный. Добавляет следующие возможности (этот список тоже не исчерпывающий):
управление подключением;
тип данных BIT для битовых строк;
отложенная проверка ограничений целостности;
производные таблицы в конструкции FROM;
подзапросы в конструкции CHECK;
временные таблицы.
В стандарт начального уровня не входят такие конструкции, как внешние соединения, новый синтаксис для внутренних соединений и т.д. Переходный уровень требует поддержки соответствующего синтаксиса внешнего и внутреннего соединения. Промежуточный уровень добавляет новые возможности, а полный и представляет собой, собственно, SQL92. В большинстве книг по SQL92 не различаются эти уровни поддержки, что сбивает с толку. В них демонстрируется, как должна работать "идеальная" СУБД, полностью реализующая стандарт SQL92. В результате нельзя взять книгу по SQL92 и применить представленные в ней приемы к СУБД, соответствующей стандарту SQL92. Например, в СУБД SQL Server предлагаемый стандартом синтаксис "внутреннего соединения" в SQL-операторах поддерживается, а в СУБД Oracle — нет. Но обе эти СУБД соответствуют стандарту SQL92. В СУБД Oracle можно выполнять внешние и внутренние соединения, но делать это надо не так, как в SQL Server. В результате начальный уровень стандарта SQL92 мало что дает, а при использовании средств более высоких уровней возможны проблемы при переносе на другую СУБД.
Не надо бояться использовать специфические средства конкретной СУБД, — за них заплачено немало денег. В каждой СУБД есть свой набор уникальных возможностей, и в любой СУБД можно найти способ выполнить необходимое действие. Используйте в текущей СУБД лучшее и реализуйте новые компоненты при переходе на другие СУБД. Используйте соответствующие приемы программирования, максимально изолирующие остальную часть приложения от этих изменений. Эти же приемы программирования применяются разработчиками переносимых приложений, поддерживающих несколько ОС. Цель в том, чтобы в полной мере использовать имеющиеся средства, но при этом иметь возможность менять реализацию в каждом конкретном случае.
Например, типичная функция многих приложений баз данных — генерация уникального ключа для каждой строки. При вставке строки система должна автоматически сгенерировать ключ. В Oracle для этого предлагается объект базы данных — последовательность (
SEQUENCE). В Informix имеется тип данных
SERIAL. Sybase и SQL Server поддерживают тип данных
IDENTITY. В каждой СУБД имеется способ решить эту задачу. Однако методы решения различны, различны и возможные последствия их применения. Поэтому знающий разработчик может выбрать один из двух вариантов:
разработать метод генерации уникального ключа, полностью независимый от СУБД;
согласиться с разными реализациями и использовать разные методы генерации ключей в зависимости от СУБД.
Теоретическое преимущество первого подхода состоит в том, что при переходе с одной СУБД на другую ничего менять не придется. Я назвал это преимущество "теоретическим", поскольку недостатки такого решения настолько велики, что делают его практически неприемлемым. Для создания полностью независимого от СУБД процесса придется создать таблицу вида:
create table id_table (id_name varchar(30), id_value number); insert into id_table values ('MY_KEY', 0);
Затем для получения нового ключа необходимо выполнить следующий код:
update id_table set id_value = id_value + 1 where id_name = 'MY_KEY'; select id_value from id_table where id_name = 'MY_KEY';
Выглядит он весьма просто, но выполнять подобную транзакцию в каждый момент времени может только один пользователь. Необходимо изменить соответствующую строку, чтобы увеличить значение счетчика, а это приведет к поочередному выполнению операций. Не более одного сеанса в каждый момент времени будет генерировать новое значение ключа. Проблема осложняется тем, что реальные транзакции намного больше транзакции, показанной выше. Показанные в примере операторы
UPDATE и
SELECT — лишь два из множества операторов, входящих в транзакцию. Необходимо еще вставить в таблицу строку с только что сгенерированным ключом и выполнить необходимые действия для завершения транзакции. Это упорядочение доступа будет огромным ограничивающим фактором для масштабирования. Подумайте о последствиях, если этот метод применить для генерации номеров заказов в приложении для обработки заказов на Web-сайте. Одновременная работа нескольких пользователей станет невозможной, — заказы будут обрабатываться последовательно.
Правильное решение этой проблемы состоит в использовании для каждой СУБД соответствующего кода. В Oracle (предполагается, что уникальный ключ необходимо генерировать для таблицы
T) лучшим способом будет:
create table t ( pk number primary key, ... ); create sequence t_seq; create trigger t_trigger before insert on t for each row begin select t_seq.nextval into :new.pk from dual; end;
В результате каждая вставляемая строка автоматически и незаметно для приложения получит уникальный ключ. Тот же эффект можно получить и в других СУБД с помощью их типов данных — синтаксис оператора создания таблицы изменится, а результат будет тем же. Мы использовали второй вариант — специфические средства каждой СУБД для неблокируемой, высокопараллельной генерации уникального ключа, что, однако, не потребовало реальных изменений в коде приложения — все необходимые действия выполнены операторами DDL.
Приведу еще один пример безопасного программирования, обеспечивающего переносимость. Если понятно, что каждая СУБД реализует одни и те же возможности по-разному, можно при необходимости создать дополнительный уровень доступа к базе данных. Предположим, вы программируете с использованием интерфейса JDBC. Если используются только простые операторы SQL,
SELECT,
INSERT,
UPDATE и
DELETE, дополнительный уровень абстракции скорее всего не нужен. Можно включать код SQL непосредственно в приложение, если использовать конструкции, поддерживаемые во всех СУБД, с которыми должно работать приложение. Другой подход, одновременно упрощающий перенос и повышающий производительность, состоит в использовании хранимых процедур, возвращающих результирующие множества. Если разобраться, окажется, что все СУБД могут возвращать результирующие множества из хранимых процедур, но способы при этом используются абсолютно разные. Для каждой СУБД придется написать свой исходный код.
Теперь появляется выбор — либо не использовать хранимые процедуры, возвращающие результирующие множества, либо писать отдельный исходный код для каждой СУБД. Я, несомненно, выбрал бы метод "отдельный код для каждой СУБД" и активно использовал бы хранимые процедуры. Казалось бы, что при этом для перехода на другую СУБД потребуется больше времени. Однако оказывается, что этот подход упрощает создание приложений, переносимых на различные СУБД. Вместо поисков идеального кода SQL, работающего во всех СУБД (причем, как правило, в одних лучше, а в других — хуже), используется код SQL, максимально эффективный в конкретной СУБД. Его можно вынести из приложения, что дает дополнительные возможности настройки. Можно исправить запрос с низкой производительностью непосредственно в СУБД, и это изменение будет немедленно учтено, без исправлений в приложении. Кроме того, при использовании этого метода можно свободно и в полном объеме использовать преимущества предлагаемых производителем СУБД расширений языка SQL. Например, СУБД Oracle поддерживает иерархические запросы с помощью конструкции
CONNECT BY
в операторах SQL. Эта уникальная возможность очень поможет при создании рекурсивных запросов. В Oracle вы свободно сможете использовать это расширение SQL, поскольку оно — "вне" приложения (скрыто в базе данных). В других СУБД для достижения аналогичных результатов, возможно, придется использовать временные таблицы и хранимые процедуры. Вы заплатили за эти возможности, так почему же их не использовать.
Такие же методы используют разработчики, создавая код, предназначенный для работы на множестве платформ. Корпорация Oracle, например, применяет описанную выше методику при разработке СУБД. Есть большой фрагмент кода (составляющий, однако, небольшую часть всего кода СУБД), который называется OSD-код (Operating System Dependent) и создается отдельно для каждой платформы. С помощью этого уровня абстракции в СУБД Oracle можно использовать специфические возможности ОС для обеспечения высокой производительности и интегрирования, не переписывая при этом код самой СУБД. Именно благодаря этому СУБД Oracle может работать как многопотоковое приложение в Windows и как многопроцессное — в UNIX. Механизмы межпроцессного взаимодействия абстрагированы до такого уровня, что могут воплощаться по-разному для каждой ОС; при этом обеспечивается такая же производительность, как и в приложениях, написанных специально для данной платформы.
Помимо синтаксических различий в языке SQL, различаются реализации операторов, различной будет и производительность выполнения одного и того же запроса, есть проблемы управления одновременным доступом, уровней изолированности транзакций, согласованности запросов и т.д. Все это более детально будет рассмотрено в главах 3 и 4, — мы увидим, как сказываются эти различия. В стандарте SQL92 попытались дать четкие определения того, как должна выполняться транзакция, как должны обеспечиваться уровни изоляции, но в конечном итоге в разных СУБД результаты получаются различными. Все это связано с реализацией. В одной СУБД приложение будет вызывать взаимные блокировки и заблокирует все, что можно. В другой СУБД это же приложение не вызывает никаких проблем и работает отлично. В одной СУБД блокирование (физически упорядочивающее обращения) намеренно использовалось в приложении, а при его переносе в другую СУБД, где блокирования нет, получается неверный ответ. Чтобы перенести готовое приложение в другую СУБД, требуется много труда и усилий, даже если при первоначальной разработке неукоснительно соблюдался стандарт.
Возможности и функции
Противники обязательного обеспечения "независимости от СУБД" приводят следующий аргумент: нужно хорошо понимать, что именно предлагает конкретная СУБД, и полностью использовать ее возможности. В этом разделе не описываются все уникальные возможности Oracle 8i, — для этого понадобилась бы отдельная большая книга. Новым возможностям СУБД Oracle 8i посвящена специальная книга в наборе документации по СУБД Oracle. Если учесть, что вместе с СУБД Oracle поставляется документация общим объемом около 10000 страниц, детальное рассмотрение каждой возможности и функции практически нереально. В этом разделе просто показано, почему даже поверхностное представление об имеющихся возможностях дает огромные преимущества.
Как уже было сказано, я отвечаю на вопросы о СУБД Oracle на Web-сайте. Если честно, процентов 80 моих ответов — ссылки (URL) на документацию. Меня спрашивают, как реализовать те или иные сложные функциональные возможности в базе данных (или вне ее). А я просто даю ссылку на соответствующее место в документации, где написано, как это уже реализовано в СУБД Oracle и как этими возможностями пользоваться. Часто такие случаи бывают с репликацией. Я получаю вопрос: "Хотелось бы сохранять копию данных в другом месте. Эта копия должна быть доступна только для чтения. Обновление должно выполняться раз в сутки, в полночь. Как написать соответствующий код?". Ответ простой: см. описание команды CREATE SNAPSHOT. Вот что такое встроенные возможности СУБД.
Можно, конечно, для интереса написать собственный механизм репликации, но это будет не самое разумное действие. СУБД делает многое и, как правило, лучше, чем создаваемые нами приложения. Репликация, например, встроена в ядро, написанное на языке C. Она работает быстро, сравнительно проста в использовании и надежна. Работает в разных версиях, на разных платформах. При возникновении проблем служба поддержки Oracle поможет их решить. После обновления версии репликация будет поддерживаться с новыми, дополнительными возможностями. Теперь предположим, что вы разработали собственный механизм репликации. Вам придется заняться его поддержкой во всех версиях СУБД, которые вы собираетесь поддерживать. Одинаковое функционирование в версии 7.3, 8.0, 8.1 и 9.0 и так далее вы должны будете обеспечивать сами. Если произойдет сбой, обращаться будет не к кому. По крайней мере, пока не удастся получить маленький тестовый пример, демонстрирующий основную проблему. При выходе новой версии вам придется самостоятельно переносить в нее код механизма репликации.
Недостаточное понимание того, что предлагает СУБД, может серьезно помешать в будущем. Недавно разработчики демонстрировали мне созданное ими "очень нужное" программное обеспечение. Это была система обмена сообщениями, решавшая проблему очередей в базе данных. Она обычно возникает при необходимости использования таблицы несколькими сеансами в качестве "очереди". Необходимо, чтобы несколько пользователей могли заблокировать очередную запись, пропустив все уже заблокированные записи (они уже обрабатываются). Проблема в том, что нет задокументированной возможности СУБД для пропуска заблокированных строк. Поэтому, не зная о существовании средств, предоставляемых СУБД Oracle, можно приняться за реализацию поддержки очередей самостоятельно (или приобрести готовое решение).
Именно это и сделала упомянутая группа разработчиков. Они создали набор процессов и придумали функциональный интерфейс для организации очередей сообщений в СУБД. Они потратили на это немало времени и сил и были уверены, что сделали нечто действительно уникальное. Когда я увидел систему в действии и узнал ее функциональные возможности, мне осталось сказать лишь одно: это аналог расширенной поддержки очередей, Advanced Queues. Эта возможность давно встроена в СУБД. Она решает задачу "получить первую незаблокированную запись в очереди и заблокировать ее". Все, что нужно, уже сделано. Разработчики, не зная о существовании такой возможности, потратили на ее реализацию много времени и сил. Кроме того, им придется тратить немало времени и на ее поддержку в дальнейшем. Их руководитель не очень обрадовался, узнав, что вместо уникального программного обеспечения получилась эмуляция встроенной возможности СУБД.
Я видел, как разработчики в СУБД Oracle 8i создавали процессы-демоны, читающие сообщения из программных каналов (это механизм межпроцессного взаимодействия в СУБД). Процессы-демоны выполняли операторы SQL, содержавшиеся в прочитанных из программного канала сообщениях, и фиксировали сделанное. Это делалось для проверки транзакций, чтобы записи проверки откатывались при откате основной транзакции. Обычно если для проверки доступа к данным использовались триггеры и основной оператор впоследствии выполнить не удавалось, все изменения откатывались (см. главу 4, где неделимость операторов рассматривается более детально). Посылая же сообщение другому процессу, можно записывать информацию в другой транзакции и фиксировать ее независимо. Запись проверки при этом оставалась, даже если основная транзакция откатывалась. В версиях Oracle до Oracle 8i это был приемлемый (и практически единственный) способ реализации описанной функции. Когда я рассказал разработчикам об автономных транзакциях, поддерживаемых СУБД (мы их подробно рассмотрим в главе 15), они очень расстроились. Автономные транзакции, реализуемые добавлением единственной строки кода, делали то же, что вся их система. Положительным моментом оказалось то, что можно было выкинуть существенную часть кода и не поддерживать его в дальнейшем. Кроме того, система заработала быстрее и стала проще для понимания. Но их это все равно мало радовало, — очень уж много времени было потрачено на изобретение велосипеда. Особенно расстроился создатель процессов-демонов, плоды трудов которого были отправлены в мусорную корзину.
С подобными случаями я сталкиваюсь постоянно: затрачиваются громадные усилия на решение проблем, уже давно решенных в самой СУБД. Если вы не потратите время на изучение того, что предлагается, рано или поздно будете наказаны, изобретая велосипед. Во второй части книги, "Структуры и утилиты базы данных", мы детально рассмотрим отдельные функциональные возможности, предлагаемые СУБД. Я выбрал те возможности и функции, которые часто используют разработчики или которые стоило бы использовать намного чаще. Описана будет, однако, лишь вершина айсберга. В СУБД Oracle намного больше средств и возможностей, чем можно описать в одной книге.
Взаимоотношения АБД и разработчиков
На обложке книги сказано, как важно для АБД представлять, чего пытаются добиться разработчики, а для разработчиков — знать стратегию, используемую АБД для управления данными. Точно известно, что в основе большинства успешно работающих информационных систем лежит плодотворное взаимодействие между АБД и разработчиками приложений. В этом разделе я хочу представить точку зрения разработчика на разделение труда между разработчиком и АБД (исходя из предположения, что при любой важной разработке необходима поддержка группы АБД).
Разработчик не обязан знать, как устанавливать и конфигурировать программное обеспечение. Этим должен заниматься АБД и, возможно, системный администратор. Настройка Net8, запуск программы прослушивания, конфигурирование режима MTS, организация пула подключений, установка СУБД, создание базы данных и т.д. возлагаются на АБД и системного администратора.
Не обязан разработчик также уметь настраивать операционную систему. Лично я обычно предлагаю сделать это системным администраторам. Разработчик приложений баз данных должен быть квалифицированным пользователем соответствующей операционной системы, но нельзя требовать от него знания тонкостей ее настройки.
Пожалуй, одной из основных забот АБД является резервное копирование и восстановление базы данных, и я считаю это обязанностью исключительно АБД. А вот знать принцип работ и использования сегментов отката и журналов повторного выполнения разработчик должен. Знать, как выполнить восстановление табличного пространства по состоянию на определенный момент времени разработчику необязательно. Знание того, что это в принципе возможно, может пригодиться, но делать это самостоятельно вам не придется.
Настройка на уровне экземпляра базы данных, определение оптимального значения параметра SORT_AREA_SIZE — этим обычно занимается АБД. Бывают ситуации, когда разработчику необходимо изменить ряд параметров сеанса, но за параметры уровня базы данных отвечает АБД. Обычно база данных поддерживает приложения нескольких разработчиков, поэтому только АБД, занимающийся поддержкой всех приложений, может принять правильное решение.
Выделение пространства на диске и управление файлами данных — обязанность АБД. Разработчики должны оговорить необходимый объем пространства (сколько им предположительно потребуется), но остальное должны делать АБД и системный администратор.
Итак, разработчики могут не знать, как запустить СУБД, но должны уметь работать в ней. Разработчик и АБД совместно решают разные части одной головоломки. АБД связывается с разработчиком, заметив, что запросы потребляют слишком много ресурсов, а разработчик обычно обращается к АБД когда не знает, как ускорить работу системы (вот когда занимаются настройкой экземпляра — когда приложение полностью настроено).
Конечно, в зависимости от среды разработки возможны варианты, но мне нравится делить обязанности. Хороший разработчик обычно — очень плохой АБД, и наоборот. У них разные навыки и опыт, а также, по моим наблюдениям, разное устройство ума и личностные характеристики.