Oracle PL/SQL для профессионалов: практические решения Коннор МакДональд, Хаим Кац, Кристофер Бек, Джоел Кальман, Дэвид Нокс
торгово-издательский дом
Ш DiaSoft
Москва» Санкт-Петербург» Киев 2005
УДК 681.3. 06(075) ББК 32.973.2 М88 МакДональд Кониор М 88 Oracle PL/SQL для профессионалов: практические решения / Коннор МакДональд, Хаим Кац, Бек Кристофер и др. ; Пер. с англ. - СПб. : ООО «ДиаСофтЮП», 2005. — 560 с. ISBN 5-93772-160-8 Эта книга издательства Apress открывает новую серию книг о СУБД Oracle. Авторы книг серии OakTable Press — общепризнанные эксперты по Oracle. Вместе с издательством Apress они создают точные и содержащие множество полезной информации книги, посвященные решению реальных проблем, попирающие общепринятые мнения и побуждающие к исследованиям. «Oracle PL/SQL для профессионалов» — не учебник по написанию кода на языке PL/SQL. Эта книга должна помочь вам научиться хорошо программировать на PL/SQL. В ней показшо, как создавать код, который будет работать быстро и надежно в многопользовательских средах с большой нагрузкой. В книге описываются огромные функциональные возможности, предоставляемые PL/SQL, включая эффективную обработку реляционных и абстрактных данных, защиту, триггеры, динамическое формирование HTML-страниц из СУБД и эффективные приемы отладки. Практические решения, представленные в этой книге, помогут понять реальную мощь и функциональные возможности, которые может дать использование PL/SQL в различных проектах.
ББК 32.973.2
Original English language edition published by Apress, 2560 Ninth Street, Suite 219, Berkeley, CA 94710 USA. Copyright © 2004 by Apress Russian-language edition copyright © 2005 by Diasoft Publishing. All rights reserved. Лицензия предоставлена издательством Apress. Все права зарезервированы, включая право на полное или частичное воспроизведение в какой Оы то ни было форме. Материал, изложенный в данной книге многократно проверен. Но поскольку вероятность технических ошибок все равно остается, издательство не может гарантировать абсолютную точность и правильность приводимых сведений. В связи с этим издательство не несет ответственности за возможные ошибки, связанные с использованием книги. Все торговые знаки, упомянутые в настоящем издании, зарегистрированы. Случайное неправильное использование или пропуск торгового знака или названия его законного владельца не должно рассматриваться как нарушение прав собственности. ISBN 5-93772-160-8 (рус.) ISBN 1-59059-217-4 (англ.)
© Перевод на русский язык. ООО «ДиаСофтЮП», 2005 © Apress, 2004 © Оформление. ООО «ДиаСофтЮП», 2005 Гигиеническое заключение № 77.99.6.953.П.438.2.99 от 04.02.1999
Оглавление Предисловие к серии ОакТаЫе Press
11
Об авторах
13
О рецензентах
15
Благодарности
16
Введение
17
Настройка
20
Установка демонстрационной схемы SCOTT/TIGER Среда SQL*Plus Настройка AUTOTRACE в SQL*Plus Средства контроля производительности Параметр TIMED_STATISTICS SQLJRACE и TKPROF Система RUNSTATS
20 20 22 23 24 24 28
Глава! Эффективность PL/SQL
37
Зачем использовать PL/SQL? Язык PL/SQL близок к данным Простейшее решение часто — самое лучшее Что такое "эффективность PL/SQL"? Производительность Влияние на систему Доказуемость Как добиться эффективности Связываемые переменные и затраты на анализ операторов Используйте существующие возможности языка PL/SQL Не используйте PL/SQL вместо SQL Оправдано ли вообще PL/SQL-решение? Вывод
37 38 38 40 40 42 42 48 49 59 64 84 84
Глава 2. Объедините все в пакет
87
Основные преимущества пакетов Перегрузка подпрограмм в пакете Общедоступные и приватные переменные пакета Инициализация Сокрытие информации Отдельные процедуры и кризис зависимостей Затраты на перекомпиляцию Разрыв цепочки зависимостей Избегайте точечной зависимости Поддержка рекурсии
87 89 90 91 91 92 99 101 103 113
Оглавление Почему разработчики избегают пакетов? Игнорирование преимуществ разделения Это пакет, а не библиотека Когда не нужно использовать пакеты Стандартные пакеты Простая трассировка кода Другие полезные подпрограммы Получение операторов DDL Интересное использование пакета DBMS_ROWID Фоновые задачи Резюме Глава 3. Бесконечная тема курсоров Сравнение неявных и явных курсоров Выборка одной строки Обработка нескольких строк Обработка первых N строк Выводы Управление курсором в различных средах Курсорные переменные Курсорные выражения Резюме
114 115 115 119 121 123 126 127 131 132 135 137 137 139 145 148 150 150 151 154 159
Глава 4. Эффективная обработка данных
161
Управление типами данных Использование атрибута %TYPE Централизация управления типами данных с помощью пакетов Избегайте неявного преобразования типов От полей к строкам — использование атрибута %ROWTYPE Использование записей в операторах DML От записей к объектам Объектные типы Расширение возможностей утилиты Runstats с помощью наборов Зачем использовать наборы в PL/SQL Множественная выборка с помощью наборов Множественная обработка Множественное связывание Передача переменных между PL/SQL-модулями Передача параметров, объявленных с помощью атрибутов %TYPE и %ROWTYPE Передача наборов как параметров Обработка транзакций в PL/SQL Автономные транзакции Обход ошибок мутирующей таблицы в триггерах Выполнение оператора DDL в транзакции Аудит операторов SELECT Аудит, результаты которого остаются после отката Резюме
161 162 166 167 169 174 175 177 178 187 191 191 195 205 205 210 218 223 223 224 226 229 231
Оглавление
7
Глава 5. Методы оптимизации PL/SQL 233 Уменьшение количества разборов и объема используемой памяти 233 Код в триггерах 233 Процедуры с правами вызывающего 238 Творческий подход: использование конвейерных функций 246 Типы данных: советы и методы 254 Ассоциативные массивы 254 Наборы 256 Особенности использования операторов DML на базе записей 259 Вызов PL/SQL-функций 266 Используйте PL/SQL для раскрытия модели данных, а не для ее расширения . 266 Динамический вызов PL/SQL 278 Использование SQL в PL/SQL 283 SQL-функции и рекурсивный SQL 283 Эффективный динамический SQL 288 Резюме 299 Глава 6. Триггеры 301 Основные понятия 301 Типы триггеров 302 Атрибуты событий 303 Порядок срабатывания триггеров 303 Несколько однотипных триггеров 305 Производительность строчных DML-триггеров BEFORE и AFTER 305 Привилегии 307 Триггеры и словарь данных 307 Зависимости триггера 309 Состояние триггера 310 Сбои триггеров 310 Ограничения триггеров 310 Триггеры DML 311 Сохранение информации аудита 312 Реализация ограничения перехода 312 Генерация суррогатного ключа 314 Триггеры INSTEAD OF 315 Мутирующие таблицы 318 Решение на базе отложенной обработки 320 Мутирующие таблицы и автономные транзакции 323 Еще об ошибке мутирующей таблицы 324 Аудит данных 326 Генерация триггеров для обеспечения аудита данных 327 Многоверсионность таблиц 329 Технология Oracle Streams 331 Очередь заданий (триггеры на временные события) 336 Планирование заданий 337 Задания и триггеры DML 338
8
Оглавление
Задания и разделяемый пул Ошибки при выполнении задания Триггеры DDL Триггер для обеспечения целостности операторов DDL Триггер журнала аудита DDL Триггеры на события базы данных Триггеры на регистрацию Триггер на ошибку сервера Триггер на событие приостановки Ошибки и триггеры на события базы данных Не надо изобретать велосипед Отчет об использовании базы данных Резюме Глава 7. Пакеты АБД Пакет для работы с файлом сообщений Структура пакета Структура файла сообщений Файл сообщений как внешняя таблица Обработка файла сообщений Исключительные ситуации Жизненный цикл уведомления Прокрутка файла сообщений Планирование и одновременный доступ Проблемы при использовании файла сообщений Просмотр содержимого файла сообщений Файл сообщений: итоги Пакет уведомления Процедура SEND_EMAIL Сохранение сообщений в базе данных Уведомления: итоги Пакет превентивного контроля Резервные копии Свободное место в каталоге архивных журналов Контроль свободного пространства в базе данных Превентивный контроль: итоги Пакет для поддержки хронологических данных Размер базы данных Сеансы базы данных Ограничения ресурсов Хронологические данные: итоги Резюме Глава 8. Пакеты для защиты Вопросы проектирования Обзор выполнения с правами создателя и вызывающего Построение пакетов
339 341 342 343 344 346 346 347 348 349 349 350 351 353 354 355 355 357 360 364 366 367 370 372 375 376 376 377 379 380 381 382 383 385 386 387 388 390 391 393 393 395 395 395 410
Оглавление Схемы, везде схемы Распространение кода Триггеры для обеспечения защиты Проверки защиты Триггеры на регистрацию: первая линия защиты Защита исходного кода Просмотр исходного кода процедур и функций Исходный текст пакета PL/SQL-утилита Wrap Резюме
414 416 417 418 425 428 429 432 432 436
Глава 9. Пакеты для Web-приложений
439
Основы PL/SQL Web Toolkit Архитектура Резюме по пакетам Тестирование из среды SQL*Plus Пакеты НТР и HTF Использование переменных среды Ключики Управление файлами Управление таблицами через Web Выполнение HTTP-запросов из базы данных Получение HTML-кода Клиент Web-службы на базе пакета UTL_HTTP Резюме
439 440 441 442 443 445 450 451 456 466 467 468 473
Глава 10. Отладка PL/SQL Защитное программирование Исключительные ситуации Снабжение кода средствами трассировки и отладки Документация Инструментальные средства Пакет DBMS_OUTPUT Встроенные функции SQLCODE и SQLERRM Функция DBMS_UTILITYFORMAT_CALL_STACK Пакет DBMS_APPLICATION_INFO Автономные транзакции Пакет UTL_FILE Отладка в реальном времени с помощью конвейерных функций Специализированная утилита DEBUG Требования Проектирование и настройка базы данных Структура пакета Реализация Основы использования Избирательная отладка Отладка производственного кода
475 475 475 481 481 482 482 487 488 489 495 498 501 505 506 506 507 508 509 513 516
10
Оглавление
Для чего может пригодиться пакет DEBUG? Резюме Приложение А. Создание утилиты DEBUG Проектирование и настройка объектов базы данных Таблицы Индексы и ограничения целостности Триггеры Объект DIRECTORY Структура пакета Реализация Процедура F() TnnARGV Процедура FA() Процедура DEBUG_IT() Поиск соответствия в строках Процедура WHO_CALLED_ME() Процедура BUILDJTQ Функция PARSEJTQ Функция FILEJTQ Процедура INITQ Процедура CLEAR() Процедура STATUS0 Последние штрихи Поиск причин проблем в пакете DEBUG Ошибка при инициализации профиля: файл не существует Ошибка при инициализации профиля: файл существует Сообщения в файл отладки не выдаются
Предметный указатель
518 519 521 ...521 521 522 522 523 524 525 525 526 527 527 529 529 532 534 537 538 541 541 542 543 543 543 544
545
Предисловие к серии OakTable Press Коротко говоря, сеть OakTable (OakTable Network) — это неформальная организация, состоящая из группы экспертов по технологиям Oracle, занимающихся поиском наилучших способов администрирования и разработки систем на базе СУБД Oracle. Мы объединили силы с издательством Apress для выпуска серии книг OakTable Press no Oracle. У членов нашей организации есть несколько общих черт. Мы применяем научный подход при работе с СУБД Oracle. Мы не верим ни во что, пока точно не проверим и не докажем это. Нам нравится "расширять границы", исследовать, находить новые и лучшие способы решения проблем. Нам нравится хороший виски. Вот, по сути, и все идеалы, которые мы хотим проповедовать с помощью серии OakTable Press (ну, возможно, кроме последнего). Каждая книга в серии будет написана и/или прорецензирована, как минимум, двумя участниками сети OakTable. Наша цель — помочь каждому автору серии OakTable Press создать доскональную, точную, новаторскую и интересную книгу. В конечном итоге мы надеемся сделать из каждой книги максимально полезное средство для облегчения вашей работы.
Кто входит в OakTable Network? Все началось где-то в 1998 году, когда группа экспертов по СУБД Oracle, в том числе Эньо Колк (Anjo Kolk), Кэри Милсеп (Сагу Millsap), Джеймс Морли (James Morle) и другие, начали встречаться раз или два в год по различным прецедентам. Каждый приносил с собой бутылку Скотча (Scotch) или Бурбона (Bourbon), а взамен получал право поспать на полу где-нибудь у меня дома. Большую часть времени мы проводили, сидя за обеденным столом, на котором были разбросаны компьютеры, кабели, бумага и т.п., обсуждая Oracle, пересказывая анекдоты и экспериментируя, изобретая новые, более эффективные способы работы с базой данных. К весне 2002 года все и сложилось. Однажды вечером я понял, что вокруг моего обеденного стола сидит 16 признанных во всем мире специалистов по Oracle. Мы спали по три-четыре человека в комнате и иногда даже были вынуждены арендовать соседский душ по утрам. Эньо Колк предложил нам назваться OakTable Network — Сеть дубового стола (в честь моего обеденного стола), и примерно через две минуты сайт h t t p : //www.OakTable.net был зарегистрирован. Сегодня в сеть OakTable имеют честь входить 42 человека, из которых примерно половина работает на корпорацию Oracle (на сайте представлен актуальный список). Комитет, состоящий из Джеймса Морли, Кэри Милсепа, Эньо Колка, Стива Адамса (Steve Adams), Джонатана Льюиса (Jonathan Lewis) и меня, рассматривает кандидатуры новых членов. Вы можете встретить нас на различных конференциях и собраниях групп пользователей, обсудить с нами технические проблемы или озадачить членов сети OakTable
J2
Предисловие к серии OakTable Press
техническим вопросом. Если мы не сможем ответить на ваш вопрос в течение 24 часов, вы получите футболку с надписью "Я бросил вызов OakTable — и выиграл", причем последние два слова написаны очень, очень мелким шрифтом! Мы по-прежнему встречаемся дважды в год в Дании: в январе, на мастер-классе Miracle (его давали: в 2001 году — Кэри Милсеп, в 2002-м — Джонатан Льюис, в 2003-м — Стив Адаме, а в 2004-м — Том Кайт), когда один из членов выступает в течение трех дней, и в сентябре-октябре, на форуме Miracle Database Forum, трехдневной конференции специалистов по базам данных. В недрах сети OakTable родилось много проектов, часть из которых закончилась курсами (такими, как Hotsos Clinic), а другие — новыми программными продуктами. Один проект привел к созданию серии OakTable Press. Мы надеемся, что вам понравятся книги, которые выйдут в свет в последующие годы. С наилучшими пожеланиями, Могенс Норгаард (Mogens Norgaard) Исполнительный директор Miracle A/S (http://www.miracleas.dk/) и соучредитель сети OakTable.
'
Об авторах Г\Г-
Коннор МакДональд, основной автор, работает с Oracle с начала 1990-х годов. Он начинал работать с базой данных Oracle версий 6.0.36 и 7.0.12. За последние 11 лет он имел дело с системами в Австралии, Великобритании, Юго-Восточной Азии, Западной Европе и США. Коннор — член сети OakTable и хорошо известен как в кругах докладчиков на темы Oracle, так и в сетевых форумах по СУБД Oracle. У него есть Web-сайт подсказок и советов ( h t t p : //www.oracledba.co.uk), где он делится своим энтузиазмом в отношении Oracle и помогает другим добиться более эффективного использования этой СУБД. Хаим Кац, сертифицированный специалист по Oracle (Oracle Certified Professional), работавший с продуктами Oracle, начиная с версии 4. Он специализируется на администрировании баз данных и разработке приложений на языке PL/SQL и за эти годы написал множество статей об Oracle в различные технические журналы. Он учил детей языку программирования Logo и преподавал системы баз данных в колледжах. Живет в Монреале, где, помимо работы с 9-ти до 17-ти в сфере информационных систем, изучает Талмуд, играет на кларнете и обсуждает вечные проблемы. Он и его жена Рута (Ruthie) сейчас с удовольствием занимаются проблемами своего большого семейства. Кристофер Бек, получивший степень бакалавра по компьютерным наукам в университете Рутжерса (Rutgers University), работает в этой области уже 13 лет. Начинал как младший разработчик программного обеспечения на языке Ada в организации, выполняющей правительственный контракт, а затем провел 9 лет в корпорации Oracle, где сейчас является ведущим технологом. Специализируется на основных технологиях базы данных и разработке Web-приложений. Когда он не работает на корпорацию Oracle и не проводит время с женой и четырьмя маленькими детьми, то возится с ОС Linux или играет в дружественную сетевую игру Quake III Arena.
Джоел Кальман — менеджер программных проектов корпорации Oracle. За последние 14 лет он занимался базами данных и системами управления контентом, начиная с баз данных SGML и издательских систем, заканчивая системами документооборота. Сейчас он управляет разработкой Oracle HTML DB — прикладного решения, позволяющего пользователям легко строить Web-приложения на основе базы данных. Когда в ежедневной работе над высокими компьютерными технологиями случается свободное время, Джоел играет в футбол, занимается резьбой по дереву, столярничеством и инвестициями. Джоел — гордый питомец университета штата Огайо (The Ohio State University), в котором он получил степень бакалавра по вычислительной технике. Живет с женой Кристин в Пауэле (Powell), штат Огайо.
14
Об авторах
Дэвид Нокс — ведущий инженер гарантийной службы (Information Assurance Center) корпорации Oracle. Он работает в корпорации с июня 1995 года. За это время принимал участие во многих проектах по защите для различных клиентов, включая Министерство обороны США (U.S. Department of Defense — DoD), разведывательные управления, федеральное и местные правительства, финансовые службы и организации системы здравоохранения. Экспертным знаниям по безопасности компьютерных систем он обязан не только практике и опыту применения систем защиты сервера Oracle и баз данных, но и научной работе в области многоуровневой защиты, криптографии, протокола LDAP и PKI. Дэвид получил степень бакалавра по компьютерным системам в Университете Мэриленда (University of Maryland) и степень магистра по компьютерным наукам — в Университете Джона Хопкинса (John Hopkins University).
О рецензентах Якоб Хаммер-Якобсен (Jakob Hammer-Jakobsen) родился в 1965 году. В 1992 году получил степень магистра, работает с Oracle с 1986 года (начинал с версии Oracle 5). Работал, в основном, как создатель бизнес-систем на основе Oracle (и других баз данных), но последние пять лет его стала интересовать и сфера АБД. Якоб читал всевозможные учебные курсы, связанные с Oracle, по всему миру; последний его курс — "Разработка Java-портлетов". Он — сертифицированный разработчик Oracle (Oracle Certified Developer) и участник OakTable.net. Он также работал на Департамент высшего образования, Университет Роскильде (University of Roskilde), Международное студенческое сообщество (International Student Foundation) в Дании, компанию Тот Pedersen International (первый дистрибутор Oracle в Европе), представительство Oracle в Дании, компании Miracle Australia и Miracle Denmark. Торбен Холм (Torben Holm) — участник сети OakTable. Он занялся компьютерами как разработчик в 1988 году, будучи старшим сержантом Королевских ВВС Дании. Работает с Oracle с 1992 года: первые 4 — как системный аналитик и разработчик приложений (на базе Oracle 7, Forms 4.0/Reports 2.0 и DBA), затем еще 2 года — как разработчик (Oracle 6/7, Forms 3.0, RPT и DBA), затем 2 года — в представительстве Oracle в Дании, в группе поддержки Prime Services как ведущий консультант. Там он решал задачи разработки и администрирования баз данных. Работал инструктором и читал курсы по PL/SQL, SQL, администрированию баз данных и WebDB. Последние 3 года Торбен работал на Miracle A/S ( h t t p : / /www. m i r a c l e a s . dk/) как разработчик приложений и АБД. Он — сертифицированный специалист по Oracle Developer 6г (и частично прошел процесс сертификации по Oracle 8г — те шаги, которые надо было пройти, времени на завершение сертификации у него нет). Его "основной" язык программирования — PL/SQL. Том Кайт (Tom Kyte) — вице-президент по основным технологиям (VP, Core Technologies) в корпорации Oracle, имеет более чем 16-летний опыт проектирования и разработки крупномасштабных баз данных и Internet-приложений. Том специализируется на основных технологиях, проектировании и архитектуре приложений, а также настройке производительности. Он ведет регулярную рубрику в журнале Oracle Magazine, а также поддерживает Web-сайт AskTom ( h t t p : //asktom. o r a c l e . com/), где технические специалисты могут получить ответы на интересующие их вопросы. Он является автором книг "Effective Oracle by Design" (посвящена лучшим приемам использования СУБД Oracle), "Expert One-on-One Oracle" (описывает создание систем на базе основных технологий Oracle; переведенную на русский язык книгу под названием "Oracle для профессионалов" выпустило издательство "ДиаСофт" в 2003 году. — Прим. ред.), а также соавтором книги "Beginning Oracle"для начинающих разработчиков на базе Oracle.
Благодарности Во-первых, я хочу поблагодарить сотрудников сети OakTable, которые многие годы оказывали мне неоценимую помощь. Я никогда не встречал людей, настолько великодушно делящихся своими знаниями (и, действительно, обширными знаниями!) и уделяющих другим столько времени. Большая честь быть "связанным" с ними. В частности, я благодарю Тома Кайта, Джонатана Льюиса и Дэйва Энсора (Dave Ensor) за то, что они вдохновили меня на более глубокое изучение СУБД Oracle, a также Могенса Норгаарда, "основателя" сети OakTable, за его потрясающее гостеприимство и виски! Спасибо также моему редактору, Тони Девису (Tony Davis), no отношению к которому я испытывал то благодарность, то бессмысленное желание наброситься на него с кулаками, — это означает, что он хорошо делал свою работу. Я также благодарен компаниям, в которых работал, за использование Oracle. Нет лучшего способа освоить мощь языка PL/SQL и других технологий Oracle, чем делать это, решая сложные задачи и оптимизируя использование инфраструктуры Oracle. Но главное — я благодарен моей жене Джиллиан за поддержку и терпимость: все то время, пока я занимался изучением Oracle, я должен был проводить с ней, но не делал этого. Изучение технологии зачастую означает, что ты закрылся в темной комнате перед экраном компьютера на долгие часы и дни, но знание того, что рядом, в соседней комнате, находится самая красивая и удивительная женщина в мире, позволяет легко обрести вдохновение. —Коннор Мак Дональд Я благодарен Тони Дэвису из издательства Apress за работу над книгой, хотя я всегда и с большим опозданием сдавал свои главы. Еще раз прошу прощения, Тони! Я также благодарен моей жене Марте за постоянное поощрение и поддержку. Люблю тебя! —Кристофер Бек Прежде всего, я хочу поблагодарить свою жену Сенди за поддержку. Если бы она не разрешала мне работать над этим проектом в "свободное время", я никогда не смог бы этого сделать. Я также признателен моим коллегам в Oracle, в особенности Тому Кайту и Патрику Сэку (Patrick Sack), за их технические знания и ценные советы. Я благодарен также моему редактору, Тони Девису, моим соавторам и рецензентам — все они помогли написать полезную и хорошую книгу. —Дэвид Нокс
Введение Я недавно зашел в сетевой магазин, набрал в окошке поиска PL/SQL и получил 38 ссылок. Тридцать восемь книг! Насколько я заметил, ни одна из них не была указана в списке мировых бестселлеров рядом с книгами про Гарри Поттера. Так что же могло вдохновить группу авторов собраться вместе для написания тридцать девятой книги на ту же тему? Причина в том, что, несмотря на множество доступных книг, мы по-прежнему сталкиваемся с некачественным или устаревшим кодом на языке PL/SQL в приложениях для Oracle. Я работал с системами Oracle по всему миру. В разных странах использовались различные приложения, архитектуры и технологии, но я заметил, что почти у всех этих систем было два общих свойства. Они либо вообще не использовали специфические функциональные возможности Oracle, либо использовали их бессистемно и неоптимально. Это наиболее очевидно при использовании языка PL/SQL — с ним работают мало и неправильно в большинстве систем, с которыми я сталкивался. Большинство книг по PL/SQL посвящено только синтаксису. В них показано, как писать на языке PL/SQL код, который будет компилироваться и работать на вашей машине (в некоторых книгах также представлены хорошие стандарты именования и структуризации кода). Но, как в любом языке программирования, есть большая разница между просто использованием языка и его правильным использованием. Ключ к созданию успешных приложений — это разумное применение знаний синтаксиса для создания устойчивых, эффективных и простых в сопровождении программ. Именно это побудило нас написать данную книгу и дать ей именно такое название. Мы не хотим сделать из вас программиста на PL/SQL, мы хотим сделать из вас профессионала-программиста, применяющего PL/SQL с умом.
О чем эта книга? Эта книга содержит множество советов, приемов и общих стратегий, позволяющих получить максимальные преимущества при использовании языка PL/SQL в проектах. Дочитав книгу до конца, вы, как и мы, будете убеждены, что PL/SQL — не просто полезный инструмент, а неотъемлемая часть любого приложения Oracle, которое вам придется когда-либо разрабатывать. Мы продемонстрируем приемы, применимые во всех версиях Oracle, от 8/до lOg. Подавляющее большинство примеров в этой книге было проверено на Oracle 9/ R2, и для их выполнения вам понадобится только утилита SQL*Plus. Ниже дано краткое содержание каждой главы, раскрывающее некоторые ключевые темы книги. Настройка. В этом разделе рассказывается о том, как настроить эффективную среду SQL*Plus, а также обеспечить работу средств оценки производительности, которые будут использоваться по ходу изложения, в частности: AUTOTRACE, SQL_TRACE, TKPROF И RUNSTATS.
18
Введение
Глава 1. Эффективность PL/SQL. В этой главе рассказывается о том, что мы понимаем под "эффективностью PL/SQL", и обсуждается главная тема всей книги — доказуемость; иными словами, необходимость убедительно доказать, что созданный код отвечает поставленным критериям производительности при всех разумных условиях. Мы продемонстрируем, почему язык PL/SQL почти всегда — лучшее средство программирования сервера, но представим также ситуации, когда PL/SQL может и не подойти, — несколько новаторских возможностей языка SQL, позволяющих вообще избежать создания процедурного кода. Глава 2. Объедините все в пакет. Пакеты — это намного больше, чем просто "логически сгруппированные процедуры". Они дают большие преимущества — от перегрузки и инкапсуляции до защиты от проблем зависимости кода от других объектов и перекомпиляции. В этой главе четко демонстрируются данные преимущества, а также обсуждаются интересные варианты использования некоторых стандартных пакетов Oracle. Глава 3. Бесконечная тема курсоров. Проблема использования явных и неявных курсоров является предметом давних обсуждений и споров. В этой главе показано, почему не надо использовать часто явные курсоры. Мы описываем также эффективное использование курсорных переменных и курсорных выражений в распределенных приложениях. Глава 4. Эффективная обработка данных. В этой главе показано, как добиться максимальной интеграции структур данных в базе данных и в PL/SQL-программе для получения более надежного и устойчивого к изменениям кода. Также рассматривается эффективное использование наборов для передачи массивов данных из программы в базу данных и наоборот. Глава 5. Методы оптимизации PL/SQL. В этой главе представлен ряд готовых решений некоторых типичных проблем программирования на PL/SQL. Показано, как избежать скрытой дополнительной обработки, и выявлены "нюансы", с которыми может столкнуться неосмотрительный разработчик. Глава 6. Триггеры. Рассматриваются основы применения триггеров и эффективное использование некоторых из имеющихся разнообразных типов триггеров. Детально описывается также достаточно новая тема потоков Oracle Streams и использование их для организации централизованного журнала аудита данных. Глава 7. Пакеты АБД. В этой главе представлен набор инструментальных средств АБД — набор пакетов, которые можно использовать для автоматизации рутинных административных действий, таких, как оценка производительности и выявление проблем, резервное копирование и восстановление, а также контроль сбоев в базе данных. Глава 8. Пакеты для защиты. Рассматривается использование PL/SQL-пакетов и триггеров для реализации эффективных механизмов защиты в базе данных. Описаны фундаментальные темы использования прав вызывающего и создателя, создания пакетов и проектирования схемы, а также конкретные решения таких задач, как аудит действий в базе данных и защита исходного кода.
Введение
19
Глава 9. Пакеты для Web-приложений. В этой главе рассматривается набор стандартных пакетов под общим названием PL/SQL Web Toolkit — они позволяют разработчикам формировать динамические Web-страницы непосредственно из базы данных. Описываются также использование "ключиков" (cookies), управление таблицами и файлами, а также непосредственное обращение к Web-службе (Web Service) из хранимой процедуры на языке PL/SQL. Глава 10. Отладка PL/SQL. Мало кому с первого раза удается выполнять ее правильно, поэтому в данной главе представлены методы эффективной отладки кода на PL/SQL, начиная от простого использования пакета DBMS_OUTPUT до более сложных пакетов вроде DBMS_APPLICATION_INFO И UTL_FILE. Завершается глава разработкой утилиты DEBUG — мощной специализированной утилиты для отладки. ный
Приложение А. Создание утилиты DEBUG. В этом приложении представлен полисходный код для утилиты DEBUG, которую мы использовали в главе 10.
Для кого предназначена эта книга? Эта книга предназначена для АБД и разработчиков, занимающихся реализацией эффективной обработки данных, защиты и механизмов администрирования в базе данных Oracle. Однако она многое даст любому разработчику, который использует в приложениях базу данных Oracle и которому нужно глубокое понимание того, как эффективно использовать PL/SQL. Если вы — абсолютный новичок в PL/SQL, вам стоит потратить некоторое время на знакомство с языком, прежде чем браться за эту книгу. Она не предназначена для начинающих. Но как только вы освоитесь и начнете работать, мы уверены, эта книга станет ценным руководством, гарантирующим создание устойчивых высокопроизводительных и простых в сопровождении PL/SQL-решений. — Коннор МакДоналъд •
•
Настройка В этом разделе описывается настройка среды для выполнения представленн ых в книге примеров. Рассматриваются следующие темы: > установка демонстрационной схемы SCOTT/TIGER; > конфигурирование среды SQL*Plus; > конфигурирование AUTOTRACE — средства утилиты SQL*Plus для показа того, как сервер Oracle выполнил или мог бы выполнить запрос, а также статистической информации об обработке запроса; > настройка и использование параметров SQL_TRACE, TIMED_STATISTICS И утилиты TKPROF, которые позволяют узнать, какие SQL-операторы были выполнены приложением и как именно; > настройка и использование утилиты RUNSTATS. Здесь даны простые инструкции по настройке различных средств оценки производительности, чтобы вы могли быстро сконфигурировать среду для выполнения примеров из книги. Детальные инструкции и информацию об интерпретации данных, предоставляемых этими инструментальными средствами, можно найти в документации Oracle или в книге Томаса Кайта "Expert Опе-оп-Опе Oracle" (Apress, ISBN: 1-59059-243-3; перевод на русский язык под названием "Oracle для профессионалов" вышел в издательстве "ДиаСофт" в 2003 году. — Прим. ред.).
Установка демонстрационной схемы SCOTT/TIGER Многие примеры из этой книги можно выполнить, используя таблицы EMP/DEPT в схеме SCOTT. МЫ рекомендуем вам создать копию этих таблиц в другой схеме, чтобы избежать побочных эффектов при изменении и использовании тех же данных другими пользователями. Для создания демонстрационных таблиц SCOTT В собственной схеме выполните следующие шаги: 1. В командной строке выполните cd [ORACLE_HOME] /sqipius/demo; 2. Зарегистрируйтесь в SQL*Plus; 3. Выполните команду SDEMOBLD.SQL. Сценарий DEMOBLD. SQL автоматически создаст и заполнит данными пять таблиц. По завершении работы он автоматически завершает сеанс SQL*Plus, так что не удивляйтесь, когда SQL*Plus исчезнет после выполнения сценария. Если вы захотите удалить эту схему, достаточно будет просто выполнить сценарий [ORACLE_HOME] / sqlplus/demo/demodrop.sql.
Среда SQL*Plus Примеры из этой книги нужно выполнять в среде SQL*Plus. Утилита SQL*Plus предоставляет много полезных опций и команд, которые используются во многих
Настройка
21
примерах. Часто используется пакет DBMS_OUTPUT. Чтобы увидеть результат работы процедур пакета DBMSOUTPUT, необходимо выполнить следующую команду SQL*Plus: SQL> set serveroutput on
Утилита SQL*Plus позволяет создать файл LOGIN.SQL — сценарий, который выполняется при каждом открытии сеанса SQL*Plus. В этом файле можно задать параметры вроде SERVEROUTPUT, и они будут устанавливаться автоматически. Пример сценария LOGIN.SQL представлен ниже (можете отредактировать его в соответствии с используемой средой). define _editor=vi set serveroutput on size 1000000 set trimspool on set long 5000 set linesize 100 set pagesize 9999 column plan_plus_exp format a80
Более того, можно использовать этот сценарий для изменения вида приглашения в SQL*Plus так, чтобы всегда было понятно, какой пользователь к какой базе данных подключен. Например, по ходу изложения вы будете встречать приглашения такого вида: scott@oracle9i_test>
Они показывают, что выполнено подключение к схеме пользователя SCOTT В базе данных ORACLE9I_TEST. Ниже представлен код сценария LOGIN.SQL, позволяющего этого добиться: column global_name new_value gname set termout off s e l e c t lower(user) | | '@' | | global_name from global_name; set sqlprompt '&gname> ' set termout on
Этот сценарий будет выполняться только один раз — при запуске. Поэтому, если вы при запуске зарегистрируетесь как пользователь SCOTT, а затем перейдете в другую схему, на приглашении SQL*Plus это не отразится. SQL*Plus: Release 8.1.7.0.0 - Production on Sun Mar 16 15:02:21 2003 (c) Copyright 2000 Oracle Corporation. All rights reserved. Enter user-name: scott/tiger Connected to: Personal Oracle8i Release 8.1.7.0.0 - Production With the Partitioning option
22
Настройка JServer Release 8.1.7.0.0 - Production scott@ORATEST> connect tony/davis Connected. scott@ORATEST> Следующий сценарий CONNECT . SQL решает проблему: set termout off connect &1 @login set termout on
Затем вы просто запускаете этот сценарий (который осуществляет подключение и выполняет сценарий login) при каждой смене схемы: scott@ORATEST> @connect tony/davis tony@ORATEST> Чтобы утилита SQL*Plus выполняла сценарий LOGIN автоматически при запуске, надо сохранить его в каталоге (там же надо сохранить и сценарий CONNECT.SQL), a затем настроить переменную среды SQLPATH так, чтобы она указывала на этот каталог. Если вы работаете в среде Windows, щелкните по кнопке Start, выберите Run и наберите команду regedit. Перейдите в раздел HKEY_LOCAL_MACHINE/SOFTWARE/ORACLE и найдите ключ SQLPATH (у меня он был в разделе НОМЕО). Щелкните на нем дважды и задайте полное имя каталога, в котором хранятся сценарии (например, С:\oracle\ora81\sqlplus\admin).
Настройка AUTOTRACE в SQL*Plus Во всей книге мы будем использовать это средство для контроля производительности выполняемых запросов, AUTOTRACE В SQL*P1US позволяет увидеть планы выполнения для выполненных запросов, а также использованные при их выполнении ресурсы. Соответствующий отчет генерируется после успешно выполненного оператора DML. По ходу изложения это средство интенсивно используется, AUTOTRACE можно сконфигурировать несколькими способами, но рекомендуется делать это следующим образом: 1. перейдите в каталог $ORACLE_HOME/rdbms/admin (в Windows для этого используется команда cd %ORACLE_HOME%\rdbms\admin. — Прим. ред.). 2. зарегистрируйтесь в SQL*Plus как любой пользователь с привилегиями CRE:ATE TABLE И CREATE PUBLIC
SYNONYM;
3. выполните команду @UTLXPLAN ДЛЯ создания таблицы PLAN_TABLE, которая используется средством AUTOTRACE; 4. выполните оператор CREATE PUBLIC SYNONYM PLAN_TABLE FOR PLANJTABLE, чтобы
любой пользователь мог обращаться к этой таблице без указания схемы; 5. выполните оператор GRANT ALL ON PLAN_TABLE пользовать эту таблицу;
TO PUBLIC, чтобы могли ис-
Настройка
23
6. выйдите из SQL*Plus и перейдите в каталог $ORACLE_HOME/sqlplus/admin; 7. зарегистрируйтесь в SQL*Plus как SYSDBA; 8. выполните команду @PLUSTRCE; 9. выполните оператор GRANT PLUSTRACE TO PUBLIC. Можно протестировать настройку, включив AUTOTRACE И ВЫПОЛНИВ простой запрос: SQL> set AUTOTRACE traceonly SQL> select * from emp, dept 2 where emp.deptno=dept.deptno; 14 rows selected. Execution Plan 0 1 2 3 4 5
0 1 2 1 4
SELECT STATEMENT Optimizer=CHOOSE MERGE JOIN SORT (JOIN) TABLE ACCESS (FULL) OF 'DEPT1 SORT (JOIN) TABLE ACCESS (FULL) OF 'EMP1
Statistics 0 8 2 0 0 2144 425 2 2 0 14
recursive calls db block gets consistent gets physical reads redo size bytes sent via SQL*Net to client bytes received via SQL*Net from client SQL*Net roundtrips to/from client sorts (memory) sorts (disk) rows processed
SQL> set AUTOTRACE off Подробнее об использовании AUTOTRACE И интерпретации предоставляемых данных см. в главе 11 руководства Oracle 9i Database Performance Tuning Guide and Reference или в главе 9 руководства SQL*Plus User's Guide and Reference в наборе документации Oracle.
Средства контроля производительности Помимо AUTOTRACE, мы будем использовать в книге и другие средства контроля производительности. В этом подразделе представлены краткие инструкции по их применению.
24
Настройка
Параметр TIMEDJTATISTICS Параметр TIMED_STATISTICS указывает, должен ли сервер Oracle регистрировать время выполнения различных внутренних операций. Если этот параметр не установлен, файл с результатами трассировки не представляет особой ценности. Как и другие параметры, TIMED_STATISTICS МОЖНО устанавливать на уровне экземпляра (в файле INIT.ORA) И на уровне сеанса. Первый вариант не должен существенно влиять на производительность, поэтому он рекомендуется в общем случае. Просто добавьте в файл INIT.ORA приведенную ниже строку, и при последующем запуске экземпляра параметр будет установлен: timed_statistics=true
На уровне сеанса можно выполнить следующую команду: SQL> alter session set timed_statistics=true;
SQL_TRACE и TKPROF Вместе параметр SQLJTRACE И утилита командной строки TKPROF ПОЗВОЛЯЮТ детально отслеживать происходящее в базе данных. Коротко: параметр SQL_TRACE позволяет записывать информацию о выполнении отдельных SQL-операторов в трассировочные файлы в файловой системе сервера баз данных. Обычно трассировочные файлы сложно понять непосредственно. Утилита же TKPROF позволяет генерировать понятные текстовые отчеты по переданному трассировочному файлу.
Параметр SQL_TRACE Параметр SQL_TRACE используется для трассировки всех SQL-операторов указанного сеанса базы данных или экземпляра в трассировочный файл в операционной системе сервера базы данных. Каждая запись в трассировочном файле отражает определенную операцию, выполненную сервером Oracle при обработке SQL-оператора. Параметр SQL_TRACE первоначально предназначался для отладки и по-прежнему хорошо для этого подходит, но точно так же его можно использовать и для анализа выполняемых в базе данных SQL-операторов с целью настройки производительности. Настройка SQL_TRACE Параметр SQL_TRACE МОЖНО устанавливать либо на уровне сеанса, либо для всего экземпляра. На уровне экземпляра он, однако, устанавливается редко, поскольку это существенно снижает производительность. Помните, что при установке SQL_TRACE в трассировочный файл записывается каждый обработанный SQL-оператор, а это требует дополнительных операций ввода-вывода. Для включения трассировки в текущем сеансе выполните показанный ниже оператор ALTER SESSION: SQL> alter session set sql_trace=true;
Включайте трассировку для сеанса на определенные периоды времени, не трассируйте долго. Для отключения трассировки выполните следующий оператор: SQL> alter session set sql_trace=false;
Настройка
25
Контроль трассировочных файлов Трассировочные файлы, генерируемые при установке SQL_TRACE, СО временем могут сильно разрастаться. На трассировочные файлы влияют несколько глобальных параметров инициализации, которые устанавливаются в файле INIT.ORA ДЛЯ экземпляра или на уровне сеанса. Если параметр SQLTRACE установлен, трассировочные файлы создаются в каталоге операционной системы, задаваемом параметром инициализации USER_DUMP_DEST. Помните, что трассировочные файлы пользовательских процессов (выделенных серверов) размещаются в каталоге USERDUMPDEST. Трассировочные файлы, сгенерированные фоновыми процессами, например, разделяемыми серверами MTS или процессами обработки очередей заданий, размещаются в каталоге BACKGROUND_DUMP_DEST. Использовать SQLJTRACE В конфигурации с разделяемыми серверами не рекомендуется. Сеанс будет переходить с одного разделяемого сервера на другой, генерируя информацию не в одном, а в нескольких трассировочных файлах, что сделает трассировку практически бесполезной. Трассировочные файлы обычно имеют имена вида: ora.trc,
где — идентификатор серверного процесса сеанса, для которого была включена трассировка. На платформе Windows для получения имени трассировочного файла сеанса можно использовать следующий запрос: SQL> select с.value || '\ORA' || to_char (a.spid,'fmOOOOO') || '.trc' 2 from v$process a, v$session b, v$parameter с 3 where a.addr = b.paddr 4 and b.audsid = userenv('sessionid') 5 and c.name = 'user_dump_dest';
На платформе Unix для получения имени трассировочного файла сеанса можно использовать такой запрос: SQL> select с.value II '/' II d.instance_name || '_ora_' || 2 to_char(a.spid,'fm99999') || '.trc' 3 from v$process a, v$session b, v$parameter c, v$instance d 4 where a.addr = b.paddr 5 and b.audsid = userenv('sessionid') 6 and c.name = 'user_dump_dest';
Размер трассировочных файлов ограничивается значением параметра инициализации MAX_DUMP_FILE_SIZE, который задается на уровне экземпляра в файле INIT .ORA. Его также можно изменить на уровне сеанса с помощью оператора ALTER SESSION, например: SQL> alter session set max_dump_file_size = unlimited; Session altered.
Утилита TKPROF Утилита TKPROF обрабатывает трассировочный файл SQL_TRACE И выдает текстовый файл — отчет. Это очень простая утилита, подытоживающая большой объем
26
Настройка
детальной информации в заданном трассировочном файле. Ее можно использовать при настройке производительности. Использование утилиты TKPROF TKPROF — простая утилита командной строки, используемая для преобразования трассировочного файла в более понятный отчет. В простейшем виде утилиту TKPROF можно использовать так: tkprof
Чтобы проиллюстрировать совместное использование TKPROF И SQL_TRACE, рассмотрим простой пример. Мы протрассируем запрос, который раньше использовали в примере с AUTOTRACE, И сгенерируем отчет по полученному трассировочному файлу. Сначала зарегистрируемся в SQL*Plus соответствующим пользователем и выполним следующий код: SQL> select с.value || '\ORA' || to_char(a.spid,'fmOOOOO') || '.trc' 2 from v$process a, v$session b, v$parameter с 3 where a.addr = b.paddr 4 and b.audsid • userenv('sessionid') 5
and c.name = 'user_dump_dest';
C.VALUEI!'\ORA'||TO_CHAR(A.SPID,'FMOOOOO')||'.TRC С:\oracle\admin\oratest\udump\ORA01528.trc SQL> alter session set timed_statistics=true; Session altered. SQL> alter session set sql_trace=true; Session altered. SQL> select * from emp, dept 2 where emp.deptno=dept.deptno; SQL> alter session set sql_trace=false; SQL> exit
Теперь просто переформатируем полученный трассировочный файл из командной строки с помощью утилиты TKPROF: C:\oracle\admin\oratest\udump>tkprof ORA01528.TRC tkprof_repl.txt
Теперь мы можем открыть файл TKPROF_REPI . тхт и просмотреть отчет. В начале отчета мы должны увидеть фактически выполненный SQL-оператор. Затем идет отчет о выполнении этого оператора, т.е. отчет о трех стадиях обработки SQL-оператора в Oracle: анализе (parse, широко используются также термины "разбор", "синтаксический разбор". — Прим. ред.), выполнении (execute) и выборке данных (fetch). Для каждой стадии обработки выводятся такие данные:
Настройка
27
> > > > >
сколько раз выполнялась стадия; процессорное время, потраченное на выполнение; реальное время выполнения; количество операций физического ввода-вывода на диск; количество блоков, обработанных в режиме "согласованного чтения" (consistent-read); > количество блоков, обработанных в "текущем" (current) режиме (чтения, происходящие при изменении данных внешним процессом в ходе обработки оператора); > количество блоков, затронутых оператором. Отчет о выполнении имеет вид: call
count
cpu
elapsed
disk
query current
rows
Parse Execute Fetch
1 1 2
0.01 0.00 0.00
0.02 0.00 0.00
0 0 0
0 0 2
0 0 8
0 0 14
total
4
0.01
0.02
0
2
8
14
После отчета о выполнении идет информация о выбранном оптимизатором подходе и идентификаторе пользователя для сеанса, включившего трассировку (по этому идентификатору в представлении ALLJJSERS МОЖНО найти имя соответствующего пользователя): Misses in library cache during parse: 0 Optimizer goal: CHOOSE Parsing user id: 52
Кроме того, мы видим, сколько раз оператор не был найден в библиотечном кеше. При первом выполнении оператора это значение будет равно 1, но при последующих вызовах оно, если используются связываемые переменные (bind variables), должно быть равно 0. Следите за отсутствием связываемых переменных — об этом свидетельствует большое количество "непопаданий" оператора в библиотечный кеш. Наконец, в отчете представлен план выполнения оператора. Эта информация практически аналогична выдаваемой при установке AUTOTRACE, НО С ОДНИМ важным отличием — выдается реальное количество строк на выходе каждого шага. Rows 14 5 4 14 14
Row Source Operation MERGE JOIN SORT JOIN TABLE ACCESS FULL DEPT SORT JOIN TABLE ACCESS FULL EMP
Детально использование параметра SQL_TRACE И утилиты TKPROF, а также интерпретация данных в трассировочном файле, описаны в главе 10 руководства Oracle 9i Database Performance Tuning Guide and Reference.
28
Настройка
Система RUNSTATS RUNSTATS — простая система тестирования, позволяющая сравнивать два выполнения кода и выдавать "стоимость" каждого с точки зрения времени выполнения, статистической информации уровня сеанса (например, количество выполненных синтаксических разборов) и использования защелок (latches). Как раз информация о защелках (внутренних блокировках. — Прим. ред.), предоставляемая этой системой, — ключевая.
Примечание Система RUNSTATS была создана Томом Кайтом, тем самым человеком, который поддерживает Web-сайт h t t p : //asktom.oracle.com. Полную информацию и пример использования RUNSTATS можно найти на странице h t t p : / / a s k t o m . o r a c l e . c o m / ~ t k y z e / r u n s t a t s . html. В главе 4 мы представим полезную модификацию этого инструментального средства с использованием наборов.
Для использования такой системы тестирования необходим доступ к представлениям V$STATNAME, V$MYSTAT и V$LATCH. Нужно получить непосредственную привилегию SELECT (не через роль) на представления SYS.V_$STATNAME, SYS.V_$MYSTAT И SYS.V_$LATCH. Затем можно создать следующее представление: SQL> create or replace view stats 2 as select 'STAT...1 || a.name name, b.value 3 from v$statname a, v$mystat b 4 where a.statistict • b.statistic# 5 union all 6 select 'LATCH.' || name, gets 7 from v$latch; View created.
После этого нужна еще небольшая таблица для хранения статистической информации: create global temporary table run_stats ( runid varchar2(15), name varchar2 (80), value int ) on commit preserve rows;
Код пакета системы тестирования представлен ниже. create or replace package runstats_pkg as procedure rs_start; procedure rs_middle; procedure rs_stop( p_difference_threshold in number default 0 ); end;
create or replace package body runstats pkg as
Настройка g_start number; g_runl number; g_run2 number; procedure rs_start is begin delete from run stats; insert into run_stats select 'before', stats.* from stats; g start := dbms_utility.get_time; end; procedure rs_middle is begin g_runl := (dbms_utility.get_time-g_start); insert into run_stats select 'after I 1 , stats.* from stats; g_start :•» dbms_utility.get_time; end; procedure rs_stop(p_difference_threshold in number default 0) is begin g_run2 := (dbms_utility.get_time-g_start); dbms_output.put_line 1 ( 'Runl ran in ' || g_runl || ' hsecs ); dbms_output.put_line ( 'Run2 ran in ' || g_run2 || ' hsecs' ); dbms_output.put_line ( 'run 1 ran in ' || round(g_runl/g_run2*100,2) 1 % of the time' ); dbms output.put_line( chr(9) ); insert into run_stats select 'after 2', stats.* from stats; dbms_output.put_line ( rpad( 'Name', 30 ) I I lpad( 'Runl', 10 ) || lpad( 'Run2', 10 ) M lpad ( 'Diff, 10 ) ); for x in ( select rpad( a.name, 30 ) | | to_char( b.value-a.value, '9,999,999' to chart c.value-b.value, '9,999,999'
||
29
30
Настройка to_char( ( (с.value-b.value)-(b.value-a.value)), '9,999,999') data from run_stats a, run_stats b, run_stats с where a.name = b.name and b.name = c.name and a . r u n i d = 'before' and b.runid = ' a f t e r 1' and c.runid = ' a f t e r 2' and (c.value-a.value) > 0 and abs( (c.value-b.value) - (b.value-a.value) ) > p_difference_threshold order by abs( (c.value-b.value)-(b.value-a.value)) ) loop dbms_output.put_line( x.data ) ; end loop; dbms_output.put_line( dbms_output.put_line ( 'Runl l a t c h e s t o t a l dbms_output.put_line ( lpad( 'Runl', 10 ) lpad( ' D i f f , 10 )
chr(9)
);
versus runs — difference and p e t '
);
| | lpad( 'Run2', 10 ) | | | | lpad( ' P e t ' , 8 ) ) ;
for x in ( select to_char( runl, '9,999,999' ) || to_char( run2, '9,999,999' ) || .. to_char( diff, '9,999,999' ) || to_char( round( runl/run2*100,2 ), '999.99' ) || '%' data from ( select sum(b.value-a.value) runl, sum(c.value-b.value) run2, sum( (c.value-b.value)-(b.value-a.value)) diff from run_stats a, run_stats b, run_stats с where a.name = b.name and b.name = с.name and a.runid = 'before' and b.runid = 'after 1' and c.runid = 'after 2' and a.name like 'LATCH%' ) ) loop dbms_output.put_line( x.data ); end loop; end; end;
Использование системы RUNSTATS Чтобы продемонстрировать, какую информацию можно получить от системы RUNSTATS, мы сравним производительность поиска по ключу (lookup) в обычной таблице (HEAP) И индекс-таблице (ют). Рассмотрим три сценария:
Настройка
31
> полный просмотр небольших таблиц; > поиск по первичному ключу в таблицах средних размеров; > поиск по вторичному индексу в таблицах средних размеров. Полный просмотр небольших таблиц Сначала создадим необходимые таблицы и индексы. SQL> create table HEAP 2
as select * from DUAL;
Table created. SQL> create table IOT { dummy primary key) 2 organization index 3 as select * from DUAL; Table created.
Теперь проанализируем обе таблицы, чтобы обеспечить согласованность полученных результатов. SQL> analyze table HEAP compute statistics; Table analyzed. SQL> analyze table IOT compute statistics; Table analyzed.
Затем выполним предварительный прогон, чтобы заполнить кеш. SQL> declare 2 x varchar2(1); 3 begin 4 for i in 1 .. 10000 loop 5 select dummy into x 6 from HEAP; 7 end loop; 8 end; 9/ PL/SQL procedure successfully completed. SQL> declare 2 x varchar2 (1) ; 3 begin 4 for i in 1 10000 loop select dummy into x 5 from IOT; 6 7 end loop;
32
Настройка 8 9 /
end;
PL/SQL procedure successfully
completed.
Делаем моментальный снимок статистической информации перед выполнением тестов: SQL> exec RUNSTATS_PKG.rs_start; PL/SQL procedure successfully
completed.
Выполняем код поиска для таблицы HEAP: SQL> declare x varchar2(1); 2 3 begin 4 for i in 1 10000 loop select dummy into x 5 from HEAP; 6 7 end loop; 8 end; 9/ PL/SQL procedure successfully
completed.
Делаем другой моментальный снимок. SQL> exec RUNSTATS_PKG.rs_middle PL/SQL procedure successfully completed.
Затем выполняем код поиска для таблицы ю т . SQL> declare 2 х varchar2(1); begin 3 4 for i in 1 .. 10000 loop 5 select dummy into x 6 from IOT; 7 end loop; 8 end; 9/ PL/SQL procedure successfully
completed.
Делаем завершающий моментальный снимок и получаем сравнительную информацию: connor@ORATEST> exec RUNSTATS_PKG.rs_stop; Runl ran in 130 hsecs Run2 ran in 74 hsecs run 1 ran in 175.68% of the time
Настройка Run2 2 0 0 0
Diff i-1
Name Runl 1 LATCH.checkpoint queue latch 1 STAT...calls to kcmgas 1 STAT...cleanouts and rollbacks STAT...immediate (CR) block cl 1 STAT...parse time cpu 1 ... LATCH.library cache 20,211 STAT...redo size 2,472 STAT...table scan blocks gotte 10,000 STAT...table scan rows gotten 10,000 STAT...table scans (short tabl 10,000 STAT...no work - consistent re 10,009 STAT...buffer is not pinned со 10,014 LATCH.undo global data 40,007 STAT...calls to get snapshot s 50,011 STAT...consistent gets 50,027 STAT..-db block gets 120,014 STAT...session logical reads 170,041 LATCH.cache buffers chains 340,125
-1 _^ -1 -1
0
20,089 1,740 0 0 0 2 3 4 10,002 10,012 18 10,030 20 ,113
33
-122 -732 -10,000 -10,000 -10,000 -10,007 -10,011 -40,003 -40,009 -40,015 -119,996 -160,011 -320,012
Runl latches total versus runs — difference and pet Runl Run2 Diff Pet 400,570 40,285 -360,285 994.34% PL/SQL procedure successfully completed.
При использовании ют мы не только выполнили все быстрее, но также существенно сократили количество установок защелок в базе данных. Отсюда можно сделать вывод, что использование ют в данном случае позволяет получить намного более масштабируемое решение. При полных просмотрах небольших таблиц лучше использовать индекс-таблицы, поскольку они не требуют дополнительной обработки, связанной с многократным чтением блока заголовка сегмента. Учтите, что представленный тест был выполнен на Oracle 8.I.7. Если повторить тест на Oracle 9/ R2, мы увидим следующее: Runl ran in 145 hsecs Run2 ran in 88 hsecs run 1 ran in 164.77% of the time Runl latches total versus runs — difference and pet Runl Run2 Diff Pet 113,812 73,762 -40,050 154.30%
Различие в количестве установленных защелок стало куда менее существенным, но по-прежнему имеет смысл при полном просмотре небольших таблиц использовать индекс-таблицы (существует также термин "таблицы, организованные по индексу". — Прим. ред.).
2 Зак. 348
34
Настройка
Поиск по первичному ключу в таблицах средних размеров Для выполнения этого теста мы удалим существующие таблицы HEAP И ЮТ И пересоздадим их следующим образом: create table HEAP ( г primary key, padding) as select rownum r, rpad(rownum,40) padding from all_objects; create table Ю Т ( г primary key, padding) organization index as select rownum r, rpad(rownum,40) from all_objects;
SQL-оператор для поиска изменится. Вместо select dummy into x from [HEAP I IOT] ;
будем выполнять: select dummy into x from [HEAP | Ю Г ] where r = i;
После этого изменения выполним те же тесты, что и раньше. На сервере версии 8.1.7 были получены следующие результаты: Runl ran in 101 hsecs Run2 ran in 96 hsecs run 1 ran in 105.21% of the time Runl l a t c h e s t o t a l versus runs — difference and pet Runl Run2 Diff Pet 50,297 40,669 -9,628 123.67%
Как мы и ожидали, результаты стали ближе, но таблица ют по-прежнему требует меньшего количества защелок и обеспечивает немного большую скорость работы, чем таблица HEAP. Поиск по вторичному индексу в таблицах средних размеров Для выполнения этого теста мы удалим существующие таблицы HEAP И ТОТ И пересоздадим их следующим образом: create table HEAP ( г primary key, с , padding) as select rownum r, mod(rownum,5000), rpad(rownum,40) padding from all_objects; create table IOT ( r primary key, с , padding) organization index as select rownum r, mod(rownum,5000), rpad(rownum,40) padding from all_objects; create index HEAP_IX on HEAP ( c) ; create index IOT IX on IOT ( c) ;
Настройка
35
SQL-оператор для поиска будет иметь вид: select max(padding) into x from [HEAP | IOT] where с = i ;
На сервере версии 8.1.7 были получены следующие результаты: Runl ran in 93 hsecs Run2 ran in 94 hsecs run 1 ran in 98.94% of the time Runl latches total versus runs — difference and pet Runl Run2 Diff Pet 75,554 75,430 -124 1 00.16%
В данном случае количество установленных защелок почти совпадает, и таблица HEAP обеспечивает немного лучшую производительность. Дело в том, что у индекстаблиц нет идентификаторов строк (ROWID), поэтому при чтении через вторичный
индекс сервер Oracle обычно должен прочитать вторичный индекс, а затем искать строку по первичному ключу. В общем, мы надеемся, что в этом разделе нам удалось продемонстрировать, насколько полезной может оказаться система RUNSTATS при оценке производительности различных вариантов решения задачи.
Глзп^ Л
Эффективность PL/SQL В этой главе мы рассмотрим эффективность PL/SQL. Мы намеренно избегаем использования термина "производительность", поскольку эффективность — это больше, чем просто производительность. Мы дадим точное определение тому, что мы понимаем под "эффективностью PL/SQL", и объясним, как сделать так, чтобы создаваемый на языке PL/SQL код соответствовал этому определению. При разумном использовании язык PL/SQL позволяет создавать высокопроизводительные приложения, которые при необходимости легко можно изменить и использовать для большого количества пользователей. Мы считаем, что PL/SQL — лучший язык для разработки приложений базы данных. Поэтому, если уж вы работаете с СУБД Oracle, следует непременно использовать PL/SQL как неотъемлемую часть создаваемого приложения.
Зачем использовать PL/SQL? Прежде чем рассматривать важные аспекты эффективности PL/SQL, давайте разберемся с вопросом, который все годы существования этого языка часто задают разработчики: "Надо ли вообще использовать PL/SQL"? С тех самых пор, когда в конце 1980-х годов организации занялись переносом своих приложений с мейнфреймов, сначала заменяя их клиент-серверными приложениями, затем — решениями на базе Web, данные приложения (хранящиеся в базе данных Oracle) все больше "отдаляются" от кода самого приложения. Как следствие, когда хранимые процедуры впервые появились в Oracle версии 7, их обычно продвигали как средство повышения производительности клиент-серверных приложений в медленных сетях. Вместо множества отдельных обращений к базе данных из клиентского приложения мы можем объединить эти обращения в хранимую процедуру, находящуюся на сервере, выполнить одно обращение из клиентского приложения и тем самым снизить зависимость нашей работы от скорости работы сети. Это ужасная недооценка значения языка PL/SQL (и давайте признаемся, что недооценка возможностей сервера со стороны компании Oracle весьма неожиданна). Что еще хуже, подобное представление приводит к мнению, что если не предполагается работа с приложением через глобальную сеть, то использовать PL/SQL вообще не нужно. Но, как мы не раз продемонстрируем в этой книге, не использовать язык PL/SQL в проекте для СУБД Oracle равносильно попытке набирать текст одним пальцем. Тем не менее, первая проблема, с которой вы можете столкнуться в проекте, — не обеспечение эффективного использования PL/SQL, а обоснование перед руководством необходимости вообще его использовать!
38
Глава 1
Часто против использования PL/SQL при разработке приложения выдвигаются следующие аргументы: > это привязывает приложение исключительно к СУБД Oracle; > "это ничего нового не дает по сравнению с использованием select * from custlog; ELAPSED_CENTISECONDS 10785 10878 11172 11116 11184 11450 11347 11701 11655 11897 11726 12055 11962 12028 12373 11859 11995 11905 12547 11977 20 rows selected.
На выполнение каждого задания нам понадобилось в среднем 115 секунд, и при этом в таблице customers было создано 200000 строк. Если учесть, что тестирование выполнялось на однопроцессорном ноутбуке, эти результаты вполне сопоставимы с теми 55-60 секундами, отведенными на выполнение на 100000 строк, которые мы получили ранее.
Как добиться эффективности Чтобы наши программы были эффективными, они должны удовлетворять требованиям по времени выполнения, не вредить другим компонентам системы и не исчерпывать все ее ресурсы. Мы также должны продемонстрировать, что эта эффективность сохраняется при всех предполагаемых условиях. В конце настоящей главы мы дадим несколько простых советов по обеспечению эффективности PL/SQL, а также представим код, демонстрирующий, как эта эффективность достигается. Мы не будем пытаться представить здесь все. В конечном итоге, эффективному использованию языка PL/SQL посвящена вся книга. Сейчас мы хотим сконцентрироваться на трех простых высокоуровневых принципах обеспечения эффективности. > Сведите к минимуму работу сервера при анализе SQL-операторов, которые необходимо выполнять. По сути, это — не проблема PL/SQL, поскольку она ни в коей мере не ограничивается выполнением SQL-операторов в PL/SQL-блоке. Речь идет о выполнении SQL-операторов из любого языка программиро-
Эффективность PL/SQL
49
вания (Java, Visual Basic и т.п.). Однако это, вероятно, основная причина плохой масштабируемости приложений Oracle, поэтому ее надо обсудить. > Разберитесь с особенностями и возможностями языка PL/SQL и научитесь их правильно использовать, чтобы не приходилось заново изобретать велосипед. > Никогда не используйте PL/SQL вместо SQL.
Связываемые переменные и затраты на анализ операторов Одна из причин, почему мне так нравится PL/SQL, — это естественная масштабируемость до высокой степени параллелизма и большого количества пользователей. Но, как и при использовании любого инструмента, при неправильном обращении вреда от него тоже может быть немало! Масштабируемость, несомненно, является неотъемлемым слагаемым эффективности PL/SQL, в частности, второго условия эффективности — отсутствия отрицательного влияния программы на другие компоненты системы. Одно из наиболее существенных препятствий для масштабируемости создаваемых для СУБД Oracle приложений — игнорирование шагов, которые должен выполнить сервер Oracle при обработке SQL-оператора. Многие разработчики знакомы со средствами вроде EXPLAIN PLAN, позволяющими проверить, оптимально ли время выполнения SQLоператора, но большинству неведомо, что нужно учитывать дополнительные действия; необходимо проверить SQL-оператор на допустимость до того, как выполнять его. Обратите внимание, что тут речь идет о создаваемых для СУБД Oracle приложениях. Мы затрагиваем проблему, важную не только при написании эффективного кода на языке PL/SQL. Любое приложение, работающее с СУБД Oracle (если предполагается его масштабируемость до большого числа пользователей), будь то приложение на Java/JDBC или Visual Basic/ODBC, и не учитывающее затраты ресурсов на обработку SQL-оператора перед его выполнением, масштабироваться не будет. Чтобы понять, почему анализ так важен (или, точнее, требует столько ресурсов), представим следующую ситуацию. Ваш начальник пришел в офис и попросил вас написать программу, которая будет использоваться многими сотрудниками отдела. Программа эта — простая и маленькая; она должна определять, является ли переданная строка символов допустимым для СУБД Oracle SQL-оператором или нет. Эта простая задача может оказаться весьма сложной; вам надо знать все допустимые ключевые слова языка SQL, последовательности и сочетания, в которых они допустимы, где могут стоять пробелы и комментарии, объекты, на которые ссылается запрос. Для каждого SQL-оператора, переданного вашей программе, нужно выполнить множество проверок. Однако можно легко расширить ее возможности, сохраняя ранее обработанные SQL-операторы в журнале. Если SQL-оператор направляется на проверку допустимости более одного раза, достаточно будет просто обратиться к журналу. Конечно, если все SQL-операторы, посылаемые из каждой программы, уникальны, то с журнала будет мало толку. Фактически со временем ваш общий журнал SQLоператоров может сильно разрастись — поиск в нем с целью проверки, не анализировался ли ранее данный SQL-оператор, может сам по себе потребовать достаточно много процессорного времени. Вы могли бы дополнительно оптимизировать этот
50
Глава 1
процесс — попросить разработчиков других приложений в отделе реализовать аналогичную журнализацию в своих программах, чтобы им надо было обращаться к вашей программе проверки синтаксиса только один раз для каждого уникального SQL-оператора. Сервер Oracle работает аналогично: рассмотренному в примере общему журналу соответствует разделяемый пул. Процесс проверки всех аспектов SQL-оператора до его выполнения называют разбором (parsing), или анализом, и он требует большие вычислительных ресурсов. Каждый новый SQL-оператор, передаваемый серверу, надо проанализировать, чтобы проверить его допустимость перед выполнением. Поэтому чем меньше новых SQL-операторов передается серверу, тем лучше системы будут масштабироваться и быстрее работать. Когда мы передаем серверу Oracle абсолютно новый SQL-оператор, будет выполняться полный анализ (hard parse, "жесткий разбор"), т.е. проверка синтаксиса и допустимости SQL-оператора. Если мы передаем серверу Oracle SQL-оператор и просим его проанализировать, но сервер его уже ранее полностью проанализировал, выполняется частичный анализ (soft parse, "мягкий разбор"). В идеале приложения анализируют свои SQL-операторы только один раз и запоминают, что больше анализировать их не нужно. Чтобы уменьшить количество различных SQL-операторов, которые сервер должен проанализировать, надо разумно использовать связываемые переменные (bind variables). Связываемая переменная — это способ задать вместо значения-литерала в SQL-операторе символ-заместитель. Очевидно, что при выполнении этого оператора надо будет передать соответствующее значение вместо символа-заместителя (иначе SQL-оператор будет бессмысленным), но многое нужно сделать еще до того, как SQL-оператор можно будет выполнить. Как связываемые переменные влияют на общий ход анализа? Мы уже говорили, что перед выполнением надо будет указать вместо символа-заместителя соответствующее значение, но сервер Oracle может выполнить анализ SQL-оператора до того, как эти значения будут подставлены. Поэтому для любого набора SQL-операторов, отличающихся только значениями входящих в них литералов, можно сократить расходы ресурсов на анализ, если заменить эти литералы связываемыми переменными.
Неиспользование связываемых переменных Мы легко можем продемонстрировать с помощью тестов, к чему приводит неиспользование связываемых переменных. Будем использовать в примерах пакет dbms_sqi, поскольку каждый вызов подпрограммы пакета описывает соответствующую стадию обработки SQL-оператора1. Запрос к таблице people будем выполнять, осуществляя поиск одной строки по первичному ключу. SQL> create table people( pid primary key )
2 3 4
organization index as s e l e c t rownum from all_objects where rownum exec literals(1); Elapsed: 00:00:31.70 SQL> exec literals(2); Elapsed: 00:00:32.05 SQL> exec literals (3); Elapsed: 00:00:31.43 SQL> exec literals(4); Elapsed: 00:00:32.21
Что же произошло? Когда мы выполняли одну процедуру l i t e r a l s , это заняло 29,68 секунды. Поскольку у нас 4 процессора, теоретически каждый процессор должен поработать и вернуть управление пользователю быстрее, чем за 30 секунд. Гдето мы потеряли чуть больше секунды. Чтобы понять, на что ушло время сеанса, необходимо обратиться к представлению V$SESSION_EVENT И проанализировать статистическую информацию об ожиданиях. Примечание Полное описание преимуществ использования статистической информации об ожиданиях см. в революционной статье Yet Another Performance Profiling MethodЭньо Колка (Anjo Kolk), Шари Ямагучи (Shari Yamaguchi) и Джима Вискузи (Jim Viscusi) на сайте oraperf.veritas.com.
Нам хотелось бы, чтобы либо процессор, либо диски активно выполняли наш запрос. Но статистическая информация об ожиданиях описывает периоды времени, когда обработка выполняться не может. В каждом сеансе мы выполнили: SQL> 2 3 4
select sid, event, time_waited from v$session_event where sid = ... and event = 'latch free';
где SID — уникальный идентификатор для каждого из четырех сеансов, в которых выполнялся тест. Когда результаты были сведены в таблицу, мы получили:
54
Глава 1 SID
EVENT 43 44 45 46
latch latch latch latch
free free free free
TIME_WAITED 79 72 69 87
Сервер Oracle должен защищать доступ к важным структурам памяти, чтобы гарантировать, что пока один сеанс анализирует оператор, никакой другой сеанс не сможет изменить ни одну из этих структур памяти, от которых зависит процесс анализа. Мы видим, что примерно 0,8 секунд в каждом сеансе (3 процента времени) тратится на событие latch free, т.е. сеанс тратит время на ожидание своей очереди доступа к общим ресурсам, необходимым для анализа SQL-оператора. Примечание Если вы используете Oracle 9.2, учтите, что для событий уровня сеанса в этой версии есть ошибка — значение идентификатора сеанса (SID) на единицу отличается от реального. Поэтому статистическая информация о событиях ожидания для сеанса, например, SID=42 в V$SESSION, будет представлена в V$SESSION_EVENT как SID=41. Эта проблема решена в версиях 10 и 9.2.0.4.
Для выполнения анализа не только требуется жизненно важное процессорное время, из-за него также другие сеансы не могут получить доступ к необходимым ресурсам, в частности, к библиотечному кешу и кешу словаря. Эта проблема не решается за счет добавления аппаратных ресурсов — постоянно выполняемый анализ не позволяет получить полную отдачу от существующего оборудования.
Связываемые переменные приходят на помощь Давайте вернемся к примеру, который мы приводили в начале раздела. Мы хотим увеличить вероятность того, что SQL-оператор будет найден в журнале уже обработанных SQL-операторов. Для этого нам надо использовать связываемые переменные. Немного изменив процедуру l i t e r a l s , мы можем создать новую, binding, в которой будут использоваться связываемые переменные. SQL> create or replace 2 procedure binding is 3 с number; 4 p number; 5 x number; 6 xl number; 7 begin 8 for i in 1 .. 10000 loop 9 с := dbms_sql.open_cursor; 10 dbms_sql.parse(c, 11 'select pid from people '|| 12 'where pid = :bl', dbms_sql.native);
Учтите, что анализируемый нами SQL-оператор фактически никогда не изменяется. Значение связываемой переменной задается после анализа оператора. С точки зрения полного синтаксического анализа, в этой процедуре мы выполняем 10000
Эффективность PL/SQL
55
одинаковых SQL-операторов. Скоро вы увидите, насколько существенно это влияет на процесс обработки оператора сервером Oracle. 13 dbms_sql.bind_variable(с,':Ы',i); 14 х := dbms_sql.execute(с); 15 xl := dbms_sql.fetch_rows(с); 16 dbms_sql.close_cursor(с); 17 end loop; 18 end; 19 / Procedure created.
Затем мы включаем трассировку и выполняем процедуру, как и в предыдущем тесте. SQL> alter session set sql_trace = true; Session altered. SQL> exec binding PL/SQL procedure successfully completed. SQL> alter session set sql_trace = false; Session altered.
При беглом просмотре трассировочного файла создается впечатление, что мы продолжаем анализировать (количество проанализированных операторов по-прежнему равно 10000), но производительность каким-то образом существенно возрастает. select pid from people where pid = :bl call
count
cpu
elapsed
disk
query
current
rows
Parse Execute Fetch
10000 10000 10000
0.99 1.17 0.77
1 .02 1 .26 с. 5 6
0 0 0
0 0
0 0
20000
0 0 0
9999
total
30000
2.93
2.85
0
20000
0
9999
Итак, процедура по-прежнему выполняет множество анализов. В конечном итоге получается, что мы вызвали процедуру dbms_sql.parse 10000 раз, так что полученные значения вполне объяснимы. Чтобы понять, почему повысилась производительность, необходимо проанализировать статистическую информацию уровня сеанса. Для этого сначала нужно создать представление, которое упростит получение статистической информации уровня сеанса. SQL> create or replace 2 view V$MYSTATS as
56
Глава 1 3 4 5
select s.name, m.value from v$mystat m, v$statname s where s.statistic# = m.statistic*;
View created. SQL> grant select on V$MYSTATS to publicGrant succeeded. SQL> create or replace public synonym V$MYSTATS for V$MYSTATS; Synonym created.
Теперь просмотрим статистическую информацию, связанную с анализом операторов. SQL> select * from v$mystats 2 where name like 'parse%'; NAME parse parse parse parse parse
VALUE time cpu time elapsed count (total) count (hard) count (failures)
107 137 10019 2 0
Ключевой показатель — parse count (hard). Хотя в нашей процедуре binding мы попросили сервер Oracle проанализировать SQL-оператор 10000 раз, сервер выполнил эту очень ресурсоемкую процедуру только дважды: один раз — при выполнении процедуры binding, второй — при анализе первого SQL-оператора. Остальные 9999 вызовов parse не требуют полного анализа, потому что выполняется один и тот же SQL-оператор. Мы повторно используем информацию анализа. Это — частичный анализ; явный вызов процедуры анализа был выполнен, но код SQL в разделяемом пуле можно использовать повторно. Вот в чем прелесть разделяемых переменных — они существенно повышают вероятность повторного использования отдельного SQL-оператора. Мы можем добиться еще большего. Как видно по тексту процедуры binding, текст проанализированного SQL-оператора не меняется при каждой итерации. Поэтому нет нужды вообще повторно анализировать SQL-оператор и можно вообще вынести вызов процедуры анализа из цикла. SQL> create or replace 2 procedure binding is 3 с number; 4 p number; 5 x number; 6 xl number; 7 begin 8 с := dbms_sql.open_cursor; 9 dbms_sql.parse(c, 10 'select pid from people 'I I 11 'where pid = :bl', dbms sql.native);
Эффективность PL/SQL
57
Обратите внимание на небольшое отличие. Вызов parse выполняется только один раз и больше не входит в тело цикла. 12 for i in 1 .. 10000 loop 13 dbms_sql.bind_variable(с,':bl',i); 14 x : = dbms_sql.execute(с); 15 xl := dbms_sql.fetch_rows(c); 16 end loop; 17 dbms_sql.close_cursor(c); 18 end; 19 / Procedure created.
При выполнении этой новой версии с включенной трассировкой в трассировочном файле можно найти свидетельство повышения производительности. select pid from people where pid = :bl call
count
Parse Execute Fetch
1 10000 10000
total
20001
cpu elapsed
disk
query current rows
0.00 0. 65 0.21
0.00 0.39 0.26
0 0 0
0 0 20000
0 0 0 0 0 9999
0.92
0.66
0
20000
0
9999
В этом случае анализ вызывался только один раз. За счет сокращения действий по анализу скорость работы возросла с исходных 30 секунд до менее чем 1 секунды. Многие разработчики просто отказываются мне верить, когда я говорю, что сервер Oracle может выполнить 10000 SQL-запросов менее чем за одну секунду. В только что представленном примере для обработки SQL-операторов специально используется пакет dbms_sql. Одно из лучших свойств языка PL/SQL — это простота реализации методики минимизации анализа и использования связываемых переменных. Давайте перепишем пример с использованием обычного статического SQL. SQL> create or replace 2 procedure EASY_AS_THAT is 3 xl number; 4 begin 5 for i in 1 .. 10000 loop 6 select pid into xl 7 from people 8 where pid = i; 9 end loop; 10 end; 11 / Procedure created.
58
Глава 1
При выполнении этого кода трассировочный файл выглядит точно так же, как и (оптимальные) результаты предыдущего примера. PL/SQL автоматически использует связываемые переменные для любых переменных PL/SQL и сводит к минимуму анализ статических SQL-операторов. Сервер Oracle предполагает использование разработчиками связываемых переменных и PL/SQL максимально упрощает их использование. Как было сказано сначала, это — сильный аргумент для использования PL/SQL в приложениях. Он позволяет создавать высокопроизводительные масштабируемые приложения Oracle. Если бы единственным языком для работы с Oracle был PL/SQL, вы, конечно же, даже и не знали бы о том, что в других процедурных языках программирования все обычно происходит совсем не так. Очень часто для правильного использования связываемых переменных надо сделать намного больше. Давайте вернемся к приведенному нами ранее в этой главе замечанию относительно языка Java и использования JDBC-классов statement и PreparedStatement. При использовании интерфейса JDBC простейшим способом обработки SQL-оператора является работа с объектом класса statement. Очень часто встречается код, в котором не используются связываемые переменные, например, такой: Statement stmt = conn.createStatement(); for (int i = 0; i < 10000; i++) { ResultSet rs = stmt.executeQuery("select pid from people where pid =" + i ) ; stmt. close {) ; }
Как мы уже доказали, этот код не будет масштабироваться. Намного эффективнее будет написать чуть больше кода с использованием PreparedStatement и связываемых переменных: PreparedStatement ps; for (int i = 0; i < 10000; i++) { pstmt = conn.prepareStatement ("select pid from people where pid » ?" ) ; pstmt.setlnt(1, i) ; ResultSet rs = pstmt.executeQuery(); pstmt.closed ; ) )
Нужны лишь несколько строк кода, чтобы избавиться от лишних затрат на повторный анализ. Однако и это еще не все. Представленный код дает тот же результат, что и пример с лишним частичным анализом. Каждый раз мы открываем pstmt, выполняем SQL-оператор и снова закрываем его. Чтобы не делать этого, для каждого нового SQL-оператора мы должны проанализировать его только один раз и выполнить столько раз, сколько нужно. На языке Java это можно сделать с использованием шаблона singleton, как показано ниже. static PreparedStatement pstmt; if (pstmt == null) ( pstmt = conn.prepareStatement ("select pid from people where pid = ?" ) ;
Эффективность PL/SQL
59
for (int i = 0; i < 10000; i++) { pstmt.setlnt(1, i); pstmt.execute(); }
Правильное выполнение SQL в JDBC требует немного размышлений и существенно большего объема кода. Мы не хотим далеко отходить от темы PL/SQL, но важность этой проблемы трудно переоценить. Если вы выполните все наши советы по созданию эффективного кода на языке PL/SQL, представленные в этой книге, кроме использования в приложении связываемых переменных, вероятно, окажется, что всю работу вы проделали зря, и приложение масштабироваться не будет. Мы еще обсудим затраты на анализ операторов при рассмотрении динамического SQL в главе 5, "Методы оптимизации PL/SQL".
Используйте существующие возможности языка PL/SQL В этом разделе мы хотели бы подчеркнуть важный аспект использования языка PL/SQL, который часто выражают призывом: "Не изобретайте велосипед!". Мы предпочитаем расширенный призыв: "Не изобретайте велосипед, который обычно получается сложнее, чем уже существующие, и зачастую на нем вообще невозможно ездить". Мощь языка PL/SQL не пропадает зря в Oracle — сервер предоставляет множество мощных возможностей на базе PL/SQL, которые надо знать и использовать в своих приложениях. Мы еще вернемся к этой теме в главе 2, "Объедините все в пакет", где рассматриваются некоторые из стандартных пакетов Oracle, но несколько полезных примеров не помешает представить уже сейчас, чтобы проиллюстрировать наш призыв.
Используйте встроенные средства обработки ошибок Рассмотрим процедуру update_emp. Она принимает в качестве параметров номер сотрудника и коэффициент понижения зарплаты. Соответствующее изменение выполняется простым SQL-оператором. SQL> 2 3 4 5 6 7 8
create or replace procedure UPDATE_EMP(p_empno number, p_decrease number) is begin update EMP set SAL • SAL / p_decrease where empno = p_empno; end ; /
Procedure created.
Чтобы уменьшить зарплату сотрудника 7379 в два раза, мы должны вызвать эту процедуру следующим образом:
60
Глава 1 SQL> exec UPDATE_EMP(7369, 2) ; PL/SQL procedure s u c c e s s f u l l y
completed.
He нужен университетский диплом, чтобы сообразить, что эта процедура может столкнуться с определенными проблемами — достаточно просто передать 0 в качестве значения параметра p_decrease. SQL> exec UPDATE_EMP(7369,0); BEGIN UPDATE_EMP(7369,0); END; * ERROR at line 1: ORA-01476: divisor is equal to zero ORA-06512: at "UPDATE_EMP", line 3 ORA-06512: at line 1
Но как раз в этот момент многие разработчики начинают улучшать код, чтобы сделать его более надежным, и совершенно зря. Обработка ошибок в PL/SQL основана на модели обработки исключительных ситуаций, а именно: ошибки перехватываются по мере возникновения и либо исправляются, либо распространяются в вызывающую среду. Однако разработчики в основном почему-то стараются не использовать средства обработки исключительных ситуаций, пытаясь предугадать все возможные ошибки, считая, что это гарантирует успешное выполнение PL/SQLпрограммы. Например, можно ошибочно расширить процедуру update_emp так, чтобы она возвращала булеву переменную, показывающую, успешно ли выполнен вызов. Для установки этой переменной мы проверяем допустимость параметра p_decrease перед тем, как выполнить изменение. SQL> create or replace 2 procedure UPDATE_EMP(p_empno number, p_decrease number, 3 p_success out boolean) is 4 begin 5 if p_decrease = 0 then 6 p_success := false; 7 else 8 update EMP 9 set SAL = SAL / p_decrease 10 where empno = p_empno; 11 p_success := true; 12 end if; 13 end; 14 / Procedure created.
Предугадать все ошибки, которые могут возникнуть в PL/SQL-программе, невозможно. Попытка сделать это заметно усложняет код и даже может привести к нарушению целостности данных (эту проблему мы рассмотрим в главе 4, "Эффективная обработка данных", после изучения особенностей управления транзакциями в языке PL/SQL).
Эффективность PL/SQL
61
Бинарные операции Для использования существующих возможностей PL/SQL сначала надо разобраться, какие возможности в языке есть. (Если вы думаете, что это вежливый способ сказать: "Идите и перечитайте руководства", вы правы.) Например, если нам надо выполнять побитовые операции с целыми числами, то, применив ряд арифметических действий, можно создать следующую PL/SQL-функцию, реализующую побитовое и. Мы не будем глубоко вникать в реализацию, т.к. практически сразу увидим, что создание такой функции было напрасной тратой времени. SQL> 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
create or replace function binary_and(x number, у number) return number is max_bin number(22) := power(2,64); l_x number := x; l_y number := y; result number := 0; begin for i in reverse 0 .. 64 loop if l_x >= max_bin and l_y >= max_bin then result := result + max_bin; end if; if l_x >= max_bin then l_x := l_x - max_bin; end if; if l_y >= max_bin then l_y := l'_y - max_bin; end if; max_bin : = max_bin/2; end loop; return result; end; /
Function created.
Время было потрачено напрасно, потому что такая функция уже существует, и называется она BITAND. ХОТЯ функция BITAND И появилась еще в Oracle версии 7 (и, вероятно, даже раньше), но корпорация Oracle не описывала ее в документации вплоть до версии 8.1.7. С точки зрения производительности нечего даже и сравнивать. При сравнении 50000 выполнений "самодельной" реализации функции BITAND на PL/SQL и встроенной реализации с помощью средства регистрации времени выполнения утилиты SQL*Plus выявляется существенное различие. SQL> 2 3 4 5 6 7 8
declare x number; begin for i in 1 .. 50000 loop x:= binary and(i,i+l); end loop; end; /
62
Глава 1 PL/SQL procedure successfully completed. Elapsed: 00:00:07.07 SQL> declare 2 x number; begin for i in 1 .. 50000 loop x:= bitand(i,i+1); end loop; end;
/ PL/SQL procedure successfully completed. Elapsed: 00:00:00.01
Заполнение пробелов С помощью SQL сложно сгенерировать результирующие множества данных, которые не обязательно существуют в базе данных. Например, если необходимо сгенерировать список объектов базы данных, созданных за последние две недели, с разбивкой по дням, то должен быть указан каждый день, даже если в этот день ни один объект не был создан. Типичный способ решения этой проблемы — таблица "заполнения", пример использования которой приводится в следующем разделе данной главы. Мы можем продемонстрировать, как построить требуемый список недавно созданных объектов. Сначала создадим таблицу src, содержащую в строках числа от 1 до 9999. (Имя src было выбрано потому, что эта таблица будет служить источником произвольных строк.) SQL> create table SRC ( x number ) pctfree 0; Table created. SQL> 2 3 4
insert into SRC select rownum from all_objects where rownum < 10000;
9999 rows created.
Теперь создадим копию представления ALL_OBJECTS В таблице Tl с усечением столбца created так, чтобы удалить время создания из даты. SQL> create table Tl as 2 select trunc(created) created 3 from all_objects; Table created.
Выполняя соединение с таблицей src, мы можем получить отчет об объектах, созданных за две последних недели, даже если в определенный день ни один объект создан не был. Для этого подойдет простое внешнее соединение.
Эффективность PL/SQL SQL> 2 3 4 5 6
63
select trunc (sysdate)-14+x created, count(created) no_of_obj from tl, src where trunc(sysdate)-14+x = tl.created(+) and x create or replace 2 type date_list is table of date; 3 / Type created.
Создадим конвейерную функцию, выдающую строки в диапазоне от переданной начальной даты (параметр p_start_date) до любой конечной (задаваемой параметром p_limit как количество дней). SQL> 2 3 4 5 6 3
create or replace function pipe_date(p_start date, p_limit number) return date_list pipelined is begin for i in 0 . . p_llmit-l loop pipe row (p_start + i) ;
В следующих разделах мы также будем использовать таблицу src, чтобы читатели могли воспроизвести примеры в версии 8 (в ней конвейерных функций нет).
64
Глава 1 7 8 9 10
end loop; return; end; /
Function created.
Вместо таблицы src мы можем теперь вызывать нашу конвейерную функцию, PIPE_DATE, для искусственного создания необходимого количества строк. В данном
случае нам достаточно получить 14 строк. Запрос будет иметь вид: SQL> 2 3 4 5
select column_value, count(created) no_of_obj from tl, table(pipe_date(trunc(sysdate) -14,14) ) where column_value = tl.created(+) group by column_value /
COLUMN VA
NO OF OBJ
29/MAY/03 30/MAY/03 31/MAY/03 01/JUN/03 02/JUN/03 03/JUN/03 04/JUN/03 05/JUN/03 06/JUN/03 . 07/JUN/03 08/JUN/03 09/JUN/03 10/JUN/03 ll/JUN/03
0 0 0 0 0 0 0 0 0 0 0 41 4 6
Мы также можем помочь оптимизатору, указав, сколько строк будет возвращено из конвейерной функции. Для этого используется подсказка CARDINALITY, помогающая серверу Oracle оптимизировать запрос. В представленном выше примере мы можем сообщить оптимизатору, что возвращено будет 14 строк. SQL> 2 3 4 5
select /*+CARDINALITY(t 14)*/ column_value, count(created) no_of_obj from tl, table(pipe_date(trunc(sysdate)-14,14)) t where column_value = tl.created(+) group by column_value /
He используйте PL/SQL вместо SQL Язык PL/SQL часто используется чрезмерно. Первое же предложение в руководстве "PL/SQL User's Guide and Reference", входящем в набор документации Oracle 9.2, гласит: "Язык PL/SQL, процедурное расширение SQL сервера Oracle...". PL/SQL разрабатывался как расширение SQL (и всегда им был), т.е. как средство, которое можно использовать, когда задачу невозможно решить с помощью SQL.
Эффективность PL/SQL
65
Давайте рассмотрим гипотетический пример, как будто бы специально созданный для реализации на PL/SQL. Мы "пересоздадим" вездесущие таблицы emp и dept для этого примера, чтобы можно было заполнить их несколько большим объемом данных. SQL> drop table EMP; Table dropped. SQL> drop table DEPT; Table dropped. SQL> 2 3 4 5 6
create table EMPNO ENAME HIREDATE SAL DEPTNO
EMP ( NUMBER(8), VARCHAR2(20), DATE, NUMBER(7, 2) , NUMBER(6) ) ;
Table created. SQL> create table DEPT ( 2 DEPTNO NUMBER(6), 3 DNAME VARCHAR2(20) ); Table created.
Добавим в таблицы первичные ключи. Это приведет к индексированию столбцов empno и deptno соответственно. SQL> alter table EMP add constraint EMP_PK 2 primary key (EMPNO); Table altered. SQL> alter table DEPT add constraint DEPT_PK 2 primary key (DEPTNO); Table altered.
Теперь пересоздадим таблицу src из предыдущего раздела, поместив в нее 200000 строк произвольных данных. Мы будем использовать эту таблицу в качестве источника данных для наполнения других таблиц. Нам неважно, какие данные будут в строках этой таблицы, главное, чтобы их было не менее 200000. Если в вашей системе уже есть такая таблица, в следующих примерах вы можете использовать ее. Если же вы используете сервер версии 9 или выше, можно пользоваться представленным ранее решением на базе конвейерных функций. SQL> create table SRC ( x varchar2(10)); Table created. SQL> begin 2 for i in 1 .. 200000 loop 3 Зак. 348
•
66
Глава 1 3 4 5
insert into SRC values ('x'); end loop; end;
6
/
PL/SQL procedure successfully completed. SQL> commit; Commit complete.
Поместим в таблицу emp информацию о 500 сотрудниках, генерируя имена и даты приема на работу с помощью псевдостолбца rownum, а случайные зарплаты и номера отделов (от 1 до 10) — с помощью пакета dbms_random. SQL> insert into EMP 2 select rownum, 3 ' Name'I|rownum, 4 sysdate+rownum/100, 5 dbms_random.value(7500,10000) , 6 dbms_random.value(1,10) 7 from SRC 8 where rownum 2 3 4
insert into DEPT select rownum, 'Dept'I Irownum from SRC where rownum определяет среднюю зарплату сотрудников соответствующего отдела; > если зарплата сотрудника меньше средней более чем на 20 процентов, в таблицу emp_sai_iog добавляется строка с именем сотрудника, названием отде-
Эффективность PL/SQL 67 ла, в котором он работает, датой приема его на работу и суммой, которой недостает до средней заработной платы; > если сотрудник получает меньше всех в отделе, отметить это, поставив в столбце m i n s a l таблицы emp_sal_log букву Y, если же это не так, ставится N. При сравнении со спецификациями модулей, которые вам приходилось видеть годами, этот может показаться достаточно хорошим. Как ни странно, как раз в этом и заключается главная проблема. Пытаясь сделать спецификацию модуля простой и понятной пользователю, разработчики написали ее так, что для каждого сотрудника в организации нужно выполнить ряд отдельных шагов. Давайте рассмотрим решение, напрямую реализующее эту спецификацию в базе данных. Во-первых, нам понадобится таблица emp_sai_iog, в которую будут записываться результаты отчета. SQL> create table EMP_SAL_LOG ( 2
ENAME
3
HIREDATE
VARCHAR2(20) , DATE,
4
SAL
NUMBER ( 7 , 2 ) ,
5 6
DNAME MIN_SAL
VARCHAR2(20), VARCHAR2(1) );
Table created.
Теперь давайте рассмотрим PL/SQL-программу, соответствующую требованиям этой спецификации. Мы разобьем ее на части, соответствующие спецификации модуля. SQL> create or replace 2 procedure report_sal_adjustment is
Во-вторых, нам понадобятся переменные, обозначающие среднюю и минимальную зарплаты для каждого отдела. 3 4 5
v_avg_dept_sal emp.sal%type; v_min_dept_sal emp.sal%type; v_dname dept.dname%type;
В-третьих, потребуется курсор, позволяющий в цикле обработать запись каждого сотрудника в таблице emp. 6 7 8 9
cursor c_emp_list is select empno, ename, deptno, sal, hiredate from emp; begin
Теперь мы начинаем выбирать строки с помощью курсора, поочередно обрабатывая строку каждого сотрудника. По номеру отдела, в котором работает сотрудник, можно определить среднюю зарплату в отделе (пункт 1 в спецификации модуля). 10 11 12
for each_emp in c_emp_list loop select avg(sal) into v avg dept sal _ _ _
68
Глава 1 13 14
from where
emp deptno = each_emp.deptno;
Затем сравним зарплату сотрудника со средней зарплатой в отделе. Если она отличается от средней более чем на 20 процентов, нужно записать ее в строку в таблице emp_sal_log (пункт 2 в спецификации модуля). Однако прежде чем сделать это, необходимо узнать название отдела, в котором работает сотрудник, и определить минимальную зарплату в этом отделе — она понадобится для выполнения пункта 3 в спецификации модуля. 15 16 17 18 19 20 21
if abs(each_emp.sal - v_avg_dept_sal ) / v_avg_dept_sal > 0.20 then select dept.dname, min(emp.sal) into v_dname, v_min_dept_sal from dept, emp where dept.deptno = each_emp.deptno and emp.deptno = dept.deptno group by dname;
Прежде чем сделать запись в таблице emp_sai_iog, нужно определить, не получает ли сотрудник наименьшую зарплату в отделе. Сравнивая минимальную зарплату в данном отделе с зарплатой сотрудника, мы сможем установить значение флага min_sal В emp_sal_log. 22 23 24 25 26 27 28 29 30 31 32 33 34
if v_min_dept_sal = each_emp.sal then insert into emp_sal_log values ( each_emp.ename, each_emp.hiredate, each_emp.sal, v_dname, ' Y ' ) ; else insert into emp_sal_log values ( each_emp.ename, each_emp.hiredate, each_emp.sal, v_dname, ' N ' ) ; end if; end if; end loop; end; /
Procedure created.
Пара вызовов (не показаны) подтверждает, что процедура удовлетворяет функциональным требованиям, поэтому давайте проверим ее эффективность. При выполнении на сгенерированных ранее тестовых данных (т.е. при 500 сотрудниках и 10 отделах) все вроде бы в порядке. SQL> exec report_sal_adjustment PL/SQL procedure successfully completed. Elapsed: 00:00:00.03
Чудесно! Процедура выполнена менее чем за 3 секунды (одно из требований спецификации нашего модуля), так что код готов к производственной эксплуатации!
Эффективность PL/SQL
69
И в зависимости от размера компании, такое решение может оказаться вполне подходящим. Но что произойдет при масштабировании для увеличения числа пользователей? В этой компании (или другой, которой мы захотим продать это решение) могут быть тысячи сотрудников. Чтобы проверить эффективность решения в различных ситуациях (в конечном итоге, мы же хотим продемонстрировать эффективность кода), можно использовать простой сценарий SQL*Plus для проверки производительности при различном количестве сотрудников и отделов. Особенности работы сценария описаны в комментариях по ходу. rem rem rem rem rem rem rem set
REPTEST.SQL Принимает два параметра: первый — число сотрудников, а второй — количество отделов, Сотрудники случайным образом распределяются по отделам и получают случайную зарплату в диапазоне от 7500 до 10000. termout off
rem rem rem rem
Число сотрудников будет передано как первый параметр, а количество отделов — как второй. Мы хотим присвоить эти значения двум подставляемым переменным SQL Plus — NUM_EMPS и NUMJDEPTS соответственно.
col x new_value num_emps col у new value num_depts rem Для присвоения выбираем значения из таблицы DUAL. select &1 х, &2 у from dual; rem Теперь удаляем данные из ячеек таблиц ЕМР rem и DEPT, чтобы заполнить их новыми данными. truncate table EMP reuse storage; truncate table DEPT reuse storage; rem Загружаем таблицу ЕМР так же, как и в rem предыдущем примере, используя таблицу SRC rem для генерации необходимого количества строк. insert into EMP select rownum, 'Name'I Irownum, sysdate+rownum/100, dbms_random.value(7500,10000), dbms_random.value(1,&num_depts) from SRC where rownum @c:\reptest 1000 100 Average run time: .93 PL/SQL procedure successfully completed. SQL> @c:\reptest 1500 150 Average run time: 1.87
Эффективность PL/SQL
71
PL/SQL procedure successfully completed. SQL> @c:\reptest 2000 200 Average run time: 3.3 PL/SQL procedure successfully completed. SQL> @c:\reptest 2500 250 Average run time: 4.96 PL/SQL procedure successfully completed.
Достаточно быстро работающий, с точки зрения разработчика, код очень плохо масштабируется. Фактически выполнение большого количества тестов показывает, что время выполнения растет экспоненциально при линейном росте количества сотрудников. Если учесть, что программа должна отрабатывать за 3 секунды, понятно, что на используемом для тестирования аппаратном обеспечении для этого решения предел — 1900 сотрудников. После этого предложенное решение не работает, поскольку процедура выполняется более 3 секунд. К счастью, мы выявили этот недостаток при тестировании, так что бедного разработчика можно просто отправить в офис с требованием повысить производительность кода. Вот тут и возникает вторая фундаментальная проблема. Улучшение процедурного решения дает другое процедурное решение Если необходимо ускорить работу, надо уменьшить объем выполняемых действий. Наш разработчик замечает, что основной объем действий связан с запросами к таблице emp, поэтому, если от них избавиться, производительность повысится. Для начала можно объединить два запроса, выбирающих среднюю и минимальную зарплаты. SQL> create or replace 2 procedure report_sal_adjustment2 is 3 v_avg_dept_sal emp.sal%type; 4 v_min_dept_sal emp.sal%type; 5 v_dname dept.dname%type; 6 cursor c_emp_list is 7 select empno, ename, deptno, sal, hiredate 8 from emp; 9 begin 10 for each_emp in c_emp_list loop
Одновременно можно получить название отдела, минимальную и среднюю зарплаты в отделе. 11 12 13 14 15 16
select avg(emp.sal), min(emp.sal), dept.dname into v_avg_dept_sal, v_min_dept_sal, v_dname from dept, emp where dept.deptno » each_emp.deptno and emp.deptno = dept.deptno group by dname;
72
Глава 1
Остальной код не меняется. 17 if abs(each_emp.sal - v_avg_dept_sal ) / v_avg_dept_sal > 0.20 then 18 if v_min_dept_sal = each_emp.sal then 19 insert into emp_sal_log 20 values ( each_emp.ename, each_emp.hiredate, 21 each_emp.sal, v_dname, ' Y ' ) ; 22 else 23 insert into emp_sal_log 24 values ( each_emp.ename, each_emp.hiredate, 25 each_emp.sal, v_dname, ' N ' ) ; 26 end if; 27 end if; 28 end loop; 29 end; 30 / Procedure created.
Мы изменили сценарий REPTEST.SQL так, чтобы в нем вызывалась новая процедура report_sai_adjustment2. При выполнении текстов для тех же значений числа сотрудников и отделов были получены результаты, представленные в таблице 1.3. Таблица 1.3. Результаты изменений в процедуре REPORT_SAL_ADJUSTMENT Число сотрудников
Количество отделов
500 1000 1500 2000 2500
50 100 150 200 250
Время выполнения 0,27 0,86 1,68 3,03 4,64
Как вы видите, производительность повысилась примерно на 10 процентов. Это позволит нормально работать двум тысячам сотрудников. Но что, если в организации будет 50000 сотрудников? Вдохновленный результатами первой попытки настройки, разработчик может продолжить изучать другие альтернативы. Мы видели, что чем меньше SQL-операторов выполняется для таблицы emp, тем, похоже, быстрее проходит процедура. Возможно, более эффективным решением будет заранее занести средние зарплаты по отделам в таблицы в памяти, чтобы не нужно было выполнять лишние SQL- операторы. Наш разработчик весьма находчив и всегда следит за последними наиболее интересными функциональными возможностями PL/SQL, поэтому он знает, что это можно сделать с помощью набора. Добавив немного кода, получаем следующее решение: SQL> create or replace 2 procedure report_sal_adjustment3 is
Теперь нам надо определить несколько типов для хранения массива (или списка) информации об отделах, в частности, средней зарплаты, минимальной зарплаты и названия отделов. Это делается в два приема: сначала определяем запись для хранения информации, затем — массив таких записей.
Эффективность PL/SQL 3 4 5 6 7 8 9 10 11 12
73
type dept_sal_details is record ( avg_dept_sal emp.sal%type, min_dept_sal emp.sal%type, dname dept.dname%type ) ; type dept_sals is table of dept_sal_details index by binary_integer; v_dept_sal dept_sals; cursor c_emp_list is select empno, ename, deptno, sal, hiredate from emp;
Мы создали новый курсор для получения требуемой информации об отделах. Он будет служить источником данных для записей PL/SQL-таблицы. 13 14 15 16 17 18
cursor c_dept_salaries is select avg(sal) asal, min(sal) msal, dname, dept.deptno from dept, emp where emp.deptno » dept.deptno group by dname, dept.deptno; begin
Для начала выберем информацию по отделам и поместим ее в PL/SQL-таблицу. Поскольку значение deptno — числовое, его можно использовать в качестве индекса PL/SQL-таблицы. 19 20 21 22 23 24
for i in c_dept_salaries loop v_dept_sal(i.deptno).avg_dept_sal := i.asal; v_dept_sal(i.deptno).min_dept_sal := i.msal; v_dept_sal(i.deptno).dname : = i.dname; end loop; for each_emp in c_emp_list loop
В основном цикле обработки больше не нужно выполнять поиск в таблице dept. Мы просто используем соответствующую информацию из PL/SQL-таблицы (v_dept_sal). 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
if abs (each_emp.sal - v_dept_sal(each_emp.deptno) .avg_dept_sal ) / v_dept_sal(each_emp.deptno).avg_dept_sal > 0.20 then if v_dept_sal(each_emp.deptno).min_dept_sal = each_emp.sal then insert into emp_sal_log values ( each_emp.ename, each_emp.hiredate, each_emp.sal, v_dept_sal(each_emp.deptno).dname, ' Y ' ) ; else insert into emp_sal_log values ( each_emp.ename, each_emp.hiredate, each_emp.sal, v_dept_sal(each_emp.deptno).dname, ' N ' ) ; end if; end if; end loop; end; /
Procedure created.
74
Глава 1
Мы немного усложнили код, но зато теперь мы должны всего лишь раз "пройтись" по таблице dept и один — по таблице emp. Посмотрите на впечатляющие результаты, которые мы получили при тестировании с помощью сценария REPTEST . SQL (после его изменения для выполнения процедуры report_sal_adjustment3). Таблица 1.4. Дальнейшее усовершенствование процедуры REPORT_SAL_ADJUSTMENT Число сотрудников Количество отделов Время выполнения 500 50 0,03 1000 100 0,05 5000 500 0,24 25000 2500 1,24 50000 5000 2,74
Мы существенно повысили производительность. Что еще важнее, масштабируемость теперь, похоже, линейная, а не экспоненциальная. Однако у этого решения есть скрытая проблема. Хранение информации об отделах, очевидно, требует определенного объема памяти. Чтобы оценить, сколько именно памяти для этого требуется, необходимо получить статистическую информацию сеанса с помощью ранее рассмотренного представления V$MYSTAT. После выполнения теста с 5000 отцелов посмотрим, сколько памяти использовано сеансом. SQL> SQL> 2 3
col value format 999,999,999 select * from v$mystats where name = 'session pga memory max' /
NAME session pga memory max
VALUE 3,669,024
Наш сеанс использовал около 4 Мбайт памяти. Теперь мы можем эффективно работать, занося в таблицу данные о более чем 50000 сотрудников, учитывая ограничения по времени выполнения. Проблема решена... Но на самом деле это не так. Существует еще более эффективное решение, не требующие дополнительного расходования памяти. Усложняя процедурное решение, наш разработчик уходил все дальше от оптимального результата. Нам не нужны все эти сложные действия в PL/SQL, да и вообще PL/SQL не нужен! Задачу можно решить в SQL с помощью аналитических функций. (Учтите, что для компиляции представленной процедуры потребуется сервер Oracle 9. Если вы используете версию 8, необходимо будет выполнять оператор i n s e r t как динамический SQL, т.е. с помощью оператора EXECUTE IMMEDIATE.) SQL> c r e a t e or replace 2 procedure report_sal_adjustment4 i s 3 begin 4 insert into emp_sal_log 5 select e.empno, e.hiredate, e.sal, dept.dname,
Эффективность PL/SQL 6
7 8 9 10 11 12 13 14 15 16 17
case when sal
else W
> avg_sal
then
75
'Y'
end case from ( select empno, hiredate, sal, deptno, avg(sal) over ( partition by deptno ) as avg_sal, min(sal) over ( partition by deptno ) as min_sal from emp ) e, dept where e.deptno = dept.deptno and abs(e.sal - e.avg_sal)/e.avg_sal > 0.20; end; /
Procedure created.
Вот и все! Наша процедура сократилась до одного SQL-оператора. Никаких PL/SQLтаблиц, никакого сложного кода — только простой SQL-оператор. При оценке масштабируемости кода с помощью сценария REPTEST . SQL МЫ получили следующие удивительные результаты, представленные в таблице 1.5. Таблица 1.5. Оптимальная реализация REPORT_SAL_ADJUSTMENT Число сотрудников Количество отделов Время выполнения 500 5000 50000 100000
50 500 5000 10000
0,01 0,08 0,83 1,71
Аналитические функции Если вы не знакомы с конструкциями, использованными в процедуре report_sal_adjustment4, значит, вы пропустили одно из наибольших достижений сервера Oracle. Эти аналитические функции, которые были доступны уже в версии 8.1.6, с каждой новой версией расширяют функциональность программы и становятся все мощнее. К сожалению, они не слишком известны, поскольку первоначально предлагались в основном для запросов хранилищ данных, связанных с определением скользящего среднего и статистических показателей регрессии. Они описаны в руководстве Data Warehousing (входящем в состав стандартного набора документации Oracle). К сожалению, это руководство обычно не читает никто, кроме пользователей, занимающихся созданием хранилищ данных. А жаль! Аналитические функции дают много замечательных возможностей для любой базы данных. Каждая версия сервера Oracle расширяет и усложняет набор SQL-операторов. Как следствие, будет (или должно быть) больше задач, которые можно решать с помощью языка SQL, а не PL/SQL. Один из классических примеров, достойный отдельного упоминания, — это обработка результатов соединения (или представления,
76
Глава 1
содержащего соединение). Мы уже потеряли счет ситуациям, когда разработчики создавали модуль на PL/SQL, потому что спецификация гласила: "Представление V построено на основе таблиц X, Y и Z. Надо изменить строки в таблице X для строк представления V, которые (некий критерий)". Модуль PL/SQL создавался потому, что по общему убеждению разработчиков, "представления, содержащие соединения, изменять нельзя". Это было верно когдато давным-давно, в версии 7.1, но уже многие годы существует целый класс представлений в Oracle, которые можно изменять непосредственно, как если бы они были обычными таблицами. Сервер Oracle даже предлагает представление в словаре данных (DBAJJPDATABLE_COLUMNS), позволяющее определить, может ли данное представление или его часть использоваться в операторах DML. Рассмотрим следующий пример спецификации модуля. Увеличить премию на 10 процентов для всех сотрудников, данные о которых содержатся в представлении YEARLY_BONUS. Это представление определено как create or replace view YEARLY_BONUS as select emp.empno, emp.ename, dept.dname, emp.bonus from EMP, DEPT where emp.hiredate < sysdate + 1 and emp.deptno = dept.deptno
Решение на языке PL/SQL, запрашивающее представление, а затем обновляющее базовую таблицу, написать сравнительно легко. create or replace procedure XXX is begin for i in ( select empno from yearly_bonus ) loop update emp set bonus = bonus * 1.1 where empno = I.empno; end; /
Но лучшее решение еще более примечательно: update yearly_bonus set bonus = bonus * 1.1;
Оператор можно просто включить в PL/SQL-процедуру, если это требуется. Вот и все, что нужно! Фактически, если представление вообще не определено, сложной обработки все равно можно избежать. Просто напишите: update ( s e l e c t emp.ename, dept.dname, emp.bonus from EMP, DEPT where emp.hiredate < sysdate + 1 and emp.deptno = dept.deptno) set bonus • bonus * 1.1;
Когда выполнены требования для изменения представлений, содержащих соединение (см. раздел "Modify a Join View" руководства "Application Developer Fundamentals"), язык PL/SQL использовать необязательно.
Эффективность PL/SQL
77
Используйте новые возможности SQL вместо PL/SQL Самое сложное в использовании языка SQL вместо PL/SQL — убедить себя, что нечто может быть реализовано в SQL, когда PL/SQL кажется естественным выбором. Представим несколько примеров, когда можно использовать SQL, но такая возможность неочевидна. Это, конечно, не исчерпывающий набор способов использования языка SQL вместо PL/SQL, и вы вообще нигде не найдете руководство, объясняющее, когда можно использовать SQL вместо PL/SQL. Надо просто подходить творчески к проблеме, которая не кажется легко разрешимой на чистом SQL. Выдача календаря Программирование на PL/SQL с использованием процедуры DBMS_OUTPUT . PUT_LINE кажется единственным способом выдать календарь на текущий (или любой другой) месяц, поскольку нет никакой явной функции в SQL, которая могла бы его сгенерировать. Но, используя популярный прием — транспонирование строк в столбцы с помощью функции decode, можно фактически сгенерировать календарь на чистом SQL. Сначала надо определить подставляемую переменную mdate для хранения даты, по которой будет генерироваться календарь на соответствующий месяц. SQL> col dte new_value mdate SQL> select '23-JUN-03' dte from dual; DTE 23-JUN-03
Затем можно использовать небольшой SQL-оператор для генерации календаря на месяц. Мы выбираем п строк из нашей таблицы src, где п — количество дней в месяце, а потом используем арифметические выражения над датами для распределения результатов в столбцы по дням недели. SQL> select 2 max(decode(dow,1,d,null)) Sun, 3 max(decode(dow,2,d,null)) Mon, 4 max(decode(dow,3,d,null)) Tue, 5 max(decode(dow,4,d,null)) Wed, 6 max(decode(dow,5,d,null)) Thu, 7 max(decode(dow,6,d,null)) Fri, 8 max(decode(dow,7,d,null)) Sat 9 from 10 ( select rownum d, 11 rownum-2+to_number( 12 to_char(trunc( 13 to_date('Smdate'),'MM'),'D')) p, 14 to_char(trunc(to_date('smdate'),'MM') 15 -1+rownum,'D') dow 16 from SRC 17 where rownum create table one_row_tab ( x primary key ) 2 organization index as select 1 from dual; Table created.
Теперь создадим процедуру, которая будет выполнять 50000 итераций выборки одной строки с помощью неявного курсора, и ее аналог для явного курсора. SQL> 2 3 4 5 6 7 8 9 10 11
create or replace procedure implicit is dummy number; begin for i in 1 .. 50000 loop select 1 into dummy from one_row_tab; end loop; end; /
Procedure created. SQL> 2 3 4 5 6 7 8 9 10 11 12 13 14 15
create or replace procedure explicit is cursor explicit_cur is select 1 from one_row_tab; dummy number; begin for i in 1 .. 50000 loop open explicit_cur; fetch explicit_cur into dummy; close explicit_cur; end loop; end; /
Procedure created.
' Надеемся, большинству читателей известно, что PL/SQL не закрывает курсоры для повышения производительности (за исключением курсорных переменных). Однако это верно как для явных, так и для неявных курсоров, так что эта "аномалия" не повлияет на результаты теста.
140
Глава 3
Наши PL/SQL-модули, как говорят, вышли на старт. Давайте посмотрим, кто выиграет "гонку". SQL> exec
implicit;
PL/SQL procedure successfully completed. Elapsed: 00:00:04.02 SQL> exec e x p l i c i t ; PL/SQL procedure successfully completed. Elapsed:
00:00:05.07
Эти тесты были повторены несколько раз для количества итераций от 50000 до 1000000, и во всех случаях код с неявным курсором работал приблизительно на 20 процентов быстрее. Причина этого в том, что PL/SQL — интерпретируемый язык, так что чем меньший объем кода используется для решения определенной задачи, тем быстрее этот код будет выполняться. Версия с явным курсором тоже вполне допустима и в реальных приложениях различие вряд ли будет настолько ощутимым. Но в любом случае неявные курсоры обеспечивают несколько большую производительность, позволяют создать более компактный код и, на наш взгляд, проще для чтения и понимания.
Явные курсоры сопротивляются В качестве интересного отступления от рассматриваемых тестов давайте рассмотрим следующее "опровержение" аргумента, что неявные курсоры работают быстрее явных, предложенное одним из участников популярного форума: раз с целью повышения производительности PL/SQL автоматически использует курсоры повторно, вынести операторы OPEN И CLOSE за пределы цикла. В соответствии с этой теорией лучшим решением будет следующий код: SQL> create or replace 2 procedure explicit2 is 3 cursor explicit_cur is 4 select 1 5 from one_row_tab; 6 dummy number; 7 begin 8 open explicit_cur; — поместить до цикла 9 for i in 1 .. 50000 loop 10 fetch explicit_cur 11 into dummy; 12 end loop; 13 close explicxt_cur; — поместить после цикла 14 end; 15 / Procedure created.
Бесконечная тема курсоров 141 SQL> exec explicit2 PL/SQL procedure successfully completed. Elapsed: 00:00:00.00
Замечательно! Явный курсор, похоже, снова участвует в гонке. Есть только одна маленькая проблема — код полностью ошибочен! Он не работает. Да, верно, PL/SQL может использовать курсоры повторно, но это — средство повышения производительности, встроенное в PL/SQL-машину, а не механизм, с помощью которого мы можем изменить переданный на выполнение PL/SQL-код. Ошибку в этом коде легко продемонстрировать. Процедура выполняет оператор SELECT К однострочной таблице, поэтому она должна успешно возвращать одну строку на каждой из 50000 итераций. Мы добавили представленный далее отладочный код для выдачи строки "Я сработала" при каждой успешной выборке. SQL> create or replace 2 procedure explicit2 is 3 cursor explicit_cur is 4 select 1 5 from one_row_tab; 6 dummy number; 7 begin 8 open explicit_cur; 9 for i in 1 .. 50000 loop 10 fetch explicit_cur 11 into dummy; 12 if explicit_cur%found then 13 dbms_output .put_line('Я 14 end if; 15 end loop; 16 close explicit_cur; 17 end; 18 /
сработала');
Procedure created.
Итак, строка "Я сработала" должна быть выдана 50000 раз. Но при выполнении процедуры мы видим следующее: SQL> set serverout on SQL> exec explicit2 Я сработала PL/SQL procedure successfully completed. Elapsed: 00:00:00.00
Фактически только самая первая из 50000 итераций выполнена. Всем остальным итерациям делать было нечего, что, конечно, и объясняет "повышение производительности".
142
Глава 3
Явные курсоры продолжают сопротивляться Учитывая комментарии, представленные в начале раздела, следовало ожидать, что сторонники явных курсоров не собираются сдаваться так просто. Вооружившись информацией о том, что неявный курсор запрашивает две строки в одной выборке, можно сфабриковать следующий тест, "доказывающий", что явные курсоры работают быстрее. SQL> create table EXPLICIT_IS_BEST 2 ( x number, у char(100)); Table created. SQL> 2 3 4
insert into EXPLICIT_IS_BEST select rownum, 'padding' from all_objects where rownum < 10001;
10000 rows created.
Мы создали таблицу из 10000 строк, у которой в первой строке в столбце х будет значение 1. Столбец Y просто увеличивает размер строки, чтобы таблица занимала на диске достаточно много места. Теперь давайте пересоздадим процедуры IMPLICIT и EXPLICIT для выборки строки из таблицы EXPLICIT_IS_BEST, в которой х = 1. Как и ранее, мы затем повторим выборку 50000 раз, чтобы получить существенное различие в производительности. SQL> 2 3 4 5 6 7 8 9 10 11 12
create or replace procedure implicit is dummy number; begin for i in 1 .. 50000 loop select 1 into dummy from explicit is best where x = 1; end loop; end; /
Procedure created. SQL> create or replace 2 procedure explicit is 3 cursor explicit_cur is 4 select 1 5 from explicit_is_best 6 where x = 1; 7 dummy number; 8 begin 9 for i in 1 .. 50000 loop 10 open explicit cur;
Бесконечная тема курсоров
143
11 fetch explicit_cur 12 into dummy; 13 close explicit_cur; 14 end loop; 15 end; 16 / Procedure created. Теперь выполним наши процедуры, чтобы определить, кто выиграл в этом специальном тесте. SQL> exec implicit PL/SQL procedure successfully completed. Elapsed: 00:06:41.00 SQL> exec e x p l i c i t PL/SQL procedure successfully completed. Elapsed: 00:00:05.04 Похоже, явный курсор вернул себе титул короля производительности. Причина более медленной работы неявного курсора в том, что для получения второй строки в результирующем множестве надо просмотреть всю таблицу (поиск закончится ничем). Код, использующий явный курсор, интересуется только первой строкой и больше ничего не ищет. Результаты теста построены, скорее, на везении, чем на существенно более высокой производительности. Строки в (обычной) таблице Oracle хранятся без соблюдения определенного порядка. Мы никогда не можем быть уверены, что строка, в которой х = 1, всегда будет "вверху" таблицы (т.е. в первых нескольких ее блоках). Фактически простые операторы DELETE И INSERT ДЛЯ ЭТОЙ строки принципиально повлияют на результат тестов. Давайте заменим строку, в которой х = 1. SQL> delete from explicit_is_best 2 where x= 1; 1 row deleted. SQL> insert into explicit_is_best 2 values (1,'padding'); 1 row created. SQL> commit; Commit complete. Кажется, что никакого изменения в таблице мы не сделали, но запрос к таблице показывает, что повторно вставленная строка вставлена не туда — она добавлена в
144
Глава 3
первый блок, который сервер Oracle счел доступным; в данном случае — в последний блок таблицы. SQL> select x 2 from explicit_is_best; X 2 5 6 7 9998 9999 10000 1 10000 rows selected.
^
И как это сказывается на наших тестах? Давайте повторно выполним процедуры и посмотрим. SQL> exec implicit PL/SQL procedure successfully completed. Elapsed: 00:06:41.00 SQL> exec e x p l i c i t PL/SQL procedure successfully completed. Elapsed: 00:06:42.72 Неожиданно процедура с явным курсором начала работать так же, как раньше — немного медленнее, чем эквивалентная процедура с неявным курсором. Только если нам повезет и мы будем искать единственную строку, находящуюся в первых нескольких блоках таблицы, явный курсор может дать преимущество. Перечитайте предыдущее предложение — нас интересует только одна строка. Фактически неявный курсор написан неправильно. Спросите у любого разработчика, как написать запрос, возвращающий только первую строку, соответствующую заданным условиям, и получите ответ: select . . . from . . . where . . . and rownum = 1 Давайте изменим процедуру с неявным курсором так, чтобы искать строку с х = 2 (которая по-прежнему находится в первом блоке таблицы) и добавим условие ROWNUM = 1 к SQL-оператору, чтобы давать ответ на тот же вопрос, что и в коде с явным курсором.
Бесконечная тема курсоров
145
SQL> create or replace 2 procedure implicit is з dummy number; 4 begin for i in 1 .. 1000 loop 5 6 select 1 7 into dummy 8 from explicit is best 9 where x = 2 and rownum - l; 10 end loop; 11 end; 12 Procedure created. SQL> exec implicit; PL/SQL procedure successfully completed. Elapsed: 00:00:04.05
Если снова сравнить этот результат с тестом исходной процедуры с явным курсором (которая выполнилась за 5,04 секунды), можно увидеть, что неявные курсоры все равно обеспечивают более высокую производительность.
Обработка нескольких строк Эти примеры демонстрируют, что неявные курсоры не хуже, а то и лучше явных в случае выборки единственной строки. Они не менее полезны и при выборке наборов строк с помощью цикла FOR ПО курсору. Непосредственно в цикле FOR ПО курсору запросы использовались редко, в основном потому, что разработчики корпорации Oracle никогда не утруждались объяснениями того, что это возможно. Мы предпочитаем создавать циклы прохода по курсору именно так, поскольку при этом не приходится возвращаться к декларативной части кода, чтобы проверить определение курсора — оператор SELECT записан непосредственно в операторе. Вот простой пример неявного курсора в цикле FOR: SQL> begin 2 for i in ( select ename from emp ) loop 3 dbms_output.put_line(i.ename); 4 end loop; 5 end; 6 / SMITH ALLEN JAMES FORD MILLER PL/SQL procedure successfully completed.
146
Глава 3
Даже если вы предпочитаете этот стиль кодирования, когда оператор SELECT задается именно там, где он и будет использоваться в курсоре, остается проблема использования атрибутов курсора, таких, как SQL%ROWCOUNT И SQL%FOUND. ЭТИ И другие атрибуты для неявного курсора в цикле FOR ПО курсору недоступны. Как же выполнить требования вроде обработки определенного количества строк или проверки существования строк в результирующем множестве вообще? Предполагает ли их выполнение возврат к явным курсорам? Нет. Давайте рассмотрим каждое из этих требований по очереди.
Подсчет количества строк Подсчет строк, выбранных с помощью курсора, проводится по двум причинам: > чтобы знать общее количество обработанных строк, когда работа с курсором будет закончена; > чтобы можно было нумеровать строки по мере выборки из курсора. При использовании явного курсора атрибут %ROWCOUNT после закрытия курсора становится недоступным. Например, если попытаться дополнить предыдущий пример так, чтобы выводить количество обработанных строк, как только цикл закончится, будет выдано сообщение об ошибке. SQL> declare 2 cursor с is select ename from emp ; 3 begin 4 for i in с loop 5 null; 6 end loop; 7 dbms_output.put_line(c%rowcount) ; 8 end; 9 / declare ERROR at line 1: ORA-01001: invalid cursor ORA-06512: at line 7
Чтобы вычислять общие суммы, надо выделить скалярную переменную и присваивать ей значение атрибута при обработке каждой строки. Это не дает никакого преимущества по сравнению с использованием неявного курсора и простым увеличением значения скалярной переменной на 1, как показано ниже: SQL> declare 2 cnt pls_integer :• 0; 3 begin 4 for i in (select ename from emp) loop 5 cnt := cnt + 1; 6 end loop; 7 dbms_output.put_line(cnt); 8
end;
Аналогично для получения номера каждой выбранной строки точно так же легко создать скалярную переменную и увеличивать ее на каждой итерации цикла. А
Бесконечная тема курсоров
147
можно использовать псевдостолбец ROWNUM И В самом запросе, содержащий номер каждой выбранной строки.
Проверка существования Типичный пример спецификации проверки существования звучит так: "Проверить, существует ли запись, удовлетворяющая . Если да, выполнить ". Часто явные курсоры используют для подобных проверок из-за слепого следования процедурной спецификации или, иными словами, из-за слишком быстрого отказа от использования SQL. Рассмотрим пример, когда нужно проверять существование "был ли кто-то принят на работу в прошлом месяце". Тогда плохая реализация на языке SQL может иметь вид: SQL> select count(*) 2 from emp 3 where hiredate > trunc(sysdate,'MM');
Плоха оно потому, что нам не надо считать сотрудников; неэффективно пересчитывать все записи в результирующем множестве просто для того, чтобы убедиться, что существует хоть одна. Вот почему многие разработчики быстро переходят к решению на языке PL/SQL, используя явный курсор и атрибут %FOUND ДЛЯ выполнения одной выборки. SQL> c r e a t e or replace 2 function IS_EMP_THERE return varchar2 i s 3 cursor С i s 4 select 1 from emp 5 where hiredate > trunc(sysdate,'MM); 6 r number; 7 v varchar2(3); 8 begin 9 open C; fetch С into r; 10 if C%FOUND then 11 12 v := 'YES'; 13 else 14 v := 'NO'; 15 end if; 16 close C; 17 return v; 18 end; 19 / Function created.
Хотя, вне всякого сомнения, верно, что одна выборка эффективнее подсчета всех строк в результирующем множестве, это решение нарушает один из основных принципов обеспечения эффективности, представленных в главе 1, "Эффективность PL/SQL", — не использовать PL/SQL, если задачу можно решить с помощью языка
148
Глава 3
SQL. Если спецификация требует проверить существование, для этого есть подходящая конструкция в языке SQL. select count(*) from dual where exists ( s e l e c t n u l l from emp where h i r e d a t e > trunc(sysdate,'MM 1 ))
В результате будет либо значение 1 (соответствующая строка существует), либо О (записей нет), и PL/SQL вообще не нужно использовать.
Обработка исключительных ситуаций Атрибуты курсора также часто применяются в разделе обработки исключительных ситуаций PL/SQL-модуля, но в подавляющем большинстве случаев использовать их не нужно. Рассмотрим пример процедуры, закрывающей курсоры в случае ошибки. Она проходит в цикле по записям в таблице ЕМР И вычисляет сумму премиальных в организации. Поскольку вычисление включает деление на сумму зарплаты, может возникнуть ошибка — деление на ноль. SQL> create or replace 2 procedure PROCESS_EMP is cursor С is 3 4 select empno, sal, comm from emp; 5 v bonus number := 10000; 6 7 begin Я for i in с loop 9 v bonus :- v bonus + i.comm / i.salj 10 end loop; 11 exception 12 when zero divide then if C%ISOPEN then 13 14 close C; 15 end if; 16 end; 17 / Procedure created.
Можно утверждать, что необходимо использовать явный курсор, чтобы закрыть его в случае ошибки. Это неверно. Любой локально определенный в разделе объявлений процедуры курсор будет автоматически закрываться при завершении процедуры (успешном или вследствие ошибки). Единственный случай, когда такая проверка может понадобиться, — когда курсор удерживается открытым в течение всего сеанса, например, если он определен в спецификации пакета.
Обработка первых N строк Можно расширить предыдущий пример с проверкой существования до более общего случая, в частности, ошибочного использования явных курсоров для выборки первого значения (или первых п значений) из набора строк. Аргумент сторонников явного курсора: если нужно выбрать только первую строку из упорядоченного ре-
Бесконечная тема курсоров 149 зультирующего множества, то объявление явного курсора для результирующего множества и затем выборка первой строки будет эффективным способом решения поставленной задачи. Поэтому процедура на базе явного курсора, возвращающая значение столбца х, соответствующего наибольшему значению в столбце Y, будет иметь представленный ниже вид. Я выполнил в цикле 500 итераций, чтобы можно было оценить производительность решений. SQL> create or replace 2 procedure explicit is 3 cursor explicit_cur is 4 select x 5 from explicit_is_best 6 order by у desc; 7 dummy number; 8 begin 9 for i in 1 .. 500 loop 10 open explicit_cur; 11 fetch explicit_cur 12 into dummy; 13 close explicit_cur; 14 end loop; 15 end; 16 / Procedure created.
Аналогичный код с неявным курсором требует несколько более сложного SQLоператора, использующего вложенное представление (inline view). SQL> create or replace 2 procedure implicit is 3 dummy number; 4 begin 5 for i in 1 .. 500 loop 6 select x into dummy 7 from ( select x 8 from explicit_is_best 9 order by у desc ) 10 where rownum = 1; 11 end loop; 12 end; 13 / Procedure created.
Часто предполагают, что для такого решения требуется больше ресурсов, поэтому давайте проверим это предположение. Мы снова будем тестировать скорость работы каждого решения. SQL> exec implicit; PL/SQL procedure successfully completed.
150
Глава 3
Elapsed: 00:00:09.09 SQL> exec explicit; PL/SQL procedure successfully completed. Elapsed: 00:00:23.06
В этот раз решение на базе неявного курсора победило с большим отрывом. Внимательный читатель может заметить, что представленный пример немного "подыгрывает" неявному курсору. В процедуре с неявным курсором содержится конструкция ROWNUM, применяемая к вложенному представлению с конструкцией ORDER BY, а когда сервер Oracle обрабатывает SQL-операторы такого вида, то может использовать оптимизацию, не применимую к SQL-оператору, использованному в процедуре с явным курсором. По сути, это верно, но если по умолчанию применяются явные курсоры, остается мало шансов додуматься до использования более эффективного SQL-оператора вместо традиционной обработки курсора. Конечно, если изменить процедуру с явным курсором так, чтобы использовался тот же SQL-оператор, что и в процедуре с неявным курсором, мы получим две процедуры, отличающиеся только типом используемого курсора. А в этом случае, как было доказано в ходе предыдущего обсуждения, неявный курсор всегда обеспечивает производительность не хуже, а то и лучше, чем явный.
Выводы В этом разделе мы не пытались доказать, что явные курсоры вообще не стоит использовать в коде. На самом деле, бывают случаи, когда они абсолютно необходимы, и вы увидите их по ходу изложения — например, для выполнения множественной выборки в главе 4, "Эффективная обработка данных". Неявные курсоры не обладают каким-то магическим свойством, делающим их лучше явных. Мы, однако, доказали, что использование неявных курсоров приводит к более компактному коду и в подавляющем большинстве случаев обеспечивает производительность не хуже, а то и лучше, чем при реализации на базе явных курсоров. Кроме того, неявные курсоры проще для понимания и уменьшают вероятность неправильного использования, характерного для работы с явными курсорами (как было продемонстрировано на примерах в этом разделе).
Управление курсором в различных средах Неудивительно, что любое приложение, работающее с базой данных, реализует свои основные функции путем выборки данных, изменения существующих данных и создания новых. В давние времена пользовательский интерфейс, код приложения и база данных сосуществовали в общей инфраструктуре (т.е. обычно на универсальной ЭВМ с единой архитектурой, например, CICS-COBOL). Однако современные приложения разворачиваются на широком спектре архитектур, работающих на множестве различ-
Бесконечная тема курсоров 151 ных серверов. Это влияет на способ обработки данных, выбранных из базы данных. При централизованной модели все компоненты приложения имеют непосредственный доступ к базе данных, а сейчас приходится передавать данные между разными серверами с различными программными архитектурами и, возможно, специфическими интерфейсами для выборки данных и возврата наборов данных.
Курсорные переменные Вот тут и вступают в игру курсорные переменные PL/SQL (которые также называют REF-курсорами). Они обеспечивают согласованный способ передачи результирующих множеств запроса между компонентами приложения. Компоненты приложения не обязательно должны быть реализованы на языке PL/SQL; курсорные переменные можно передавать между разными программными средами, такими, как Java-программы, клиентские программы Oracle, например, созданные с помощью Forms Developer Suit, а также программными интерфейсами Рго*С или OCI. Тогда как обычный курсор в PL/SQL фиксирован, т.е. связан со статически определенным SQL-оператором, курсорная переменная не связана ни с каким конкретным запросом; она просто указывает на результирующее множество курсора. Результирующее множество никогда не передается, а вот курсорная переменная, которая на него указывает, может передаваться между PL/SQL-модулями или, в общем случае, между PL/SQL и клиентским приложением. Конечно, наиболее эффективный способ передать большое результирующее множество между компонентами приложения — это не передавать его вообще; передается только указатель на местонахождение данных. Рассмотрим пример. Сначала создадим функцию, возвращающую курсорную переменную. SQL> 2 3 4 5 6 7 8
create or replace function emp_list return sys_refcursor is re sys_refcursor; begin open re for select * from emp; return re; end; /
Function created.
Примечание Тип SYS_REFCURSOR появился в версии 9. Если вы работаете с версией 8, надо будет добавить строку type s y s _ r e f cursor i s r e f cursor перед определением переменной RC.
Теперь можно создать процедуру LISTEMPS, которая будет использовать эту курсорную переменную для выдачи данных из таблицы ЕМР. Обратите внимание, что в следующей процедуре мы не определяем никакого курсора — она просто содержит собственную курсорную переменную, которая будет указывать на курсор, который мы только что создали в функции EMP_LIST.
152
Глава 3
SQL> 2 3 4 5 6 7 8 9 10 11 12 13 14
create or replace procedure list emps is e sys refcursor; r emp%rowtype; begin e := emp_list; loop fetch e into r; exit when e%notfound; dbms output.put line(r.empnol1','I 1r.hiredate); end loop; close e; end; /
Procedure created.
Теперь можно выполнить нашу процедуру LIST_EMPS ДЛЯ получения информации о сотрудниках. SQL> exec list_emps; 7369,1V/DEC/8O 7499,20/FEB/81 7521,22/FEB/81 7566,02/APR/81 7654,28/SEP/81 7698,01/MAY/81 7782,09/JUN/81 7788,19/APR/87 7839,17/NOV/81 7844,08/SEP/81 7876,23/MAY/87 7900,03/DEC/81 7902,03/DEC/81 7934,23/JAN/82 PL/SQL procedure successfully completed.
Сейчас вам может казаться, что в этом нет ничего особенного. То же самое запросто можно было бы сделать с помощью обычного статического курсора. Но давайте рассмотрим второй вариант использования той же курсорной переменной, на этот раз — из утилиты SQL*Plus (которая также поддерживает локальные курсорные переменные). SQL> variable x refcursor SQL> declare 2 r emp%rowtype; 3 begin 4 :x := emp list; 5 loop 6 fetch :x into r; 7 exit when :x%notfound;
Бесконечная тема курсоров 153 8 dbms_output.put_line(r.empnolI','IIr.hiredate); 9 end d loop; l 10 close :x; 11 end; 12 / 7369,17/DEC/80 7499,20/FEB/81 7521,22/FEB/81 7566,02/APR/81 7654,28/SEP/81 7698,01/MAY/81 7782,09/JUN/81 7788,19/APR/87 7839,17/NOV/81 7844,08/SEP/81 7876,23/MAY/87 7900,03/DEC/81 7902,03/DEC/81 7934,23/JAN/82 PL/SQL procedure successfully completed.
Вот в чем сила курсорных переменных. В этом примере клиентская программа (SQL*Plus) и PL/SQL-модуль на сервере смогли совместно обращаться к результирующему множеству. Еще более простой пример показывает, что клиентская программа (такая, как SQL*Plus) может заниматься выборкой данных из курсорной переменной после того, как она была открыта функцией EMPLIST. Команда PRINT, когда ей передана курсорная переменная, выбирает все строки из курсора и выдает их на экран. SQL> exec :x := emp_list; PL/SQL procedure successfully completed. SQL> print x EMPNO 7369 7499 7521 7566 7654 7698 7782 7788 7839 7844 7876 7900 7902 7934
ENAME
JOB
SMITH ALLEN WARD JONES MARTIN BLAKE CLARK SCOTT KING TURNER ADAMS JAMES FORD MILLER
CLERK SALESMAN SALESMAN MANAGER SALESMAN MANAGER MANAGER ANALYST PRESIDENT SALESMAN CLERK CLERK ANALYST CLERK
MGR
HIREDATE
SAL
7902 7698 7698 7839 7698 7839 7839 7566
17/DEC/80 20/FEB/81 22/FEB/81 02/APR/81 28/SEP/81 01/MAY/81 09/JUN/81 19/APR/87 17/NOV/81 08/SEP/81 23/MAY/87 03/DEC/81 03/DEC/81 23/JAN/82
800 1600 1250 2975 1250 99999 2450 3000 5000 1500 1100 950 3000 1300
7698 7788 7698 7566 7782
154
Глава 3
Курсорная переменная была определена на "клиенте", открыта на сервере, а результаты были выбраны клиентом. Нам не пришлось строить все результирующее множество на сервере и передавать его целиком на "клиент" (в утилиту SQL*Plus) — у клиента был непосредственный доступ к курсору Любое клиентское приложение (написанное на Java, Pro*C и т.д.), поддерживающее курсорные переменные, может получить доступ к результирующему множеству курсора без необходимости выборки всего результирующего множества, сохранения и передачи приложению.
Курсорные выражения Любую PL/SQL-переменную, определенную в модулях, можно включить в SQLзапросы, выполняемые в этих PL/SQL-модулях. Например, вот как легко сослаться на переменную V_BONUS при вычислении максимальной зарплаты в следующем анонимном блоке: SQL> declare 2 v_bonus number := 10000; 3 v_max_sal number; 4 begin 5 select max(sal)+v_bonus 6 into v_max_sal 7 from emp; 8 end; 9 / PL/SQL procedure successfully completed.
Мы уже видели в предыдущем разделе, что можно использовать переменные, фактически являющиеся указателями на результирующее множество, как обычные переменные. Представленный выше анонимный блок показывает, что можно включать переменную в результирующее множество, возвращаемое запросом. Нельзя ли то же самое сделать с курсорной переменной? Да, можно. Эта возможность — курсорное выражение (cursor expression) — появилась в версии 9. Вот простой пример курсорного выражения в запросе: s e l e c t deptno, dname, cursor(select empno, ename from emp where deptno = d.deptno) from dept d;
Третий столбец в результирующем множестве запроса — полноценный курсор. Можно выбирать строки из основного результирующего множества (таблицы DEPT), и для каждой выбранной строки автоматически будет открываться вложенный курсор по таблице ЕМР, ИЗ которого тоже можно выбирать данные. Выполнение этого примера в среде SQL*Plus показывает, как это происходит. В среде SQL*Plus по ходу выборки каждой строки из таблицы DEPT сама утилита SQL*Plus будет автоматически выбирать и выдавать строки из вложенного курсорного выражения.
Бесконечная тема курсоров
155
SQL> select deptno, dname, 2 cursor(select empno, ename 3 from emp 4 where deptno = d.deptno) 5
from dept d; DEPTNO 10
DNAME
CURSOR(SELECTEMPNO,E
ACCOUNTING
CURSOR STATEMENT : 3
CURSOR STATEMENT : 3 EMPNO 7782 7839 7934
ENAME CLARK KING MILLER
20 RESEARCH
CURSOR STATEMENT : 3
CURSOR STATEMENT : 3 EMPNO
ENAME
7369 7566 7788 7876 7902
SMITH JONES SCOTT ADAMS FORD
30 SALES
CURSOR STATEMENT : 3
Вы опять можете подумать, что тут нет ничего особенного, и можно реализовать это простым соединением таблиц ЕМР И DEPT. НО у представленного решения есть тонкое отличие. Рассмотрим клиентское приложение, которому надо реализовать более сложные требования: > выбрать список отделов из таблицы DEPT; > для некоторых отделов (в зависимости от определенного внешнего по отношению к приложению условия): > получить список сотрудников отдела; > получить список клиентов отдела. Это нельзя сделать с помощью соединения, поскольку имеются два отношения "один ко многим" — между отделом и сотрудниками, а также между отделом и клиентами. Без использования курсорных выражений в приложении пришлось бы применять следующее решение (представленное в виде псевдокода): open dept_cursor for each row in dept_cursor
156 Глава 3 open emp_cursor for each row in emp_cursor print details close emp_cursor open cust_cursor for each row in cust_cursor print details close cust_cursor
Для любого приложения, выполняющего этот код на сервере приложений, может потребоваться большое количество пересылок данных по сети на сервер баз данных и обратно, с непрерывным открытием и закрытием курсоров. Использование курсорных выражений позволяет всю обработку курсоров выполнить за один вызов. Это можно продемонстрировать, используя утилиту SQL*Plus в качестве клиентского приложения, которое хочет получить результаты за один вызов. Сначала мы создаем функцию DEPT_EMP_CUST, возвращающую курсорную переменную, содержащую основной курсор по таблице DEPT И вложенные курсорные выражения по таблицам ЕМР И CUSTOMER. SQL> 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
create or replace function dept_emp_cust return sys_refcursor is re sys_refcursor; begin open re for select deptno, dname, cursor(select empno, ename from emp where deptno = d.deptno) emps, cursor(select custid, custname from customers where purchasing_dept = d.deptno) custs from dept d; return re; end; /
Function created.
Затем, используя уже продемонстрированные возможности утилиты SQL*Plus, можно открыть локально определенную курсорную переменную х и выполнить команду print х для получения всех строк из таблицы DEPT, а также выборки строк из вложенных курсорных выражений. SQL> variable x refcursor SQL> exec :x := dept_emp cust; PL/SQL procedure successfully completed. SQL> print x
Бесконечная тема курсоров 157 DEPTNO 10
DNAME
EMPS
CUSTS
ACCOUNTING
CURSOR STATEMENT : 3 CURSOR STATEMENT : 4
CURSOR STATEMENT : 3 EMPNO ENAME 7782 CLARK 7839 KING 7934 MILLER CURSOR STATEMENT : 4 CUSTID CUSTNAME 7 Cust7 9 Cust9 14 Custl4
Единственный вызов функции создал курсорную переменную, в свою очередь, содержащую два вложенных курсора для выдачи детальной информации об отделах, сотрудниках и клиентах. Мы не утверждаем, что надо регулярно включать курсорные выражения в SQLоператоры. Помните: выборка каждой строки из основной таблицы приводит к выполнению второго запроса, указанного в курсорном выражении. Например, с помощью стандартного средства AUTOTRACE В SQL*P1US2 МОЖНО убедиться, насколько отличается выполняемый объем действий для двух аналогичных запросов, один из которых содержит ссылку на курсорное выражение, а другой — нет. Сначала рассмотрим запрос без курсорного выражения. SQL> SQL> SQL> 2 3
set arraysize 50 set autotrace on statistics select empno, deptno — no cursor expression here from emp /
Statistics 0 recursive calls 0 db block gets 1249 consistent gets 244 physical reads 0 redo s i z e 603709 bytes sent v i a SQL*Net t o c l i e n t 2
Во введении к этой книге описаны основные детали настройки AUTOTRACE. Полное описание см. в главе 9 руководства SQL*Plus Users Guide and Reference.
158
Глава 3 11488 1001 0 0 50000
bytes received via SQL*Net from client SQL*Net roundtrips to/from client sorts (memory) sorts (disk) rows processed
Теперь создадим пустую таблицу, из которой будем выбирать данные в курсорном выражении. Таблица пуста, так что единственным дополнительным действием, затраты ресурсов на выполнение которого мы будем оценивать, будет обработка вложенного курсора, а не строк, которые могли бы быть им возвращены. SQL> create table IX ( х number primary key ) 2 organization index; Table created. SQL> analyze table IX estimate statistics; Table analyzed. SQL> select empno, cursor(select x from ix) 2 from emp 3 / Statistics 250070
0 75112
0 0 13102130 7577901 75000
0 0 50000
recursive calls db block gets consistent gets physical reads redo size bytes sent via SQL*Net to client bytes received via SQL*Net from client SQL*Net roundtrips to/from client sorts (memory) sorts (disk) rows processed
Обратите внимание на существенное увеличение количества рекурсивных вызовов и объема выполненного логического ввода-вывода — значение consistent gets возросло с 1249 до 75112! Очевидно, какие-то дополнительные действия можно было ожидать, но с учетом того, что курсорное выражение не выберет ни одной строки из пустой таблицы ix, объем этих дополнительных действий можно назвать огромным. С этим, возможно, связан и тот факт, что использование курсорных выражений не позволяет выбирать более одной строки за одну операцию выборки. Утилита SQL*Plus поддерживает выборку массивом (array fetching), поэтому давайте посмотрим, что произойдет при выполнении запроса для выборки 200 строк с помощью массива размером 50 строк при наличии курсорных выражений и без них. Следующие результаты были получены из трассировочного файла, сформатированного с помощью утилиты TKPROF, для запроса, не использующего курсорные выражения.
Бесконечная тема курсоров
159
select * from emp where rownum < 200 cpu
elapsed
disk
query
current
rows
1 1 5
0.00 0.00 0.00
0.03 0.00 0.26
0 0 2
0 0 11
0 0 24
0 0 199
7
0.00
0.30
2
11
24
199
call
count
Parse Execute Fetch total
Здесь мы видим выборку массивов в действии — для получения 200 строк потребовалось всего пять выборок. Однако если включить в запрос курсорное выражение, выборка массивов, похоже, не будет использоваться. select empno, cursor(select x from ix) from emp where rownum < 200 call
count
cpu
elapsed
disk
query
current
rows
Parse Execute Fetch
1 1 101
0.01 0.00 0.11
0.00 0.00 0.07
0 0 0
0 0 104
0 0 0
0 0 199
total
103
0.12
0.08
0
104
0
199
Детально выборку массивом мы рассмотрим в следующей главе. Пока что важно просто понимать, что невозможность использования выборки массивом может ограничивать производительность приложений. Поэтому курсорные выражения надо использовать осторожно, поскольку похоже, что в лучшем случае мы сможем выбирать по две строки за раз.
Резюме Как было сказано в начале главы, обработка курсоров не должна вызывать столько дискуссий и непонимания при программировании на PL/SQL (да и на любом языке, обрабатывающем данные из базы данных Oracle). Как только вы освоите работу с курсорами в PL/SQL-модулях, тщательно изучите возможности курсорных переменных и курсорных выражений при создании приложений, работающих вне сервера Oracle. Передача указателя на результирующее множество между уровнями приложения вместо передачи данных, образующих результирующее множество, позволяет повысить эффективность, особенно если объем данных велик. Обработка курсоров, несомненно, является частью более широкой темы управления данными в языке PL/SQL. В следующей главе мы глубже изучим эту тему и рассмотрим, как можно улучшить работу с курсорами за счет использования обработки массивов.
Глава 4
Эффективная обработка данных Как и многие специалисты по информационным технологиям моего поколения, я начал программировать на языке COBOL на больших ЭВМ. Меня всегда удивляло в этом языке достаточно свободное обращение с типами данных (особенно после того, как мне долгие годы учебы в университете долбили важность строгой типизации). При обработке файла на COBOL прочитанную из файла последовательность байтов можно рассматривать как строку символов, как дату, число или просто как двоичные данные — если у меня получалось успешно поместить байты в определенную переменную, компилятору COBOL этого вполне хватало. Фактически достаточно часто можно было увидеть определения наподобие этих: YEAR-END-STRING PIC X(8) YEAR-END-NUM REDEFINES YEAR-END-STRING
PIC 99999999
Этот код означает, что значение, хранящееся в переменной YEAR-END, может интерпретироваться либо как строка, либо как число — в зависимости от требований приложения. Такая гибкость не дается даром. Повреждения данных выявляются только при неправильном использовании переменной и вовсе не обязательно при первоначальном присвоении ей значения. В представленном примере мы так же легко могли присвоить строковой переменной (YEAR-END-STRING) значение "HELLO", чтобы вызвать сбой при обращении к ней как к числовому полю (YEAR-END-NUM). Ничто явно не связывает COBOL-программу с данными, на которые она ссылается, надо просто писать код и надеяться на лучшее (по крайней мере, именно так я и делал!). Чем теснее взаимосвязь между структурами данных в базе данных и структурами данных PL/SQL-программы, тем более надежным и устойчивым при изменениях будет приложение. В этой главе я покажу важность глубокого понимания того, как лучше всего установить эту взаимосвязь.
Управление типами данных Язык PL/SQL предлагает всеобъемлющий и строгий контроль над структурами данных. Я написал "предлагает" всеобъемлющий контроль, потому что удивительно, насколько много приложений не используют его. В любом хорошо продуманном приложении Oracle значительные усилия потрачены на то, чтобы определить соответствующие типы данных и бизнес-правила в базе данных. Для каждого столбца в базе данных мы обычно определяем: > его тип данных; б Зак. 348
162
Глава 4
> его максимально допустимую длину (и точность, если это надо); > ограничения на допустимые значения. Это может быть реализовано непосредственно через ограничение проверки или косвенно, с помощью ограничения целостности ссылок или триггера. В базе данных мы не просто указываем, что зарплата должна быть числом, — мы также задаем правила, четко определяющие понятие зарплаты для нашего приложения. Например, мы можем задать, что зарплата — это число положительное, не превышающее х, с точностью до сотых, и т.д. Базы данных, не поддерживающие такие детальные определения, обычно критикуются конкурентами как архитекгурно ущербные. Однако существование подобной ошибочной практики при программировании на PL/SQL не вызывает подобной критики. Например, достаточно часто можно увидеть код наподобие следующего: declare v_salary number; v_surname varchar2(2000);
Хотя этот код и задает для переменной подходящий тип данных, никакого контроля, который обычно требуется при задании столбцов таблиц, он не обеспечивает. Неужели мы действительно предполагаем, что у кого-то зарплата будет измеряться миллиардами долларов или фамилия будет длиной в страницу текста? Конечно, нет. Очевидно, что переменные должны определяться с соответствующей точностью и масштабом. Фактически в PL/SQL можно достичь даже большего, и мы сейчас покажем, как.
Использование атрибута %TYPE Подавляющее большинство переменных PL/SQL непосредственно связаны со столбцами в базе данных и, как известно большинству разработчиков, можно использовать атрибут %TYPE для явного задания этой взаимосвязи в коде. Жаль, что в руководствах важность атрибута %TYPE недооценивается. "...Если определение столбца в базе данных изменится, тип данных (переменной) изменяется во время выполнения соответствующим образом " — PL/SQL User's Guide.
Все верно, но в руководствах не подчеркивается, что это помогает защитить будущее PL/SQL-кода. В идеале со дня внедрения в производство модель данных и ее реализация удовлетворяла бы текущим и будущим требованиям как бизнеса, так и пользователей системы. Однако печальный факт состоит в том, что первый запрос на расширение возможностей поступает от сообщества пользователей в среднем через 42 секунды после начала эксплуатации системы. Адаптировать проект реляционной базы даннных к новым требованиям бизнеса или пользователей не слишком сложно. Это одна из привлекательных черт реляционной модели. С помощью нескольких нажатий на клавиши мы можем вставить, удалить или изменить метаданные, определяющие особенности работы системы, а также выполнить более "травматичные" операции вроде добавления атрибутов (на-
Эффективная обработка данных 163 пример, новых столбцов в существующие таблицы) или абсолютно новых отношений (новых таблиц и правил целостности) в модель данных. Так почему же тогда мы видим проекты обновления приложений, затягивающиеся на месяцы и годы (и стоимостью в миллионы)? Обычно причина кроется во всех изменениях кода, появляющихся в результате "простого" изменения модели данных. Использование атрибута %TYPE защищает от подобных изменений. Давайте рассмотрим классический пример: увеличение со временем допустимой длины столбца. В качестве предварительного шага пересоздадим стандартные демонстрационные таблицы в нашей схеме. SQL> @$ORACLE_HOME/sqlplus/demo/demobld.sql Building demonstration tables. Please wait. Demonstration table build is complete.
Затем создадим две процедуры, выполняющие одно и то же действие: они просто выбирают максимальную зарплату из таблицы ЕМР. Вторая процедура использует атрибут %TYPE для переменной, содержащей зарплату, первая — нет. SQL> create or replace 2 procedure WITHOUTJTYPE is 3 v_salary number(7,2); 4 begin 5 select max(sal) 6 into v_salary 7 from emp; 8 end; 9 / Procedure created. SQL> 2 3 4 5 6 7 8 9
create or replace procedure WITHJTYPE is v_salary emp.sal%type; begin select max(sal) into v_salary from emp; end; /
Procedure created.
Сотрудник SMITH со значением EMPNO = 7369, грубо говоря, нашел золотую жилу и должен получить прибавку к зарплате в объеме миллион долларов. Для этого, прежде чем изменять информацию о сотруднике SMITH, нам нужно увеличить размер столбца SAL таблицы ЕМР. SQL> alter table EMP modify sal number(10,2); Table altered.
164
Глава 4
SQL> update EMP set sal = 1000000 2 where EMPNO = 7369; 1 row updated.
Давайте попытаемся получить значение наибольшей зарплаты в организации с помощью процедуры WITHOUTJTYPE. SQL> exec WITHOUTJTYPE; BEGIN WITHOUT TYPE; END;
ERROR at line 1: ORA-06502: PL/SQL: numeric or value error: number precision too large ORA-06512: at "WITHOUTJTYPE", line 4 ORA-06512: at line 1
Как и ожидалось, увеличенный размер новой зарплаты в таблице ЕМР привел к ошибке в процедуре. Ее придется отредактировать так, чтобы переменная V_SALARY могла принять большие значения. Однако, если позаботиться и использовать атрибут %TYPE, такое изменение кода не понадобится. Можно убедиться, что процедура WITH_TYPE таким проблемам не подвержена. SQL> exec WITH_TYPE PL/SQL procedure successfully completed.
Также использование атрибута %TYPE В процедуре немедленно устанавливает зависимость между кодом и базовой таблицей, что помогает проанализировать влияние изменений. При использовании с пакетами, как описано в главе 2, "Объедините все в пакет", можно отслеживать зависимости и не расходовать ресурсы на постоянную перекомпиляцию. Хотя использование атрибута %TYPE ДЛЯ защиты кода от изменений на уровне столбцов является хорошим приемом, но что делать, если одна или несколько переменных никак не связаны со столбцом таблицы базы данных? В этих случаях атрибут %TYPE все равно можно использовать. Рассмотрим фрагмент кода, в котором есть четыре переменные, определенные как строка из 30-ти символов: procedure MY_PROC(p_input varchar2) vl varchar2(30); v2 varchar2(30); v3 varchar2(30) ; v4 varchar2(30) ; begin vl := p_input;
is
end;
Этот код может долго нормально работать в производстве до одного рокового дня, когда некто попытается передать процедуре переменную пакета MY_PKG . GLOB_VAR, где пакет определен следующим образом:
Эффективная обработка данных 165 package MY_PKG is glob_var varchar2(40); end; SQL> exec MY_PROC(my_pkg.glob_var); BEGIN MY_PROC(my_pkg.glob_var); END;
ERROR at line 1: ORA-06502: PL/SQL: numeric or value error: character string buffer too small ORA-06512: at "MY_PROC", line 33 ORA-06512: at line 1
Быстрый просмотр кода позволяет понять, что проблема связана с переменной vi. Дальнейший анализ (просмотр кода пакета MY_PKG) приводит к выводу, что для устранения этой конкретной ошибки длину переменной vi необходимо увеличить до 40 символов. После этого можно исправить код и перекомпилировать процедуру за пару секунд. Однако вопросы остаются. >• Почему возможность ошибки не была учтена до ее возникновения? Т.е. если переменная MY_PKG . GLOBJVAR регулярно передается процедуре MY_PROC, значит, либо ошибка была возможна всегда, либо размер переменной GLOB_VAR был изменен без соответствующего изменения процедуры MY_PROC. > Надо ли увеличивать также длину переменных V2, V3 и V4? > Как избавиться от этой ошибки в дальнейшем? Можно использовать атрибут %TYPE ДЛЯ установки связи между переменными в PL/SQL-приложении. Процедуру MYPROC МОЖНО переписать так, чтобы она непосредственно ссылалась на определение переменной MY_PKG . GLOBJVAR. create or replace procedure MY_PROC(p_input varchar2) vl my_pkg.glab_var%type; v2 my_pkg.glab_var%type; v3 my_pkg. glob_var%type; v4 my_pkg. glob_var%type; begin
is
end;
В результате тип данных локальных переменных связывается с типом данных переменной пакета, MY_PKG . GLOB_VAR. Теперь процедура MY_PROC зависит от пакета MYPKG, что предотвращает возникновение рассмотренной проблемы в дальнейшем. Если мы изменим пакет MY_PKG, увеличив, например, длину переменной GLOB_VAR, до 60-ти символов, то SQL> create or replace 2 package MY_PKG is 3 glob_var varchar2(60) 4 end; 5 / Package created.
:= zpad('x',60);
166
Глава 4
переменные процедуры MY_PROC будут автоматически изменены соответствующим образом, и при выполнении сообщение об ошибке получено не будет. SQL> exec MY_PROC(my_pkg.glob_var); PL/SQL procedure successfully completed.
Централизация управления типами данных с помощью пакетов В рассмотренном выше примере нет информации (кроме самого исходного кода) о том, что процедура MY_PROC зависит от определения типа MY_PKG . GLOB_VAR. ХОТЯ можно обратиться к представлению USER_DEPENDENCIES, НО ОНО ТОЛЬКО покажет, что процедура MYPROC зависит от пакета MY_PKG, НО не покажет точно, почему. Чтобы обойти это ограничение, можно эффективно централизовать управление типами данных в PL/SQL-программах с помощью доступного всем разработчикам пакета, в котором определены все типы и подтипы. Предыдущий пример процедуры MY PROC можно переделать, определив пакет, целью которого будет объявление всех типов в приложении. Назовем этот пакет APPLICATIONJTYPES. SQL> create or replace 2 package APPLICATIONJTYPES is 3 subtype short_varchar2 is varchar2(40); 4 end; 5 / Package created.
Процедура MY_PROC И пакет MY_PKG больше не определяют собственные типы — они используют типы, определенные в пакете APPLICATIONJTYPES. SQL> 2 3 4 5 6 7 9 9 10
create or replace procedure MY_PROC(p_input application_types.short_varchar2) vl application_types.short_varchar2; v2 applicatlon_types.short_varchar2; v3 appl±cat±on_types.short_varchar2; v4 application_types.short_varchar2; begin null; end; /
is
Procedure created. SQL> create or replace 2 package MY_PKG is 3 glob_var application_types.short_varchar2 := rpad('x',40); 4 end; 5 / Package created.
Эффективная обработка данных
167
Если изменить определение типа SHORT_VARCHAR2, изменение определения в пакете APPLICATION_TYPES распространится на все остальные модули. Однако будьте осторожны при реализации такого уровня управления, поскольку каждый PL/SQLмодуль, вероятно, будет зависеть от этого пакета. Если ваш цикл разработки приложений предполагает добавление и изменение определений типов в произвольные моменты времени, вы снова столкнетесь с проблемами зависимостей, описанными в главе 2 на примере глобальных переменных. В дисциплинированной и хорошо формализованной среде разработки этот вариант управления типами, тем не менее, вполне может оказаться подходящим.
Избегайте неявного преобразования типов Гарантированная строгая типизация всех переменных также в некоторой степени повышает производительность. PL/SQL-машина очень благосклонна к разработчикам при работе с типами данных (что, как я считаю, плохо). Попытайтесь присвоить строку числовой переменной, и сервер Oracle попытается преобразовать ее в число и выполнить присвоение. Присвойте дату переменной типа VARCHAR2, И она будет автоматически преобразована в строку. Помимо того, что это — не лучшая практика программирования, преобразования типов данных заметно снижают производительность. Рассмотрим пример типичного приема, используемого ленивыми разработчиками: преобразования строк в даты. Используя регистрацию текущего времени до и после выполнения, с помощью функции DBMS_UTILITY.GET_TIME мы можем создать процедуру, которая будет определять, сколько времени требуется на выполнение 1000000 преобразований типов данных. SQL> 2 3 4 5 6 7 8 9 10 11 12
create or replace procedure data_type_test is x date; у varchar2(12) : = '01-MAR-03'; t number := dbms_utility.get_time; begin for i in 1 .. 1000000 loop x := y; — implicit char to date end loop; dbms_output.put_line((dbms_utility.get_time-t)||'cs'); end; /
Procedure created.
Мы включим вывод результатов сервера и выполним тест. SQL> set serverout on SQL> exec data_type_test 771cs (Фактически это — среднее значение пяти выполнений)
Впечатляющий результат! Он означает, что на простом ноутбуке, на котором выполнялся тест, можно выполнить порядка 140000 преобразований типов в секунду. Очевидно, часть из 7,7 секунд общего времени выполнения ушла на PL/SQL-код, а
168
Глава 4
не на преобразование типов. Сколько же времени было потрачено на преобразование типов? Следующий тест позволит ответить на этот вопрос. Мы пересоздадим процедуру, чтобы выполнялись те же действия, но без преобразования типов, поскольку они совпадают. SQL> create or replace 2 procedure data_type_test is 3 x date; 4 у x%type :- to_date('01-MAR-03'); 5 t number := dbms_utility.get_time; 6 begin 7 for i in 1 .. 1000000 loop 8 x := y; 9 end loop; 1 10 dbms_output.put_line((dbms_utility.get_time-t)I|'cs ); 11 end; 12 / Procedure created.
Выполним эту более правильную версию. SQL> exec data_type_test 31cs (Фактически это — среднее значение пяти выполнений)
Ух, ты! 96% времени выполнения ушло исключительно на преобразование типов данных. Хотя оно и выполняется быстро, все-таки требует существенных вычислительных ресурсов.
Замечание о переменных-счетчиках цикла Интересный результат обнаруживается при тестировании преобразования типов данных. В руководстве по PL/SQL утверждается, что переменная-счетчик цикла (представленная как "i" в следующих примерах) имеет тип INTEGER. МЫ присваиваем переменную I переменной X, тоже определенной как INTEGER И, если верить руководству, следующая процедура оптимальна, и преобразование типов не понадобится. SQL> create or replace 2 procedure num_test_as_integer is 3 x integer; 4 t number := dbms_utility.get_time; 5 begin 6 for i in 1 .'. 10000000 loop 7 x := i; 8 end loop; 9 dbms_output.put_line((dbms_utility.get_time-t)|I'cs'); 10 end; 11 / Procedure created. SQL> exec num_test_as_integer 500cs (среднее значение 5 выполнений)
Эффективная обработка данных
169
Теперь давайте повторим тест, но на этот раз определим переменную х типа PLS_INTEGER. SQL> 2 3 4 5 6 7 8 9 10 11
create or replace procedure num_test_as_pls is x pls_integer; t number := dbms_utility.get_time; begin for i in 1 . . 10000000 loop x:-'i; end loop; dbms_output.put_line((dbms_utility.get_time-t)||'cs'); end; /
Procedure created.
Если эта процедура работает быстрее предыдущей, есть большая вероятность, что переменная-счетчик цикла фактически имеет тип PLS_INTEGER: SQL> exec num_test_as_pls 319cs (рабочее значение 5 выполнений)
В этот момент мы заподозрили, что переменные цикла, вероятно, имеют тип PLS_INTEGER. Мы можем представить более точное доказательство, изменив цикл так, чтобы счетчик выходил за пределы допустимых значений данных типа PLS_INTEGER. SQL> begin 2 for i in power(2,31) .. power(2,31)+10 loop 3 null; 4 end loop; 5 end; 6 / begin * ERROR at line 1: ORA-01426: numeric overflow ORA-06512: at line 2
Таким образом, мы можем быть уверены, что счетчик цикла имеет тип PLS__INTEGER, а не INTEGER, как утверждается в руководстве.
От полей к строкам - использование атрибута %ROWTYPE Регулярное использование атрибута %TYPE В PL/SQL-коде гарантирует, что изменения поля (или столбца) в базе данных будут автоматически учитываться в PL/SQLприложениях. А что, если мы добавим или удалим целые столбцы из таблицы? Все PL/SQL-программы, выбирающие или обрабатывающие строки, могут перестать работать. Еще одна мощная возможность PL/SQL — защита от этих серьезных изменений в базе данных путем объявления переменных с помощью атрибута %ROWTYPE. Рассмотрим фрагмент SQL-кода:
170
Глава 4
select * into varl, var2, ..., varN from table where
Можно использовать атрибут %TYPE В объявлении каждой из переменных VARI, VAR2 и т.д., чтобы защититься от изменений типа данных столбцов таблицы. Но что, если в таблице произойдут структурные изменения, например, будет добавлен или удален столбец? Код почти на любом другом языке, кроме PL/SQL, несомненно, перестанет работать. Язык PL/SQL предлагает простое решение этой проблемы: использование атрибута %ROWTYPE. Он раз и навсегда защитит PL/SQL-программу от множества возможных изменений в базе данных. Рассмотрим процедуру WITH_ROWTYPE, выбирающую строку из простой таблицы т. SQL> create table T ( 2 cl number, 3 c2 number ) ; Table created. SQL> insert into T values (1,2); 1 row created. SQL> create or replace 2 procedure WITH_ROWTYPE is 3 r T%ROWTYPE; 4 begin 5 select * 6 into r 7 from T 8 where rownum = 1; 9 end; 10 / Procedure created.
Переменную R называют записью, и каждое поле записи соответствует столбцу базовой таблицы. Давайте убедимся, что наша процедура работает при текущем определении таблицы т. SQL> exec WITH_ROWTYPE PL/SQL procedure successfully completed.
Что же происходит при изменении определения таблицы? SQL> a l t e r table T add сЗ number; Table altered.
Эффективная обработка данных
171
SQL> exec WITH_ROWTYPE; PL/SQL procedure successfully completed.
Наша процедура по-прежнему работает. Хотя понятно, что с точки зрения функциональности могут потребоваться некоторые изменения кода, по крайней мере, изменение таблицы не привело к возникновению ошибки в приложении. В версии 9 можно даже переименовать столбцы, а наша процедура все равно останется действительной. SQL> alter table T rename column Cl to C01; Table altered. SQL> exec WITH_ROWTYPE; PL/SQL procedure successfully completed.
Она продолжает работать даже после такого существенного изменения, как удаление столбца. SQL> alter table T drop column C2; Table altered. SQL> exec WITH_ROWTYPE; PL/SQL procedure successfully completed.
Использование атрибута %ROWTYPE делает PL/SQL-программы очень устойчивыми. Даже представления базы данных не так устойчивы к изменениям. Давайте удалим и пересоздадим таблицу т, а затем определим представление v на основе этой таблицы. SQL> drop table Т; Table dropped. SQL> create table T ( 2 cl number, 3
c2 number);
Table created. SQL> create or replace 2 view V as select * from T; View created.
Теперь добавим столбец к базовой таблице т. SQL> alter table T add сЗ number; Table altered.
172
Глава 4
Добавление столбца к базовой таблице делает представление недействительным и требует его перекомпиляции. SQL> alter view V compile; View altered.
Теперь сравним наше представление и таблицу. SQL> desc V Name
Null?
Cl C2
Type NUMBER NUMBER
SQL> desc T Name
Null?
Cl C2 C3
Type NUMBER NUMBER NUMBER
Новый столбец отсутствует в перекомпилированном представлении. Причина этого в том, что представление, определенное как SELECT * FROM TABLE, сохраняется в базе данных в момент создания как s e l e c t c o l l , col2, . . . , colN from t a b l e
Представление не учитывает новый столбец, поскольку текст представления хранится в базе данных не как SELECT *. Из-за этого, по-видимому, не стоит использовать SELECT * при определении представлений. Подумайте о возможных последствиях, если придется удалить базовую таблицу и пересоздать ее с теми же именами столбцов, но в другом порядке. Конечно, циничные читатели могут заметить, что часть преимуществ использования переменных, определенных с помощью атрибута %ROWTYPE, исчезает, как только мы начинаем ссылаться на отдельные поля в переменной. Например, если мы расширим представленную ранее процедуру WITH_ROWTYPE ДЛЯ передачи результатов из таблицы т в другую таблицу: SQL> drop table T; Table dropped. SQL> create table T ( 2 cl number, 3
c2 number );
Table created. SQL> insert into T values (1,2); 1 row created.
Эффективная обработка данных
173
SQL> create table Tl as select * from T; Table created. SQL> 2 3 4 5 6 7 8 9 10 11 12 13
create or replace procedure WITH_ROWTYPE is r T%ROWTYPE; begin select * into r from T where rownum = 1; insert into Tl values (r.cl, r.c2); end; /
SQL> exec WITH_ROWTYPE PL/SQL procedure successfully completed.
Никаких проблем нет, но мы больше не защищены от изменений в базовых таблицах, поскольку ссылаемся на отдельные поля в переменной, объявленной с помощью атрибута %ROWTYPE. ЕСЛИ МЫ добавим столбец в таблицу т, процедура завершится сбоем при выполнении оператора INSERT, как продемонстрировано далее: SQL> alter table T add сЗ number; Table altered. SQL> alter table Tl add c3 number; Table altered. SQL> exec WITH_ROWTYPE BEGIN WITH ROWTYPE; END;
ERROR at line 1: ORA-06550: line 1, column 7: PLS-00905: object WITH_ROWTYPE is invalid ORA-06550: line 1, column 7: PL/SQL: Statement ignored
Для версий 7 и 8 сервера Oracle единственным решением были явные ссылки на столбцы таблицы в операторе INSERT. ЭТО позволяет восстановить работоспособность процедуры, но приводит к еще худшим последствиям: при добавлении столбца в таблицу процедура будет молча игнорировать его значения при вставке. Однако новые возможности работы с записями в операторах DML, начиная с версии 9.2, дают отличное решение всех этих проблем.
174
Глава 4
Использование записей в операторах DML Хотя выбрать строку в переменную, объявленную с помощью атрибута %ROWTYPE, в PL/SQL можно было всегда, теперь можно использовать такие переменные в операторах INSERT и UPDATE. Можно переписать процедуру WITH_ROWTYPE так, чтобы использовать новые возможности DML-операторов на базе записей при вставке данных. (Мы пересоздали и заново заполнили данными таблицы т и Т1, как в предыдущем примере.) SQL> create or replace 2 procedure WITH_ROWTYPE is r T%ROWTYPE; 3 4 begin select * 5 6 into r 7 from T 3 where rownum = 1; 9 10 insert into Tl values r/ 11 12 end; 13 / Procedure created.
Давайте посмотрим, что произойдет при добавлении столбца в таблицы т и T I . SQL> alter table T add c5 number; Table altered. SQL> alter table Tl add c5 number; Table altered.
Процедура по-прежнему работает без изменений. SQL> exec WITH_ROWTYPE PL/SQL procedure successfully completed.
Немного изменив процедуру, мы можем продемонстрировать использование записей в операторе UPDATE. МОЖНО изменить строку на основе всей записи, не ссылаясь на ее отдельные поля. SQL> create or replace 2 procedure WITH_ROWTYPE is 3 r T%ROWTYPE; 4 begin ict ** 5 select 6 r into 7 from T 8 where rownum 9
Эффективная обработка данных 10 11 12 13 14
175
update Tl set row = r where rownum = 1; end; /
Procedure created.
Аналогичных возможностей использования записей в операторе DELETE нет, потому что DELETE в любом случае удаляет всю строку. Примечание Гибкость DML-операторов на основе записей "не дается даром". В следующей главе мы рассмотрим ряд проблем, о которых следует помнить при использовании DML-операторов на основе записей.
От записей к объектам Подключитесь к любой базе данных и выполните SELECT * FROM DBAJTYPES — вы редко увидите какие-то типы, кроме используемых предопределенными схемами (например, при установке опций Spatial и Text). В момент, когда знакомые термины "столбцы" и "строки" заменяются "наборами", "массивами переменной длины" и т.п., разработчики для СУБД Oracle начинают говорить: "Меня это не интересует — я объектно-ориентированным программированием не занимаюсь". С момента первого появления объектных возможностей в версии 8.0 их обычно старались не использовать, вероятно, из-за незнания, неуверенности и сомнения или, возможно, по причине отсутствия программных средств, которые могли бы успешно с ними интегрироваться. Утилита SQCPlus хорошо работает с объектами, но в среде SQL*Plus сейчас создается не так уж много приложений. Докладчик на конференции OracleWorld2002 в Сан-Франциско адекватно описал общее отношение к объектам: "В этой аудитории есть кто-то достаточно глупый и недальновидный, чтобы использовать объекты в базе данных?". В основе таких комментариев лежит тот факт, что Oracle — прежде всего и в основном реляционная СУБД. Небольшое исследование показывает, что объекты все равно хранятся в реляционном виде. Например, давайте создадим объектный тип, а затем — таблицу, содержащую столбец этого объектного типа. SQL> create or replace firstname 2 surname 3 4 date of birth 5 incident date item count 6 items retrieved 7 8 ) 9 Туре created.
type STOLEN_ITEMS as object varchar2(30), varchar2(30), date, date. number (4), varchar2(1)
176 Глава 4 SQL> create table CRIMES ( 2 person_id number(10), 3 crime_details stolen_items ) ; Table created.
Что сервер Oracle сделал "за кадром", чтобы хранить данные этого типа? Если обратиться к представлению DBA_TAB_COLUMNS, МЫ увидим только имена столбцов, использованные в операторе создания таблицы. Давайте более детально рассмотрим определение представления DBA_TAB_COLUMNS. SQL> SQL> 2 3 4
set long 50000 select text from dba_views where view_name = 'DBA_TAB_COLUMNS' /
TEXT select OWNER, TABLE_NAME, COLOMN_NAME, DATA_TYPE, DATA_TYPE_MOD, DATA_TYPE_OWNER, DATA_LENGTH, DATA_PRECISION, DATA_SCALE, NULLABLE, COLUMN_ID, DEFAULT_LENGTH, DATA_DEFAULT, NUM_DISTINCT, LOW_VALOE, HIGH_VALUE, DENSITY, N0M_NULLS, NUM_BUCKETS, LAST_ANALYZED, SAMPLE_SIZE, CHARACTER_SET_NAME, CHAR_COL_DECL_LENGTH, GLOBAL_STATS, USER_STATS, AVG_COL_LEN, CHAR_LENGTH, CHARJJSED, V80_FMT_IMAGE, DATA_UPGRADED from DBA_TAB_COLS where HIDDEN_COLUMN = 'NO'
Для нескрытых столбцов представление DBA_TAB_COLUMNS просто обращается к представлению DBA_TAB_COLS. И в этом — ключ. Выбирая из представления DBA_TAB_COLS все столбцы, а не только нескрытые, можно выявить, как "за кадром" сервер действительно создал таблицу. SQL> 2 3 4
select column_name, hidden_column, data_type from DBA_TAB_COLS where table_name = 'CRIMES' /
COLUMN NAME
HID
DATA TYPE
PERSON_ID CRIMEJ3ETAILS SYS_NC00003$ SYS_NC00004$ SYS_NC00005$ SYS_NC00006$ SYS_NC00007$ SYS_NC00008$
NO N0 YES YES YES YES YES YES
NUMBER STOLEN ITEMS VARCHAR2 VARCHAR2 DATE DATE NUMBER VARCHAR2
Наш объектный тип данных был разбит на составляющие столбцы для создания обычной реляционной структуры. Поскольку хранение объектов в любом случае
Эффективная обработка данных
177
реализуется с помощью реляционных структур, сторонники чисто реляционной модели спрашивают: "Почему бы не делать это самим и не контролировать реализацию полностью?". Признаюсь, до недавнего времени я не видел преимуществ использования базы данных Oracle для хранения объектов. Возможно, потому, что опыт программирования и использования баз данных я приобрел до того, как объекты стали "следующей грандиозной идеей". Хотя я не сомневаюсь в теоретических преимуществах объектно-ориентированного подхода как к кодированию, так и к моделированию структур данных, подавляющее большинство программных средств на рынке по-прежнему эффективно работает только с реляционными данными и большинство конечных пользователей рассматривают данные с реляционной точки зрения. Однако с появлением версий 9.2 и 10g реализация объектов стала ближе к тому, что желательно для сторонников чистого объектно-ориентированного подхода: подтипы, которые могут наследовать методы и атрибуты от своих родительских типов, развитие определений типов со временем, доступное пользователю управление методами-конструкторами, а также поддержка абстрактных типов. Все это приводит к мысли, что объекты — это "всерьез и надолго".
Объектные типы Что бы вы ни думали о хранении объектов в базе данных Oracle, я должен подчеркнуть важное отличие между объектами, хранящимися в базе данных, и объектными типами, определенными в базе данных. Объектный тип описывает, как будет определен объект, тогда как объект — это фактическое воплощение объектного типа. Объектные типы позволяют использовать в PL/SQL-программах типы данных с более сложной структурой, а также являются важной частью средств множественной выборки, которые мы вскоре опишем. Объектные типы расширяют возможности и увеличивают гибкость языка PL/SQL, поэтому изучить их должен любой разработчик, использующий PL/SQL. Различные объектные типы Oracle можно обобщенно разделить на две категории: > записи. Любая логическая группировка скалярных переменных. В эту категорию входят описанные ранее в настоящей главе переменные, объявленные с помощью атрибута %ROWTYPE, записи с явно определенными (с помощью конструкции RECORD) полями и записи, поля которых явно определены с помощью SQL-оператора CREATE TYPE AS OBJECT; > наборы. Любой упорядоченный список элементов. Элементом может быть все, что угодно: от простой скалярной переменной до сложного объекта (в том числе и другие наборы). Именно наборы мы рассмотрим далее в этой главе, поскольку они обеспечивают хранение и обработку результирующих множеств SQL в переменных PL/SQL. Я не собираюсь пересказывать синтаксис для работы с наборами из стандартной документации. Лучший способ продемонстрировать возможности наборов в PL/SQL — изучить по шагам полный пример и показать, насколько эффективными могут быть наборы.
178
Глава 4
Расширение возможностей утилиты Runstats с помощью наборов В этом разделе мы создадим расширенную версию утилиты RUNSTATS', созданной Томом Кайтом — человеком, поддерживающим популярный Web-сайт h t t p : // asktom. oracle. com. Мы уже представили многочисленные примеры сравнения времени выполнения двух альтернативных вариантов решения задачи на языке PL/SQL, но утилита Тома расширяет этот подход, показывая различие затрат ресурсов на выполнение двух вариантов решения с точки зрения времени выполнения, статистической информации уровня сеанса и использования защелок. Такое средство, естественно, поможет при сравнении производительности двух решений одной задачи. Мы снова обращаемся к нашему любимому требованию — демонстрируемости, т.е. доказательству, что одно решение лучше другого. Хотя чаще всего время выполнения является критическим фактором для определения пригодности решения, использование утилиты RUNSTATS позволяет получить другую важную информацию о коде, такую, как количество обращений к базе данных для получения результата, а также затраты на упорядочивание обращений, требуемые кодом. Оба эти показателя могут быть важнее, чем время выполнения, если решение предназначено для среды с большим количеством одновременно работающих пользователей. Настройка и использование исходной версии утилиты RUNSTATS описаны в разделе "Настройка" в начале нашей книги, но, если коротко, то при использовании сначала делается моментальный снимок значений различных статистических показателей на уровне сеанса и системы. SQL> exec runstats_pkg.rs_start;
Затем выполняется код первого варианта решения. Делается другой моментальный снимок значений тех же статистических показателей. SQL> exec runstats_pkg.rs_middle;
После этого выполняется код альтернативного варианта решения. Мы делаем завершающий моментальный снимок, вычисляем и представляем сравнительные показатели двух протестированных подходов. SQL> exec runstats_pkg.rs_stop;
Результат работы стандартной утилиты, RUNSTATS СОСТОИТ ИЗ двух разделов. Total Elapsed Time Runl ran in 123 hsecs Run2 ran in 456 hsecs run 1 ran in 26.9% of the time Latching / S t a t i s t i c a l LATCH.shared pool LATCH.library cache
differences Runl 12,543 25,787
Run2 39,543 49,023
Diff 27,000 23,236
'Утилиту runstats можно найти на странице сайта http://asktom.oracle.com/~tkyte.
Эффективная обработка данных .. . STAT.. .db block gets STAT.. .consistent gets STAT.. .physical reads
4, 123 123, 432 1,453
8, 432 354, 392 4,243
179
4, 309 230, 960 2, 790
Впечатляет! Мы можем получить простой понятный отчет в виде таблицы, в которой представлены затраты на выполнение каждого протестированного фрагмента кода с точки зрения различных статистических показателей системы и защелок. Хотя это средство покрывает большинство вариантов тестирования, с которыми сталкиваются разработчики, я хочу расширить его возможности, чтобы можно было делать следующее: > тестировать более двух различных вариантов решения; > более гибко управлять форматом результатов, работая с результатами как с результирующим множеством SQL, и тем самым получая возможность сортировать их по разным критериям или выдавать только строки, соответствующие определенным условиям; > использовать для хранения моментальных снимков статистических показателей наборы вместо таблиц базы данных. При этом утилиту можно будет легко установить и использовать в различных организациях без необходимости вмешательства АБД. Я опишу свою реализацию поэтапно, используя ее для демонстрации того, как наборы могут эффективно расширять возможности языка PL/SQL.
Переделка утилиты Runstats Сначала мы создадим представление, содержащее моментальный снимок статистических показателей сеанса и информации о защелках (оно не отличается от исходной версии Тома). Учетная запись, от имени которой это представление будет создаваться в вашей системе, должна иметь привилегию SELECT на представления динамической производительности V$STATNAME, V$MYSTAT, V$LATCH И V$TIMER. SQL> create or replace view stats 1 2 as select 'S: || a.name name, b.value 3 from v$statname a, v$mystat b 4 where a.statistic! = b.statistic! 5 union all 6 select 'L:' || name, gets 7 from v$latch . . . . 8 union all 9 select 'E:Elapsed', hsecs from v$timer; View created.
Строки, начинающиеся с префикса S:, содержат статистическую информацию для данного сеанса. Строки, начинающиеся с префикса L:, содержат информацию о действиях с защелками в базе данных (к сожалению, статистическая информация об использовании защелок на уровне сеанса не поддерживается), а строка, начина-
180
Глава 4
юшаяся с префикса Е:, отмечает текущий момент времени. Если выбрать информацию из представления STATS, МЫ получим текущее состояние статистических показателей сеанса, а также общесистемной информации о защелках. SQL> select * from stats; NAME
VALUE
S:logons cumulative S:logons current S:opened cursors cumulative L:latch wait list L:event range base latch L:post/wait queue E:Elapsed
1 1 359 0 0 12 453411
489 rows selected.
Учтите, что, как и при использовании пакета Statspack, все эти цифры взяты в определенный момент времени, поэтому анализировать имеет смысл только различие между несколькими моментальными снимками из представления STATS. В нашей версии утилиты RUNSTATS вместо вставки моментального снимка в таблицу базы данных он будет сохраняться в наборе. При каждом обращении к этому представлению надо будет задавать для моментального снимка идентификатор, чтобы можно было отличать один результат от другого. Для отражения такой структуры в типе данных PL/SQL потребуются два объектных типа. Первый, STATSLINE, представляет одну строку результатов, полученную из представления STATS. SQL> create or replace type stats_line as object 2 ( snapid number(10), 3 name varchar2(66), 4 value int ) ; 5
'
Type created.
Столбцы NAME и VALUE берутся из представления STATS, а столбец SNAPID ПОЗВОЛИТ различать моментальные снимки данных этого представления. Второй тип — набор, позволяющий все результирующее множество, полученное из представления, сохранить в одном объекте. SQL> create or replace type stats_array as 2 table of stats_line; 3 / Type created.
Итак, теперь мы можем создать моментальный снимок представления STATS И сохранить его в одной переменной с помощью следующего кода:
Эффективная обработка данных
181
declare v_snap_id number := 0; s stats_array := stats_array(); begin v_snapid := v_snapid + 1; for i in ( select * from stats) loop s.extend; s(s.count) := stats_line(v_snapid, i.name,i.value ) ; end loop; end;
Мы еще вернемся к этому PL/SQL-коду, когда поместим его в пакет RUNSTATS. Как и в исходной версии RUNSTATS, после создания нескольких моментальных снимков их надо обработать и вернуть результаты вызывающему. Например, если мы сравниваем три различных варианта кода, мы будем создавать моментальные снимки из представления STATS четыре раза (один раз — перед началом и дальше — после каждого выполнения). Мы получим примерно такой результат: Name Е:Elapsed S:consistent gets и т. д.
Runi 100 200
Run2 105 230
Diff2 5 30
Pct2 5% 15%
Run3 120 250
Diff3 20 50
Pct3 20% 25%
где DIFF2 — отличие между тестами RUNI И RUN2, DIFF3 — отличие между тестами RUNI и RUN3, и т.д. Это упрощает выявление оптимального кода. Но мы хотим иметь возможность обрабатывать отчет так, как если бы это было результирующее множество SQL, чтобы можно было получать ответы на более сложные вопросы, вроде: "По каким статистическим показателям различие между тестами RUN3 И RUNI составляет более 20%?". Точно так же, как мы определили объектные типы для хранения результатов запроса к представлению STATS, нам нужно определить объектные типы для выдачи отчета в вызывающую среду. Сначала мы создадим объектный тип для хранения одной строки представленных ранее результатов. SQL> create or replace 2 type rurl stats line as object ( 3 tag varchar2(66), runi int, 4 5 run2 int, 6 diff2 int, 7 pct2 int, run3 8 int. diff3 int, 9 10 pct3 int, run4 11 int, diff4 int, 12 pct4 int, 13 14 run 5 int, diff5 int, 15 16 pct5 int, 17 run 6 int,
182
Глава 4 18 19 20
/
diff6 pct6
int, int);
Type created. Мы предполагаем, что сравнивать придется не более шести вариантов кода, но это легко изменить, чтобы учесть специфические требования. (На самом деле, если вы — разработчик, проверяющий более шести разных вариантов решения задачи, пожалуйста, переходите на работу ко мне!) Затем нам понадобится объектный тип для хранения массива строк результата. SQL> create or replace type run_stats_output 2 as table of run_stats_line; 3 / Type created. А теперь осталось объединить эти типы кодом, который позволил бы их использовать широкой общественности. Спецификация пакета RS Наша новая версия утилиты RUNSTATS будет называться RS. SQL> create or replace package rs as 2 procedure snap(reset boolean default false); 3 function display return run_stats_output ; 4 function the_stats return stats_array; 5 s stats_array := stats_array(); 6 v_snapid integer := 0; 7 end; 8 / Package created. Давайте рассмотрим содержание спецификации пакета более детально. s stats_array := stats_array(); Эта строка определяет пустой, но проинициализированный массив для хранения моментальных снимков. Переменная не обязательно должна входить в спецификацию пакета (она может быть скрыта в теле пакета), но мы оставили ее в спецификации, чтобы любопытный разработчик мог легко отыскать конкретные значения в массиве. v_snapid integer : = 0; Эта переменная просто содержит количество выполненных моментальных снимков. procedure snap Эта процедура будет выполнять запрос к представлению STATS И сохранять результаты в объекте STATS_ARRAY. Я добавил булев параметр reset, чтобы мы могли
Эффективная обработка данных
183
использовать туже процедуру для начала новой серии моментальных снимков. Поэтому типичное использование этой процедуры будет таким: snap(true); — для новой серии моментальных снимков snap; snap; и т.д. function display return run_stats_output
Эта функция будет обрабатывать массив созданных моментальных снимков и проходить в цикле по данным для получения описанных ранее результатов. Очевидно, эта функция выполняет "грязную работу": преобразовывает исходные данные в осмысленную сравнительную таблицу тестов, для которых мы делали моментальные снимки. Преимущество возврата результатов в виде набора состоит в том, что любая функция, возвращающая набор, может стать источником данных для SQL-запроса с помощью оператора TABLE (). Такие функции (вполне естественно) называют табличными. function the_stats return stats_array;
Эта функция позволяет разработчику получить исходные данные для всех созданных моментальных снимков. Тело пакета RS В теле пакета выполняются основные действия. Давайте рассмотрим каждый компонент его исходного кода. Следующий код позволяет создавать моментальный снимок представления STATS и сохранять его в одной переменной. SQL> create or replace package body rs as 2 3 procedure snap(reset boolean default false) is „
I. •
4 begin 5 if reset then 6 s := stats_array() ; 7 v_snapid := 0; 8 end if; 9 v_snapid := v_snapid + 1; 10 for i in ( select * from stats) loop 11 s.extend; 12 s(s.count) := s t a t s _ l i n e (v_snapid, i . name, L v a l u e ) ; 13 end loop; 14 end;
Этот код, по сути, не отличается от представленного ранее, но мы добавили параметр RESET, позволяющий начать создание нового набора моментальных снимков. Для каждой строки в представлении STATS МЫ увеличиваем массив s и задаем идентификатор моментального снимка для соответствующих пар имя-значение.
184
Глава 4
Примечание Такой код можно сделать более эффективным с помощью средств множественной выборки, описанных далее в этом разделе. Поскольку мы их еще не рассматривали, они здесь не используются.
Две таблицы, на основе которых строится следующий запрос, демонстрируют, насколько легко работать с объектными типами PL/SQL как с реляционными структурами. 15 16 function display return run_stats_output is 17 output run_stats_line := 18 run_stats_line(null, 19 null,null,null,null, 20 null,null,null,null, 21 null,null,null,null, 22 null,null,null,null); 23 ret run_stats_output : = run_stats_output(); 24 base_val number; 25 begin 26 for i in ( select hi.snapid, lo.name, lo.value, hi.value-lo.value amt 27 from ( select * from table(the_stats) ) lo, 28 ( select * from table(the_stats) ) hi 29 where lo.name = hi.name 30 and lo.snapid = hi.snapid -1 31 order by 2,1) loop
Конструкция TABLE () преобразует набор объектов в результирующее множество SQL. Учтите, что каждый моментальный снимок не содержит никакой информации о сути выполненных тестов — только отличие одного моментального снимка от следующего имеет значение. Отличие (столбец АМТ) между данным моментальным снимком и предыдущим выдается по результатам запроса в цикле FOR ПО курсору. Например, если мы возьмем SQL-запрос из исходного кода и выполним его отдельно, то мы увидим, что следующий результат получен по двум тестам (т.е. было создано три моментальных снимка). SQL> select hi.snapid, lo.name, 2 from ( select 3 ( select 4 where lo.name 5 and lo.snapid 6 order by 2,1 7 /
hi.value-lo.value amt * from table(rs.the_stats) * from table(rs.the_stats) = hi.name = hi.snapid -1
SNAPID NAME 2 3 2 3
E:Elapsed E:Elapsed L:cache buffers chains L:cache buffers chains
AMT 2672 1291 49967 57323
) lo, ) hi
Эффективная обработка данных 2 L:cache buffers lru chain 3 L:cache buffers lru chain
185
16555 869
Первая строка в результатах (со SNAPID=2) представляет отличие между моментальными снимками 1 и 2, т.е. статистическую информацию и время выполнения для первого теста (который ограничен моментальными снимками 1 и 2). Вторая строка (со SNAPID=3) представляет отличие между моментальными снимками 2 и 3, т.е. соответствует второму тесту, и т.д. Результат цикла FOR ПО курсору также объясняет, как выполняется последующая обработка. Каждый раз, когда мы обнаруживаем строку, в которой SNAPID равен 2, надо добавлять новую строку в выдаваемое результирующее множество (за исключением самой первой строки). 32 33 34 35 36 37 38 39 40
case i.snapid when 2 then if output.tag is not null then ret.extend; ret (ret.count) := output; end if; base_val := i.amt; o u t p u t . t a g := i.name; output.runl := i.amt;
Для всех строк со значением SNAPID, больше 2, мы добавляем соответствующие значения в текущую строку результатов. Таким образом мы постепенно заполняем строку результата. 41 42 43
44 45 46 47 43 49 50
51 52 53 54 55 56 57 58 59 60 61 62
when 3 then output.run2 := i.amt; output.diff2 := i.amt - base_val; output.pct2 := i.amt / greatest(base Vcil, 1) when 4 then output.run3 := i.amt; output.diff3 := i.amt - base val; output.pct3 := i.amt / greatest(base v£il,l) when 5 then output.run4 := i.amt; output.diff4 : = i.amt - base val; output.pct4 := i.amt / greatest (base veil, 1) when 6 then output.run5 := i.amt; output.diff5 := i.amt - base_val; output.pct5 := i.amt / greatest (base VE il,l) when 7 then output.run6 := i.amt; output.diff6 := i.amt - base val; output.pct6 := i.amt / greatest (base Vctl,l) end case; end loop;
* 100;
* 100;
* 100;
* 100;
* 100;
После прохождения в цикле по всем строкам из моментальных снимков мы возвращаем значение RET, представляющее собой массив строк результатов.
186
Глава 4 63 ret.extend; 64 ret(ret.count) := output; 65 return ret; 66 end; 67 68 function the_stats return stats_array is 69 begin 70 return s; 71 end; 72 73 end; 74 /
Package body created.
Примечание Это тело пакета не будет компилироваться в версии 8/ из-за использования оператора CASE. Для версий Oracle до 9 надо будет преобразовать его в стандартный набор операторов IF-THEN-ELSE.
Теперь можно выполнять запросы к функции DISPLAY С ПОМОЩЬЮ оператора TABLE () точно так же, как мы это делали с функцией THE_STATS В пакете. Чтобы продемонстрировать возможности нового пакета RS, вернемся к примеру из главы 1, в котором мы сравнивали времена выполнения PL/SQL-программы, использующей PL/SQL-таблицы для обработки записей из таблиц ЕМР И DEPT (REPORT_SAL_ADJUSTMENT3), И программы, использующей для достижения тех же целей чистый язык SQL (REPORT_SAL_ADJUSTMENT4). SQL> exec rs.snap (true); PL/SQL procedure successfully completed. SQL> exec REPORT_SAL_ADJUSTMENT3
—
код теста #1
PL/SQL procedure successfully completed. SQL> exec rs.snap PL/SQL procedure successfully completed. SQL> exec REPORT_SAL_ADJUSTMENT4
—
код теста 42
PL/SQL procedure successfully completed. SQL> exec rs.snap PL/SQL procedure successfully completed.
Поскольку мы выбираем результаты с помощью стандартного языка SQL, допускается любая необходимая их обработка с помощью обычных конструкций WHERE,
Эффективная обработка данных
187
ORDER BY и т.д. Например, мы можем выдать только те результаты, в которых отличие между двумя тестами в процентах (столбец РСТ2) составляет больше нуля. SQL> 2 3 4 5 6
select tag, runl, run2, diff2, pct2 from table(rs.display) where runl > 0 and pct2 > 0 order by runl /
TAG L:shared pool L:library cache L:SQL memory manager workarea S:recursive cpu usage S:CPU used by this session S:CPU used when call started E:Elapsed S:bytes sent via SQL*Net to cl S:bytes received via SQL*Net f S:buffer is not pinned count S:no work - consistent read ge S:table scan blocks gotten S:consistent gets S:session logical reads S:recursive calls S:sorts (rows) S:table scan rows gotten L:cache buffers chains
RUN1
RUN2
DIFF2
PCT2
37 45 69 133 229 229 234 242 351 50253 50253 50253 50261 50261 50994 52079 100500 100522
36 43 4 100 101 101 147 242 351 253 253 253 258 264 491 52079 50500 913
-1 -2 -65 -33 -128 -128 -87 0 0 -50000 -50000 -50000 -50003 -49997 -50503 0 -50000 -99609
97 96 6 75 44 44 63 100 100 1 1 1 1 1 1 100 50 1
Хотя, читая еще главу 1, мы узнали, что решение на чистом SQL проще для написания и имеет более высокую производительность с точки зрения чистого времени выполнения (строка с префиксом Е: в представленных выше результатах), с помощью пакета RS МЫ можем это продемонстрировать намного убедительнее. Можно увидеть, что повышение производительности происходит, вероятно, из-за уменьшения объема логического ввода-вывода, что также уменьшает количество используемых защелок цепочек буферов кеша (cache buffers chains) — жизненно важного показателя для поддержки одновременной работы нескольких пользователей. Подведем итоги. Утилита RUNSTATS И созданная нами расширенная версия в виде пакета RS демонстрируют, насколько просто объектные типы PL/SQL могут использоваться для представления сложных структур данных и, что не менее важно, их легко можно преобразовать для использования в SQL-операторах.
Зачем использовать наборы в PL/SQL Возможность использовать и обрабатывать сложные структуры как одну переменную — замечательное свойство объектных типов. Однако вы наверняка заметили, что в примере с RUNSTATS процесс запоминания результатов запроса к представлению STATS в наборе STATSARRAY все равно выполнялся построчно. Это логически экви-
188
Глава 4
валентно проблемам с полями и записями, когда в некоторых случаях приходится обращаться к отдельным полям. Хотелось бы абстрагироваться до уровня наборов. Иными словами, поскольку наборы предназначены для представления множества строк в виде одной сущности, было бы хорошо иметь возможность помещать в набор множество вместо выполнения цикла по отдельным элементам множества. Основная причина того, что мы используем наборы, состоит в том, что они побуждают разработчиков чаще думать в терминах множеств, а не строк. Хотя нет никаких функциональных причин, мешающих разработчику обрабатывать результирующие множества построчно, с точки зрения эффективности и производительности обычно это — плохая идея. Оставив на время тему объектов, рассмотрим следующую простую демонстрацию в среде SQL*Plus, которая показывает снижение производительности при построчной выборке результирующего множества2. Мы сравним время выполнения при выборке из большой таблицы SRC по одной строке и пакетами по 500 строк. SQL> SQL> SQL> SQL>
set autotrace traceonly statistics set arraysize 1 set timing on select * from SRC;
142304 rows selected. Elapsed: 00:00:35.02 Statistics 0 0 83293 16551 0 63311705 783040 71153 0 0 142304
recursive calls db block gets consistent gets physical reads redo size bytes sent via SQL*Net to client bytes received via SQL*Net from client SQL*Net roundtrips to/from client sorts (memory) sorts (disk) rows processed
SQL> set arraysize 500 SQL> select * from SRC; 142304 rows selected. Elapsed: 00:00:24.02 Statistics 0 0 2
recursive calls db block gets
Спасибо Джонатану Льюису (Jonathan Lewis) за эту демонстрацию.
Эффективная обработка данных 189 16830 consistent gets 14758 physical reads 0 redo size 54736825 bytes sent via SQL*Net to client 3623 bytes received via SQL*Net from client 286 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 142304 rows processed
Мы увидели два одинаковых запроса к одной и той же таблице. Первый работал намного дольше и выполнил больше физических и логических операций ввода-вывода. Когда параметр ARRAYSIZE имеет значение 1, мы, фактически, говорим серверу следующее: > зарегистрируй наш интерес к соответствующему блоку базы данных; > найди первую строку в этом блоке, соответствующую критериям запроса; > отметь, что этот блок нас больше не интересует, поскольку мы уже заполнили массив строк (из одной строки), запрошенный клиентским приложением; >• верни данные строки клиентскому приложению (SQL*Plus). Как только наша программа в SQL*Plus захочет выбрать вторую строку, надо повторить тот же процесс. Конечно, вероятность того, что соответствующий блок уже есть в буферном кеше, велика, но остаются расходы на установку защелок, гарантирующих присутствие блока в кеше и дальнейшую регистрацию нашего к нему интереса, чтобы он не был изменен или удален из буферного кеша за то недолгое время, пока мы выбираем соответствующую строку. Примечание Даже когда значение ARRAYS IZE явно установлено равным 1, фактически сервер использует ARRAYSIZE 2 из-за оптимизации, связанной с предварительной выборкой. Это уже обсуждалось в главе 3 при исследовании производительности неявных курсоров.
При установке значения 500 параметра ARRAYS IZE МЫ даем серверу более точную информацию о наших требованиях (т.е. "нам нужно много строк"). Теперь мы просим сервер о следующем: > выбери соответствующий блок базы данных с диска (или из буферного кеша); > найди первую строку в этом блоке, соответствующую критериям запроса; > сохраняй наш интерес к блоку, пока мы: > не заполним массив данных; > не просмотрим все строки в блоке; > в последнем случае мы переходим к следующему блоку и повторяем это, пока массив не будет заполнен; > верни массив данных строк клиентской программе (SQL*Plus). PL/SQL-приложения в этом отношении не отличаются от SQL*Plus. Если вы хотите выбрать п строк, то их выборка по одной снижает производительность. К со-
190
Глава 4
жалению, в ранних версиях PL/SQL это было единственно возможным решением. Впрочем, это не совсем верно. Начиная с версии 7.2, можно было обрабатывать массивы с результатами в PL/SQL, но только с помощью пакета DBMS_SQL. Например, чтобы воспроизвести предыдущий пример с обработкой массивов в SQCPlus с помощью пакета DBMS_SQL, нам придется написать достаточно нетривиальный код. SQL> 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
create or replace procedure ARRAY_PROCESS is s integer := dbms_sql.open_cursor; nl dbms_sql.number_table; d number; с number; BEGIN dbms_sql. parse (s, 'select * from S R C , DBMS_SQL.native); dbms_sql.define_array(s,l,nl,500,1); d := dbms_sql.execute(s); loop с := DBMS_SQL.FETCH_ROWS(s); DBMS_SQL.COLUMN_VALUE(s, 1, n1) ; exit when с < 500; end loop; DBMS_SQL.CLOSE_CURSOR(s); END; /
Procedure created.
Сравните его с небольшим кодом для получения того же результата с помощью построчной обработки. SQL> 2 3 4 5 6 7 8
create or replace procedure SINGLE ROW _PROCESS is begin for i in ( select * from SRC ) loop null; end loop; end; /
Procedure created.
С учетом исторических реалий легко понять, почему многие разработчики не утруждались написанием дополнительного кода, необходимого для обработки массивом. Однако если потратить время на написание более сложного кода с обработкой массивом, можно повысить производительность. Сравним время выполнения двух только что созданных процедур. SQL> exec single row process; ~™
—
PL/SQL procedure successfully completed.
Эффективная обработка данных
191
Elapsed: 00:00:22.02 SQL> exec array_process; PL/SQL procedure successfully completed. Elapsed: 00:00:17.01
Конечно, производительность — это еще не все. Как мы покажем в главе 5, у использования динамического SQL есть свои недостатки (не отслеживаются зависимости, нельзя выявить ошибки в коде при компиляции и т.д.). Решение на базе пакета DBMS_SQL сложнее реализовать, оно потребует больше усилий на сопровождение в случае изменения базовой таблицы. В прошлом язык PL/SQL сильно критиковали за низкую производительность, но наиболее типичной ее причиной была построчная обработка, а не органические недостатки PL/SQL-машины. Эта проблема не связана только с PL/SQL, например, в версии 8.1 предкомпилятору Рго*С была добавлена опция PREFETCH ДЛЯ решения этой проблемы в соответствующей среде. Новая опция PREFETCH преобразовывала сгенерированный код времени выполнения Рго*С для использования выборки массивом, даже если разработчик написал его в традиционной построчной манере. Версия 8.1 внесла изменения и в язык PL/SQL — стало намного проще использовать обработку массивом в PL/SQL, как будет описано далее.
Множественная выборка с помощью наборов Наборы — принципиально важная часть механизма реализации выборки массивом в PL/SQL. Этот процесс еще называют множественной обработкой (bulk collection). В спорте термин "bulking up" (можно перевести как "накачка мышц". — Прим. ред.) часто применяют в отношении соперника, который долго готовился к соревнованиям, в основном, в спортзале (или у своего доктора-специалиста по допингу), для увеличения силы, скорости и получения более высоких результатов непосредственно на площадке. Эта терминология вполне подходит и для программирования на языке PL/SQL. Использование множественной обработки в PL/SQL может потребовать больше времени на подготовку (иными словами, на написание кода), но обычно дает более быстрое и мощное решение. К сожалению, возможности множественной обработки в PL/SQL относятся, пожалуй, к наименее используемым в современных PL/SQL-приложениях. Большая часть производственного кода на PL/SQL, с которым нам приходилось сталкиваться, хотя и работает на версиях Oracle 8, 9 и 10, но успешно скомпилировалась и работала бы также и в версии 7.3. Это, может, и хорошо, если бы создавались программы, которые работали бы на всех этих версиях, но такая ситуация является, скорее, признаком незнания разработчиков об имеющихся возможностях новых версий, чем следствием явного требования независимости от версии.
Множественная обработка Начиная с версии 8.1, выборка строк из базы данных массивом стала настолько же простой, как и выборка одной строки. Вот код для выборки одной строки из представления ALL_OBJECTS:
192 Глава 4 SQL> declare 2 x number; 3 begin 4 select object_id 5 into x 6 from all_objects 7 where rownum declare 2 type numlist is table of number; 3 x numlist; 4 begin 5 select object__id 6 bulk collect into x 7 from all_objects 8 where rownum create or replace 2 directory REPORT_DIR as 'C:\TEMP'; Directory created.
Примечание В Oracle 9.2 средства пакета UTL_FILE больше не требуют обязательного использования параметра UTL_FILE_DIR — они используют объект-каталог, первоначально предназначавшийся для внешних заданий. Пакет UTL_FILE был заметно усовершенствован в версии 9.2 (подробнее об этом см. в руководстве "Supplied PL/SQL Packages and Types Reference"). Если вы используете прежние версии Oracle, надо добавить каталог C:\TEMP (или любой подходящий для вашей системы каталог) в качестве значения параметра инициализации UTL FILE DIR.
Эффективная обработка данных
193
Затем мы переделаем процедуру REPORT_SAL_ADJUSTMENT4 ДЛЯ выдачи отчета в файл, а не в таблицу. Обработаем в цикле тот же созданный ранее высокопроизводительный SQL-оператор, извлекая строки по одной и выдавая их в файл с помощью пакета UTL_FILE. SQL> 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
create or replace procedure report_sal_adjustment4 is f utl_file.file_type; begin f := utl_file.fopen('REPORT_DIR\ ' r e p o r t . d a t ' , ' W ' ) ; for i in ( select e.empno, e.hiredate, e.sal, dept.dname, case when sal > avg_sal then 'Y' else 'N' end status from ( select empno, hiredate, sal, deptno, avg(sal) over ( partition by deptno ) as avg_sal, min(sal) over ( partition by deptno ) as min_sal from emp ) e, dept where e.deptno • dept.deptno and abs(e.sal - e.avg_sal)/e.avg_sal > 0.10 ) loop utl_file.put_line(f,i.empnol| i.hiredate|| i.sal I I i.dname]| i.status); end loop; utl_file.fclose(f); end; /
Procedure created.
Теперь создадим вторую версию, использующую множественную обработку. Вы увидите, что это — один из случаев, когда надо использовать явный курсор, так что бы можно было определить набор на основе столбцов курсора. Мы назвали курсор CJTEMPLATE, поскольку его определение образует шаблон для определения набора, RESULTSET, в который будет выполняться множественная выборка. SQL> create or replace 2 procedure report_sal_adjustment4_bulk is 3 f utl_file.file_type; 4 cursor c_template is 5 select e.empno, e.hiredate, e.sal, dept.dname, 6 case when sal > avg_sal then 'У 7 else 'N' 8 end status 9 from ( 10 select empno, hiredate, sal, deptno, 11 avg(sal) over ( partition by deptno ) as avg_sal, 12 min(sal) over ( partition by deptno ) as min_sal 7 Зак. 348
194
Глава 4 13 14 15
from emp ) e, dept where e.deptno = dept.deptno and abs(e.sal - e.avg_sal)/e.avg_sal > 0.10;
Итак, вот определение нового типа — набора (или массива) строк, каждая из которых будет содержать поля, соответствующие предыдущему определению курсора. 16 17 18 19 20 21
type resultset is table of c_template%rowtype; r resultset; begin f := utl_file.fopen('REPORT_DIR', 'report.dat', 'W') ; open c_template;
Вот когда проявляется различие в обработке. Вместо обработки строк по одной в цикле мы можем выбрать все строки в результирующем множестве R. 22 23 24
fetch c_template bulk collect into r; close c_template;
Теперь, когда переменная R содержит все результирующее множество, мы проходим в цикле по этому набору в памяти, а не обращаемся постоянно к базе данных для получения новых строк из курсора. 25 26 27 28 29 30 31 32 33 34
for i in 1 .. г.count loop utl_file.put_line(f,r(i).empnol| r(i).hiredatel| r(i).sal|| r(i).dnamel| r(i).status); end loop; utl_file.fclose(f); end; /
Procedure created.
Примечание В наших тестах на Windows и AIX создание набора на основе курсора с помощью %ROWTYPE завершилось ошибкой компиляции в версиях 9.2.0.1 и 9.2.0.2. Быстрый поиск на сайте Metalink выявил, что эта ошибка была устранена в версии 9.2.0.3.
Насколько же лучше версия процедуры с множественной обработкой по сравнению с построчной версией? Чтобы ответить на этот вопрос, мы можем использовать нашу новую утилиту RS — аналог RUNSTATS. МЫ создаем моментальные снимки перед и после выполнения каждой версии. SQL> exec rs.snap(true); PL/SQL procedure successfully completed.
Эффективная обработка данных
195
SQL> exec report_sal_adjustment4 PL/SQL procedure successfully completed. SQL> exec rs.snap; PL/SQL procedure successfully completed. SQL> exec
report_sal_adjustment.4_bulk
PL/SQL procedure successfully completed. SQL> exec rs.snap; PL/SQL procedure successfully completed.
Теперь мы можем использовать стандартные SQL-операторы для поиска отличий, если они есть. SQL> 2 3 4 5 6
select tag, runl, run2, diff2, pct2 from table(rs.display) where runl > 0 and pct2 != 100 order by 2 /
TAG S:recursive cpu usage L:shared pool L:library cache S:CPU used by this session E:Elapsed L:cache buffers chains S:recursive calls
RUN1
RUN2
DIFF2
PCT2
151 152 211 268 326 1208 15267
119 92 107 208 270 913 493
-32 -60 -104 -60 -56 -295 -14774
79 61 51 78 83 76 3
Использование множественной обработки ускорило выполнение на 17 процентов. Обратите также внимание на аналогичное сокращение количества защелок (строки, начинающиеся с буквы ь), что повысит масштабируемость решения.
Множественное связывание Выборка данных из базы массивами, конечно, составляет только половину решения. PL/SQL содержит также средства для передачи данных массивом в другом направлении, т.е. из PL/SQL-программы в базу данных для вставки строк, удаления или изменения существующих значений в таблице. Это называется "множественным связыванием" с целью уменьшения затрат ресурсов на переключение контекста в PL/SQL. При запуске PL/SQL-программы PL/SQL-машина на сервере выполняет код. При выявлении в коде SQL-оператора PL/SQL-машина передает его SQL-машине и ждет ответа. Поэтому типичный PL/SQL-блок будет постоянно переключаться между двумя машинами по ходу выполнения, как показано на рис. 4.1.
196 Глава 4 declare v_emp numb e r; begin v_erap:=73 69; delete from emp where empno=v_emp;
Рис. 4 . 1 . Переключение контекстов между PL/SQL-машиной и SQL-машиной
v_emp:=7468; delete from emp where empno=v_emp;
га О со
v_emp:7210; delete from emp where empno=v_emp;
O со
v_emp:=7340; d e l e t e from emp where empno=v_emp;
end;
Количественный анализ затрат на переключение контекста между двумя машинами представлен в главе 5 (см. раздел "Используйте PL/SQL для представления модели данных, а не для ее расширения"). При множественном связывании мы собираем несколько выполнений оператора DML в пакет для получения тех же результатов с меньшим количеством переключений контекста. Тот же блок, что был представлен выше, можно написать, как показано на рис. 4.2. declare type empno_list
is
table of number; emps empno_listeemplno_list(736 9,741 5§7210, 7311 0) ;
Рис. 4 . 2 . Уменьшение количества переключений контекстов
О со.
begin
Си
forall i in 1 ..4 delete from emp where empno=emps(i); end;
Эффективная обработка данных
197
Как только вы привыкнете использовать наборы в PL/SQL, начать использовать множественное связывание для выполнения операторов DML будет несложно, а производительность при этом может существенно повыситься. Множественное связывание можно делать с помощью наборов любого типа: вложенных таблиц, массивов переменной длины или ассоциативных массивов. Пример, приведенный ниже, показывает, как легко перейти от построчной обработки к множественному связыванию. Мы добавим 50000 записей в PL/SQL-набор, а затем поочередно вставим каждую строку в таблицу базы данных аналогичной структуры. Следующая таблица будет использоваться для вставки строк: SQL> create table BULK_BIND_TARGET ( 2 x number, 3 у date, 4 z varchar2(50) ); Table created.
Мы начнем с построчной версии, передавая строки из набора в таблицу, используя для прохода по строкам стандартный цикл FOR. КОД СОСТОИТ ИЗ двух частей. Первая часть просто заполняет набор начальными данными. SQL> declare 2 type numlist is table of number; 3 type datelist is varray(50000) of date; 4 type charlist is table of varchar2(50) 5 index by binary_inte.ger; 6 7 n numlist := numlist(}; 8 d datelist := datelist (); 9 с charlist; 10 begin 11 for i in 1 .. 50000 loop 12 n.extend; 13 n(i) := i; 14 d.extend; 15 d(i) := sysdate+i; 16 c(i) : = rpad(i,50); 17 end loop; 18
Вторая часть проходит по записям набора в цикле и вставляет их построчно в целевую таблицу. 19 20 21 22
for i in 1 .. 50000 loop insert into bulk_bind_target values (n(i), d(i), c(i)); end lbop; end;
23
/
PL/SQL procedure successfully completed. Elapsed: 00:00:07.03
198
Глава 4
Мы видим, что в этой системе можно добиться вставки со скоростью порядка 7000 строк в секунду. Можно ли добиться большего с помощью множественного связывания? Не сомневайтесь. А изменить код для использования множественного связывания можно несколькими нажатиями клавиш. Начальная часть кода для заполнения набора данными не изменяется. SQL> declare 2 type numlist is table of number; 3 type datelist is varray(50000) of date; 4 type charlist is table of varchar2(50) 5 index by binary_integer; 6 7 n numlist := numlist(); 8 d datelist := datelist(); 9 с charlist; 10 begin 11 for i in 1 .. 50000 loop 12 n.extend; 13 n(i) := i; 14 d.extend; 15 d(i) :« sysdate+i; 16 c(i) : = rpad(i,50); 17 end loop; 18
Чтобы использовать множественное связывание, надо заменить ключевое слово FOR словом FORALL и удалить обертку LOOP/END LOOP. (ЭТИ дополнительные ключевые слова не нужны, поскольку FORALL влияет только на следующий оператор DML,
так что область его действия определена неявно.) 19 20 21 22
forall i in 1 .. 50000 insert into bulk_bind_target values (n(i), d(i), c(i)); end; /
PL/SQL procedure successfully completed. Elapsed: 00:00:01.05
Неожиданно скорость обработки повысилась до 50000 строк в секунду! Как видим, множественное связывание повышает производительность. Судя по синтаксису, предполагается определенный вид построчной обработки — в идеальном мире соответствующий оператор выглядел бы примерно так: insert into bulk_bind_targer values( ALL n(i), ALL d(i), ALL c(l) )
Но это уже несущественная деталь. Учтите, что в версиях 8 и 9 наборы не должны быть разреженными, т.е. значения индекса должны идти подряд, но в версии Oracle 10g это ограничение снято. Множественное связывание поддерживается для вставки, изменения и удаления строк и должно рассматриваться как один из основных вариантов реализации для любого кода, выполняющего пакеты операторов DML.
Эффективная обработка данных
199
Помните об одном из основных принципов, представленных в главе 1, — предпочтительнее использовать SQL, а не PL/SQL, если это возможно. Использование SQL-оператора вместо PL/SQL-кода можно считать частным случаем множественного связывания, когда набор данных строится на основе таблицы.
Обработка ошибок при множественном связывании Помимо некоторого увеличения объема кода, при использовании множественного связывания недостатков немного. Дополнительные усилия потребуются, в частности, при обработке ошибок. Если ошибка в операторе DML возникает в ходе обычной построчной обработки, понятно, какая строка вызвала ошибку, — обрабатываемая сейчас! Однако при множественном связывании одна операция может использовать много строк в наборе, из которых одна или несколько — проблемные. Например, если мы вставляем строки в таблицу и столбец должен обязательно получить значение (т.е. он определен с ограничением NOT NULL), TO наличие в наборе, используемом для множественного связывания, элемента со значением NULL, вызовет ошибку. SQL> create table MANDATORY_COL ( 2 x number not null ); Table created. SQL > declare 2 type numlist is table of number 3 index by binary_integer; 4 n numlist; 5 begin 6 for i in 1 .. 50 loop 7 n(i) := i; 8 end loop; 10
n(37) := null;
— вызовет проблему
12 forall i in 1 .. 50 13 insert into MANDATORY_COL values (n(i)); 14 end; 15 / declare ERROR at line 1: ORA-01400: cannot insert NULL into ("MANDATORY_COL"."X") ORA.-06512: at line 12
Более того, по умолчанию все результаты множественной вставки откатываются, и в целевой таблице строк вообще не окажется. SQL> select * from no rows selected
MANDATORY_COL;
200
Глава 4
Очевидно, что проблему вызывает строка N (37), мы ведь явно задали в ней значение NULL. Но это лишь потому, что мы можем увидеть это в коде. В сообщении об ошибке нет и намека на то, что проблема связана именно с этим элементом, сказано, что, как минимум, в одном элементе набора оказалось значение NULL. Новая возможность версии 9 решает эту проблему. Добавлены ключевые слова SAVE EXCEPTIONS, позволяющие потребовать от оператора FORALL продолжить обработку после выявления строки, вызывающей ошибку, и сохранить список индексов элементов, при работе с которыми произошли ошибки. Давайте изменим пример, чтобы продемонстрировать использование этой возможности. SQL> set serverout on SQL> declare 2 type numlist is table of number 3 index by binary_integer; 4 n numlist; 5 begin 6 for i in 1 .. 50 loop 7 n(i) :- i; 8 end loop; 9 10 n(37) : = null; — will cause a problem 11
На этот раз при выполнении операции множественного связывания мы будем записывать все выявленные ошибки, а не просто отказываться от выполнения операции в целом. 12 13 14
forall i in 1 .. 50 save exceptions insert into MANDATORY_COL values (n(i));
Теперь, когда возникнет исключительная ситуация, у нас будет доступ к информации, необходимой для выяснения причины ошибки и, что еще важнее, индекс соответствующей строки в наборе. 15 exception when others then 16 dbms_output.put_line('Errors:'|Isql%bulk_exceptions.count); 17 for i in 1 .. sql%bulk_exceptions.count loop 18 dbms_output.put_line('index:'|Isql%bulk_exceptions(i).error_index) ; 19 dbms_output.put_line('code:'I Isql%bulk_exceptions(i).error_code); 20 dbms_output.put_line('message:'); 21 dbms_output.put_line(sqlerrm(sql%bulk_exceptions(i).error_code)); 22 end loop; 23 end; 24 / Errors: 1 index : 37 code: 1400 message: -1400: non-ORACLE exception PL/SQL procedure successfully completed.
Эффективная обработка данных
201
При использовании ключевых слов SAVE EXCEPTIONS В операторе FORALL любые ошибки попадают в новый набор, SQL%BULK_EXCEPTIONS, содержащий строку для каждой выявленной ошибки. Каждая строка содержит: > поле ERROR_INDEX — индекс элемента набора, использованного в FORALL; > поле ERROR_CODE — код ошибки Oracle. Кроме того, поскольку атрибут %BULK_EXCEPTIONS — набор, можно перехватывать и обрабатывать несколько ошибок. К сожалению, пример в документации версии 9.0 создает впечатление, что, поскольку мы откладываем обработку исключительных ситуаций, исключительная ситуация не будет возбуждаться. Это неверно. Текст в документации фактически правильный, но в коде примера просто пропущен раздел обработки исключительных ситуаций. Мы все равно попадаем в раздел обработки исключительных ситуаций после завершения выполнения оператора FORALL. Обратите внимание, что, поскольку в обработчике произошедшие ошибки обработаны, успешно вставленные строки останутся. SQL> select * from MANDATORY COL;
1 2 3 4 36 38 ... 50
Может ли множественное связывание навредить? Пару месяцев назад мы с женой решили вымостить дорожку перед нашим новым домом кирпичом. Наш местный поставщик очень любезно выгрузил груду кирпича на переднем дворе как можно дальше от того места, где кирпич предполагалось класть! Мне приходилось бегать между грудой кирпичей и дорожкой, поднося кирпичи жене, которая их укладывала. Носить их по одному было просто безумием — так мы бы никогда это дело не закончили. Мне скоро надоело, поэтому я достал из гаража тачку и начал возить кирпичи на ней. Это ускорило процесс укладки, но выявило также ряд проблем при "множественной обработке" кирпичей. 1. Потребовалось больше ресурсов (иными словами, усилий и пота!) для перемещения тачки, полной кирпичей, чем для переноса кирпичей по одному. 2. Моей жене пришлось выделять место рядом с дорожкой, чтобы я мог выгрузить кирпичи. 3. Моя жена сидела и ждала, пока я загружу и подкачу первую тачку с кирпичами. Причем, надо отметить, что этот факт волновал ее намного меньше, чем меня! 4. Процесс шел, несомненно, быстрее, чем при переносе кирпичей по одному, но не так быстро, как я предполагал — по мере добавления в тачку все большего
202
Глава 4
количества кирпичей я столкнулся с тем, что жена просто не успевает их укладывать. "Множественная обработка" кирпичей, несомненно, лучше беготни туда-сюда с одним кирпичом, но сил при этом требуется больше, а увеличение количества перевозимых за раз кирпичей не гарантирует ускорения работы в целом. Те же аргументы применимы и к средствам обработки массивом в языке PL/SQL. При обработке массива записей надо выделить память для массива записей, обрабатываемых одной операцией. Сотни или тысячи пользователей, одновременно выполняющих множественную обработку, могут легко привести систему в негодность. В следующем примере мы будем выбирать в набор все строки из представления ALL_OBJECTS, которое обычно содержит десятки тысяч строк: SQL> create or replace 2 procedure BULK_TEST is 3 mem used number; — 4 t number; 5 type recs is 6 table of ALL_OBJECTS%rowtype; 7 r recs; 8 cursor cl is select * from ALL_OBJECTS; 9 begin 10 t := dbms_utility.get_time; 11 open cl; 12 fetch cl 13 bulk collect into r; 14 close cl;
После выполнения множественной выборки можно обратиться к представлению V$MYSTATS, чтобы определить, сколько памяти потребовалось для сохранения всей
этой информации. 15 16 17 18 19 20 21 22
select value into mem_used from v$mystats where name = 'session pga memory max'; dbms_output.put_line('- Time: 'I I (dbms_utility.get_time-t)); dbms_output.put_line('- Max Mem: 'I|mem_used); end; /
Procedure created.
Теперь давайте выполним процедуру, чтобы увидеть результаты. SQL> set serverout on SQL> exec bulk_test - Time: 1911 - Max Mem: 98249020
Результат будет зависеть от количества строк в представлении ALL_OBJECTS, НО В данном случае множественная обработка потребовала примерно 100 Мбайт памяти!
Эффективная обработка данных
203
Представьте себе, что тысяча пользователей пытается одновременно выполнить эту процедуру... Для сервера придется покупать немало памяти! Конструкция LIMIT Как и в приведенном примере с укладкой кирпича, вместо попытки погрузить на тачку все кирпичи и перевезти их за раз можно добиться не худших результатов с помощью нескольких "ходок" с разумной загрузкой. Начиная с версии 8.1.6 Oracle, при множественной обработке поддерживается конструкция LIMIT, ограничивающая количество строк, обрабатываемых одной операцией. Это может показаться существенным ограничением производительности, но при множественных операциях действует закон сокращающихся доходов — больший размер набора не обязательно означает более высокую скорость работы. С помощью конструкции LIMIT МОЖНО сбалансировать производительность и использование памяти при множественной обработке. Следующая процедура LIMITJTEST будет выполнять множественную выборку из таблицы SRC с помощью выборок за раз массивов размером 1, 4, 16, 64, 256, 1024, 4096 и 16384 строки. Мы начали новый сеанс, чтобы гарантировать, что информация об использовании памяти сброшена в исходное состояние. SQL> create or replace 2 procedure LIMITJTEST is 3 mem_used number; 4 t number; 5 type recs is 6 table of SRC%rowtype; 7 r recs; 8 cursor cl is select * from SRC 9 begin 10 for i in 0 .. 7 loop 11 t := dbms utility.get time; — — 12 open cl; 13 loop 14 fetch cl 15 bulk collect into r 16 limit power(2,i*2); 17 exit when cl%notfound; 18 end loop; 19 close cl; 20 select value 21 into mem_used 22 from v$mystats 23 where name = 'session pga memory max'; 24 dbms_output.put_line('Rows:'IIpower (2,i*2)); 25 dbms_output.put_line('- Time: 'I I (dbms_utility.get_time-t)); 26 dbms output.put line('- Max Mem: 'I|mem used); — — — 27 end loop; 28 end; 29 / Procedure created.
204
Глава 4
Выполнение этой демонстрационной процедуры дает весьма интересные результаты. SQL> exec limit test — Rows: 1 - Time: 2482 - Max Mem: 566284 Rows:4 - Time: 2065 - Max Mem: 566284 Rows:16 - Time: 1915 - Max Mem: 697892 Rows:64 - Time: 1920 - Max Mem: 936532 Rows:256 - Time: 1890 - Max Mem: 2014508 Rows:1024 - Time: 1917 - Max Mem: 5288324 Rows:4096 - Time: 1921 - Max Mem: 10372420 Rows:16384 - Time: 1885 - Max Mem: 16056916 PL/SQL procedure successfully completed.
В этом примере, хотя максимальная производительность и была достигнута при наибольшем размере массива, все результаты со значением LIMIT более 16 отличались от оптимального не более чем на 1 процент. Но обратите внимание на объем используемой памяти при увеличении размера массива: выборка по 16384 строки за раз требует 16 Мбайтов памяти для каждого пользователя. Это, несомненно, помешает масштабируемости! При создании приложений, если (как мы надеемся) вы используете множественную обработку, потратьте время на оценку количества выбираемых строк (будет ли оно большим или будет увеличиваться со временем с ростом базы данных). Если да, сразу же добавьте конструкцию LIMIT; ЭТО ничего не стоит сделать, но обеспечивается защита от использования непредвиденных объемов памяти в будущем. Учтите, что при использовании множественной выборки с конструкцией LIMIT атрибут курсора %NOTFOUND устанавливается, когда количество выбранных строк меньше указанного предела. Так что, если проверка атрибута %NOTFOUND выполняется сразу после оператора FETCH, ВЫ, вероятно, будете обрабатывать не все данные. Например, если в таблице 20 строк, а ваш код имеет вид loop
fetch cursor
Эффективная обработка данных 205 bulk collect into resultset limit 50; exit when cursor%notfound; (обработка) end loop;
вы никогда не попадете в раздел кода (обработка), поскольку при выборке двадцатой — последней — строки устанавливается атрибут %NOTFOUND. Надо выходить из цикла после обработки: loop fetch cursor bulk collect into resultset limit 50; (обработка) exit when cursor%notfound; end loop;
•
Помните, что реляционные базы данных были созданы на основе теории и исходя из необходимости работы с множествами данных. Средства множественной обработки в языке PL/SQL обеспечивают эту возможность. За счет множественной обработки мы можем приблизиться к аксиоме о том, что любое множество должно обрабатываться автоматически. Множественная обработка более тесно связывает PL/SQL-приложения с множествами данных и в большинстве случаев повышает производительность без особых затрат.
Передача переменных между PL/SQL-модулями Прелесть поддержки больших и сложных структур данных в языке PL/SQL состоит в том, что эти структуры так же легко передавать между разными компонентами приложения, как если бы они были простыми скалярными переменными. Как мы вскоре покажем, надо решить некоторые проблемы, прежде чем планировать передачу больших наборов между PL/SQL-модулями. Однако следует также помнить, что есть ряд особенностей и при передаче простых переменных или переменных, объявленных с помощью атрибута %ROWTYPE. Примечание В этом разделе мы не собираемся описывать различные способы передачи параметров между PL/SQL-подпрограммами; официальной документации по этой теме более чем достаточно. Кратко излагая документацию, параметры можно передавать как: > > >
IN — значение передается в процедуру; OUT —значение получается из процедуры; IN OUT —аналог "передачи по ссылке". •
Передача параметров, объявленных с помощью атрибутов %TYPE и %ROWTYPE Как мы уже упоминали ранее в этой главе, любая переменная, связанная со столбцом или строкой в базе данных, по идее, должна быть объявлена с помощью атрибута %TYPE или %ROWTYPE. Когда же дело доходит до объявления параметров, реше-
206
Глава 4
ние использовать эти атрибуты менее определенно. Чтобы объяснить, почему, давайте рассмотрим типичный сценарий, когда параметр объявлен с использованием атрибута %TYPE. Сначала мы создаем таблицу, TEN_BYTE_COLUMN, которая, как легко можно предположить, содержит один столбец с максимальной длиной 10 байтов. SQL> create table TEN_BYTE_COLUMN ( 2 col varchar2(10)); Table created.
Затем мы создаем простую процедуру ADD_ROW ДЛЯ добавления строки в эту таблицу. Процедура принимает параметр P_COL, объявленный с использованием атрибута %TYPE. (Иными словами, его тип данных совпадает с типом данных столбца COL в нашей таблице.) SQL> 2 3 4 5 6 7
create or replace procedure ADD_ROW(p_col TEN_BYTE_COLUMN.COL%TYPE) begin insert into TEN_BYTE_COLUMN values (p_col); end; /
is
Procedure created.
Вставка литерала длиной 10 байтов срабатывает, как и ожидалось. SQL> exec add_row('0123456789'); PL/SQL procedure successfully completed.
Однако если передать, как параметр, большее значение: SQL> exec add_row('01234567890123456789') ; BEGIN add row('01234567890123456789'); END;
ERROR at line 1: ORA-01401: inserted value too large for column ORA-06512: at "ADD_ROW", line 3 ORA-06512: at line 1
Обратите внимание, что, судя по номеру строки, в которой произошла ошибка, значение параметра было пропущено в процедуру, а ошибка произошла лишь при использовании этого параметра в операторе INSERT (В строке 3). Использование при объявлении параметра атрибута %TYPE означает проверку только типа данных переданного параметра, но не точности (длины) соответствующего столбца базы данных. То, что типы данных проверяются, можно доказать с помощью аналогичного теста, в котором столбец COL В таблице TEN_SIG_DIGITS имеет числовой тип данных, а мы пытаемся передать процедуре нечисловое значение. SQLSQL> create or replace 2 procedure ADD NUM_ROW(p col TEN SIG DIGITS.COL%TYPE) is 3 begin
Эффективная обработка данных 207 4 5 6 7
insert into TEN_SIG_DIGITS values (p_col); end; /
Procedure created. SQL> exec add num row('asd'); BEGIN add num row('asd'); END;
ERROR at line 1: ORA-06502: PL/SQL: numeric or value error: character to number conversion error ORA-06512: at line 1
Обратите внимание, что ошибка произошла в строке 1, а не 3, как в предыдущем примере, и что это ошибка PL/SQL, а не сервера. Проблема возникает только с параметрами; связанные с базой данных локальные переменные проверяются полностью. Создание простейшей процедуры с локальной переменной вместо параметра позволяет это продемонстрировать. SQL> 2 3 4 5 6 7
create or replace procedure LOCAL_TYPE_VAR is x ten_byte_column.col%type; begin x := '01234567890123456789'; end; /
Procedure created. SQL> exec local_type_var; BEGIN local_type_var; END; * ERROR at line 1: ORA-06502: PL/SQL: numeric or value error: character string buffer too small ORA-06512: at "LOCAL_TYPE_VAR", line 4 ORA-06512: at line 1
Мы предполагаем, что причина этого ослабления правил проверки типов для параметров связана с предположением, что любая переменная, которая будет передаваться в PL/SQL-процедуру, будет определена с привязкой к типу столбца и тем самым допустимость ее точности и размера уже будет проверена. Предупреждение У нас нет доказательства этого утверждения, поэтому, пожалуйста, не принимайте его как несомненный факт.
208
Глава 4
Аналогично, как и для любого выражения, сервер Oracle пытается автоматически преобразовать типы данных с параметрами, тип которых привязан к базе данных, так что вызовы вроде SQL> exec add_row(sysdate); — —
помните, что процедура принимает параметр типа varchar2
не считаются ошибочными — значение SYSDATE неявно преобразуется в VARCHAR2 перед вставкой в таблицу. Итак, объявление параметра с использованием атрибута %TYPE дает два результата: > проверяется только тип данных, но не точность (длина) значения; > подпрограмма PL/SQL включается в дерево зависимостей от соответствующей таблицы.3 Если вы уже читали главу 2, надеемся, вы думаете, что второй результат, несомненно, положительный. Однако указание атрибута %TYPE ДЛЯ параметров дает интересную аномалию, связанную с тем, что при его использовании в пакете нарушается одно из основных преимуществ пакетов — защита от проблем зависимостей за счет разделения кода на спецификацию и тело. Например, если две только что определенных процедуры поместить в пакет, мы получим следующий код: SQL> 2 3 4 5 6
create or replace package PKG is procedure ADD_ROW(p_col TEN_BYTE_COLUMN.COL%TYPE); procedure ADD_NUM_ROW(p_col TEN_SIG_DIGITS.COIATYPE); end; /
Package created. SQL> 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
create or replace package body PKG is procedure ADD_ROW(p_col TEN_BYTE_COLUMN.COL%TYPE) begin insert into TEN_BYTE_COLUMN values (p_col) ; end;
is
procedure ADD_NUM_ROW(p_col TEN_SIG_DIGITS.COL%TYPE) begin insert into TEN_SIG_DIGITS values (p_col); end;
is
end;
' Интересная особенность, на которую мне указал Том Кайт: если атрибут %ТУРЕ для параметра процедуры ссылается на переменную в спецификации пакета и эта переменная объявлена как not null, ограничение сохранится и для параметра (тогда как аналогичный атрибут %TYPE, ссылающийся на столбец таблицы с ограничением not null, не позволяет сохранить это ограничение).
Эффективная обработка данных 17
209
/
Package body created.
Просмотрев цепочку зависимостей, мы видим, что спецификация пакета тоже начинает зависеть от соответствующих таблиц. SQL> 2 3 4 5 6
select name, type from user_dependencies where referenced_name in ( 'TEN_BYTE_COLUMN', 'TEN_SIG_DIGITS') and name like 'PKG%'
NAME
TYPE
PKG PKG PKG PKG
PACKAGE PACKAGE BODY PACKAGE PACKAGE BODY
Это интересный побочный эффект, но он неизбежен, поскольку если мы удалим столбец COL из любой таблицы, спецификация пакета станет некорректной. Сравните это с результатами, которые получаются, когда атрибут %TYPE при объявлении параметров не используется. SQL> 2 3 4 5 6
create or replace package PKG is procedure ADD_ROW(p_col varchar2); procedure ADD_NUM_ROW(p_col number); end; /
Package created. SQL> 2 3 4 5 6 7 8 10 11 12 13 14 15 16 17
create or replace package body PKG is procedure ADD_ROW(p_col varchar2) is begin insert into TEN_BYTE_COLUMN values (p col); end; procedure ADD_NUM_ROW(p_col number) is begin insert into TEN_SIG_DIGITS values (p_col); end; end; /
Package body created.
210
Глава 4
Теперь при проверке дерева зависимостей спецификация пакета отсутствует. SQL> s e l e c t name, type
2 3 4 5 6
from user_dependencies where referenced_name in ( 'TEN_BYTE_COLUMN', 'TEN_SIG_DIGITS') and name like 'PKG%';
NAME
TYPE
PKG PKG
PACKAGE BODY PACKAGE BODY
Что же лучше? Используя классическое для Oracle клише "это зависит...", мы не утверждаем, что вы не должны использовать атрибут %TYPE ДЛЯ параметров; не стоит использовать его без разбора для всех параметров без учета последствий. Если в проекте требуется максимально учитывать зависимости PL/SQL-программ от таблиц базы данных, с которыми они работают, для анализа последствий изменения, использование параметров с атрибутом %TYPE В спецификации пакета будет оправданным. Например, пусть процедура ADD_ROW использует динамический SQL вместо статического. Одна из проблем с динамическим SQL состоит в том, что при его использовании обычно теряется необходимая информация о зависимостях. Использование параметра, объявленного с использованием атрибута %TYPE, гарантирует, что процедура не выпадет из цепочки зависимостей. С другой стороны, учтите, что если вы используете разделение спецификации и тела пакета для ограничения влияния изменения в базе данных, применение параметров, объявленных с использованием атрибута %TYPE, связывает с объектом как тело, так и спецификацию пакета, тем самым сводя на нет те преимущества, которые вы пытаетесь получить. Аналогичная ситуация возникает и при использовании атрибута %ROWTYPE В спецификации пакета: это может вызвать проблемы с цепочкой зависимостей. Их решить уже сложнее — тогда как для скалярной переменной атрибут %TYPE МОЖНО заменить соответствующим скалярным типом данных, для параметра, объявленного с использованием атрибута %ROWTYPE, такая замена невозможна.
Передача наборов как параметров В документации по языку PL/SQL утверждается, что передача больших наборов, как параметров, снижает производительность по сравнению с передачей скалярных переменных, и это утверждение соответствует здравому смыслу. Однако ряд простых тестов показывает, что производительность остается сопоставимой. Давайте создадим определение набора как таблицы REC_LIST записей типа REC. SQL> create or replace 2 type rec is object 3 ( a number, 4 b number, 5 с varchar2(30));
Эффективная обработка данных 6
211
/
Type created. SQL> 2 3 4
create or replace type rec_list is table of rec; /
Type created.
Затем создадим две процедуры: SIMPLEPARM, которая принимает в качестве параметра скалярное значение, и BIGPARM, которая принимает набор только что определенного типа REC_LIST. SQL> c r e a t e or replace procedure SIMPLE_PARM(p number) x number; begin null; end;
is
Procedure created. SQL> create or replace 2 procedure BIG_PARM(p rec_list) is 3 x number; 4 begin null; end;
Procedure created.
Для оценки затрат на передачу набора по сравнению с передачей значения простого скалярного типа мы создадим набор из 50000 элементов, а затем запишем время выполнения последовательности вызовов процедур SIMPLE_PARM И BIG_PARM. SQL> declare 2 х rec_list := rec list () ; tl number; 3 4 t2 number; 5 begin 6 x.extend(50000); 7 for i in 1 .. 50000 loop 8 x(i) := ]:ec (i, i, rpad (i, 30) ) end loop; 9 10 tl := dbms utility.get time; for i in 1 .. 500000 loop 11 12 simple parm(i); end loop; 13
212
Глава 4
14 t2 := dbms_utility.get_time; 15 dbms_output.put_line('Simple: 'I I ( t 2 - t l ) ) ; 16 for i in 1 .. 500000 loop 17 big_parm(x); 18 end loop; 19 tl := dbms_utility.get_time; 20 dbms_output.put_line('Collection:'||(tl-t2)); 21 end; 22 / Simple: 62 Collection: 50 PL/SQL procedure successfully completed.
Очень интересный результат! Повторные тесты показывают, что передача набора в качестве параметра выполняется быстрее, чем передача скалярного параметра. Однако, возможно, тут срабатывает оптимизация, потому что к параметрам в вызываемых процедурах не обращаются. Повторим тесты с измененными процедурами, в которых переданные параметры используются. SQL> 2 3 4 5 6 7
create or replace procedure SIMPLE_PARM(p number) is x number; begin x := p; end; /
Procedure created. SQL> 2 3 4 5 6 7
create or replace procedure BIG_PARM(p Rec_list) is x number; begin x := p(l) .a; end; /
Procedure created.
Давайте посмотрим, не пропал ли обнаруженный нами ранее эффект. SQL> declare 2 х rec list := rec listO ; 3 tl number; 4 t2 number; 5 begin 6 for i in 1 . . 50000 loop 7 x.extend; 3 x(i) := rec(i,i,rpad(i,30) 9 end loop; 10 tl := dbms utility.get time; for i in 1 . . 500000 loop 11
Эффективная обработка данных 213 12 simple_parm(i); 13 end loop; 14 t2 := dbms_utility.get_time; 15 dbms_output.put_line('Simple: '|I(t2-tl)); 16 for i in 1 .. 500000 loop 17 big_parm(x); 18 end loop; 19 tl := dbms_utility.get_time; 20 dbms_output.put_line('Collection:'|I(tl-t2)); 21 end; 22 / Simple: 97 Collection:131 PL/SQL procedure successfully completed.
Этот результат, пожалуй, больше соответствует ожидаемому. Однако 500000 вызовов немногим больше, чем за секунду, — это очень впечатляет. Ситуация сильно меняется при передаче больших параметров, таких, как использованные в этом примере наборы, в режиме IN OUT. Давайте посмотрим, как повлияет на производительность передача параметров в режиме IN OUT. МЫ изменим процедуры SIMPLE_PARM и BIG_PARM так, чтобы с переданными параметрами не выполнялось никаких действий, но параметры теперь определены как IN OUT. SQL> 2 3 4 5 6
create or replace procedure SIMPLE PARM(p in out number) is begin null; end; /
Procedure created. SQL> 2 3 4 5 6
create or replace procedure BIG_PARM(p in out Rec_list) is begin null; end; /
Procedure created.
Теперь можно снова выполнить тесты. Нам пришлось отказаться от теста, в котором каждая процедура выполнялась 500000 раз — он так и не завершился. Фактически, чтобы вызовы процедуры BIG_PARM были выполнены за разумное время, пришлось сократить количество итераций в тесте до 50! Мы уменьшили количество итераций для процедуры SIMPLE_PARM ДО тех же 50-ти, чтобы можно было сравнить результаты. SQL> declare 2 х rec list := rec list();
214
Глава 4 3 s number := 1; 4 tl number; 5 t2 number; 6 begin 7 for i in 1 .. 50000 loop 8 x.extend; 9 x(i) := rec(i,i,rpad(i,30)) ; 10 end loop; 11 tl := dbms_utility.get_time; 12 for i in 1 .. 50 loop 13 simple_parm(s); 14 end loop; 15 t2 := dbms_utility.get_time; 16 dbms_output.put_line('Simple: ' | I (t2-tl) ) ; 17 for i in 1 .. 50 loop 18 big_parm(x); 19 end loop; 20 tl := dbms_utility.get_time; 21 dbms_output.put_line('Collection:'||(tl-t2)); 22 end; 23 / Simple: 0 Collection:1118 PL/SQL procedure successfully completed.
Жалких 50 вызовов потребовали 11 секунд! Тестирование при количестве элементов в наборе REC_LIST от 1000 до 50000 показали, что снижение производительности прямо пропорционально объему переданных данных, как показано на рис. 4.3. Передача параметра процедуре в режиме IN OUT требует намного больше действий потому, что PL/SQL-машина должна сохранять копию значения параметра до начала выполнения процедуры. Она нужна потому, что если процедура завершится сбоем и будет возбуждена исключительная ситуация, потребуется восстановить исходное значение параметра. Производительность при передаче набора в режиме IN OUT 500 400 300 200 100 0
S m
Я 2
оч-
2 ч чт> е
Тысяч элементов в наборе
Рис. 4 . 3 . Производительность при передаче больших наборов в качестве параметров
Эффективная обработка данных
215
Использование подсказки компилятору NOCOPY В таких ситуациях имеет смысл использовать подсказку компилятору NOCOPY. Она требует от компилятора не делать резервную копию переданного параметра. Производительность при этом резко повышается. Обратите внимание, что в следующем примере мы снова выполняем 500000 итераций вместо 50-ти. Процедура SIMPLEPARM остается неизменной, а вот процедура BIG_PARM теперь компилируется с подсказкой NOCOPY для параметра. SQL> 2 3 4 5 6
create or replace procedure BIG_PARM(p in out nocopy rec_list) is begin null; end; /
Procedure created. SQL> declare 2 x rec_list := rec_list(); 3 tl number; 4 t2 number; 5 begin 6 for i in 1 .. 50000 loop 7 x.extend; 8 x(i) := rec(i,i,rpad(i,30)); 9 end loop; 10 t2 := dbms_utility.get_time; 11 for i in 1 .. 500000 loop 12 big_parm(x); 13 end loop; 14 tl := dbms_utility.get_time; 15 dbms_output.put_line('Collection:'||(tl-t2)); 16 end; 17 / Collection:49
Производительность вернулась к показателям, близким к тем, что получены для простого скалярного параметра. Это не означает, что все наборы надо передавать с подсказкой NOCOPY. В частности, надо быть осторожным с подсказкой NOCOPY при обработке ошибок в процедурах. Как было сказано, при отсутствии подсказки NOCOPY параметр, переданный в режиме IN OUT, получит в случае сбоя процедуры то же значение, которое было до вызова процедуры. Давайте, например, изменим процедуру BIG_PARM, удалив подсказку NOCOPY и специально вызывая ошибку после изменения переданного параметра. Мы изменим значение одного из элементов набора с z на Q, а затем выполним деление на ноль. SQL> c r e a t e or replace 2 procedure BIG_PARM(p in out Rec_list) 3 1 number; 4 begin
is
216
Глава 4 5 6 7 8
р(2).с : = 'z'; — поменять 'q' на 'z1 1 := 1/0; — возбудит исключительную ситуацию (т.е. деление на — ноль) end; /
Procedure created.
Теперь выполним автономный блок для перехвата ошибки в процедуре BIG_PARM и проверим, какие изменения произошли в переменной, переданной процедуре BIG_PARM. SQL> declare 2 х rec_list; 3 begin 4 x := rec_list( 5 rec(l,l,'p'), 6 rec(2,2,1q'), 7 rec (3,3, 'г')); 8 big_parm(x); 9 exception when others then 10 for i in 1 .. 3 loop 11 dbms_output.put_line(x(i).c); 12 end loop; 13 end; 14 / P q •*- Значение не изменилось, поскольку в процедуре BIG_PARM произошла исключительная ситуация г
Обратите внимание, что присвоение значения z было отменено. Второму элементу в наборе вернули исходное значение Q. Однако при указании подсказки NOCOPY результат будет непредсказуем. Значения могут быть восстановлены, но это не гарантируется. Повторение представленного выше примера с добавлением подсказки NOCOPY в процедуру BIG_PARM дает другой результат. SQL> 2 3 4 5 6 7 8
create or replace procedure BIG_PARM(p in out nocopy Rec_list) is 1 number; begin 1 p(2) .c := 'z ; 1 := 1/0; end; /
Procedure created. SQL> declare 2 x rec_list; 3 begin 4 x := rec list(
Эффективная обработка данных 217 5 6 7 8 9 10 11 12 13
rec(l,l,'p'), rec(2,2,'q'), rec(3,3,T')); big parm(x); — exception for i in when 1 .. others 3 loop then dbms_output.put_line(x(i) .c) ; end loop; end;
P z — Значение изменилось, хотя в процедуре BIG_PARM и произошел сбой! г
Значение параметра не сбрасывается в исходное. Результаты использования названы в документации непредсказуемыми, потому что это — подсказка компилятору. Компилятор PL/SQL не обязан ее учитывать. Даже с учетом этой проблемы подсказка NOCOPY используется намного реже, чем могла бы. Можно надеяться, что во множестве приложений большая часть вызовов процедур заканчивается успешно. Если восстановление значений в случае ошибки обязательно, часть преимуществ производительности, которые дает подсказка NOCOPY, МОЖНО получить путем создания собственной версии NOCOPY, т.е. копируя самостоятельно значения переменных и восстанавливая их в случае ошибки. Это продемонстрировано в следующем примере: NOCOPY
SQL> declare 2 х rec list := rec list(); 3 orig x rec list; А tl number; 5 t2 number; 6 begin 7 for i in 1 .. 50000 loop 8 x.extend; 9 x(i) := rec(i,i,rpad(i,30)); 10 end loop; 11 t2 := dbms utility.get time; 12 for i in 1 . . 50 loop 13 orig x := x; 14 begin 15 big parm(x); exception when others then 16 x := orig x; 17 18 end; 19 end loop; 20 tl := dbms utility.get time; 21 dbms output.put line('Collection:' 1 i (tl-t2)); 22 end;
23
/
Collection:553
218
Глава 4
Хотя этот пример показывает, что производительность существенно снижается при копировании набора, если в большинстве случаев вызов процедуры BIGPARM заканчивается успешно, полученное решение все равно работает вдвое быстрее обычной передачи параметров в режиме IN OUT. Есть ряд ограничений на то, какие переменные и в каких контекстах можно передавать как параметры с подсказкой NOCOPY СПИСОК ограничений представлен в справочном руководстве по языку PL/SQL. Если вы нарушите одно из этих ограничений, никакие сообщения об ошибках получены не будут; подсказка NOCOPY просто будет проигнорирована.
Обработка транзакций в PL/SQL Большую часть этой главы мы посвятили обсуждению механизмов выборки данных из базы и передаче данных между PL/SQL-модулями. Можно завершить обсуждение обработки данных, изучив некоторые аспекты управления транзакциями в языке PL/SQL. Рассмотрим следующую процедуру, используемую для отслеживания изменений информации о сотрудниках. Мы будем отслеживать суммарную зарплату в каждом отделе с помощью нового столбца TOT_SAL В таблице DEPT. SQL> @$ORACLE_HOME\sqlplus\demo\demobld Building demonstration tables. Please wait. Demonstration table build is complete. SQL> alter table dept add tot_sal number; Table altered. SQL> update dept 2 set tot_sal = ( select sum(sal) 3 from emp where deptno = dept.deptno ); 4 rows updated.
Мы также будем записывать самое последнее изменение строки для сотрудника в таблице EMP_DELTAS И вести аудит всех изменений в таблице EMP_AUDIT. SQL> c r e a t e t a b l e EMP_AUDIT ( 2 date_rec date, 3 empno number, 4 sal number(4)); Table created. SQL> create table EMPJDELTAS 2
( empno number, change_type varchar2(10));
Table created. SQL> insert into emp_deltas 2 select empno, null from emp; 14 rows created.
Эффективная обработка данных 219
Теперь можно написать процедуру для синхронизации всех этих таблиц при изменении зарплаты сотрудника. SQL> create or replace 2 procedure UPDATE_EMP(p_empno number, p sal number) is 3 begin 4 update DEPT 5 set TOT_SAL = TOT_SAL + 6 ( select p_sal-sal 7 from EMP 8 where empno = p_empno ) 9 where deptno = ( select deptno 10 from EMP 11 where empno = p_empno); 12 13 update EMP 14 set sal = p_sal 15 where empno = p_empno; 16 17 update EMP_DELTAS 18 set change_type = 'SAL' 19 where empno = p_empno; 20 21 insert into EMP_AUDIT 22 values (sysdate,p_empno,p_sal); 23 24 exception 25 when o t h e r s then 26 rollback; 27 raise; 28 end; 29 /
Procedure created.
He вникая особенно в эффективность или неэффективность используемых SQLоператоров, можно сразу найти проблему в коде. Проблема связана со строками. 24 25 26 27
exception when o t h e r s then rollback; raise;
Совершенно ясно, что требуется, чтобы при возникновении ошибки в любом из операторов DML в процедуре любые уже выполненные изменения были отменены, а ошибка распространялась в вызывающую среду. Представленный код абсолютно не нужен и может даже угрожать целостности данных, как вскоре будет показано. Одно из замечательных свойств языка PL/SQL состоит в том, что любой блок кода всегда рассматривается как логическая единица работы — он либо выполняется успешно, либо не выполняется, но выполнен частично быть не может. Мы используем только что представленный пример для доказательства того, что блок PL/SQL работает именно так. Мы изменили код, выбросив обработчик исключительных
220
Глава 4
ситуаций, и перекомпилировали процедуру. Сначала нам надо убедиться, что процедура работает в нормальном режиме и изменяет те строки, что мы и предполагали. SQL> exec update_emp(7499,2000); PL/SQL procedure successfully completed. SQL> select empno, deptno, sal 2 from emp 3 where empno = 7499; EMPNO
DEPTNO
7499
30
SAL 2000
SQL> select * 2 from emp audit; DATE_REC 22/JUN/03
EMPNO
SAL
7499
2000
SQL> select * 2 from emp_deltas; EMPNO CHANGEJTYP 7369 7499 SAL 7521
Никаких проблем! Все таблицы изменены, как и ожидалось. Теперь давайте выполним процедуру и явно вызовем ошибку, передавая значение зарплаты, слишком большое для таблицы EMP_AUDIT. Обратите внимание на исходный код: эта таблица — последняя из изменяемых процедурой, так что изменение таблиц Е;МР И EMP_DELTAS будет успешно выполнено до того, как произойдет сбой при вставке строки в таблицу EMP_AUDIT. SQL> exec update_emp(7698,99999); BEGIN update_emp(7698,99999); END;
ERROR at line 1: ORA-01438: value larger than specified precision allows for this column ORA-06512: at "UPDATE_EMP", line 20 ORA-06512: at line 1
Ошибка произошла в строке 20, но в каком состоянии находится база данных? Надо ли отменять результаты выполнения первых двух операторов DML, выполненных для сотрудника с номером 7698? Давайте посмотрим.
Эффективная обработка данных
221
SQL> select empno, deptno, sal 2 from emp 3
where empno » 7698; EMPNO
DEPTNO
SAL
7698
30
2850
SQL> select * 2 from emp_audit; DATE REC EMPNO
SAL
z 22/JUN/03
7499
2000
SQL> select * 2 from emp_deltas; EMPNO CHANGE TYP 7369 7499 SAL 7521 7566 7654 7698
Никаких изменений нет — все действия, выполненные PL/SQL-блоком, отменены. Важно понимать этот принцип, поскольку безоглядное использование отката в PL/SQL-блоке обычно приводит к нежелательным побочным эффектам в вызывающей среде. Именно так можно нарушить целостность данных. Давайте вернем в код обработчик исключительных ситуаций и посмотрим, что произойдет, если транзакция была начата до выполнения процедуры UPDATE_EMP. Пусть мы хотим удалить сотрудника с номером 7369. SQL> delete from emp — независимое изменение таблицы emp 2> where empno = 7369; 1 row deleted.
Мы переходим к изменению информации о сотруднике с номером 7698, устанавливая ему слишком большую зарплату (что вызовет ошибку и переход в раздел обработки исключительных ситуаций, где изменения откатываются). SQL> exec update_emp(7698,99999) ; BEGIN update_emp(7698,99999); END; * ERROR at line 1: ORA-01438: value larger than specified precision allows for this column ORA-06512: at " UPDATE_EMP", line 26 ORA-06512: at line 1
222
Глава 4
Любые изменения, выполненные процедурой UPDATE_EMP, были отменены. Как там транзакция, которая была начата до вызова процедуры, т.е. удаление сотрудника с номером 7369? SQL> select * from emp 2 where empno = 7369; EMPNO ENAME 7369 SMITH
JOB CLERK
MGR HIREDATE
SAL
7902 17/DEC/80
800
COMM
Сотрудник 7369 чудесным образом снова появился! PL/SQL-блок откатил не только свои изменения, но и все предшествующие незафиксированные изменения. Все может быть еще хуже. Если бы процедура выполняла откат при выявлении ошибки и не передавала ошибку в вызывающую среду, откатывалась бы активная на момент вызова процедуры транзакция, а вызывающая среда об этом даже и не узнала бы! Как видите, обработка транзакций в PL/SQL существенно связана с тем, как написаны обработчики исключительных ситуаций в приложении. Неявный откат выполненных в PL/SQL-процедуре изменений выполняется, если ошибка не была перехвачена обработчиком. Даже при наличии исправленного кода (обработчика исключительных ситуаций в процедуре UPDATE_EMP нет) проблемы могут возникнуть, если обработчики исключительных ситуаций пишутся без учета транзакций. Например, рассмотрим изменение поведения транзакции при использовании вызова процедуры UPDATE_EMP в блоке с обработчиком, который перехватывает (и игнорирует) все ошибки. SQL> 2 3 4 5
begin update_emp(7698,99999); exception when others then end; /
null;
PL/SQL procedure successfully completed.
Поскольку не было никаких ошибок, откат изменений не происходит. В этом случае мы создали очень нежелательную ситуацию, когда изменение зарплаты сотрудника произошло без внесения соответствующей записи аудита в таблицу EMP_AUDIT. SQL> select * from emp 2 where empno = 7698; EMPNO ENAME 7698 BLAKE
JOB MANAGER
SQL> select * from emp_audit 2 where empno = 7698; no rows selected
MGR HIREDATE 7839 01/MAY/81
SAL 99999
Эффективная обработка данных
223
Единственное достойное место для операторов, завершающих транзакцию оператор DDL), — в вызывающей среде верхнего уровня, т.е. в исходном приложении. Любая программа (на языке PL/SQL или на любом другом), которая может быть вызвана с верхнего уровня, не должна явно завершать транзакцию из-за возможных нежелательных побочных эффектов. Неиспользование операторов COMMIT и ROLLBACK в PL/SQL-коде также побуждает разработчиков создавать приложения, в которых транзакции фиксируются только при необходимости, а не походя. Единственное исключение из этого правила — автономные транзакции, которые мы рассмотрим далее. (COMMIT, ROLLBACK,
Автономные транзакции Если операторы управления транзакцией (COMMIT, ROLLBACK) ДОЛЖНЫ использоваться только в вызывающей среде верхнего уровня, как быть с автономными транзакциями, с учетом того, что в них явный оператор COMMIT ИЛИ ROLLBACK должен быть выполнен обязательно? Мы утверждаем, что даже автономные транзакции соответствуют представленному принципу, поскольку процедура, объявленная как автономная, никогда не будет частью родительской транзакции, и тем самым мы все равно фиксируем транзакцию, когда она логически завершена. Этот раздел будет очень коротким. Насчет автономных транзакций у нас сложилась четкая аксиома — их используют слишком часто. Утверждается, что автономные транзакции идеально подходят для использования в четырех ситуациях, в трех из которых их использование приводит к ошибкам; и только в четвертой их применение, возможно, оправдано.
Обход ошибок мутирующей таблицы в триггерах Вопреки популярному мнению, корпорация Oracle не придумала ошибку мутирующей таблицы, только чтобы навредить разработчикам! Ошибки мутирующей таблицы не позволяют SQL-операторам в строчном триггере получать несогласованное представление данных. Аналогично автономные транзакции тоже избавляют от несогласованного представления данных, прежде всего потому, что просто не видят любые незафиксированные изменения, которые привели к срабатыванию триггера. Вот почему автономные транзакции не подходят для решения проблемы мутирующей таблицы, как описано в документе Metalink Note: 65961.1. "Поскольку все изменения базы данных являются частью транзакции, если родительская транзакция изменила данные, но не зафиксировала изменения на момент начала автономной транзакции, эти изменения в порожденной транзакции невидимы. Важно помнить, что если триггер работает как автономная транзакция, то, хотя он и имеет доступ к соответствующим значениям .OLD и :NEW, он не видит строк, вставленных в таблицу вызывающей транзакцией. Поэтому использование автономного триггера для получения максимального значения, находящегося сейчас в таблице... [иными словами, типичная задача, решение которой приводит к ошибке мутирующей таблицы]... вряд ли сработает".
224
Глава 4
Выполнение оператора DDL в транзакции При выполнении оператора DDL сервер всегда выполняет COMMIT ДО И после оператора, поэтому для того, чтобы это выполнение не влияло на существующую незафиксированную транзакцию, логичным представляется поместить оператор DDL в процедуру, объявленную как автономная транзакция. Хотя это, похоже, решает поставленную задачу, но ничем не отличается от выполнения соответствующего оператора DDL в другом сеансе; если исходная транзакция будет отменена, результат выполнения DDL останется, как продемонстрировано далее. Мы создадим процедуру, которую можно использовать для добавления столбца в таблицу, но мы хотим отслеживать добавляемые столбцы в таблице LIST_OF_CHANGES. SQL> create table list_of_changes 2 ( tname varchar2(30) , 3 cname varchar2(30), 4 changed date); Table created.
Чтобы можно было выполнять операторы DDL, не влияя на текущую транзакцию, мы (ошибочно) создаем процедуру RUN_DDL ДЛЯ выполнения любого переданного в качестве параметра оператора DDL в рамках автономной транзакции. SQL> 2 3 4 5 6
create or replace procedure RUN_DDL(m varchar2) is pragma autonomous_transaction; begin execute immediate m; end;
Procedure created.
Мы создаем процедуру ADD_COLUMN, которая будет записывать информацию об изменении столбцов в таблицу LIST_OF_CHANGES, а затем вызывать RUN_DDL для выполнения изменения. Чтобы сымитировать происходящее при сбое этой процедуры, мы намеренно добавили ошибку (деление на ноль) в конец кода. SQL> 2 3 4 5 6 7 8 9 10 11 12
create or replace procedure ADD_COLUMN(p_table varchar2, p_column varchar2) is v number; begin insert into LIST_OF_CHANGES values (p_table, p_column, sysdate); run_ddl( 'alter table 'I|p_table|I' add '||p_column); v := 1/0; — происходит ошибка, и вставка отменяется end; /
Procedure created.
Эффективная обработка данных
225
Теперь добавляем столбец NEWCOL В таблицу ЕМР. SQL> exec add_column('emp','newcol number'); BEGIN add column('emp','newcol number'); END;
ERROR at line 1: ORA-01476: divisor is equal to zero ORA-06512: at "ADD_COLUMN", line 9 OPA-06512: at line 1
Как мы уже видели, ошибка в процедуре приводит к откату вставки строк в таблицу LIST_OF_CHANGES. SQL> select * from list_of_changes; no rows selected
А как же наша автономная транзакция? Она остается! SQL> desc emp Name EMPNO ENAME HIREDATE SAL DEPTNO NEWCOL
Null? NOT NULL
Type NUMBER (10) VARCHAR2(20) DATE NUMBER(10,2) NUMBER(6) NUMBER
Дело не в том, что нельзя включить выполнение оператора DDL "в рамки транзакции"; просто этого нельзя сделать с помощью автономной транзакции. Использование задания (с помощью пакета DBMS_JOB) дает требуемый результат ценой небольшой задержки между завершением транзакции и выполнением оператора DDL. Мы можем изменить нашу процедуру ADD_COLUMN так, чтобы процедура RUN_DDL выполнялась, как задание. SQL> 2 3 4 5 6 7 8 9 10 11 12
create or replace procedure ADD_COLUMN(p_table varchar2, p_column varchar2) is v number; j number; begin insert into LIST_OF_CHANGES values (p_table, p_column, sysdate); dbms_job.submit(j , 'run_ddl(''alter table '||p_table| I' add 'I Ip_column[ | ' " ) ; ' ) ; end; /
Procedure created. SQL> exec add_column('emp','newcol2 number'); PL/SQL procedure successfully completed. 8 Зак 348
226
Глава 4
DDL-оператор для добавления столбца еще не выполнен, он только поставлен на выполнение с помощью задания. SQL> s e l e c t what from u s e r _ j o b s ; WHAT r u n _ d d l ( ' a l t e r t a b l e emp add newcol2 n u m b e r ' ) ;
В этот момент мы можем зафиксировать транзакцию и вызвать выполнение задания или выполнить откат для отмены всех изменений. Оператор DDL теперь стал частью единой транзакции.
Аудит операторов SELECT До появления автономных транзакций было невозможно выполнять операторы DML из оператора SELECT. ВО всех версиях Oracle нельзя начать транзакцию в операторе SELECT. Например, рассмотрим требование регистрировать любой запрос пользователя к таблице ЕМР. Будем сохранять соответствующую информацию в таблице EMP_AUDIT. SQL> drop table EMP_AUDIT; Table dropped. SQL> create table EMP_AUDIT ( 2 empno number(10), 3 viewed date ); Table created.
Создадим функцию, которая добавляет строку в таблицу EMP_AUDIT ДЛЯ любого номера сотрудника, который ей передан. SQL> 2 3 4 5 6 7 8
create or replace function AUDIT_ROW(p_empno number) return number is begin insert into EMP_AUDIT values (p_empno, sysdate); return 0; end; /
Function created.
Если попытаться вызвать функцию AUDIT_ROW для регистрации попыток обращения к таблице ЕМР, будут выданы следующие сообщения об ошибках: SQL> select AUDIT_ROW(empno) 2 from emp; select AUDIT_ROW(empno) ERROR at line 1: ORA-14551: cannot perform a DML operation inside a query ORA-06512: at "AUDIT ROW", line 3
Эффективная обработка данных
227
Изменить функцию так, чтобы она выполнялась как автономная транзакция, несложно, и это позволяет выполнить запрос. SQL> 2 3 4 5 6 7 8 9 10
create or replace function AUDIT_ROW(p_empno number) return number is pragma autonomous_transaction; begin insert into EMP_AUDIT values (p_empno, sysdate); commit; return 0; end; /
Function created. SQL> select AUDIT_ROW(empno) EMPNO 2 from emp; EMPNO 7369
14 rows selected.
А как обеспечить вызов функции AUDIT_EMP при любом запросе к таблице ЕМР? Конечно, с помощью представления! Мы можем включить вызов функции в представление, чтобы скрыть его от пользователя. SQL> create view EMP_WITH_AUDIT as 2 select e.*, AUDIT_ROW(empno) x 3 from emp e; View created. SQL> select * 2 from emp with audit 3 where rownum < 10; EMPNO ENAME 1 2 3 4 5 6 7 8 9
Namel Name2 Name3 Name4 Name5 Name6 Name7 Name8 Name9
9 rows selected.
HIREDATE 24/JUN/03 24/JUN/03 24/JUN/03 24/JUN/03 24/JUN/03 24/JUN/03 25/JUN/03 25/JUN/03 25/JUN/03
SAL
DEPTNO
X
1000000 9950.33 8012.93 7688.15 9375.71 8407.97 7918.62 9061.74 8692.89
249 420 66 200 40 244 245 122 433
0 0 0 0 0 0 0 0 0
228
Глава 4
Выполнив запрос к представлению, мы можем проверить, какие строки были просмотрены, с помощью запроса к таблице EMP_AUDIT. SQL> select * from emp_audit; EMPNO VIEWED 1 2 3 4 5 6 7 8 9
03/JUL/03 03/JUL/03 03/JUL/03 03/JUL/03 03/JUL/03 03/JUL/03 03/JUL/03 03/JOL/03 03/JUL/03
9 rows selected.
Это кажется впечатляющим результатом, но при использовании такого подхода существует ряд проблем. Давайте очистим таблицу аудита, а затем выполним несколько различных запросов к нашему новому представлению EMP_WITH_AUDIT. SQL> truncate table emp_audit; Table truncated. SQL> select count(*) 2
from emp_with_audit;
COUNT(*) 50000 SQL> select empno, hiredate, sal 2 from emp_with_audit 3
where rownum < 5; EMPNO HIREDATE
1 24/JUN/03 1000000 2 24/JUN/03 9950.33 3 24/JUN/03 8012.93 4 24/JUN/03 7688.15 SQL> select * 2 from emp_with_audit 3 where hiredate = to date('27/06/03 04 :44 : 00', "DD/MM/YY HH:MI:SS'); EMPNO ENAME HIREDATE SAL DEPTNO 226 Name226
27/06/03 04 :44:00
8446.87
38
Эффективная обработка данных
229
Мы пересчитали все записи в таблице ЕМР (через представление EMPWITHAUDIT). Мы также выбрали данные первых пяти строк, но только из столбцов EMPNO, HIREDATE и SAL. Наконец, мы выбрали строку по конкретному значению HIREDATE. В таблице EMP_AUDIT должно быть много записей. SQL> s e l e c t * from emp_audit; EMPNO 226
VIEWED 03/07/03 1 7 : 5 9 : 3 6
SQL-машина достаточно умна и пытается избежать вызова функции, если это возможно. Только самый последний запрос привел к созданию строки в таблице EMP_AUDIT. Поэтому мы фактически не получили механизм строгого аудита. А насчет тех строк, обращение к которым все же регистрируется, подумайте о дополнительных ресурсах, необходимых для работы такого механизма. Каждая выбранная запись приводит к зафиксированной транзакции. Единственный способ, когда так можно добиться успеха, — контроль каждого запроса, который будет использоваться для выборки данных из таблицы. Если такой уровень контроля невозможен, лучший аудит, на который можно надеяться, будет обеспечен с помощью средств детального аудита, появившихся в версии 9. Можно добавить правило для отслеживания запросов к таблице ЕМР. SQL> 2 3 4 5 6 7 8
begin DBMS_FGA.ADD_POLICY( object_schema => user, object_name => 'EMP', policy_name => 'AUDIT_EMP_RECORDS', audit column => 'salary'); 9end; /
PL/SQL procedure successfully completed.
После этого запросы к таблице ЕМР МОЖНО отслеживать через представление Полное обсуждение такой возможности выходит за рамки нашей книги, детально она описана в руководстве "Application Developers Fundamental DBA_FGA_AUDIT_TRAIL.
Guide".
Аудит, результаты которого остаются после отката Возможно, единственное оправданное использование автономных транзакций — для реализации "безтранзакционного" аудита. Чаще всего автономные транзакции применяются для регистрации ошибок в приложении, в частности, для записи информации о непредвиденных ошибках в универсальном обработчике всех ошибок. Мы детально рассматриваем этот метод в главе 10, "Отладка", но для полноты изложения здесь давайте вернемся к первому примеру в этом разделе, в котором процедура UPDATE_EMP содержала обработчик WHEN OTHERS. МЫ можем использовать его для регистрации детальной информации об ошибке перед повторным возбуждением исключительной ситуации и ее распространением в вызывающую среду (и тем
230
Глава 4
самым откатом всех выполненных процедурой изменений). Мы создадим таблицу ERRS для регистрации всех произошедших ошибок. SQL> create table ERRS 2 ( module varchar2(30) , 3 errdate date, 4 errmsg varchar2(4000)); Table created.
Мы также создадим простую процедуру для добавления строк в таблицу в автономной транзакции. SQL> 2 3 4 5 6 7 8 9 10
create or replace procedure err_logger(p_module varchar2, p_msg varchar2) is pragma autonomous_transaction; begin insert into errs values (p_module,sysdate,substr(p_msg, I, 4000)); commit; end; /
Procedure created.
Теперь изменим процедуру UPDATE_EMP так, чтобы в ней вызывалась процедура регистрации ошибок. Обратите внимание, что оператор отката выброшен, как было описано ранее. SQL> create or replace 2 procedure UPDATE_EMP(p_empno number, p_sal number) is 3 begin 4 update DEPT 5 set TOT SAL = TOT SAL + — — 6 ( select p_sal-sal 7 from EMP 8 where empno = p_empno ) 9 where deptno = ( select deptno 10 from EMP 11 where empno - p_empno); U update EMP 14 set sal = p_sal 15 where empno = p_empno; 16 17 update EMP_DELTAS 18 set change_type = 'SAL' 19 where empno » p_empno; 20 21 insert into EMP_AUDIT 22 values (sysdate,p_empno,p_sal); 23
Эффективная обработка данных 24 25 26 27 28 29
231
exception when others then err_logger(•UPDATE_EMP',sqlerrm); raise; end; /
Procedure c r e a t e d .
Теперь вызовем процедуру с очень большим значением зарплаты и запишем информацию об обнаруженных ошибках. SQL> exec update_emp(7698, 99999) ; BEGIN update_emp(7698, 99999); END;
ERROR at line 1: ORA-01438: value larger than specified precision allows for this column ORA-06512: at "UPDATE_EMP", line 26 ORA-06512: at line 1
SQL> select * from errs; MODULE
ERRDATE
ERRMSG UPDATE_EMP 03/JUL/03 ORA-01438: value larger than specified precision allows for this column
Резюме Если сделать из этой главы единственный вывод, он будет таким: не надо создавать медленные, неустойчивые PL/SQL-приложения, все еще успешно компилирующиеся на сервере версии 7.2. Используйте новые возможности, обеспечивающие более тесную связь PL/SQL и используемых структур данных в базе. В конечном итоге возможность достижения этой тесной интеграции с используемыми структурами данных, вероятно, самая главная особенность PL/SQL. Ни один из языков, используемых для доступа к базе данных Oracle, не связан с данными так сильно. Если потратить время на увеличение этой интеграции, вы будете создавать приложения, более устойчивые к изменениям, которые неизбежно происходят в базе данных по мере развития требований к приложению со временем. Несмотря на столь тесную интеграцию PL/SQL и SQL, при уменьшении количества переключений контекстов между ними за счет использования наборов и множественного связывания в коде можно повысить производительность.
Глава 5
Методы оптимизации PL/SQL ЕСЛИ ВЫ начинаете первый серьезный проект на языке PL/SQL, представленные в этой книге проверенные методы и советы помогут вам создать код, который будет не просто работать, а работать хорошо. В соответствии с декларированным нами принципом демонстрируемости, в книге приведены многочисленные примеры тестирования производительности и устойчивости создаваемого PL/SQL-кода. Однако никто не любит тратить время на проверку гипотез, если в конечном итоге выясняется, что эта работа уже кем-то была проделана. В этой главе мы, по сути, представляем "приемы и методы", во многом основанные на том, что мы обсуждали в предыдущих четырех главах, и, кроме того, рассматриваем:
> ряд конкретных протестированных решений типичных проблем, возникающих при разработке приложений на языке PL/SQL; > некоторые "скрытые затраты ресурсов", с которыми вы можете столкнуться, и способы их избежать; > типичные "ловушки", в которые регулярно попадают разработчики. Мы надеемся, что материал этой главы сэкономит ваши усилия, а также вдохновит на изучение собственных гипотез и создание тестов для их проверки.
Уменьшение количества разборов и объема используемой памяти Как описано в главе 1, уменьшение количества разборов и контроль использования памяти — важные шаги на пути создания эффективных PL/SQL-приложений. В этом разделе мы рассмотрим ряд приемов, позволяющих избежать проблем с разбором кода триггеров и процедур с правами вызывающего, а также опишем применение конвейерных функций (pipelined functions) для сокращения объема используемой памяти при передаче данных между PL/SQL-модулями.
Код в триггерах Многие годы я рекомендовал разработчикам сокращать до минимума объем кода в триггерах. Я рекомендовал для выполнения действий триггера использовать простой вызов процедуры с передачей соответствующих параметров. В основе этой рекомендации лежала давняя проблема с исходным кодом триггера, состоящая в том, что скомпилированный код триггера не хранится в базе данных.
234
Глава 5
В ранних версиях Oracle 7 исходный код проверялся на допустимость и сохранялся при создании триггера, но скомпилированная версия этого кода не сохранялась. При первом срабатывании триггера его код компилировался перед загрузкой в разделяемый пул. Если за время работы экземпляра этот код удалялся из разделяемого пула как устаревший, то при следующем обращении приходилось перекомпилировать его. Для процедур и пакетов скомпилированный код хранился в базе данных, и эта проблема их не затрагивала. Начиная с версии Oracle 7.3, проблема была решена: скомпилированный код триггера теперь хранится в базе данных. Поэтому я радостно начал рассказывать разработчикам, что они теперь вольны включать в триггеры код любого объема. Однако оказалось, что я не прав: вынесение всего кода триггера в хранимые программные единицы PL/SQL по-прежнему дает преимущества, если в коде триггера есть SQL-операторы. В главе 1 было показано, что разбор существенно влиягт на производительность в среде с большим количеством одновременно работающих пользователей. Перенос кода триггера, содержащего SQL-операторы, в PL/SQL- процедуры может уменьшить количество выполняемых приложением разборов, как мы сейчас продемонстрируем. Рассмотрим следующий пример, в котором мы выполняем аудит вставки строк в таблицу т с помощью триггера, копирующего интересующую нас часть информации в таблицу T_AUDIT: SQL> create table T (code number); Table created. SQL> create table T_AUDIT ( 2 code number, 3
ins_date date );
Table created. SQL> create or replace 2 trigger TRG 3 before insert on T 4 for each row 5 begin 6 insert into T_AUDIT 7 values (:new.code, sysdate); 8 end; 9 / Trigger created.
Мы будем использовать установку SQLTRACE ДЛЯ регистрации количества операций разбора при добавлении некоторого числа строк в таблицу т. Сперва отметим начало отсчета, выполнив "моментальный снимок" статистической информации о разборе для данного сеанса. SQL> alter session set sql trace = true;
Методы оптимизации PL/SQL 235 Session altered. SQL> set feedback off SQL> select * from v$mystats 2 where name like 'parse%'; NAME
VALUE
parse parse parse parse parse
time cpu time elapsed count (total) count (hard) count (failures)
16 24 648 20 0
Итак, до начала добавления строк в таблицу т мы выполнили в текущем сеансе 648 разборов. Теперь вставим 10 строк десятью отдельными операторами INSERT SQL> SQL> SQL> SQL> SQL> SQL> SQL> SQL> SQL> SQL>
insert insert insert insert insert insert insert insert insert insert
into into into into into into into into into into
T T T T T T T T T T
values values values values values values values values values values
(1) (1) (1) (1) (1) (1) (1) (1) (1) (1)
и проанализируем текущее состояние информации о разборе: SQL> s e l e c t * from v$mystats 2 where name l i k e 'parse%'; VALUE
NAME
parse parse parse parse parse
time cpu time elapsed count (total) count (hard) count (failures)
16 24 679 20 0
Простое вычитание (648-ми из 679-ти) показывает, что была выполнена 31 операция разбора. Мы повторим тест, но на этот раз триггер будет делегировать работу по вставке строк в таблицу аудита процедуре T_AUDIT_PROC. SQL> 2 3 4 5 6 7
create or replace procedure T_AUDIT_PROC(p_code number) i s begin insert into T_AUDIT values (p_code, sysdate); end; /
Procedure created.
236
Глава 5
Затем переопределим триггер так, чтобы он просто вызывал эту процедуру. SQL> 2 3 4 5 6
create or replace trigger TRG before insert on T for each row call t_audit_proc(:new.code) /
Trigger created.
Запишем новую точку отсчета (статистическую информацию по разборам до выполнения вставок): SQL> select * from v$mystats 2 where name like 'parse%'; NAME
VALUE
parse parse parse parse parse
time cpu time elapsed count (total) count (hard) count (failures)
16 24 785 24 0
5 rows selected.
Повторим вставку десяти строк и получим статистическую информацию о разборах на момент завершения теста. SQL> SQL> SQL> SQL> SQL> SQL> SQL> SQL> SQL> SQL> SQL>
set feedback off insert into T values insert into T values insert into T values insert into T values insert into T values insert into T values insert into T values insert into T values insert into T values insert into T values
(1) (1) (1) (1)
(1) (1) (1)
(1) (1) (1)
SQL> select * from v$mystats 2 where name like 'parse%'; NAME parse parse parse parse parse
VALUE time cpu time elapsed count (total) count (hard) count (failures)
16 24 806 24 0
Методы оптимизации PL/SQL
237
Затем отключим трассировку (об этом — чуть позже). SQL> alter session set sql_trace » false;
Количество операций разбора для второго теста равно 806 - 785 = 21! В обоих случаях был выполнен одинаковый объем работы (т.е. вставлено 10 строк в таблицу т и 10 строк — в таблицу T_AUDIT), НО, возлагая ее выполнение на процедуру, мы сократили количество операций разбора с 32 до 21. Чтобы понять, почему так произошло, надо изучить полученный трассировочный файл. Для первого теста мы получили в трассировочном файле записи следующего вида: PARSING IN CURSOR #2 INSERT into T_AUDIT values (:bl, sysdate) END OF STMT PARSE #2:c=0,e=4843,p=0,cr=l,cu=0,mis=l,r=0,dep=l,C3g=0,tiTO=4716573269 EXEC #2:c=0,e=741,p=0,cr=l,cu=7,mis=0,r=l,dep=l,og=4,tim=4716575055 PARSING IN CURSOR #2 INSERT into T— AUDIT values (:bl, sysdate) END OF STMT PARSE #2:c=0,e=166,p=0,cr=0,cu=0,mis=O,r=0,dep=l,og=4,tim=4204155472 EXEC #2:c=0,e=266,p=0,cr=l,cu=l,mis=0,r=l,dep=l,og=4,tim=420415664 3 PARSING IN CURSOR #2 INSERT into T_AUDIT values (:bl, sysdate) END OF STMT PARSE #2:c=0,e=94,p=0,cr=0,cu=0,mis=0,r=0,dep=l,og=4,tim=4204187769 EXEC #2:c=0,e=261,p=0,cr=l,cu=l,mis=0,r=l,dep=l,og=4,tim=4204189065 EXEC #l:c=0,e=2215,p=0,cr=2,cu=2,mis=0,r=l,dep=0,og=4,tim=4204189588
Для каждой строки, добавленной в таблицу т, мы видим разбор оператора вставки в таблицу аудита, T_AUDIT. Точнее, мы видим один вызов процедуры разбора для каждого выполнения триггера. Если же проверить трассировочный файл для второго теста (когда мы вызывали процедуру), мы увидим совсем другой результат. PARSING IN CURSOR #2 INSERT into T_AUDIT values (:bl, sysdate) END OF STMT PARSE #2:c=0,e=3314,p=0,cr=2,cu=0,mis=l,r=0/dep=l,og=0,tin«=4960810746 EXEC #2:c=0,e=624,p=0,cr=l,cu=2,mis=0,r=l,dep=l,og=4,tim=4960812448 EXEC #2:c=0,e=179,p=0,cr=0,cu=l,mis=0,r=l,dep=l,og=4,tim=4960843780 EXEC #2:c=0,e=180,p=0,cr=0,cu=l,mis=0,r=l,dep=l,og=4,tim=4960874 62 9 EXEC
#2:c=0,e=182,p=0,cr=0,cu=l,mis=0,r=l,dep=l,og=4,tim=4960936653
Мы видим только один разбор оператора вставки в таблицу аудита. Пока сеанс не будет отключен, любое количество выполненных операторов INSERT ДЛЯ табли-
238
Глава 5
цы т приведет всего к одному разбору для операции аудита (вставки в табяицу T_AUDIT).
Как уже говорилось в главе 1, все, что сокращает количество операций разбора, повышает общую масштабируемость и эффективность системы. Итак, простое правило: избегайте выполнения SQL-операторов в триггере. Если это необходимо, поручите данное действие процедуре.
Процедуры с правами вызывающего Мы подробно рассмотрим особенности выполнения процедур с правами вызывающего и создателя в главе 8, но здесь мы хотим обсудить их с точки зрения производительности PL/SQL. Одна из проблем, постоянно сбивающих с толку разработчиков на PL/SQL, — это факт, что в стандартном режиме выполнения с правами создателя роли не действуют как при компиляции, так и при выполнении. Например, пусть пользователю CONFUSED была предоставлена роль DBA И тем самым — доступ к представлению DBA_USERS из среды SQL*Plus. SQL> select count (*) from dba users; COUNT(*) 32
Пользователь CONFUSED создает процедуру, использующую это представление. SQL> conn CONFUSED/PASSWORD Connected. SQL> create or replace 2 procedure GET_ROW is x number; 3 4 begin execute immediate ' 5 select 1 6 from dba_users 7 where rownum = 1' into x; 8 9 end; 10 Procedure created.
Пока все вроде бы нормально (на самом деле, только потому, что пользователь CONFUSED применил динамический SQL, который сервер не проверяет при компи-
ляции). В любом случае, когда он пытается выполнить процедуру, она не срабатывает. SQL> exec get_row; BEGIN get row; END;
ERROR at line 1:
Методы оптимизации PL/SQL
239
ORA-00942: t a b l e or view does not e x i s t ORA-06512: a t "CONFUSED.GET_ROW", l i n e 4 ORA-06512: a t l i n e 1
В этот момент разработчик оказывается сбитым с толку (вот почему мы дали ему такое имя). Пользователь имеет привилегию DBA, НО ОН не может увидеть представление DBAJJSERS в своей процедуре. Однако пользователь может увидеть его, выполняя запрос из среды SQL*Plus. Дело в том, что PL/SQL-процедуры работают только с непосредственно предоставленными привилегиями, а не с полученными через роль (даже если это роль DBA). Процедуры с правами вызывающего часто трактовались как решение этой "проблемы", поскольку стандартные привилегии (полученные непосредственно или через роль) при их выполнении сохраняются. Если пользователь CONFUSED пересоздает процедуру для работы с правами вызывающего, проблема "решена". SQL> 2 3 4 5 6 7 8 9 10
create or replace procedure GET_ROW authid current_user is x number; begin execute immediate ' select 1 from dba_users where rownum = 1' into x; end; /
Procedure created. SQL> exec get_row; PL/SQL procedure successfully completed.
Конечно, самым правильным решением, вероятно, было бы предоставление привилегии SELECT на представление DBA_USERS пользователю CONFUSED. НО поскольку многие не знают о привилегиях, с которыми работают процедуры с правами создателя, права вызывающего рассматриваются как решение несуществующей проблемы. Почему бы просто не использовать права вызывающего для всех PL/SQL-npoграмм и избежать любых проблем с ролями и т.п.? Потому что использование процедур с правами вызывающего снижает производительность. В руководстве по языку PL/SQL утверждается. "Подпрограммы с правами вызывающего позволяют повторно использовать код и централизовать алгоритмы работы приложения".
Код действительно используется повторно — в базе данных хранится всего одна его копия, но фактически реальное преимущество повторного использования с точки зрения производительности, совместное использование кода, для процедур с правами вызывающего теряется.
240
Глава 5
Чтобы продемонстрировать это, рассмотрим следующий пример. Сначала создадим 11 учетных записей пользователей, с INVIO no INV20, у каждого из которых есть идентичная таблица, LOOKUP. ЭТО МОЖНО сделать с помощью небольшого блока с динамическим SQL (для его выполнения надо подключиться от имени учетной записи с привилегиями DBA). SQL> begin 2 for i in 10 .. 20 loop 3 execute immediate 4 'grant connect, resource to inv'||i|| 5 ' identified by xxx'; 6 execute immediate 7 'create table inv'||i||'.lookup '|I 8 '( uno number primary key, '|| 9 ' data varchar2(20)) ' I | 10 'organization index'; 11 execute immediate 12 'insert into inv'||i||'.lookup '|! 13 "values (' | | i | Г ,' 'data' | | i | | ' " ) ' ; 14 execute immediate 15 'analyze table inv'||iI|'.lookup estimate statistics'; 16 end loop; 17 end; 18 /
PL/SQL procedure successfully completed. Если обратиться к представлению DBA_TABLES, МЫ увидим одноименные таблицы в каждой из только что созданных схем. SQL> s e l e c t owner, table_name 2 3
from dba_tables where owner like 'INV
';
OWNER
TABLE_NAME
INV10 INV11 INV12 INV13 INV14 INV15 INV16 INV17 INV18 INV19 INV20
LOOKUP LOOKUP LOOKUP LOOKUP LOOKUP LOOKUP LOOKUP LOOKUP LOOKUP LOOKUP LOOKUP
Каждая таблица LOOKUP содержит одну строку данных, которая имеет значение только для владельца этой таблицы. Создадим еще одну учетную запись пользователя, DEFINER, которая содержит единственную таблицу, консолидирующую таблицы,
I Методы оптимизации PL/SQL
241
принадлежащие пользователям INNnn (т.е. в таблице будет 11 строк данных, по одной для каждой из учетных записей INNnn). SQL> grant connect, resource to definer identified by xxx; Grant succeeded. SQL> create table definer.lookup 2 ( uno number primary key, 3 data varchar2(20)) 4 organization index; Table created. SQL> 2 3 4
insert into definer.lookup select rownum+9, 'data'||rownum from all_objects where rownum create or replace 2 procedure definer.get(p_uno number) is 3 d varchar2 (20) ; 4 begin 5 select data 6 into d 7 from lookup 8 where uno = p uno; 9 end; 10 / Procedure created. Наконец, создадим процедуру с правами вызывающего, которая позволит пользователям iNVnn получать те же данные из их собственных таблиц LOOKUP. SQL> create or replace 2 procedure definer.invoker_proc authid current_user is 3 d varchar2(20) ; 4 begin 5 select data 6 into d 7 from lookup; 8 end; 9 / Procedure created.
242
Глава 5
Прежде чем продолжать, давайте подытожим то, что мы сделали. У нас есть таблица, содержащая 11 строк данных, по одной для каждой учетной записи INVnn. У каждого из пользователей INVnn есть свой экземпляр одноименной таблицы, содержащий всего одну строку, имеющую отношение к данному пользователю. Для пользователей INVnn доступны две процедуры для получения "принадлежащих" им строк: > DEFINER. GET (userno number) — процедура с правами создателя, которая будет искать в одной общей таблице по переданному номеру пользователя; > DEFINER. INVOKER_PROC — процедура с правами вызывающего, которая будет обращаться к таблице, принадлежащей пользователю, вызывающему эту процедуру. Кажется, что подойдут оба способа, поскольку каждая процедура требует всего одного вызова для получения одной строки данных. Более того, кажется, что с точки зрения производительности процедура с правами вызывающего должна быть лучше, потому что ей достаточно обработать одну строку данных, содержащуюся в "локальной" таблице схемы. Давайте вызовем нашу процедуру с правами вызываюидего 100000 раз, а затем сделаем то же самое с процедурой с правами создателя. SQL> begin 2 for i in 1 .. 100000 loop 3 definer.invoker_proc; — 4 end loop; 5 end; 6 /
invokers right access to 1 row table
PL/SQL procedure successfully completed. Elapsed: 00:00:07.04 SQL> begin 2 for i in 1 .. 100000 loop 3 definer.get(10); — definers rights to 1 row from 11 row table 4 end loop; 5 end; 6
/
PL/SQL procedure successfully completed. Elapsed: 00:00:09.04
Итак, процедура с правами вызывающего работает немного быстрее. Но нечто непосредственно не демонстрируемое этими тестами влияет на возможность (или невозможность) совместного использования SQL. Как уже было показано ранее, совместное использование кода — важное условие масштабируемости. При выполнении процедуры с правами создателя, даже при подключении от имени 11-ти различных пользователей, они могут совместно использовать один и тот же SQL-оператор. Давайте продемонстрируем это, сбросив разделяемый пул, а затем выполним процедуру DEFINER.GET ОТ имени каждого из пользователей INNW. SQL> alter system flush shared_pool;
Методы оптимизации PL/SQL 243 System altered. SQL> conn invlO/xxx Connected. SQL> exec definer.get (10); PL/SQL procedure successfully completed. SQL> conn invll/xxx Connected. SQL> exec definer.get(11) ; PL/SQL procedure successfully completed.
SQL> conn inv20/xxx Connected. SQL> exec definer.get(20); PL/SQL procedure successfully completed.
Теперь поищем в разделяемом пуле рекурсивные SQL-операторы, накопившиеся за 11 выполнений этой процедуры. SQL> select sql_text, executions 2 from v$sql 3 where sql text like '%lookup%'; SQL TEXT SELECT data
EXECUTIONS from lookup
where uno = :bl
11
В разделяемом пуле есть только один экземпляр этого SQL-оператора. Любой сеанс, выполняющий процедуру DEFINER.GET, будет использовать SQL-оператор из этой процедуры. Сравните это с тем, что происходит при вызове процедуры DEFINER. INVOKER PROC ОТ ИМеНИ КЭЖДОГО ИЗ пользователей INVnn. SQL> alter system flush shared_pool; System altered. SQL> conn invlO/xxx Connected. SQL> exec definer.invoker_proc; PL/SQL procedure successfully completed.
244
Глава 5
SQL> conn inv20/xxx Connected. SQL> exec definer.invoker_proc; PL/SQL procedure successfully completed.
Давайте еще раз обратимся к разделяемому пулу и посмотрим на существенное отличие. SQL> select sql_text, executions 2 from v$sql 3 where sql_text like '%lookup%'; EXECUTIONS
SQL TEXT SELECT SELECT SELECT SELECT SELECT SELECT SELECT SELECT SELECT SELECT SELECT
data data data data data data data data data data data
from from from from from from from from from from from
lookup lookup lookup lookup lookup lookup lookup lookup lookup lookup lookup
Хотя текст SQL-операторов одинаков, тот факт, что в разделяемом пуле есть 11 экземпляров SQL, свидетельствует, что SQL-операторы не могут использоваться совместно. Когда совместно использовать их не получается, можно обратиться к представлению V$SQL_SHARED_CURSOR, чтобы выяснить причину. Каждый столбец этого представления описывает возможную причину того, почему невозможно совместное использование. SQL> desc v$sql_shared_cursor Name ADDRESS KGLHDPAR UNBOUND_CURSOR SQL_TYPE_MISMATCH OPTIMIZER_MISMATCH OUTLINE_MISMATCH STATS_ROW_MISMATCH LITERAL_MISMATCH SEC_DEPTH_MISMATCH EXPLAIN_PLAN_CURSOR BUFFERED_DML_MISMATCH PDML_ENV_MISMATCH INST_DRTLD_MISMATCH SLAVE_QC_MISMATCH TYPECHECK MISMATCH
Null?
Type RAW(4) RAW(4) VARCHAR2(1) VARCHAR2(1) VARCHAR2(1) VARCHAR2(1) VARCHAR2(1) VARCHAR2U) VARCHAR2(1) VARCHAR2(1) VARCHAR2(1) VARCHAR2(1) VARCHAR2(1) VARCHAR2(1) VARCHAR2(1)
Методы оптимизации PL/SQL 245 AUTH_CHECK_MISMATCH BIND_MISMATCH DESCRIBE_MISMATCH LANGUAGE_MISMATCH TRANSLATIOM_MISMATCH ROW_LEVEL_SEC_MISMATCH INSUFF_PRIVS INSUFF_PRIVS_REM REMOTE_TRANS_MISMATCH LOGMINER_SES SION_MISMATCH INCOMP_LTRL_MISMATCH OVERLAP_TIME_MISMATCH SQL_REDIRECT_MISMATCH MV_QUERY_GEN_MISMATCH USER_BIND_PEEK_MISMATCH TYPCHK_DEP_MISMATCH NO_TRIGGER_MISMATCH FLASHBACK CURSOR
VARCHAR2(1) VARCHAR2(1) VARCHAR2(1) VARCHAR2(1) VARCHAR2(1) VARCHAR2(1) VARCHAR2(1) VARCHAR2(1) VARCHAR2(1) VARCHAR2(1) VARCHAR2(1) VARCHAR2(1) VARCHAR2(1) VARCHAR2(1) VARCHAR2(1) VARCHAR2(1) VARCHAR2(1) VARCHAR2(1)
Итак, мы можем использовать столбец ADDRESS В представлении V$SQL ДЛЯ наших 11-ти экземпляров SQL-оператора для поиска в представлении V$SQL_SHARED_CURSOR. Этот адрес — адрес родительского оператора (KGLHDPAR) дочерних курсоров. SQL> select * from v$sql_shared_cursor 2 where KGLHDPAR = select address from v$sql where sql_text like 'SELECT data%' and rownum = 1 )
ADDRESS 7C37C3AC 7C0B8950 7C265E70 7C2A1E90 7C390474 7C3587A4 7C35E2C0 7C05C604 7C0604B0 7C3136B8 7C14DEE4
U S O O S L S E B P I S T A B D L T R I I R L I O S M U T N F N N N N N N N NN N N N NN N N N N N N N N N N NN N N N N N N N N N N N N NN N N N N
N N
N N N N N NN N N N N N
N N N N N N N NN N N N N
N N
N N N N N NN N N N N N
N N N N N N N NN N N N N
N N
N N N N N NN N N N N N
N N N N N N N NN N N N N
N N
N N N N N NN N N N N N
N N N N N N N NN N N N N
N N
N N N N N NN N N N N N
N N N N N N N NN N N N N
N N
N N N N N NN N N N N N
N N N N N N N NN N N N N
N N
N N N N N NN N N N N N
N N N N N N N NN N N N N
N N
N N N N N NN N N N N N
N N N N N N N NN N N N N
N N
N N N N N NN N N N N N
N N N N N N N NN N N N N
N N
N N N N N NN N N N N N
11 rows selected
Проверив эти столбцы, мы увидим, что причины невозможности совместного использования — AUTH_CHECK_MISMATCH И TRANSLATION_MISMATCH.
Таблица LOOKUP В каждом SQL-операторе транслируется в другую таблицу, т.е. локальную версию таблицы LOOKUP В схеме iNVnn. Если SQL-операторы не используются повторно, это может препятствовать масштабированию; поэтому процедуры
246
Глава 5
с правами вызывающего могут нести такую угрозу. Если вы создаете PL/SQL-npoцедуры, которые будут использоваться редко или немногими пользователями, возникновение существенных проблем маловероятно, но будьте осторожны в других средах, где совместное использование SQL-операторов — ключевой фактор производительности. Процедуры с правами вызывающего бывают уместны, например, для реализации универсальных утилит, использующих динамический SQL (поскольку при обработке SQL может потребоваться использование привилегий пользователя, выполняющего процедуру). Но они не являются "решением" проблемы, связанной с незнанием особенностей работы процедур с правами создателя.
Творческий подход: использование конвейерных функций Многие PL/SQL-программы, по сути, соответствуют модели производитель/потребитель. Иными словами, они потребляют данные, выбирая их из базы данных, или наоборот, вырабатывают данные, которые потребляются клиентским приложением либо сохраняются в базе данных. Однако есть и другой тип PL/SQL-программ — они просто обрабатывают переданные им данные и передают полученный результат другому процессу. Схематически они находятся "между" процессом-производителем и потребителем, как показано на рис. 5.1.
База данных
Потребляет данные
—•
Обрабатывает данные
Производит результаты
Рис. 5 . 1 . Типичное отношение производитель/потребитель между PL/SQL-программами
Примечание Я решил использовать здесь термины производитель, обработчик и потребитель, тогда как в индустрии для соответствующих процессов чаще используются термины извлечение, преобразование, загрузка (extract, transform, load — ETL). Я специально избегал общепринятых терминов, поскольку они наводят на мысль о хранилищах данных, промежуточных таблицах и т.п., но сфера применения конвейерных функций (pipelined functions) Oracle намного шире. Любая процедура, получающая данные и выполняющая их преобразование для передачи следующему процессу, попадает под вывеску "обработчик" и поэтому является кандидатом на оформление в виде конвейера, как будет показано далее.
На рис. 5.1 показано весьма упрощенное представление этого конвейерного процесса и скрыты затраты на передачу данных между каждой из трех стадий обработки. Многие процессы производитель/обработчик/потребитель сбрасывают свои результаты в таблицу базы данных для считывания на следующей стадии. Более реалистичная схема представлена на рис. 5.2. Часто спецификация программы для выполнения такого рода обработки детально описывает, как выбирать данные из временной области хранения, а также помещать в другую временную область хранения результаты. Однако, как и во многих представленных в этой книге примерах, ключом к успешной разработке PL/SQL-
Методы оптимизации PL/SQL 247 приложений является творческий подход. Вместо слепого следования спецификации всегда имеет смысл потратить немного времени и подумать, не будет ли менее очевидное решение лучше.
База данных
Сохраняет выбранные данные для следующего процесса
1 1
Выбирает данные
Временная область хранения
Сохраняет выбранные данные для следующего процесса
Выбирает данные
С~1_ Временная область хранения
Рис. 5 . 2 . Скрытые затраты ресурсов при работе PL/SQL-программ типа производитель/потребитель
Использование временных областей хранения существенно увеличивает расходы ресурсов с точки зрения использования пространства в сегменте данных и в журнале повторного выполнения в связи с хранением дополнительных копий данных. С появлением наборов в версии 8 стало возможным передавать между стадиями сложные структуры данных, но, как уже говорилось в главе 4, существенные затраты памяти делают этот подход неприемлемым при увеличении размера наборов. В любом случае использование таблицы или набора предполагает, что процесс "начинается и заканчивается". В идеале, как только из базы данных выбрана первая строка (или набор строк), они должны быть доступны процессу обработки, а затем — процессу-производителю для записи обратно в базу данных. При использовании наборов или временных таблиц вместо передачи данных между стадиями обработки сразу после их получения все данные собираются, прежде чем сможет начаться следующая стадия. Появившаяся в версии 9 возможность создания конвейерных функций решает эту проблему. Читатели, работавшие с ОС UNIX, уже знакомы с понятием передачи результата выполнения одной команды по конвейеру другой команде. В Oracle идея та же: функция может начинать возвращать строки вызывающей программе по ходу выполнения функции, не обязательно ожидая завершения выполнения. Чтобы продемонстрировать потенциальные преимущества использования конвейерных функций, мы разберем пример, в котором конвейер не используется, рассмотрим недостатки, а затем предложим решение на базе конвейера, при котором этих недостатков не существует. Для этого примера мы будем преобразовывать строки из таблицы ЕМР (изменяя столбцы SAL и HIREDATE), а затем загружать измененные строки в результирующую таблицу ЕМР2.
248
Глава 5
Примечание Для упрощения примера мы будем решать задачу, которую можно (и нужно) решать с помощью стандартного SQL — вообще без PL/SQL. Можете предполагать, хотя это и не показано в примере, что выполняется серьезная обработка, которая требует использовать PL/SQL!
Давайте рассмотрим решение без использования конвейерных функций. Есть три PL/SQL-процесса: производитель, читающий строки из таблицы ЕМР, обработчик, ДЛЯ изменения зарплаты и даты приема на работу, и потребитель, загружающий измененные строки в таблицу ЕМР2. Сначала создадим таблицу ЕМР2 ДЛЯ хранения результатов. SQL> 2 3 4
create table EMP2 as select empno, ename, hiredate,sal, deptno from emp where rownum = 0;
Table created.
Затем создадим пакет, PKG, который будет содержать процессы потребитель, обработчик и поставщик. Для передачи информации между процессами мы определим набор EMPLIST для хранения записей о сотрудниках. SQL> 2 3 4 5 6 7 8 9 10 11
create or replace package PKG is type emp_list is table of emp2%rowtype; function CONSUMER return sys_refcursor; function MANIPULATOR(src_rows sys_refcursor) return emp_list; procedure PRODUCER(changed_rows emp_list); end; /
Package created.
Реализация очень проста. Функция CONSUMER запрашивает таблицу ЕМР И возвращает курсорную переменную (указатель) для результирующего множества. Функция MANIPULATOR принимает результирующее множество на входе, выбирает записи, изменяет дату приема на работу и зарплату, а затем возвращает измененную переменную типа набора. Функция PRODUCER проходит в цикле по записям в наборе и добавляет эти строки в таблицу ЕМР2. SQL> 2 3 4 5 6 7
create or replace package body PKG is function CONSUMER return sys_refcursor is src_rows sys_refcursor; begin open src rows for
Методы оптимизации PL/SQL 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 33 34
249
select empno, ename, hiredate,sal, deptno from emp; return src_rows; end; function MANIPULATOR(src_rows sys_refcursor) return emp_list is changed_rows emp_list; begin fetch src_rows bulk collect into changed_rows; close src_rows; for i in 1 .. changed_rows.count loop changed_rows(i).sal := changed_rows(i).sal+10; changed_rows(i).hiredate :- changed_rows(i).hiredate + 1; end loop; return changed_rows; end; procedure PRODUCER(changed_rows emp_list) is begin forall i in 1 .. changed_rows.count insert into emp2 values changed_rows(1); commit; end; end; /
Package body created.
С помощью вызова трех подпрограмм пакета можно получить в итоге полное преобразование данных, при котором мы выбираем данные из таблицы ЕМР, изменяем дату приема на работу и зарплату и сохраняем измененные данные в таблице ЕМР2. SQL> set timing on SQL> exec pkg.producer(pkg.manipulator(pkg.consumer)); PL/SQL procedure successfully completed. Elapsed: 00:00:08.02 SQL> select count(*) from emp2; COUNT(*)
Пока100000 все отлично. Мы обработали и передали 100000 строк из таблицы ЕМР В таблицу ЕМР2 за 8 секунд. Однако цена решения — объем используемой памяти, который можно оценить, обратившись к знакомому представлению V$MYSTATS.
250
Глава 5
SQL> select * from v$mystats 2 where name like '%pga%' 3 / VALUE
NAME
55893380 61660548
session pga memory session pga memory max
Шестьдесят один мегабайт! Проблема этого подхода очевидна. В соответствии с нашим принципом демонстрируемости давайте посмотрим, что произойдет, когда в таблице ЕМР будет 1000000 строк. SQL> exec pkg.producer(pkg.manipulator(pkg.consumer)); ERROR a t l i n e 1: ORA-04030: out of process memory when trying to allocate 16396 bytes ^(koh-kghu call ,pl/sql vc2)
Сеанс работы с базой данных завершился сбоем (а вскоре после этого то же самое произошло и с ноутбуком), поскольку для хранения набора из 1000000 записей о сотрудниках не хватило оперативной памяти. Самое печальное, что сохранять записи в памяти нам вообще не нужно — нам надо только преобразовывать их "по пути" ИЗ ЕМР В ЕМР2.
Вот тут и могут помочь конвейерные функции. Мы изменим процесс так, чтобы использовались конвейерные функции. Конвейерные функции не могут работать с записями PL/SQL — только с объектными типами, так что придется создать обьектные типы-аналоги типа EMP_LIST В решении без конвейерных функций. SQL> create or replace 2 type emp2rec is object empno number(10), 3 4 ename varchar2(20) , 5 hiredate date, 6 sal number(10,2), 7 deptno number(6) ); 8 /
(
Type created. SQL> create or replace 2 type emp21ist as table of emp2rec; 3 / Type created.
Спецификация пакета практически не изменилась. Мы просто указываем, что функция MANIPULATOR — конвейерная. Вместо сбора всего множества записей она может передавать каждую преобразованную запись на следующую стадию обработки (в нашем случае — процедуре PRODUCER), как только преобразование закончено.
Методы оптимизации PL/SQL
251
SQL> create or replace 2 package PKG2 is 3 function CONSUMER return sys_refcursor; 4 function MANIPULATOR(src_rows sys_refcursor) return emp21ist ^pipelined; 5 procedure PRODUCER; 6 7 end; 8 / Package created.
Тело пакета немного отличается, поскольку здесь главное — не передавать данные между различными стадиями. Но наша функция CONSUMER не меняется, просто открывая курсорную переменную для исходных данных. SQL> create or replace 2 package body PKG2 is 4 5 6 7 8 9 10 11 12
function CONSUMER return sys_refcursor is src_rows sys_refcursor; begin open src_rows for select empno, ename, hiredate,sal, deptno from emp; return src_rows; end;
Функция MANIPULATION тоже не сильно отличается от неконвейерной версии, за исключением конструкции PIPE, — измененные строки возвращаются в вызывающую среду в цикле по курсору. Как только строка изменяется, она передается вызывающему. 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
function MANIPULATOR(src_rows sys_refcursor) return emp21ist pipelined is r emp2rec; e emp2%rowtype; begin loop fetch src rows into e; — exit when src rows%notfound; e.sal := e.saI+10; e . h i r e d a t e := e . h i r e d a t e + 1; r pipe ow (emp2rec(e.empno,e.ename,e.hiredate,e.sal,e.deptno end loop; close src rows; return; end;
) ) ;
252
Глава 5
Процедуре PRODUCER надо только открыть курсор на источник данных, передать соответствующий указатель (курсорную переменную) процедуре преобразования, а затем, по мере возврата результатов по конвейеру, загружать их в таблицу ЕМР2. 29 procedure PRODUCER is 30 re sys_refcursor := consumer; 31 begin 32 insert into emp2 33 select * from table(cast(manipulator(re) as emp21ist )); 34 end; 35 36 end; 37 / Package body created.
Конечный результат тот же, поскольку требуемые действия мы не меняли, а раз мы просто передаем элементы набора по мере их получения, а не запоминаем большие наборы, то памяти потребуется намного меньше. Давайте запустим процедуру PRODUCER, а затем снова проверим статистическую информацию об использовании памяти (надо отключиться и подключиться снова для очистки статистической информации уровня сеанса перед выполнением второго теста). SQL> exec pkg2.producer; PL/SQL procedure successfully completed. Elapsed: 00:00:22.02 SQL> select * from v$mystats 2 where name like '%pga%' 3 / NAME
VALUE
session pga memory session pga memory max
434676 434676
Обратите внимание, что памяти требуется намного меньше, и этот показатель не изменится при увеличении количества обрабатываемых строк. Но время выполнения несколько разочаровывает, если вспомнить, что неконвейерная версия процедуры выполнялась всего за 8 секунд. Но теперь мы полностью может контролировать баланс между производительностью и использованием памяти. Например, ничто не мешает использовать средства обработки массивом в функции MANIPULATOR — мы можем использовать немного больше памяти, чтобы повысить производительность. Вместо выборки и отправки по конвейеру одной строки мы будем выбирать 100 строк перед передачей их потребителю. 13 14 15
function MANIPULATOR(src_rows sys_refcursor) return emp21ist pipelined is type elist is table of emp2%rowtype;
Методы оптимизации PL/SQL
253
16 г emp2rec; 17 е elist; 18 begin 19 loop 20 fetch src_rows bulk collect into e limit 100; 21 for i in 1 .. e.count loop 22 e(i).sal := e(i).sal+10; 23 e(i).hiredate := e(i).hiredate + 1; 24 pipe row (emp2rec( 25 e(i).empno,e(i).ename, 26 e(i).hiredate,e(i).sal,e(i).deptno ) ) ; 21 end loop; 28 exit when src_rows%notfound; 29 end loop; 30 close src_rows; 31 return; 32 end; 33 SQL> exec pkg2.producer; PL/SQL procedure successfully completed. Elapsed: 00:00:09.08 SQL> select * from v$mystats 2 where name like '%pga%' 3 / NAME session pga memory session pga memory max
VALUE 1483788 1549324
Поэтому, просто применяя контролируемую множественную обработку, мы получаем почти такую же производительность, как и у не конвейерной версии, но используем при этом в 50 раз меньше памяти. Давайте увеличим количество строк в таблице ЕМР ДО 1 миллиона и увидим, что ранее существовавшие проблемы с памятью теперь решены. SQL> exec pkg2.producer; PL/SQL procedure successfully completed. SQL> select * from v$mystats 2 where name like '%pga%' 3 / NAME session pga memory session pqa memory max
VALUE 516640 8643104
254
Глава 5
SQL> select count(*) from emp2; COUNT(*) 999999
На этот раз мой сеанс не завершился сбоем, но определенный рост объема используемой памяти наблюдается. На момент написания этой главы я не мог определить, с чем он связан. Еще одно преимущество связано с тем, что, поскольку процедура MANIPULATOR принимает на входе параметр типа REF CURSOR И просто отправляет строки по конвейеру в качестве результата, процедуры PRODUCER И CONSUMER становятся лишними. Мы можем включить процесс преобразования в стандартный SQL-оператор. Получим код следующего вида: SQL> insert into emp2 2 select * 3 from table( 4 cast ( 5 pkg2.manipulator( 6 cursor( select empno, ename, hiredate,sal, deptno from emp)) 7 as emp21ist ) ) ; 100000 rows created.
Вероятно, самое сложное при работе с конвейерными функциями — найти возможности для их использования в приложении. Любой процесс, предполагающий выдачу потока результатов из функции, можно усовершенствовать с помощью конвейерной функции. В главе 1 мы рассмотрели использование конвейерных функций для эффективного возврата любого количества строк без дополнительных расходов ресурсов на их размещение в реляционной таблице. В главе 10 мы рассмотрим, как использовать их для выдачи отладочной информации в реальном времени.
Типы данных: советы и методы Как уже говорилось в главе 4, язык PL/SQL поддерживает широкий спектр разнообразных структур данных. Мы предлагаем вам экспериментировать, чтобы определить, какие из них будут наиболее эффективны для вашего приложения. Результаты не всегда соответствуют ожиданиям.
Ассоциативные массивы В версии Oracle 9.2 появился новый вид ассоциативных массивов, в котором в качестве индекса массива может использоваться строка. Моя первая мысль при их появлении была: а будут ли они обеспечивать такую же высокую производительность, как и массивы, индексированные целыми числами? Иными словами, если я запомню числовое значение в индексе типа VARCHAR2, будет ли он работать так же хорошо, как если бы то же значение хранилось в обычном числовом индексе?
Методы оптимизации PL/SQL 255 Для проверки мы просто создадим два массива: один с индексом типа другой — с индексом типа VARCHAR2. Затем присвоим значения 100000 элементам каждого массива и определим общее время выполнения с помощью уже знакомой нам функции DBMS_UTILITY.GET_TIME.
BINARY_INTEGER,
SQL> declare 2 type varchar2_tab is table of number 3 index by varchar2(100); 4 vc varchar2_tab; 5 type num_tab is table of number 6 index by binary_integer; 7 n num_tab; 8 t number; 9 begin 10 t := dbms_utility.get_time; 11 for i in 1 .. 100000 loop 12 n(i) := 1; 13 end loop; 14 dbms_output.put_line('Index by Number : 'II 15 (dbms_utility.get_time-t)); 16 t := dbms_utility.get_time; 17 for i in 1 .. 100000 loop 18 vc(i) := i; 19 end loop; 20 dbms_output.put_line('Index by Varchar2: 'I| 21 (dbms_utility.get_time-t)); 22 end; 23 / Index by Number : 12 Index by Varchar2: 54 PL/SQL procedure successfully completed.
Первый тест, похоже, показывает, что ассоциативные массивы не настолько эффективны, как уже существующая схема индексирования. Этого результата вы, видимо, и ожидали. В конечном итоге числовой индекс просто "кажется" более естественным. Но, конечно же, ассоциативные массивы можно также использовать для хранения данных с разреженным индексом, т.е. когда значения индексов массива не всегда идут подряд. Поэтому для полноты надо протестировать и этот сценарий. Код очень похож на предыдущий. Мы просто сохраняем элементы массива с индексами 1000, 2000, 3000 и т.д. вместо 1, 2, 3. SQL> declare type varchar2_tab is table of number 2 3 index by varchar2(100); 4 vc varchar2_tab; 5 type num_tab is table of number 6 index by binary_integer; 7 n num_tab; 8 t number; 9 begin
256
Глава 5
10 t := dbms_utility.get_time; 11 for i in 1 . . 100000 loop 12 n(i*1000) := i; 13 end loop; 14 dbms_output.put_line('Index by Number : 'II 15 (dbms_utility.get_time-t)); 16 t := dbms_utility.get_time; 17 for i in 1 .. 100000 loop 18 vc (1*1000) := 1; 19 end loop; 20 dbms_output.put_line('Index by Varchar2: 'I I 21 (dbms_utility.get_time-t)); 22 end; 23 / Index by Number : 136 Index by Varchar2: 73 PL/SQL procedure successfully completed. Действительно интересный результат. Когда индексы разрежены, использование символьного типа данных для индекса, даже когда значения индекса — числовые, похоже, оказывается более эффективным. Возможно, выполняется специальная оптимизация, гарантирующая, что ассоциативные массивы с индексами типа VARCHAR2 будут эффективны при разреженных индексах. В приложениях, для которых производительность имеет принципиальное значение, ассоциативные массивы с индексами типа VARCHAR2 могут оказаться наиболее подходящими, даже если значения индексов "по сути" числовые.
Наборы Еще одна область для исследований — обработка в языке PL/SQL наборов, каждый элемент которых — запись. Используя наборы такого рода, данные можно представить в виде двух структур: > массива записей (сначала создаем строку, затем добавляем ее в список); > записи из массивов (создаем несколько списков, а потом объединяем их в запись). Какую именно структуру надо использовать? Чтобы ответить на этот вопрос, необходимо учесть два параметра: > насколько эффективно можно наполнить структуру данными? > насколько эффективно можно выбирать из нее данные? Следующий пакет выполняет сравнительно примитивное тестирование для получения начальных ответов на эти вопросы. Наш пакет PKG будет включать две процедуры: REC_OF_ARRAY_TEST И ARRAY_OF_REC_TEST. Обе ОНИ ВЫПОЛНЯЮТ ОДНО И ТО Же, но со структурами данных, соответствующими именам процедур. SQL> create or replace 2 package pkg i s
Методы оптимизации PL/SQL 257 3 4 5 6
procedure rec_of_array_test; procedure array_of_rec_test; end; /
Package created.
Нам надо определить ряд типов в пакете, чтобы можно было тестировать структуры данных. Тип SRECLIST — это массив записей (SREC), В котором запись SREC содержит три скалярные переменные, а тип ARRAY_REC — запись, каждое поле которой — массив (NUM_LIST). SQL> 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
create or replace package body pkg is type srec is record (a number, b number, с number, d number); type srec_list is table of srec; type num_list is table of number; type array_rec is record (a num_list, b num_list, с num_list, d num_list ); procedure rec_of_array_test is s number := dbms_utility.get_time; e number; vl srec_list; q number; begin vl := srec_list(); vl.extend(500000);
Для каждой структуры данных выполним два теста (с фиксацией времени выполнения). Сделаем 2000000 присвоений, т.е. выполним 500000 итераций цикла, каждой из которых присвоим значения четырем базовым элементам записи, А, в, с и D. 17 18 19 20 21 22 23 24
for l in 1.. 500000 loop vl (i) .a : == i; vl (i) -b : ==i; vl (i) .c : •= i ; vl (i) -d : ==i; end loop; e : =dbms utility.get time; dbms output .put line( 'Populate:
I (e-s));
Затем, чтобы протестировать производительность выборки данных, выберем 250000 псевдослучайных записей из первой структуры. Как обычно, запишем время до и после теста, полученное с помощью функции DBMS_UTILITY.GET_TIME. 25 26 27 28 29 30 31 9 Зак. 348
for i in 1 .. 250000 loop q := vl(i*2-l).a; q := vl(i*2).b; q := vl(500000-i).c; q := vl(500000-2*i+l).d; end loop; s := dbms utility.get tinv
258
Глава 5 32 dbms_output.put_line('Retrieve From: '||(s-e)); 33 end; 34
Затем выполним такой же тест с другой структурой данных. 35 procedure array_of_rec_test is 36 s number := dbms_utility.get_time; 37 e number; 38 vl array_rec; 39 q number; 40 begin 41 vl.a := num_list(); vl.a.extend(500000); 42 vl.b := num_list(); vl.b.extend(500000); 43 vl.c := num_list(); vl.с extend(500000); 44 vl.d := num_list(); vl.d.extend(500000); 45 for i in 1 .. 500000 loop 46 vl.a(i) := i; 47 vl.b(i) := i; 48 vl.c(i) := i; 49 vl.d(i) := i; 50 end loop; 51 e := dbms_utility.get_time; 52 dbms_output.put_line('Populate: '||(e-s)); 53 for i in 1 .. 250000 loop 54 q := vl.a(i*2-l) ; 55 q := vl.b(i*2) ; 56 q := vl.c(500000-i); 57 q := vl.d(500000-2*i+l); 58 end loop; 59 s := dbms_utility.get_time; 60 dbms_output.put_line('Retrieve From: '| | (s-e)); 61 end; 62 63 end; 64 / Package body created.
Теперь мы готовы выполнить тестирование производительности каждой из структур данных. SQL> exec pkg.rec_of_array_test Populate: 392 Retrieve From: 342 PL/SQL procedure successfully completed. SQL> exec pkg.array_of_rec_test Populate: 291 Retrieve From: 286 PL/SQL procedure successfully completed.
Методы оптимизации PL/SQL
259
Итак, как при заполнении, так и при выборке, похоже, более эффективно использовать массив записей, а не запись из массивов. Можно ли объяснить, почему так происходит? Вероятно, нет. Без сомнения, кто-то из разработчиков Oracle мог бы нам это сказать, но не так важно знать, почему, раз мы разработали тест, доказывающий наши предположения. В версии 10g это различие в производительности все еще наблюдается, но кто знает, что будет работать быстрее в версии 11 или 12?
Особенности использования операторов DML на базе записей Как уже говорилось в главе 4, теперь можно использовать переменные, объявленные с помощью атрибута %ROWTYPE, В операторах INSERT И UPDATE. Напомню, что можно выполнять операторы DML, не обращаясь к полям переменной-записи. Например, можно написать процедуру, которая позволяет обновлять всю запись, не ссылаясь на отдельные ее поля. SQL> 2 3 4 5 6 7 8 9 10 11 12 13 14
create or replace procedure WITH_ROWTYPE is r T%ROWTYPE; begin select * into r from T where rownum = 1; update Tl set row = r where rownum = 1; end; /
Procedure created.
Все, что пока было сказано об использовании операторов DML на базе записей, — положительное, но есть несколько не совсем приятных особенностей, о которых надо знать. Мы рассмотрим их в следующих подразделах.
Нельзя использовать конструкцию RETURNING Конструкция RETURNING не поддерживает работу с записями. Поэтому код update Tl set row = г returning * into rl;
не компилируется. По-прежнему надо указывать отдельные поля, что не позволяет обеспечить независимость от изменения структуры базы данных. Так обстоят дела и в версии 10g.
Поля сопоставляются по порядку, а не по именам При работе с содержимым переменной, объявленной с помощью атрибута %ROWTYPE, легко понять по именам, какие поля записи соответствуют столбцам базы
260
Глава 5
данных, но компилятор PL/SQL и не пытается этого делать. PL/SQL-машину интересует только совпадение количества полей и соответствие базовых типов данных полей столбцам таблицы в порядке, задаваемом определением переменной, т.е. при сопоставлении слева направо. Точность и длина для числовых и символьны?: типов данных при этом могут отличаться. Например, давайте создадим две таблицы т и Т1. SQL> c r e a t e t a b l e T ( c l , c2) as 2 s e l e c t 1,2 from dual; Table created. SQL> c r e a t e t a b l e Tl ( c2 number, cl number ) ; Table created.
Обратите внимание, что при создании таблицы Tl мы изменили порядок следования столбцов. Теперь создадим процедуру COPY_ROW, которая просто копирует строку из таблицы т в Т1. SQL> create or replace 2 procedure COPY ROW is 3 r T%ROWTYPE; 4 begin 5 select * 6 into r 7 from T 8 where rownum = 1; а у
10 11 12 13
insert into Tl values r; end; /
Procedure created.
Теперь вопрос в том, как значения столбцов будут передаваться из таблицы т в т1, поскольку столбцы в таблице т идут в порядке cl, С2, тогда как в Tl порядок столбцов — С2, cl? SQL> exec copy_row; PL/SQL procedure successfully completed. SQL> select * from tl; C2
Cl
Итак, имена столбцов (а, следовательно, и имена полей записи) не имеют значения — PL/SQL-машина использует столбцы в строке таблицы и поля в записи по порядку.
Методы оптимизации PL/SQL
261
Может генерироваться дополнительная информация повторного выполнения Мой коллега по OakTable, Джонатан Льюис, сообщил мне о малоизвестном факте, что при задании для столбца в таблице базы данных значения, не отличающегося от уже имеющегося, сервер Oracle считает это "изменением" и генерирует для этого изменения информацию отмены и повторного выполнения. Чтобы доказать это, мы измерим, какой объем информации повторного выполнения генерируется при "нормальном" изменении (когда значение столбца действительно меняется). Сначала создадим таблицу со 100-байтовым столбцом и добавим в нее строки для тестирования. SQL> create table REDOJTEST 2 ( col char(100)); Table created. SQL> 2 3 4
insert into REDO_TEST select 'x' from all_Objects where rownum < 100;
99 rows created.
Теперь выполним первоначальное изменение строк, задав новые значения, чтобы оценить, какой объем информации повторного выполнения необходим для выполнения "нормального" изменения. Мы определим, какой объем информации повторного выполнения уже сгенерирован нашим сеансом, используя представление V$MYSTATS, созданное в главе 1. SQL> select value 2 from v$mystats 3
where name = 'redo size'; VALUE
2986004
Затем мы изменим значения в строках, чтобы узнать, сколько информации повторного выполнения генерируется. SQL> update redo_test 2 set col = ' у ' ; 99 rows update. SQL> s e l e c t value 2 from v$mystats 3
where name = 'redo size'; VALUE
3028700
262
Глава 5
Итак, изменение 100-байтового столбца в 99-ти строках генерирует 3028700 2986004 = 42696 байтов информации повторного выполнения. Теперь повторил! тест, но столбцу зададим то же значение, которое у него уже есть. SQL> update redo_test 2 set col = 'у'; 99 rows update. SQL> select value 2 from v$mystats 3 where name = 'redo size'; VALUE 3071192
Мы получили почти такое же (3071192 - 3028700 = 42492) количество байтов сгенерированной информации повторного выполнения. Сервер Oracle не пытается "анализировать" и решать, нужно ли вообще что-то менять. Он записывает изменение (а, следовательно, и информацию для повторного его выполнения) независимо от указанных значений. Как это связано с операторами DML на базе записей? Новый синтаксис операторов DML, позволяющий использовать записи, не входит в некий новый стандарт ANSI; это просто удобное свойство языка PL/SQL, обеспечивающее защиту от изменений в базовых структурах данных; в частности, оно сокращает часть кода, в которой вам надо явно перечислять компоненты записи, объявленной с помощью атрибута %ROWTYPE. ЕСЛИ ВКЛЮЧИТЬ трассировку выполнения процедуры COPY_RDW ИЗ рассмотренного ранее примера, можно обнаружить, что оператор DML, использующий запись, преобразован в обычный оператор UPDATE, В котором перечислены все столбцы. Таким образом, оператор DML, использующий для изменения строки запись, фактически будет выглядеть в трассировочном файле следующим образом: UPDATE T1 set C1 = :Ы,С2 = :Ь2 where rownum = 1
Итак, хотя с точки зрения кодирования и сопровождения использование операторов DML на базе записей дает преимущества, но если "типичное" изменение в приложении меняет только один из столбцов, то при переходе на использование записей переписывается вся строка, что приводит к увеличению объема информации повторного выполнения. Конечно, если при типичном изменении приложгние меняет значения большинства столбцов, объем дополнительно генерируемой информации повторного выполнения может не представлять особой проблемы, но в любом случае, если неизменяемые столбцы по размеру составляют большую часть строки, перевод существующего PL/SQL-кода на использование операторов DML на базе записей может существенно сказаться на системе. Например, в следующей таблице REC_DML столбец Y занимает большую часть строки:
Методы оптимизации PL/SQL
263
SQL> create table REC_DML ( 2 x number, 3 у char (100) , 4 z number ) ; Table created. SQL> 2 3 4
insert into rec_dml select rownum, rownum, rownum from SRC where rownum < 1000;
999 rows created.
Если обычный код изменения выполнял изменение любых столбцов, кроме Y (В следующем примере мы будем изменять только столбец z), то объем генерируемой информации повторного выполнения будет сравнительно небольшим. Мы объединили проверки объема информации повторного выполнения до и после изменения в PL/SQL-блок, чтобы в конце блока выдавать объем сгенерированной им информации повторного выполнения. SQL> 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Redo
declare — обычный код изменения redo_amount number; р rec_dml%rowtype; begin select value into redo_amount from v$mystats where name = 'redo size'; for i in ( select * from rec_dml) loop update rec_dml set z = z + 1 where x = i.x; end loop; select value-redo_amount into redo_amount from v$mystats where name = 'redo size dbms_output.put_line('Redo generated: 'I Iredo_amount); end; / generated: 236972
PL/SQL procedure successfully completed.
Давайте посмотрим, что произойдет, если мы, не задумываясь о последствиях, перейдем на использование оператора DML на базе записи в этом блоке. SQL> declare — новый оператор DML на базе записи 2 redo_amount number; 3 р rec_dml%rowtype; 4 begin
264
Глава 5
5 select value 6 into redo_amount 7 from v$mystats 8 where name = 'redo size'; 9 for i in ( select * from rec_dml) loop 10 p.x : = i.x; 11 p.у := i.y; 12 p.z := i.z+1; 13 update recjdml 14 set row = p 15 where x = i.x; 16 end loop; 17 select value-redo_amount 18 into redo_amount 19 from v$mystats 20 where name = 'redo size'; 21 dbms_output.put_line('Redo generated: '||redo_amount); 22 end; 23 / Redo generated: 461040 PL/SQL procedure successfully completed.
Объем сгенерированной информации повторного выполнения почти удвоился по сравнению с традиционным изменением. Стоит ли такой объем дополнительной информации повторного выполнения защиты от изменений кода? Решать вам, но если вы все-таки решите перейти на использование операторов DML на базе записей для упрощения сопровождения, не забудьте выполнить ряд тестов, чтобы проверить, что объем генерируемой информации повторного выполнения не создаст проблем при переносе кода в производственную среду.
Нельзя ссылаться на значения-.OLDи :NEW в триггерах Как и в предыдущих версиях Oracle, "записи" :OLD И :NEW, доступные в строчных триггерах, не считаются фактически записями; это "коррелирующие имена". Поэтому нельзя использовать их как записи в операторах DML. Однако, как было продемонстрировано ранее в этой главе, код триггера должен делегировать выполнение любых SQL-операторов в PL/SQL-процедуре. В таких случаях можно использовать SQL-оператор для генерации кода триггера, который будет преобразовывать коррелирующие переменные :OLD И :NEW В записи, а затем передавать эти записи как параметры соответствующей процедуре. Затем мы можем поместить все требуемые действия (включая, если нужно, выполнение операторов DML на базе записей) в вызываемую процедуру. Следующий сценарий создаст записи с помощью атрибута %ROWTYPE и заполнит их прежними и новыми данными строки для передачи процедуре. Сценарий запрашивает имя таблицы и выдает соответствующий оператор CREATE TRIGGER, выбирая необходимую информацию из представления USER_TAB_COLUMNS. SQL> s e l e c t 2 case when column_id = 1 then 3 'create or replace trigger '||table name I|
Методы оптимизации PL/SQL
265
4 '_trg'I |chr(10) I I 5 'after update on '|Itable_name||chr(10)|| 6 'for each row'||chr(10)|| 7 'declare'| |chr (10) | | 8 ' p_new '||table_name||'%rowtype;'||chr(10) || 9 ' p_old '|Itable_name||'%rowtype;'||chr(10)|| 10 'begin'I |chr(10) I I 11 ' p_new.'I|lower(column_name)II' : = :new.'||lower(column_name) I I 12 ';'| |chr(10) I I 13 ' p_old. ' I | lower (column_name) I | ' :«• ^*:old.'||lower(column_name)II';' 14 else 15 ' p_new.'| |lower(column_name) I |' : = :new.' | |lower(column_name) I I 16 ';'||chr(10)|I 17 ' p_old.'||lower(column_name)I|' •-•: old. ' | | lower (column_name) | Г ;' 18 end trg 19 from user_tab_columns 20 where table_name = upper('&&table_name') 21 union all 22 select ' trgproc_&&table_name(p_new,p_old);' from dual 23 union all 24 select 'end;' from dual 25 / Enter value for table_name: EMP TRG create or replace trigger EMP_trg after update on EMP for each row declare p_new EMP%rowtype; p_old EMP%rowtype; begin p_new.empno := :new.empno; p_old.empno : = :old.empno; p_new.ename := :new.ename; p_old.ename := :old.ename; p_new.hiredate := :new.hiredate; p_old.hiredate := :old.hiredate; p_new.sal := :new.sal; p_old.sal := :old.sal; p_new.deptno := :new.deptno; p_old.deptno := :old.deptno; trgproc_emp(p_new,p_old); end;
266
Глава 5
Использование подобного (или измененного в соответствии с вашими требованиями) сценария может быть эффективным способом обеспечения согласованного подхода к написанию кода триггеров в приложении.
Вызов PL/SQL-функций Вызывать PL/SQL-функции из SQL надо осторожно, поскольку даже если сама по себе PL/SQL-функция и процедура весьма эффективна, определенные ресурсы расходуются на вызов PL/SQL-модуля из SQL-оператора. Сервер Oracle включает отдельные механизмы ("машины") для выполнения PL/SQL- и SQL-кода, и каждый переход с одной машины на другую требует определенных ресурсов. В этом разделе мы оценим расходы ресурсов, а затем поищем пути их сокращения. Мы также продемонстрируем, насколько важно следовать правильной практике использования связываемых переменных и минимизации разбора, представленной в главе 1, в ситуациях, когда PL/SQL-код вызывается динамически.
Используйте PL/SQL для раскрытия модели данных, а не для ее расширения Для начала — простой запрос к представлению USER_OBJECTS ИЗ проекта, над которым я сейчас работаю (он связан с регистрацией пациентов госпиталя). SQL> select object_name 2 from user_objects 3 where object_type = 'FUNCTION'; OBJECT NAME GET_ANSWER_NAME GET_ASSESSSMENT_NAME GET_GENDER_NAME GET_LEGALSTATUS_NAME GET_PROGRAM_NAME и т. д. и т. п.
Если просмотреть исходный код любой из этих функций, можно найти часто используемый прием: SQL> 2 3 4
select text from user_source where name = 'GET_GENDER_NAME' order by line;
TEXT function get_gender_name(p_id sex.id%type) return sex.name%type is v_name sex.name%type; begin
Методы оптимизации PL/SQL select into from where return end;
267
name v_name sex id = p_id; v_name;
Как видите, функция GET_GENDER_NAME является частью набора PL/SQL-функций для возврата имени (NAME) ИЛИ описания из таблиц-справочников по суррогатному ключу (ID). К сожалению, получилось так, что я не перенес их все в пакеты! Тем не менее, весь поиск по ключам выполняется с помощью этих функций, что повышает производительность приложения и упрощает дальнейшее его сопровождение. Итак, с учетом представленного примера, в любом месте приложения, где надо будет получить пол пациента по известному коду, использование этой функции гарантирует, что включенный в нее SQL-оператор будет использоваться совместно, а любые дальнейшие изменения в структуре базовой таблицы надо будет учесть только в этой функции, а не в сотне мест по всему коду приложения. Проблема в том, что при наличии таких PL/SQL-функций разработчики начинают считать их "бесплатными" и использовать для получения пола пациента в любом контексте функцию GET_GENDER_NAME. Таким образом, эти функции начинают появляться в SQL-операторах. Например, для выдачи детальной информации о пациенте, включая пол, пишут следующие запросы: SQL> select id, 2 first_name| I ' 'I Ilast_name full_name, 3 get_gender_name(sexid) gender 4 from persons; ID FULL_NAME 10504 John Peterson 10507 Andrew Betent 10514 Michelle Jones 10524 Peta Lazenby
GENDER MALE MALE FEMALE FEMALE
•
Результаты получаются правильные, но одна из основных причин использования суррогатных ключей состоит в том, что реляционные базы данных создавались для соединения данных. Из представленного запроса достаточно очевидно, что столбец SEXID в таблице PERSONS является внешним ключом, ссылающимся на таблицу SEX. Поэтому в запросе надо использовать соединение без каких бы то ни было вызовов PL/SQL. SQL> select p.id, 2 p.first_nameI I' 'I |p.last_name full name, 3 sex.name 4 from persons p. 5 sex 6 where sex.id = p.sexid;
268
Глава 5 ID FULL NAME 10504 10507 10514 10524
John Peterson Andrew Betent Michelle Jones Peta Lazenby
NAME MALE MALE FEMALE FEMALE
Далее эта проблема усугубляется тем, что подобные функции появляются в определениях представлений, потому что зачастую запросы к представлениям требуют получения текстового описания по суррогатному ключу, а не самого суррогатного ключа. Например, для получения детальной информации о пациенте мы могли создать представление PERSON_DETAILS следующим образом: create or replace view PERSON_DETAILS ( person_id, first_name, last_name, gender, suburb, hospital . . .) as select person_id, first_name, last_name, get_gender_name (sex_id), get_svburb_descr (suburb_±d) , get_hospital_name (cur_hospital_id) from
person;
Как только эти функции будут использованы в определениях столбцов представлений, их начнут применять в условиях конструкции WHERE. Медленно, но уверенно мы заменяем SQL-соединения альтернативной реализацией на языке PL/SQL. Вспомните о причинах создания языка PL/SQL: это расширение SQL (в данном случае — возможностей соединения таблиц), но не замена его. Надо быть осторожным при вызове PL/SQL-функций из SQL-операторов, потому что, как было сказано в начале этого раздела, сервер Oracle использует разные механизмы для выполнения кода PL/SQL и SQL, и при переключении контекста с одного на другой расходуются определенные ресурсы. Приблизительно оценить затраты на переключение контекста между PL/SQL- и SQL-машиной можно с помощью следующего теста (спасибо Дэйву Энсору (Dave Ensor) за предоставленную методику проведения эксперимента). Для оценки затрат на переключение контекста можно сравнить время выполнения набора вызовов триггера, выполняющего только присвоение значения переменной PL/SQL, и такого же триггера, который также выполняет простой оператор SELECT (и тем самым обращается к SQL-машине). Общая стоимость переключения контекста будет определяться по следующей формуле:
Методы оптимизации PL/SQL Реьляна_переключение_контекста Peмявь^пoлнeнкя_тpJ>fГ'гIepa_бeз_select
269
ремя'• выполнеиия _операторов_зе1есЬ
Тест можно провести следующим образом: сначала мы протрассируем вставку 20000 строк в таблицу, по которой создан строчный триггер, выполняющий лишь простое присвоение значения переменной PL/SQL. SQL> create table T2 ( k number(5)) Table created. SQL> create or replace package P is 2 v number := 0; 3 end; 4 / Package created.
Наш триггер просто прибавляет 1 к ранее объявленной переменной пакета. SQL> 2 3 4 5 6 7
create or replace trigger T2JTRG before insert on T2 for each row begin p.v := p.v + 1; end;
Trigger created.
Мы включаем SQL_TRACE и добавляем в таблицу 20000 строк, тем самым вызывая срабатывание триггера 20000 раз. SQL> alter session set sql_trace = true; Session altered. SQL> 2 3 4
insert into T2 /* выполнение_триггера_без_зе1есЛ */ select rownum from all_objects where rownum alter session set sql_trace = false;
Прежде чем анализировать трассировочный файл, давайте повторим тест, но на этот раз в триггере мы также будем выполнять простой оператор SELECT. SQL> 2 3 4
create or replace trigger T2_TRG before insert on T2 for each row
270
Глава 5 5 declare 6 1 number; 7 begin 8 select 1 into 1 from dual; 9 p.v := p.v + 1; 10 end; 11 /
Trigger created.
Мы очищаем таблицу и снова загружаем в нее те же 20000 строк. SQL> truncate table T2; Table truncated. SQL> alter session set sql_trace = true; Session altered. SQL> 2 3 4
insert into T2 /* выполнение_триггера_с_зе1ес^ */ select rownum from all_objects where rownum alter session set sql_trace = false; Session altered.
При изучении трассировочных файлов мы проделали следующее: сначала проанализировали результаты теста, в котором в триггере выполнялось только присвоение значения PL/SQL-переменной. i n s e r t i n t o T2 /* выполнение_триггера_без_зе1е^ */ s e l e c t rownum from a l l _ o b j e c t s where rownum вычислить все возможные значения PL/SQL-функции до выполнения запроса так, чтобы вообще не пришлось вызывать PL/SQL-функцию. Давайте рассмотрим каждый из этих подходов.
272
Глава 5
Уменьшение количества вызовов Прежде чем сокращать количество вызовов PL/SQL-функции в запросе, нам, очевидно, надо научиться определять, сколько раз она была вызвана. Вероятно, самый простой способ решить эту задачу — воспользоваться переменной пакета, как продемонстрировано в следующем примере. Пакет COUNTER — простой набор процедур для увеличения значения переменной пакета на единицу. Мы будем использовать этот пакет для подсчета количества выполнений PL/SQL-функции при вызове ее из SQL-оператора. В пакете — три простые процедуры, выполняющие такие действия: > сбросить счетчик в ноль; > увеличить счетчик на 1; > выдать текущее значение счетчика. Код пакета COUNTER представлен ниже. SQL> 2 3 4 5 6 7
create or replace package counter is procedure reset; procedure inc; procedure show; end; /
Package created. SQL> 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
create or replace package body counter is cnt pls_integer := 0; procedure reset is begin cnt := 0; end ; procedure inc is begin cnt := cnt + 1; end; procedure show is begin dbms_output.put_line('Execution Count: 'I lent); end; end; /
Package body created.
Теперь, если мы хотим зарегистрировать, сколько раз функция DO_SOMETHING была вызвана при выполнении в некотором SQL-операторе, мы просто меняем соответствующую функцию так, чтобы она увеличивала значение переменной пакета при каждом вызове.
Методы оптимизации PL/SQL 273 SQL> 2 3 4 5 6 7 8 9
create or replace function do_something(p varchar2) return varchar2 is begin counter.inc; — (любая обработка, выполняемая функцией) return р; end; /
Function created.
Для подсчета количества выполнений останется выполнить запрос и выдать значение счетчика. Перед началом тестирования мы сбрасываем счетчик в ноль. SQL> exec counter.reset; PL/SQL procedure successfully completed.
Затем создаем таблицу BIG_TAB, являющуюся копией таблицы DBA_SOURCE (можете попросить АБД создать вам эту таблицу). SQL> create table big_tab 2 as select owner, name, type, line 3 from dba_source; Table created. SQL> analyze table big_tab estimate statistics; Table analyzed.
Теперь выполним запрос к таблице BIG_TAB, вызывая функцию DO_SOMETHING для каждой строки, возврашенной запросом. SQL> s e l e c t name, l i n e , 2 from big_tab; NAME STANDARD STANDARD STANDARD STANDARD STANDARD STANDARD STANDARD
do_something(owner) LINE 1 2 3 4 5 6 7
DO SOMETHING(OWNER) SYS SYS SYS SYS SYS SYS SYS
143349 rows selected.
Запрос вернул 143349 строк, и мы можем вызвать соответствующую процедуру пакета COUNTER, чтобы узнать, сколько раз была вызвана функция DO_SOMETHING.
274
Глава 5
SQL> exec counter.show; Execution Count: 143349 PL/SQL procedure successfully completed.
To, что функция в данном случае была выполнена один раз для каждой строки таблицы, вполне понятно. Однако более интересные результаты получаются при анализе операторов посложнее. Мы сбросим счетчик и на этот раз выполним запрос, в котором функция DO_SOMETHING упоминается трижды. SQL> exec counter.reset; PL/SQL procedure successfully completed. SQL> select name, line, do_something(owner) 2 3 4 5
from big_tab where do_something(owner) i s not null order by do_something(owner) desc /
NAME STANDARD STANDARD STANDARD STANDARD STANDARD STANDARD STANDARD
LINE
1 2
3 4 5 6 7
DO_SOMETHING(OWNER)
SYS SYS SYS SYS SYS SYS SYS
143349 rows selected. SQL> exec counter.show; Execution Count: 286698 PL/SQL procedure successfully completed.
Количество выполнений выросло вдвое. Это впечатляющий результат, поскольку он показывает, что сервер Oracle не просто выполняет функцию каждый раз, когда она упоминается в SQL-запросе. Оптимизатор может применять различные преобразования SQL-запросов для повышения производительности, так что предсказать количество выполнений PL/SQL-функции в SQL-запросе в общем случае сложно, если вообще возможно. Однако, построив текст SQL-оператора соответствующим образом, разработчик может "помочь" оптимизатору отказаться от излишних вызовов функции — этот процесс мы сейчас и опишем. В нашем тестовом случае первый шаг на пути сокращения количества выполнений состоит в уяснении того факта, что, хотя в таблице BIG_TAB И находятся 143349 строк, большое количество разных значений в столбце OWNER маловероятно. Простая проверка показывает, что фактически различных значений в столбце OWNER всего 15.
Методы оптимизации PL/SQL 275 SQL> select count(distinct owner) 2 from dba_source; COUNT(DISTINCTOWNER)
С учетом этого можно попытаться использовать вложенное представление для вычисления результатов функции DO_SOMETHING ТОЛЬКО ДЛЯ различных значений в столбце OWNER. SQL> exec counter.reset; SQL> select name, line, function_result 2 from big_tab a, 3 ( select distinct owner, do_something(owner) function_zresult 4 from big_tab ) f 5 where a.owner = f.owner 6 / SQL> exec counter.show; Execution Count: 143349 PL/SQL procedure successfully completed.
Никаких улучшений нет, но отрицательный результат первой попытки следовало ожидать, поскольку удаление дублирующихся значений должно выполняться после вызова функции DO_SOMETHING (Т.К. каждый вызов может вернуть любое допустимое значение). Это приводит к следующей версии кода, в которой сначала определяется набор уникальных владельцев, а затем к результатам применяется функция DO_SOMETHING. SQL> exec counter.reset; SQL> select name, line, function result — 2 from big_tab a, 3 ( select owner, do_something(owner) function_result 4 from ( select distinct owner 5 from bigjtab ) ) f 6 where a.owner = f.owner 7 / SQL> exec counter.show; Execution Count: 143349 PL/SQL procedure successfully completed.
По-прежнему безуспешно. Судя по тексту запроса, количество выполнений должно уменьшиться, следовательно, оптимизатор применял какое-то преобразование вложенного представления в запросе, что привело к увеличению количества выполнений функции. Пора обратиться к руководствам. Быстрый просмотр руководства "Performance Tuning Guide" показывает, что "различность" — недостаточный критерий, чтобы не дать оптимизатору применять преобразования к вложенным представлениям.
276
Глава 5
"Оптимизатор может объединять представление с блоком ссылающегося запроса, когда представление ссылается на одну или несколько базовых таблиц и если оно не содержит следующие конструкции: >
операторы для работы с множествами (UNION, UNION ALL, INTERSECT, MINUS);
>
конструкцию CONNECT BY;
>
псевдостолбец ROWNUM;
>
функции агрегирования (AVG, COUNT, MAX, MIN, SUM) в списке выбора".
Это подсказывает путь к подходящему решению. Если изменить вложенный запрос так, чтобы в нем упоминался псевдостолбец ROWNUM, TO (В соответствии с руководством) вложенное представление не будет объединено с внешним запросом. Давайте проверим эту гипотезу. SQL> exec counter.reset; SQL> select name, line, function_result 2 from big_tab a, 3 ( select owner, rownum, do_something(owner) function_result 4 from ( select distinct owner 5 from big_tab ) ) f 6 where a.owner • f.owner 7 / SQL> exec counter.show; Execution Count: 15 PL/SQL procedure successfully completed.
Получилось! Вместо изменения запроса и добавления столбца ROWNUM МОЖНО использовать подсказку NOMERGE, как описано в руководстве "Performance Tuning Guide". В любом случае рекомендуется подробно прокомментировать код, поскольку человеку, который будет его поддерживать, будет неочевидно, зачем потребовался псевдостолбец ROWNUM или подсказка NO_MERGE.
Вычисление значений заранее с помощью индексов по функции Вместо того чтобы пытаться уменьшить количество выполнений функции, что, если бы мы смогли сохранить все возможные результаты выполнения функции DO_SOMETHING так, чтобы при выполнении запроса любой сложности вообше не приходилось ее вызывать? Ну, для сохранения результатов PL/SQL-функции отлично подходит индекс по функции, так что мы рассмотрим его использование для сокращения количества вызовов. Примечание Для создания и использования индексов по функции необходимы определенные привилегии и соответствующая установка параметров инициализации. Подробнее об этом читайте в разделе "Создание индекса по функции" в руководстве "Database Administrator's Guide".
Методы оптимизации PL/SQL 277 Давайте создадим индекс по функции на базе выражения DO_SOMETHING (OWNER) . SQL> create index big_ix on 2 big_tab ( do_something(owner)); create index big_ix on big_tab ( do_something(owner)) * ERROR at line 1: ORA-30553: The function is not deterministic
Прежде всего, вы обнаружите, что любая функция, которая используется в индексе по функции, должна быть определена как детерминированная (deterministic), т.е. вы должны гарантировать серверу Oracle, что при одних и тех же входных данных функция будет всегда возвращать одинаковый результат. Итак, после соответствующего переопределения функции ее можно использовать в индексе. SQL> 2 3 4 5 6 7
create or replace function do_something(p_owner varchar2) return varchar2 deterministic is begin counter.inc; return p_owner; end;
Function created. SQL> create index big__ix on 2 big_tab ( do_something(owner)); Index created.
Чтобы индексы по функции учитывались оптимизатором, необходимо собрать статистическую информацию, потому что индексы по функции "видны" только оптимизатору, основанному на стоимости. SQL> analyze index big_ix estimate s t a t i s t i c s ; Index analyzed.
Теперь осталось убедить оптимизатор использовать индекс вместо вызова функции. Мы начнем с самого первого запроса из первого примера. SQL> exec counter.reset; SQL> select name, line, do_something(owner) 2 from big tab; — SQL> exec counter.show; Execution Count: 143349
Изучение плана выполнения показывает, что был использован полный просмотр таблицы, т.е. индекс не был использован. Более того, при более глубоком изучении трассировки события 100531 оказывается, что индекс даже и не рассматривался. Это ' Полное обсуждение трассировки события 10053 выходит за рамки нашей книги, но поиск в Google по этому событию позволит вам получить много источников информации о том, как это событие использовать и как интерпретировать результаты трассировки.
278
Глава 5
решение оптимизатора абсолютно верно, поскольку в индексе никогда не хранятся неопределенные значения и, конечно, нет никаких гарантий, что функция DO_SOMETHING не вернет неопределенное значение. Если функция DO_SOMETHING действительно возвращает неопределенное значение для некоторых строк, то использование индекса по функции приведет к тому, что часть данных из результирующего множества будет пропущена. К сожалению, в Oracle нет способа сообщить оптимизатору, что результат используемой функции (в индексе) не будет неопределенным, так что мы можем лишь сообщить об этом оптимизатору в самом запросе. SQL> exec counter.reset; SQL> select name, line, do_something(owner) 2 from big_tab 3 where do_samething(owner) is not null; SQL> exec counter.show; Execution Count: 286698
Даже при указании этого дополнительного условия, похоже, оптимизатор не выбирает наш индекс. По крайней мере, в этом случае трассировка события 10053 показывает, что индекс рассматривался оптимизатором, но он счел его использование неэффективным. Возможно, если бы таблица была больше, оптимизатор мог бы его выбрать. В любом случае можно вынудить оптимизатор его использовать с; помощью подсказки. SQL> exec counter.reset; SQL> select /*+ INDEX(big_tab Ыд_±х) */ name, 2 line, do_something(owner) 3 from big_tab 4 where do_something(owner) is not null SQL> exec counter.show; Execution Count: 0
Итак, в этом конкретном случае использование индекса по функции для ускорения вызовов PL/SQL-функции из SQL-оператора возможно, но пришлось задать соответствующую подсказку. Подводя итоги, мы снова подтвердим утверждение: язык PL/SQL является расширением SQL, но не его заменой. Не дайте преимуществам PL/SQL, связанным с тесной интеграцией с базой данных, превратиться в фактор, ограничивающий производительность.
Динамический вызов PL/SQL В главе 1 уже были продемонстрированы существенные затраты ресурсов на разбор запросов в приложениях. Но вы также видели, что одним из преимуществ PL/SQL является то, что использование связываемых переменных и сведение разборов к минимуму происходит автоматически, если вы используете статические операторы SQL в программных единицах PL/SQL. Все PL/SQL-переменные становятся связываемыми переменными, поэтому разбор сводится к минимуму, что повышает производительность. Хотя PL/SQL и сводит разбор к минимуму автоматически, хорошей практики использования статического SQL и связываемых переменных в PL/SQL еще не дос-
Методы оптимизации PL/SQL 279 таточно — надо также позаботиться об этом при выполнении соответствующего PL/SQL-кода. Часто встречается неправильный прием кодирования — передача строк вместо подстановки соответствующих значений параметров при вызове PL/ SQL (обычно из другой среды например, Java). Как и SQL-оператор, блок PL/SQLкода также должен быть разобран. Рассмотрим, например, процедуру, принимающую числовой параметр х: SQL> create or replace 2 procedure DO WORK(x number) is 3 • у number; " 4 begin 5 у := x; 6 end; 7 / Procedure created.
Можно протестировать производительность этой процедуры, вызывая 10000 раз анонимный блок, в котором для передачи параметра процедуре используются связываемые переменные. С помощью средств регистрации времени выполнения в среде SQL*Plus легко выяснить, что можно вызывать процедуру примерно 2000 раз в секунду. SQL> SQL> 2 3 4 5 6
set timing on begin for i in 1 .. 10000 loop execute immediate 'begin do_work(:x); end;' using i; end loop; end; /
PL/SQL procedure successfully completed. Elapsed: 00:00:05.05
Однако достаточно часто PL/SQL-процедуры вызываются из среды, отличной от PL/SQL, например, Java или Visual Basic, в которой анонимный блок просто строится как строка и параметры передаются как литералы. При этом производительность катастрофически падает. Это можно сымитировать с помощью следующего PL/SQLблока. Мы вызываем процедуру DO_WORK 10000 раз, как и раньше, но каждый раз параметр х передается как литерал, а не через связываемую переменную. SQL> 2 3 4 5 6
begin for i in 1 .. 10000 loop execute immediate 'begin do_work('||i||'); end;'; end loop; end; /
PL/SQL procedure successfully completed. Elapsed: 00:01:47.02
280
Глава 5
Производительность при этом снизилась в 20 раз. Нет никакого оправдания привнесению в базу данных такой проблемы производительности. Даже если используется среда разработки, в которой связывание проблематично или невозможно, проблему разбора все равно можно в некоторой степени обойти с помощью пары альтернативных вариантов: использовать таблицу для хранения значений параметров или хранить параметры в наборе. Оба эти варианта мы сейчас рассмотрим.
Использование таблицы В среде, где связывание переменных не поддерживается, можно использовать возможности, предоставляемые в Oracle параметром CURSOR_SHARING2, И таблицу базы данных для передачи параметров. Например, можно преобразовать предыдущий пример, процедуру DOWORK, так, чтобы параметры брались из таблицы. Можно использовать временную таблицу, чтобы сократить расходы ресурсов при добавлении строк. SQL> create global temporary table parms 2 3
( parm_val number ) on commit preserve rows;
Table created.
Затем мы изменяем процедуру DO_WORK так, чтобы обрабатывались все строки в таблице PARMS, а не одна. SQL> create or replace 2 procedure DO WORK is
r> С • 3 4 5
~
begin for i in ( select * from parms ) loop — обработка для каждой строки
6 end loop; 7 end; 8 / Procedure created. Итак, для "передачи параметров" процедуре мы теперь можем добавлять значения в таблицу PARMS. Можно установить параметр CURSOR_SHARING, чтобы гарантировать, что SQL-операторы, используемые для добавления параметров, тоже не вызывают лишние разборы. SQL> a l t e r session set cursor_sharing = force; Session a l t e r e d . — Имитируем с помощью PL/SQL вызывающую среду, в которой — нельзя использовать связываемые переменные 2
Параметр CURSOR_SHARING появился в версии Oracle 8\. Для приложений, не использующих связываемые переменные, при соответствующей установке параметра CURSOR SHARING все вхождения литералов в SQL-операторы заменяются сгенерированными системой связываемыми переменными. Это часто можно использовать как средство "первой помощи" при решении проблемы разбора.
Методы оптимизации PL/SQL SQL> 2 3 4 5 6 7 8
281
begin for i in 1 .. 10000 loop execute immediate 'insert into parms values ('I I ill')'; end loop; execute immediate 'begin do_work; end;'; execute immediate 'truncate table parms'; end; /
PL/SQL procedure successfully completed.
Elapsed: 00:00:05.02
За счет использования временной таблицы это решение пригодно и в многопользовательской среде, поскольку каждый сеанс может видеть только свои значения в таблице PARMS.
Использование типа Еще одной альтернативой, позволяющей избежать лишних разборов процедуры, является "упаковка" литералов в набор и передача процедуре всего набора. Продолжая предыдущий пример, можно передавать процедуре набор числовых значений с помощью предопределенного типа, NUM_LIST. SQL> create or replace 2 type лит list is table of number; 3 / Type created. SQL> 2 3 4 5 6 7 8
create or replace procedure DO_WORK(x num_list) is begin x.count loop for i in 1 null; end loop; end; 1
Procedure created.
Для обеспечения вызова процедуры с соответствующим литералом можно использовать небольшой фрагмент PL/SQL-кода, так что процедура DO_WORK будет вызываться следующим образом: do_work ( num_list (1, 2,3,.... n) ) ; SQL> set timing on SQL> declare 2 v varchar2(32767) := 'num_list('; 3 begin 4 for i in 1 .. 5000 loop
282
Глава 5 5 6 7 8 9 10
v := v || case when i = 1 then to_char(i) else ','||i end; end loop; v := v ! I ') '; execute immediate 'begin do_work('||v||'); end;'; end; /
PL/SQL procedure successfully completed. Elapsed: 00:00:01.08
Итак, использование типа или таблицы позволяет избежать затрат на разбор при передаче литералов в качестве параметров PL/SQL-кода в любой вызывающей среде. Даже если приходится использовать среду, допускающую только чистый динамический SQL, есть возможность свести к минимуму необходимое количество разборов. Оба рассмотренных варианта мы назвали "первой помощью", т.е. они представляют собой, прежде всего, способы обойти порочную практику неиспользования связываемых переменных. Каждый способ требует дополнительных затрат ресурсов, которые не всегда очевидны. Рассмотрим второй способ, когда большой набор генерируется и передается процедуре. Если посмотреть на статистическую информацию уровня сеанса после вызова процедуры DO_WORK, оказывается, что передача большого набора влияет на использование памяти. SQL> select * from v$mystats; NAME
VALUE
session pga memory max
14910880
Мы "сожрали" 14 Мбайтов памяти сеанса для передачи большого набора!
ПАРАМЕТР CURSOR_SHARING ДЛЯ "ОДНОРАЗОВЫХ" СЦЕНАРИЕВ Установка параметра CURSORJSHARING на уровне сеанса может пригодиться, если вам когда-нибудь встретится большой файл с операторами DML (например, статические сценарии для заполнения таблицы данными). Предположим, вам надо выполнить сценарий следующего вида: insert into MY_TABLE values (1,2,3); insert into MYJTABLE values (4,5,6); insert into MY_TABLE values (7,8,9); etc
Добавление в начале сценария оператора ALTER SESSION SET CURSOR_SHARING = FORCE повысит производительность при выполнении сценария и, что важнее, уменьшит его влияние на остальные сеансы в системе.
Методы оптимизации PL/SQL
283
Использование SQL в PL/SQL В этой главе уже было продемонстрировано, что переход между PL/SQL- и SQLмашиной требует определенных ресурсов. К сожалению, затраты на этот переход могут возникать даже без вашего ведома, как будет продемонстрировано в этом разделе. Конечно, даже с учетом этих затрат на переход нельзя писать PL/SQL-программы вообще без использования SQL-операторов — от базы данных мало толку, если вы не обращаетесь к ней с запросами! Так что в этом разделе мы рассмотрим и способы минимизации использования динамического SQL, который обычно требует больше затрат на обработку, чем статический SQL.
SQL-функции и рекурсивный SQL Сервер Oracle предлагает ряд функций, предоставляющих информацию о текущей среде, например, SYSDATE, USER И UID, И ЭТИ функции можно использовать в языке PL/SQL. Например, пусть имеется таблица со столбцами CHANGE_DATE И CHANGEBY, которая используется для регистрации того, кто изменил запись о сотруднике и когда это было сделано. Мы можем использовать триггер для автоматизации аудита этих изменений в таблице ЕМР, записывая соответствующую информацию в новую таблицу, AUDIT ЕМР. SQL> create table AUDIT ЕМР number(10), empno varchar2(30) reason change_date date, varchar2(30) change by Table created. SQL> 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
create or replace trigger AUDIT_EMP_CHANGES before insert or update or delete on EMP for each row declare v_change date := sysdate; v uid number := uid/ v_reason varchar2(30); begin if inserting then v_reason := 'Addition'; elsif deleting then v_reason := 'Deletion'; elsif updating then v_reason := 'Pre-Modification'; end if; insert into audit_emp values ( :new.empno, v_reason, v_change, v_uid) end; /
284
Глава 5
Мы используем функции SYSDATE И UID ДЛЯ записи текущего времени и идентификатора текущего пользователя для добавления строк в таблицу аудита. Однако при включении трассировки SQL> alter session set sql_trace = true; Session altered,
и изменении затем 14-ти строк триггер срабатывает 14 раз: SQL> d e l e t e from emp; 14 rows deleted.
открываются интересные подробности этого невинно выглядящего кода. После обработки трассировочного файла с помощью утилиты TKPROF В нем можно обнаружить следующий SQL-оператор: SELECT uid from sys.dual
count
cpu
elapsed
disk
query
current
rows
Parse Execute Fetch
1 14 14
0.00 0.00 0.00
0.00 0.00 0.00
0 0 0
0 0 42
0 0 0
0 0 14
total
29
0.00
0.00
0
42
0
14
call
В нашем коде не содержался этот оператор в явном виде, так откуда же он там взялся? Хотя различные функции вроде UID И SYSDATE В PL/SQL-коде допустимы, некоторые из них могут вернуть значение только с помощью SQL-машины. Поэтому, чтобы получить значение UID, наш триггер, обеспечивающий аудит изменений информации о сотрудниках, сгенерировал рекурсивный SQL-оператор для каждой строки, затронутой исходным оператором DML. Это же поведение мы видели в главе 2 при использовании функции SYSCONTEXT. Обратите внимание, что функция SYSDATE такой проблемы не вызывает — этот тест выполнялся на сервере версии 9.2, но вы можете обнаружить рекурсивные SQL-операторы и для SYSDATE, В зависимости от используемой версии сервера Oracle. Вероятно, самый простой способ определить, вызывает ли использование функции в PL/SQL выполнение рекурсивного SQL-оператора, — создать простой тест и проверить трассировочный файл. Например, чтобы проверить функцию SOUNDEX, включим трассировку и выполним в цикле несколько итераций вызова SOUNDEX. SQL> alter session set sql_trace = true; Session altered. SQL> declare 2 x varchar2(80);
Методы оптимизации PL/SQL 285 3 begin 4 for i in 1 .. 100 loop 5 x := soundex('asdasd'); 6 end loop; 7 end; 8
/
PL/SQL procedure successfully completed. SQL> alter session set sql_trace =false; Session altered.
Просмотр трассировочного файла показывает, что функция SOUNDEX также выполняется SQL-машиной. SELECT soundex (:Ы) from sys.dual
call
count
cpu
elapsed
disk
query
current
rows
Parse Execute Fetch
0 100 100
0.00 0.01 0.01
0.00 0.00 0.00
0 0 0
0 0 300
0 0 0
0 0 100
total
200
0.02
0.00
0
300
0
100
Однако здравомыслящий разработчик легко сможет избежать появления этого рекурсивного SQL-оператора. Если функции используются для установки значений, которые будут использоваться в SQL-операторе (как в представленном ранее триггере), функции можно указывать непосредственно в этом SQL-операторе. В примере с триггером, обеспечивающим аудит, нам не нужно сохранять значения UID И SYSDATE в переменных. Мы можем просто использовать их при вставке строки в таблицу аудита. Код нужно изменить следующим образом: SQL> 2 3 4 5 6 7 8 9 10 11 12 13 14 15
create or replace trigger AUDIT_EMP_CHANGES before insert or update or delete on EMP for each row declare v_reason varchar2(30); begin if inserting then v_reason := 'Addition'; elsif deleting then v_reason := 'Deletion'; elsif updating then v_reason := 'Modification'; end if;
286
Глава 5 16 17 18 19
insert into audit_emp values ( :new.empno, v_reason, sysdate, uid) ; end; /
Trigger created.
Если функции предполагается использовать не в SQL-операторах (например, вы можете вносить записи аудита в файл, а не в таблицу), надо постараться избавиться от лишних вызовов функций. Например, значения функций USER И UID не будут меняться за время сеанса, так что можно присвоить их переменным пакета и использовать эти переменные при необходимости. Можно создать в спецификации пакета переменную, которая инициализируется при первом обращении к пакету значением функции и ID. SQL> 2 3 4 5
create or replace package U is id number := uid; end; /
Package created.
Вот версия триггера, реализующего аудит, которая записывает информацию в файл и не выполняет лишних вызовов функции UID, используя вместо нее переменную пакета. SQL> 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
create or replace trigger AUDIT_EMP_TO_FILE before insert or update or delete on EMP for each row declare f utl_file.file_type; v_reason varchar2(30); begin f := utl_file.fopen('/tmp','audit.dat','A'); if inserting then v_reason := 'Addition1; elsif deleting then v_reason := 'Deletion'; elsif updating then v_reason := 'Modification'; end if; utl_file.put_line(f,'EMPNO '||:new.empno); utl_file.put_line(f,'REASON '||v_reason); utl_file.put_line(f,'DATE '||sysdate); utl_file.put_line(f,'UID 'Mu.id); utl_file.fclose(f); end; /
Trigger created.
Методы оптимизации PL/SQL 287 При трассировке сеанса в ходе выполнения нескольких изменений данных в таблице ЕМР операторами DML можно убедиться, что использовался всего один рекурсивный SQL-оператор. SELECT uid from sys.dual
call
count
cpu
elapsed
disk
query
current
Parse Execute Fetch
1 1 1
0.00 0.00 0.00
0.00 0.00 0.00
0 0 0
0 0 3
0 0 0
0 0 1
total
3
0.00
0.00
0
3
0
1
Читатели, желающие получить исчерпывающий список функций, генерирующих рекурсивный SQL, могут обратиться к телу базового пакета PL/SQL, STANDARD. ИСХОДНЫЙ текст тела этого пакета находится в файле $ORACLE_HOME/rdbs/admin/ stdbody. sql. Поиск по этому файлу позволяет выяснить, какие функции выполняются с помощью "select from dual". Например, функция SOUNDEX определена следующим образом: function SOUNDEX(ch VARCHAR2 CHARACTER SET ANY_CS) return VARCHAR2 CHARACTER SET ch%CHARSET is с VARCHAR2(2000) CHARACTER SET ch%CHARSET; begin select soundex(ch) into с from sys.dual; return c; end SOUNDEX;
На момент написания этой главы список функций, генерирующих рекурсивные SQL-операторы, в версии 9.2 был таким: NLS_CHARSET_NAME NLS_CHARSET_ID NLS_CHARSET_DECL_LEN USERENV SYS_CONTEXT SYS_GUID SOUNDEX UID USER SYSTIMESTAMP DBTIMEZONE
Если вы используете любое средство генерации кода, не забудьте бегло просмотреть полученный код. По моему опыту, сгенерированный код (например, программные интерфейсы для работы с таблицей в Designer) часто перегружен лишними обращениями, в частности, к функции USER.
288
Глава 5
Эффективный динамический SQL Как обсуждалось в главе 1, каждый новый оператор SQL, передаваемый серверу, необходимо разобрать, чтобы проверить его допустимость перед попыткой выполнения, так что чем меньше новых SQL-операторов передается серверу, тем лучше система будет масштабироваться. Это не означает, что динамическому SQL нет места в языке PL/SQL — если бы он никогда не был нужен, корпорация Oracle никогда бы не разработала пакет DBMSSQL и уж точно не добавляла бы возможности непосредственного выполнения динамического SQL, появившиеся в версии 8/. Однако это не означает, что можно использовать динамический SQL бездумно. Как ни странно, наиболее типичный пример использования динамического SQL в книгах связан с "неизвестным" именем таблицы. Следующий код принимает имя таблицы как параметр и возвращает количество строк в соответствующей таблице: SQL> 2 3 4 5 6 7 8 9 10
create or replace procedure GET_ROW_COUNT(p_table varchar2) is с number; begin execute immediate 'select count (*) from 'I|p_table into c; dbms_output.put_line(cI Г rows'); end; /
Procedure created. SQL> set serverout on SQL> exec get_row_count('emp'); 17 rows PL/SQL procedure successfully completed. SQL> exec get_row_count('dept'); 4 rows PL/SQL procedure successfully completed.
Но, если серьезно, как часто встречаются проекты, для которых код написан, а точное имя таблицы для определенного запроса заранее не известно? Могут, конечно, быть приложения столь универсальные, что в них подобная возможность необходима, но я уверен, что их очень мало. В производственной среде я вижу только две основных причины использования динамического SQL в PL/SQL.
Выполнение операторов DDL С появлением глобальных временных таблиц разработчикам осталось очень немного причин создавать, изменять или удалять объекты в производственной базе данных. Однако для администраторов возможность включить оператор DDL в PL/SQLпроцедуру очень полезна. Например, может понадобиться позволить начальнику
Методы оптимизации PL/SQL
289
отдела переустанавливать пароли только для пользователей, работающих в его отделе. Ясно, что администратор не хочет предоставлять начальнику привилегию ALTER USER, потому что тогда начальник сможет изменить параметры любой учетной записи пользователя (и не только пароль), поэтому помещение соответствующего оператора DDL в процедуру сделает этот процесс более контролируемым и безопасным. Следующая процедура RESET_PASSWORD гарантирует, что начальник сможет изменить пароли только у пользователей, являющихся сотрудниками его отдела. Затем администратор может предоставить начальнику привилегию на выполнение этой процедуры. SQL> create or replace 2 procedure reset_password(p_empno varchar2) i s 3 v_username varchar2(30); 4 begin 5 6 7 8 9 10 11 12 13 14 15 16 17 18
select ename into v_username from emp where empno = p_empno and mgr = ( select empno from emp where ename = user ) ; execute immediate 'alter user "'||v_username|I'" 'II ' identified by 'I Iv_username; exception when no_data_found then raise_application_error(-20000, 'You are not authorised to alter employee 'I|p_empno); end; /
Procedure created.
Посмотрим, что произойдет, когда начальник, BLAKE, зарегистрируется в системе и попытается сбросить пароль для сотрудника, работающего не в его отделе. SQL> conn blake/blake Connected. SQL> exec admin.reset_password(7788); BEGIN admin.reset password(7788); END; — ERROR at line 1: ORA-20000: You are not authorised to alter employee 7788 ORA-06512: at "ADMIN.RESET_PASSWORD", line 14 ORA-06512: at line 1
Однако когда регистрируется пользователь ADAMS, ОН может сбросить этот пароль. SQL> conn adams/adams Connected. SQL> exec admin.reset_password(7788); PL/SQL procedure successfully completed. 10 Зак.348
290
Глава 5
С помощью PL/SQL мы можем расширить обработку операторов DDL, чтобы их можно было включить в текущую транзакцию. В отличие от некоторых других реляционных СУБД, при обработке оператора DDL СУБД Oracle всегда завершает текущую транзакцию (фиксируя ее), прежде чем выполнять оператор DDL в отдельной транзакции. Это может представлять собой проблему, если DDL-операторы выполняются в приложениях. Что, если в предыдущем примере мы хотим регистрировать изменения паролей, записывая информацию о них в таблицу аудита? У нас есть два варианта: > мы сначала добавляем запись в таблицу аудита, а затем обрабатываем оператор DDL для сброса пароля; > мы сначала сбрасываем пароль, а затем добавляем запись в таблицу аудита. Оба варианта проблематичны с точки зрения обеспечения целостности данных. Если мы сначала добавим запись аудита, но сбросить пароль не получится по какойлибо причине, окажется, что мы зарегистрировали изменение, которое не произошло. Если же мы сначала сбросим пароль, но не получится вставить запись в таблицу аудита, мы потеряем информацию аудита. Надо, чтобы оба шага либо выполнились, либо не выполнились. Решение будет состоять в том, чтобы выполнять DDL-оператор в задании, созданном с помощью средств пакета DBMSJOB. Сначала мы создаем таблицу для хранения записей аудита, а затем переписываем процедуру RESET_PASSWORD С использованием пакета DBMS_JOB. SQL> create table PASSWORD_AUDIT ( 2 empno number, 3 date_change date ) ; Table created. SQL> 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
create or replace procedure admin.reset_password(p_empno varchar2) is v_username varchar2(30); v_job number; begin select ename into v_username from emp where empno = p_empno and mgr = ( select empno from emp where ename = user ) ; insert into password_audit values ( p_empno, user, sysdate); d b m s j o b . submit (v_job, 'begin execute immediate ' || '''alter user "'I Iv_username||'" '|| 1 ' identified by '||v_usernameII' '; end;'); exception when no_data_found then raise_application_error(-20000, 'You are not authorised to alter employee '||p_empno);
Методы оптимизации PL/SQL 291 21
end;
Procedure created.
Теперь выполним процедуру для сброса пароля сотрудника CLARK С номером 7782. SQL> exec admin.reset_password(7782); PL/SQL procedure successfully completed.
Обратите внимание, что в этот момент транзакция открыта. Сброс пароля еще не произошел. Мы добавили строку в таблицу PASSWORD_AUDIT ДЛЯ сотрудника CLARK SQL> select * from password_audit; EMPNO DATE CHAN 7782 31/OCT/03
и послали на выполнение задание для сброса пароля сотрудника CLARK: SQL> select what from
user_jobs;
WHAT begin execute immediate 'alter user "CLARK"
identified by CLARK"; end;
Поскольку посылка задания на выполнение входит в транзакцию, в этот момент мы можем либо откатить изменение, в результате чего будут отменены как запись в таблицу аудита, так и выполнение задания по сбросу пароля, либо зафиксировать изменение; при этом запись окажется внесенной в таблицу аудита, а задание — выполненным.
Поддержка изменяемых конструкций WHERE В любой системе, позволяющей вводить произвольные запросы, можно использовать динамический SQL для формирования конструкции WHERE, которая не известна до момента выполнения. Фактически один из самых первых примеров использования динамического SQL в руководстве "PL/SQL Reference" иллюстрирует именно это. CREATE PROCEDURE delete_rows { table_name IN VARCHAR2, condition IN VARCHAR2 DEFAULT NULL) AS where clause VARCHAR2(100) := ' WHERE ' || condition; — BEGIN IF condition IS NULL THEN where_clause : = NULL; END IF; EXECUTE IMMEDIATE 'DELETE FROM ' | | table_name I I where_clause; END;
292
Глава 5
Оправдывает ли заранее неизвестная конструкция WHERE дополнительные расходы ресурсов на выполнение динамического SQL? Сложно сказать. Как всегда, придется идти на компромисс. Использовать динамический SQL проще, но, как было показано, он может повысить затраты ресурсов на разбор. Если же есть разумное количество возможных вариантов, то использование статического SQL для каждого из этих вариантов может оказаться эффективнее. Рассмотрим, например, процедуру, поддерживающую различные варианты конструкции WHERE ДЛЯ стандартной таблицы DEPT: SQL> d e s c d e p t Name
Null?
DEPTNO DNAME LOC
Type NUMBER(2) VARCHAR2U4) VARCHAR2(13)
Если предположить, что все условия для этой таблицы будут задаваться в виде СТОЛБЕЦ = ЗНАЧЕНИЕ и связаны будут только операторами AND ИЛИ OR, МЫ получаем
15 возможных запросов: 1. deptno = ... 2. dname = ... 3. loc = ... 4. deptno = ... and dname = ... 5. deptno = ... or dname - ... 6. deptno = ... and loc = ... 7. deptno = ... or loc = ... 8. loc = ... and dname = ... 9. loc = ... or dname = ... 10. deptno = ... and dname = ... and loc = ... 11. deptno = ... and ( dname » ... or loc = ...) 12. ( deptno = ... and dname = ) ... or loc = ... 13. ( deptno = ... or dname = ) ... and loc = ... 14. deptno = ... or ( dname = ... and loc = ...) 15. deptno = ... or dname = ... or loc = ...
Однако с учетом того, что столбец DEPTNO — первичный ключ этой таблицы, часть представленных вариантов можно отбросить как ненужные. В данном случае варианты 4, 5, 6, 10, 11 и 12 вряд ли кому-то понадобятся, что дает нам девять возможных SQL запросов, которые могут быть выполнены к таблице DEPT. ЭТО, пожалуй, достаточно небольшой набор, чтобы просто статически записать каждый из запросов в процедуре и выбрать требуемый пользователю вариант по значению параметра PJDPTION. Чтобы упростить проверку каждого из возможных статических SQL-
Методы оптимизации PL/SQL
293
операторов, мы использовали явный курсор, чтобы все операторы оказались собранными вместе в начале процедуры. Решение на базе статического SQL может выглядеть так: SQL> create or replace 2 procedure STATIC_ADHOC ( 3 p_deptno number, 4 p_dname varchar2, 5 p_loc number, 6 p_option number ) is 7 cursor cl is 8 select * from dept 9 where deptno = p_deptno; 10 cursor c2 is 11 select * from dept 12 where dname = p_dname; 13 cursor c3 is 14 select * from dept 15 where loc = p_loc; 16 cursor c7 is 17 select * from dept 18 where deptno = p_deptno or loc = p_loc; 19 cursor c8 is 20 select * from dept 21 where loc = p_loc and dname = p_dname; 22 cursor c9 is 23 select * from dept 24 where loc = p_loc or dname = p_dname; 25 cursor cl3 is 26 select * from dept 27 where ( deptno = p_deptno or dname = p_dname) and loc = p_loc; 28 cursor cl4 is 29 select * from dept 30 where deptno • p_deptno or ( dname = p_dname and loc = p_loc); 31 cursor cl5 is 32 select * from dept 33 where deptno = p_deptno or dname « p_dname or loc = p_loc; 34 r dept%rowtype; 35 begin 36 if p_option = 1 then 37 open cl; fetch cl into r; close cl; 38 elsif p_option = 2 then 39 open c2; fetch c2 into r; close c2; 40 elsif p_option = 3 then 41 open c3; fetch c3 into r; close c3; 42 elsif p__option = 7 then 43 open c7; fetch c7 into r; close c7; 44 elsif p_option = 8 then 45 open c8; fetch c8 into r; close c8; 46 elsif p__option = 9 then 47 open c9; fetch c9 into r; close c9;
294
Глава 5 48 49 50 51 52 53 54 55 56
elsif p_option = 13 then open cl3; fetch cl3 into r; close cl3; elsif p_option = 1 4 then open cl4; fetch cl4 into r; close cl4; elsif p_option = 15 then open cl5; fetch cl5 into r; close cl5; end if; end; /
Procedure created.
Конечно, легко будет построить и чисто динамический аналог, в котором вызывающий должен сам построить конструкцию WHERE ПОЛНОСТЬЮ. МОЖНО просто использовать REF CURSOR (см. главу 3) для создания курсора по любой переданной КОНСТРУКЦИИ WHERE. SQL> 2 3 4 5 6 7 8 9 10 11
create or replace procedure DYN_ADHOC(p_where varchar2) is type re is ref cursor; с re; r dept%rowtype; begin open с for 'select * from dept where '||p_where; fetch с into r; close c; end; /
Procedure created.
Но в результате во всех запросах не будут использоваться связываемые переменные. Как было продемонстрировано в главе 1, в системах с большим количеством одновременно работающих пользователей это может стать критическим фактором, препятствующим достижению требуемой производительности. Давайте протестируем две представленные выше версии. Мы создадим копию таблицы DEPT, на основании которой будем генерировать разные запросы. В нашем случае в таблице будет 5000 строк (полученных на основе данных, сгенерированных сценарием REPTEST.SQL, который использовался в главе 1). SQL> create table DEPT_COPY as select * from DEPT; Table created.
Затем мы очистим исходную таблицу DEPT, чтобы производительность каждого из возможных запросов была одинаковой (т.е. любой запрос не найдет ни одной строки). SQL> truncate table DEPT; Table truncated.
Методы оптимизации PL/SQL
295
А теперь, чтобы протестировать производительность, мы будем выполнять каждую процедуру с разными вариантами конструкции WHERE, которые считаются возможными. Проходя в цикле по созданной ранее копии таблицы DEPT, МОЖНО будет использовать значение псевдостолбца ROWNUM для генерации некоторых "случайных" вариантов возможных условий. SQL> set timing on SQL> begin 2 for i in ( 3 select dc.*, 4 rownum r,
Мы берем остаток от деления номера строки на девять, чтобы получить один из девяти возможных вариантов конструкции WHERE. 5 6 7 8 9 10 11 12 13 14 15
decode(mod(rownum,9)+1, 1,'(deptno= 'I Ideptnol I' or dname='''| |dname| |'' ') and ^loc=' I H o c , 2,'deptno='|IdeptnolI' or ( dname='''||dname||''' and ^•loc=' | I loci | ') ', 3,'deptno = '|Ideptnol|' or dname = '''|IdnamelI''' or loc = "•' I H o c , 4,'deptno = 'IIdeptnol|' or loc = '|Iloc, 5, 'deptno = 'Mdeptno, 6, 'dname = '"II dname I I " ' 1 , 7,'loc = 'I Iloc| |' and dname = '''I |dnameII'''', 8, ' loc = ' I I loc || ' or dname = '''II dname I I ' " 1 , 9,'loc = '|Iloc) where_clause from dept copy dc ) loop
Затем передаем конструкцию WHERE версии с динамическим SQL, включая любые литералы, как параметр. 16 DYN_ADHOC(i.where_clause) ; 17 end loop; 18 end; 19 / PL/SQL procedure successfully Elapsed: 00:01:57.00
completed.
А теперь делаем то же самое для статической версии, в которой мы заранее записали все возможные варианты конструкций WHERE В процедуре STATIC_ADHOC, так что надо передать только число в диапазоне от 1 до 9. SQL> declare 2 options num_list := num_list(1,2,3,7, 8,9,13,14,15); 3 v_where varchar2(500); 4 begin 5 for i in ( select dc.*, rownum r from dept_copy dc ) loop 6 STATIC_ADHOC(i.deptno, i.dname, i.loc, options (mod(i.r, 9)+1) ); 7 end loop; 8 end;
296
Глава 5 9
/
PL/SQL procedure successfully completed. Elapsed: 00:00:43.01
Итак, хотя для процедуры STATIC_ADHOC СО статическим SQL пришлось написать больше кода, она обеспечивает существенно более высокую производительность. Процедура со статическим SQL имеет ряд других ограничений: если появится новый вариант конструкции WHERE, КОД придется менять, в отличие от процедуры DYN_ADHOC. Как и в большинстве случаев, в Oracle надо правильно сбалансировать требования приложения. Если главная проблема — затраты на разбор, то, потратив дополнительное время на создание процедур, включающих различные варианты статического SQL, можно ускорить работу за счет менее универсального решения, по сравнению с полностью динамическим. Если вы не знаете заранее, что затраты ресурсов на разбор вполне допустимы (например, в системе поддержки принятия решений или аналогичной системе, где будут выполняться небольшое количество раз SQL-операторы), уменьшение количества разборов является желанной целью, даже при использовании динамического SQL. Чтобы обеспечить это, в динамическом SQL тоже надо использовать связываемые переменные. Поскольку в предыдущем примере количество связываемых переменных при компиляции неизвестно, есть три варианта написания кода: > использовать пакет DBMS_SQL вместо оператора EXECUTE IMMEDIATE; > аналогично тому, как несколько вариантов запросов обрабатывались в процедуре STATIC_ADHOC, можно использовать три динамических курсора: один для обработки одной переданной связываемой переменной, другой — для двух связываемых переменных и третий — для трех. Это сделать сложнее, чем кажется на первый взгляд, потому что надо будет обеспечить соответствие между каждой связываемой переменной и столбцом, с которым она должна быть СЕ'.язана; > использовать контекст приложения и функцию SYS_CONTEXT вместо связываемых переменных. Реализация первых двух вариантов достаточно понятна, а вот третий вариант надо объяснить несколько подробнее. Как вы уже видели, необходимо создать контекст, к которому можно будет обращаться с помощью функции SYS_CONTEXT SQL> create or replace context DEPT_WHERE 2 using RUN_TEST; Context created.
где RUNTEST — имя процедуры, которая будет использовать контекст для передачи значений процедуре DYN_ADHOC С динамическим SQL. Процедура RUNJTEST похожа на PL/SQL-блок, который мы использовали ранее для тестирования процедуры DYN_ADHOC, но вместо передачи литералов в конструкции WHERE МЫ установим значение контекста и затем передадим в конструкции WHERE соответствующую функцию SYS CONTEXT.
Методы оптимизации PL/SQL
297
SQL> create or replace 2 procedure RUNJTEST is 3 w_deptno varchar2(80) := 'SYS_CONTEXT(" DEPT_WHERE " , ' ' D E P T N O ' ' ) ' ; 4 w_dname varchar2(80) := 'SYS_CONTEXT("DEPT_WHERE'',''DNAME")'; 5 w_loc varchar2(80) := 'SYS_CONTEXT(''DEPT_WHERE" , " L O C ' ' ) ' ; 6 begin 7 for i in ( 8 select dc.*, 9 rownum r, 10 decode(mod(rownum,9)+1, 11 1,'(deptno='||w_deptno|I' or dname='||w_dname|| "•') and loc=' | |w_loc, 12 2,'deptno='||w_deptno|I' or ( dname='||w_dname|| *•' andloc=' I I w_loc II')', 13 3, 'deptno = 'I|w_deptno||' or dname = '||w_dname|I' or loc =' *»| |w_loc, 14 4,'deptno = '||w_deptno|I' or loc = '||w_loc, 15 5,'deptno = '||w_deptno, 16 6 r 'dname = '||w_dname, 17 7,'loc = '||w_loc|I' and dname = 'I|w_dname, 18 8,'loc = '||w_loc|I' or dname = '||w_dname, 19 9,'loc = 'I|w_loc) where_clause 20 from dept_copy dc ) loop 21 dbms_session.set_context('DEPT_WHERE','DEPTNO',i.deptno); 22 dbms_session.set_context('DEPT_WHERE','DNAME',i.dname); 23 dbms_session.set_context('DEPT_WHERE', 'LOC',i.loc); 24 DYN_ADHOC(i.whe re_clause); 25 end loop; 26 end; 27 / SQL> exec run_test; PL/SQL procedure successfully completed.
Сразу непонятно, что именно здесь было сделано, но результат использования контекста можно увидеть, выбрав несколько строк из представления V$SQL. SQL> select sql_text from v$sql 2 where sql text like '%SYS_CONTEXT%' 3 / SQL_TEXT select * from dept where deptno = SYS_CONTEXT('DEPT_WHERE','DEPTNO') select * from dept where loc = SYS_CONTEXT('DEPT_WHERE','LOC) select * from dept where dname = SYS_CONTEXT('DEPT_WHERE','DNAME
Устанавливая значения контекста вместо литералов, мы можем сократить затраты на разбор из-за уменьшения количества различных SQL-операторов. С точки
298
Глава 5
зрения совместного использования SQL вызов SYS_CONTEXT СТОЛЬ же эффективен, как и использование связываемой переменной. Как и для статического SQL, производительность динамического SQL тоже может быть повышена за счет множественного связывания и множественной выборки, как уже говорилось в главе 4.
Другие проблемы с динамическим SQL Помимо снижения производительности и проблем при одновременном доступе, которые мы рассмотрели ранее, есть еще две серьезных проблемы при использовании динамического SQL. Нет сообщений об ошибках при компиляции Рассмотрим следующую PL/SQL-процедуру, которая строит явно некорректный SQL-оператор (как сказано в комментариях в коде): SQL> 2 3 4 5 6 7 8 9 10 11
create or replace procedure SILLY PROC is с sys refcursor; begin open с for 'select no such column,,,
avg() '] i 'from '|| 'where colx = 1 4 5'; end; /
1
II
— — — —
слишком много запятых пропущено выражение не указана таблица! неправильный литерал
Procedure created.
Тем не менее, процедура компилируется без ошибок. В качестве динамического SQL-оператора можно написать все, что угодно, и компилятор абсолютно спокойно это воспримет. Мы не получаем никаких комментариев по качеству кода (или в этом случае — об имеющихся ошибках), пока не выполним его. SQL> exec silly_proc; BEGIN silly_proc; END; * ERROR at line 1: ORA-00936: missing expression ORA-06512: at "SILLY_PROC", line 4 ORA-06512: at line 1
Нет проверки зависимостей Любой объект, упоминаемый в динамическом SQL, не участвует в сложном механизме проверки зависимостей в словаре данных. Это усложняет анализ последствий при изменении объектов и фактически снова приводит к рассмотренной ранее проблеме: ошибки не выявляются до момента выполнения.
Методы оптимизации PL/SQL
299
Резюме У всех разработчиков есть свои "страшные истории" о проблематичном коде, с которым им пришлось столкнуться, и "истории успеха", связанные с созданными ими решениями этих проблем. В настоящей главе представлены некоторые из типичных проблем, с которыми мы сталкивались в PL/SQL-приложениях за многие годы, а также решения или способы обхода этих проблем. Мы надеемся, что вы тоже будете делиться своим опытом с другими разработчиками в организации и сообществом пользователей Oracle. Ничто так не расстраивает разработчиков, как трата времени на решение проблем, если в итоге выясняется, что эта проблема уже ранее встречалась и была решена другими разработчиками. Общими усилиями мы можем уменьшить объем лишней работы, поделившись своим опытом. Мы настоятельно рекомендуем потратить время на изучение и ответы в таких форумах, как USENET, ORACLE-L и OTN, а также активно участвовать в работе местной группы пользователей Oracle.
Глава I лава 6 о
Триггеры Активная база данных выполняет процедуры при возникновении определенных событий и выполнении заданных условий. Эти автоматически выполняемые процедуры называются триггерами. Триггеры обычно классифицируются по инициирующему событию {событию срабатывания). Триггеры DML (на изменения данных) срабатывают (выполняются) при добавлении, изменении или удалении строк. Триггеры INSTEAD OF срабатывают при изменении представлений. Триггеры DDL (на операторы языка определения данных) срабатывают при создании, изменении или удалении объектов. Триггеры на события базы данных срабатывают, например, когда сервер запускается или останавливается, когда пользователь регистрируется и завершает сеанс или при выявлении ошибок. Более общо триггеры делят на две группы: триггеры на пользовательские события и триггеры на системные события. Триггеры на пользовательские события включают триггеры DML, DDL и триггеры на регистрацию и завершение сеанса. Триггеры на системные события реагируют на запуск и остановку сервера, а также другие события уровня системы в целом. Активные базы данных также отвечают на события, привязанные ко времени, выполняя процедуры в указанные моменты времени или с заданной периодичностью. Сервер Oracle обеспечивает это с помощью очереди заданий PL/SQL и стандартного пакета DBMS_JOB. К предмету обсуждения имеют отношение еще две возможности Огас1е9/. Технология Oracle Streams позволяет серверу базы данных перехватывать изменения, выполненные операторами DML и DDL, непосредственно из журнала повторного выполнения и дает средства использовать эти изменения совместно с другими процессами. Поддержка версий таблиц позволяет работать с несколькими версиями данных таблицы. Примечание Версии сервера Oracle отличаются по возможностям и иногда даже по их реализации. Примеры из этой главы были проверены на сервере Oracle Enterprise Edition версии 9.2.0.3, работающем на Microsoft Windows 2000. Все примеры (кроме использующих средства Oracle Streams) будут работать и в стандартной редакции сервера Oracle.
Основные понятия Триггер — это объект базы данных, задающий событие и процедурный код, который надо выполнять при возникновении этого события. Событие называют условием срабатывания триггера, а процедурный код — телом триггера.
302
Глава 6
В следующем примере событие — это регистрация конкретного пользователя. Процедура, или тело триггера, состоит из нескольких строк кода (условный оператор между ключевыми словами BEGIN И END), задающих действия сервера Oracle при регистрации указанного пользователя. — Триггер на регистрацию пользователя Alex create or replace trigger save_the_database_from_alex after logon on alex.schema begin if to_char(current_timestamp,'hh24') between 08 and 18 and to_char(current_timestamp,'d') between 2 and 5 then raise_application_error(-20000, 'Do you realize that this is a production database?'); end if; end; После создания этого триггера, если Алекс (разработчик, регистрирующийся с указанным в коде именем) попытается подключиться к серверу, произойдет следующее: SQL> connect alex/xxxxx ERROR: ORA-00604: e r r o r occurred a t recursive SQL level 1 ORA-20000: Do you r e a l i z e that t h i s i s a production database? ORA-06512: a t l i n e 4 Warning: You are no longer connected to ORACLE. Событие регистрации приводит к срабатыванию триггера, а код триггера, связанный с этим событием, запрещает регистрацию, поскольку она произошла не в рабочее время.
Типы триггеров Давайте более детально рассмотрим различные виды триггеров: триггеры DML, триггеры INSTEAD OF, триггеры DDL и триггеры на события базы данных. Триггеры DML ассоциируются либо с оператором, либо со строкой в таблице базы данных. Операторный триггер выполняется один раз при выполнении оператора и не зависит от количества строк в результирующем множестве. Строчный триггер может срабатывать много раз при выполнении оператора, в зависимости от количества строк в результирующем множестве. (Пример такой ситуации представлен далее.) Триггеры INSTEAD OF можно задавать только для представлений. Нельзя создать DML-триггер для представления (хотя можно создать DML-триггеры для базовых таблиц представления). Триггер INSTEAD OF переопределяет способ изменения представления или обеспечивает изменение представлений, которые сервер обычно не позволяет изменять. Триггеры DDL и триггеры на события, связанные с данными, можно создавать на уровне базы данных или на уровне схемы. Триггер на регистрацию, представлен-
Триггеры 303 ный в предыдущем разделе, — это пример триггера на событие в базе данных, созданного на уровне схемы.
Атрибуты событий Для каждого события срабатывания в теле триггера доступны определенные атрибуты. Например, атрибут ORA_LOGIN_USER содержит имя регистрирующегося пользователя. Атрибут ORA_SYSEVENT идентифицирует событие, вызвавшее срабатывание триггера. Текст SQL-оператора, вызвавшего срабатывание (ORA_SQL_TXT), также доступен в теле триггера. Некоторые из этих атрибутов события поддерживаются для всех типов триггеров, а другие — только для избранных. Полный список атрибутов можно найти в документации Oracle (в руководстве "Oracle9i Application Developer's Guide — Fundamentals').
Строчные DML-триггеры имеют доступ к значениям столбцов, изменяемых оператором, который вызвал срабатывание триггера. Триггер на удаление может увидеть значения столбцов в удаляемой строке. Триггер на вставку может увидеть значения столбцов вставляемой новой строки, а триггер на изменение может видеть как исходные, так и измененные значения. Эти значения называются значениями столбцов :OLDH : NEW. Имена :OLDH :NEW называют коррелирующими. Они используются как префикс для имен столбцов, например, имя : OLD.AMOUNT ссылается на исходное значение столбца AMOUNT В текущей строке.
Порядок срабатывания триггеров Триггеры могут выполняться до или после оператора, вызвавшего их срабатывание. Триггеры на регистрацию, запуск и ошибки сервера не могут срабатывать до запуска сервера или до возникновения ошибки. Триггеры на завершение сеанса и остановку сервера не могут срабатывать после завершения сеанса пользователя или после остановки сервера. Но триггеры на операторы DML, строчные триггеры и большинство остальных типов триггеров могут срабатывать до или после события, инициирующего их срабатывание. Следующий код создает четыре триггера для таблицы DEPT, учитывающие все возможные условия срабатывания триггера DML: SQL> — создаем полный набор триггеров для таблицы dept SQL> — Создаем операторный триггер before SQL> create or replace trigger depths 2 before insert or update or delete 3 on dept 4 begin 5 dbms_output.put_line('перед оператором (dept)'); 6 end; 7 / Trigger created. SQL> — Создаем строчный триггер before SQ_L> create or replace trigger dept_br 2 before insert or update or delete 3 on dept 4 for each row
304
Глава 6 5 6 7 8
begin dbms_output.put_line('...перед строкой (dept)'); end; /
Trigger created. SQL> SQL> 2 3 4 5 6 7 8
— Создаем строчный триггер after create or replace trigger dept_ar after insert or update or delete on dept for each row begin dbms_output.put_line('...после строки (dept)'); end; /
Trigger created. SQL> SQL> 2 3 4 5 6 7
— Создаем операторный триггер after create or replace trigger deptbs after insert or update or delete on dept begin dbms output.put line('после оператора (dept)'); — — end; /
Trigger created.
Мы также создали аналогичные триггеры для таблицы ЕМР. Теперь удалим несколько строк, выполнив стандартный SQL-оператор DELETE: SQL> — Удаляем 3 строки из emp SQL> delete from 2 emp 3 where deptno = 10; перед оператором (emp) ...перед строкой (emp) ...после строки (emp) ...перед строкой (emp) ...после строки (emp) ...перед строкой (emp) ...после строки (emp) после оператора (emp) 3 rows deleted.
Сначала срабатывает операторный триггер BEFORE. ДЛЯ каждой строки, обрабатываемой DML-оператором, выполняется строчный триггер BEFORE, строка изменяется, и срабатывает строчный триггер AFTER. Наконец, после обработки всех строк выполняется операторный триггер AFTER.
Триггеры
305
Когда две таблицы связаны ограничением внешнего ключа, соответственно, срабатывают триггеры по обеим таблицам. В данном случае мы объявили ограничение с конструкцией ON DELETE CASCADE, так что при удалении одной строки из таблицы DEPT удаляются три соответствующие строки из таблицы ЕМР. Интересно, что первым срабатывает операторный триггер BEFORE ДЛЯ таблицы ЕМР, НО последним — операторный триггер AFTER ДЛЯ таблицы DEPT. SQL> delete from dept 2 where deptno = 10; перед оператором (emp) перед оператором (dept) ...перед строкой (dept) ...перед строкой (emp) ...после строки (emp) ...перед строкой (emp) ...после строки (emp) ...перед строкой (emp) ...после строки (emp) ...после строки (dept) после оператора (emp) после оператора (dept) 1 row deleted.
Несколько однотипных триггеров Можно задать несколько триггеров для одного события. Можно создать много операторных DML-триггеров BEFORE для одной таблицы или несколько DDL-триггеров для одного события в одной и той же схеме. В этом случае порядок срабатывания аналогичных триггеров не определен. Иными словами, он не предопределен, так что однотипные триггеры могут срабатывать в любом порядке. Если между несколькими триггерами есть зависимости, лучше перенести код из каждого триггера в хранимую процедуру и вызывать эту процедуру из одного триггера.
Производительность строчных DML-триггеров BEFORE и AFTER Предполагая, что алгоритм работы триггера один и тот же, какой строчный триггер лучше использовать: BEFORE или AFTER? Мы проведем эксперимент, используя триггер, реализующий ограничение перехода, т.е. задающий правило, которое определяет допустимые изменения значения столбца. Следующий триггер типичен для систем превентивного сопровождения; оборудование работает некоторое время, затем общее время работы изменяется, а выполнение правил изменения обеспечивается триггером. 1 2 3 4
— Проверить, что новое значение количества часов налета разумно — для столбца flying_hrs задано ограничение NOT NULL create or replace trigger check flying hrs after update of flying_hrs on aircraft
306
Глава 6
5 6 7 8 9 10 11* SQL>
for each row begin if :new.flying_hrs < :old.flying_hrs then raise_application_error(-20001, 'The number of flying hours doesn't look right.'); end if; end; /
Trigger created.
В эксперименте используются две идентичные таблицы. Триггеры для каждой из них одинаковы, но для первой таблицы задан строчный триггер BEFORE, а для второй — строчный триггер AFTER. В таблицах достаточно строк, чтобы любые различия в обработке можно было заметить. Вот начальная информация: SQL> select trigger_name,table_name,trigger_type 2 from user_triggers 3 where table_name like 'AIR%'; TRIGGER_NAME
TABLE_NAME
TRIGGERJTYPE
CHECK_FLYING_HRS1 CHECK_FLYING_HRS2
AIRCRAFT1 AIRCRAFT2
BEFORE EACH RCW AFTER EACH ROK
Строчный триггер BEFORE должен быть менее эффективен, поскольку он должен перечитывать затрагиваемые данные. Давайте проверим это, оценив относительные затраты ресурсов на срабатывание триггеров с помощью пакета RUNSTATS. SQL> exec runStats_pkg.rs_start; PL/SQL procedure successfully completed. SQL> update aircraftl 2
set flying_hrs = flying_hrs + 3.15;
2048 rows updated. SQL> exec runStats_pkg.rs_middle; PL/SQL procedure successfully completed. SQL> update aircraft2 2
set flying_hrs = flying_hrs + 3.15;
2048 rows updated. SQL> exec runStats_pkg.rs_stop(1000); Runl ran in 77 hsecs Run2 ran in 136 hsecs run 1 ran in 56.62% of the time
Триггеры 307 Name STAT...redo entries LATCH.redo allocation STAT...session logical reads STAT...db block gets STAT...db block changes LATCH.cache buffers chains STAT...redo size
Runl 4,595 4,606 4,755 4,718 9,231 25,422 933,000
Run2 2,547 2,551 2,676 2,637 5,120 13,263 546,660
Diff -2,048 -2,055 -2,079 -2,081 -4,111 -12,159 -386,340
Runl latches total versus runs — difference and pet Runl Run2 Diff Pet 31,091 16,816 -14,275 184.89%
Строчный триггер AFTER выполнил примерно вдвое меньше логических чтений и использовал примерно вдвое меньше ресурсов, чем строчный триггер BEFORE.
Привилегии Никакие привилегии выполнения с триггером не связаны. Если у вас есть привилегия на выполнение оператора, вызывающего срабатывание триггера, триггер будет выполнен неявно. Триггеры работают с привилегиями владельца и при отключенных ролях. Чтобы триггер получил доступ к объекту базы данных, владелец триггера должен либо владеть объектом, либо иметь соответствующие привилегии, полученные непосредственно. Для создания триггера в своей схеме необходима привилегия CREATE TRIGGER, a для создания триггера в любой другой схеме — привилегия CREATE ANY TRIGGER. ЕСТЬ также привилегия ADMINISTER DATABASE TRIGGER, необходимая для создания триггера для базы данных.
Триггеры и словарь данных Оператор CREATE TRIGGER сохраняет информацию о триггере в словаре данных. В отличие от PL/SQL-процедур (которые хранятся в виде строк текста), тела триггеров хранятся в базе данных в столбцах типа LONG. ЭТО усложняет поиск соответствующей строки в теле триггера по ее номеру в сообщении об ошибке. Например: SQL> alter trigger reorder compile; Warning: Trigger altered with compilation errors. SQL> show error Errors for TRIGGER REORDER: LINE/COL
ERROR
10/23
PLS-00103: Encountered the symbol "=" when expecting one of the following: := . ( @ % ; indicator
12/4
PLS-00103: Encountered the symbol "END"
308
Глава 6
Сервер Oracle сообщает об ошибке в строке номер 10, но что это за строка? Можно самостоятельно посчитать строки в теле триггера, но лучше автоматизировать решение этой задачи. Вот функция, получающая строку из тела триггера по номеру, которую можно предложить для использования разработчикам. — Создать массив create or replace type outputLines as table of varchar2 (4000);
— Функция для выдачи строки из тела триггера по номеру — Считает символы chr(10) — переводы строк — позиция символа перевода строки create or replace function TriggerText (p_owner in varchar2, p_trigger in varchar2) return outputLines authid current_user pipelined as body long; j number; — позиция символа перевода строки begin select trigger_body into body from all_triggers where trigger_name = p_trigger and owner - p owner; — body := body || chr(10); while ( body is not null ) loop j := instr ( body, chr(10) ); pipe row ( substr ( body, 1, j-1 ) ); body := substr( body, j+1 ); end loop; return; end;
Мы используем эту функцию следующим образом: SQL> select rownum,column_value 2 from table (triggertext('SCOTT1,'REORDER1) ROWNUM 1 2 3 4 Ь 6 7 8 9 10
COLUMN VALUE begin if :new.qty_on_hand - :new.qty_allocated + :new.qty_on_order < :new.reorder_level then insert into Pending_orders (item_no,qty,ord_date) values (:new.item_no, :new.reorder_level, sysdate); :new.qty_on_order = :new.gty_on_order + :new.reorder_level;
Триггеры 309 11 12 13
end end;
if;
Можно также обратиться к строкам, указанным в сообщении об ошибке. SQL> select * 2 from 3 (select rownum line, column_value 4 from table (system.triggertext(user,'REORDER')) ) 5 where line between 8 and 12; LINE
COLUMN_VALUE
8 9
sysdate);
10 11 12
:new.qty_on_order = :new.qty_on_order + :new.reorder_level; end
if;
Зависимости триггера Можно сделать запрос к представлению USER_TRIGGER_COLS, чтобы узнать, значения : NEW и : OLD каких столбцов использует триггер. SQL> - - Запрашиваем столбцы, используемые в триггере reorder SQL> s e l e c t column_name,column_usage
2 3
from where
user_trigger_cols trigger_name = 'REORDER';
COLUMN NAME
COLUMN USAGE
ITEMJTO QTY_ON_HAND QTY_ON_ORDER QTY_ALLOCATED REORDER_LEVEL
NEW IN NEW IN NEW IN OUT NEW IN NEW I N
Результаты (для показанного ранее триггера) свидетельствуют о том, что столбец QTY_ON_ORDER изменяется в триггере, а остальные столбцы только читаются. Можно выполнить запрос к представлению USER_DEPENDENCIES ДЛЯ поиска таб-
лиц или процедур, на которые ссылается триггер. SQL> select referenced name,referenced type 2 from user dependencies 3 where name= 'REORDER'; REFERENCED NAME
REFERENCEDJTYPE
STANDARD PENDING_ORDERS INVENTORY
PACKAGE TABLE TABLE
310
Глава 6
Состояние триггера Триггер может находиться в одном из двух возможных состояний: включенном и выключенном. Обычно триггер включен и действует, но бывают ситуации, например, когда при выполнении действий по сопровождению таблицы не хотелось бы, чтобы код триггера выполнялся, поэтому триггер отключается (оператором a l t e r trigger ИМЯ_ТРИГГЕРА disabled). Отключенный триггер никогда не срабатывает. После завершения сопровождения триггер снова включается. Следующий SQL-оператор позволяет получить состояние триггера (включен/отключен) из представления USERJTRIGGERS: SQL> select status 2 from user_triggers 3
where trigger_name = 'REORDER';
STATUS ENABLED
Как и многие другие объекты базы данных, триггер может быть недействительным. Это означает, что какая-то часть кода триггера не может быть успешно скомпилирована. Проблема, как правило, возникает из-за синтаксической ошибки или ссылки на объект, к которому владелец триггера не имеет доступа. Недействительный триггер не работает. Следующий SQL-оператор позволяет получить информацию о состоянии триггера (действителен или нет) из представления USER_OBJECTS: SQL> select status 2 from user objects 3 where object name =
1
REORDER'
STATUS VALID
Сбои триггеров Если в триггере возникает ошибка (необработанная исключительная ситуация), все действия, выполненные триггером, и действия, выполненные оператором, который вызвал срабатывание триггера, отменяются. Исключение из этого правила — когда пользователь, выполняющий триггер, имеет привилегии АБД, а триггер создан на событие базы данных, например, на запуск, остановку или регистрацию пользователя. Вызвавшее срабатывание действие выполняется, даже если в коде триггера происходит сбой.
Ограничения триггеров При создании триггера вы не создаете всю программу, а добавляете часть кода намного большему скрытому алгоритму обработки события. Это означает, что на триггеры накладывается много ограничений. Эти ограничения относятся к событиям, вызывающим срабатывание триггеров, а также к типам операторов, которые могут выполняться в триггерах.
Триггеры
311
Часть таких ограничений перечислена ниже. Этот список — неисчерпывающий и неупорядоченный. В некоторых случаях есть способы обхода этих ограничений: > триггеры DML привязаны к конкретной таблице или представлению. Нельзя создать один триггер DML на всю базу данных или схему. Триггеры DDL, наоборот, связаны либо со схемой, либо с базой данных в целом, но не с конкретным объектом; > строчные триггеры DML имеют доступ к значениям столбцов : NEW И : OLD, НО изменять значения : NEW МОЖНО ТОЛЬКО В триггерах before-insert и before-update. Нельзя изменять значения :NEW В триггерах AFTER, а значения :OLD вообще нигде и никогда нельзя изменять; > строчные триггеры DML не могут читать из изменяющихся (мутирующих) таблиц (их мы рассмотрим далее в этой главе); > с операторами управления транзакциями, вроде COMMIT И ROLLBACK, никакие триггеры не связаны. Триггеры DML срабатывают в момент выполнения оператора INSERT, UPDATE и DELETE, независимо от того, будет ли соответствующая транзакция зафиксирована; > операторы управления транзакциями, вроде COMMIT, ROLLBACK И SAVEPOINT, В триггерах DML не допускаются. Если триггер вызывает процедуру, она также не может выполнять никакие операторы управления транзакциями; > операторы DDL не допускаются в триггерах DML, поскольку они неявно выполняют COMMIT и тем самым нарушают предыдущее ограничение. В триггерах DDL допускается только ограниченное множество операторов DDL; > нельзя создать триггер на оператор SELECT. Невозможно создать триггер, который срабатывает при обращении к генератору последовательности. Контролировать использование операторов SELECT позволяет детальный аудит (пакет DBMS_FGA);
> нельзя создавать триггеры DML для таблиц, принадлежащих пользователю SYS. Если вы не будете учитывать эти ограничения, то столкнетесь с проблемой при попытке создания триггера или при его срабатывании. Следующий пример показывает сообщение об ошибке, выдаваемое не при создании триггера, а во время его выполнения: update emp ERROR at line 1: ORA-04092: cannot COMMIT in a trigger ORA-06512: at "SCOTT.ZEMPBS", line 3 ORA-04088: error during execution of trigger 'SCOTT.ZEMPBS'
Триггеры DML Теперь мы сконцентрируемся на триггерах DML и рассмотрим несколько простых примеров.
312
Глава 6
Сохранение информации аудита Следующий триггер будет записывать имя пользователя Oracle, выполнившего последнее изменение, и дату этого изменения. Триггер создается как BEFORE, поскольку мы не можем изменять значение : NEW В триггере AFTER. 1 2 3 4 5 6 7 8 9*
CREATE OR REPLACE TRIGGER deptBR before update or insert ON dept FOR EACH ROW DECLARE begin :new.last update := sysdate; :new.last user := user; end;
Инициирующее событие — оператор INSERT ИЛИ UPDATE. Нет смысла устанавливать значения столбцов в триггере на удаление, поскольку строку мы в любом случае собираемся удалять. Обратите внимание, что триггер — строчный. Если оператор UPDATE затрагивает много строк, триггер будет срабатывать для каждой строки. Вот как он работает: SQL> select * 2 from dept 3 where deptno = 10; DEPTNO 10
DNAME
LOC
ACCOUNTING
NEW YORK
LAST UPDA
LAST USER
SQL> update dept 2 set loc = 'ULAN BATOR' 3 where deptno = 10; 1 row updated. SQL> select * 2 from dept 3 where deptno = 10; DEPTNO 10
DNAME
LOC
LAST UPDA
LAST USER
ACCOUNTING
ULAN BATOR
01-SEP-03
ALEX
Реализация ограничения перехода Мы уже видели один пример триггера, реализующего ограничение перехода. Теперь давайте рассмотрим еще один, реализующий упорядоченное изменение. Представленный далее триггер TBUR_SCOUT задает порядок присвоения рангов для таблицы SCOUT:
Триггеры SQL> 2 3 4 5 6 8*
create table scout (id number, rank varchar2(30) not null constraint check_rank check (rank in 1 ('Scout','Tenderfoot','Star Scout ,'Life Scout','Eagle Scout')) );
Table created. SQL> 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
create or replace trigger tbur_scout before update on scout for each row when (new.rank old.rank) declare type ranklist is varray(lO) of varchar2(30); ranks ranklist := ranklist('Scout','Tenderfoot','Star Scout', 'Life Scout1,'Eagle Scout'); function diff (p_new in varchar2,p_old in varchar2) return number is newRank number; oldRank number; begin for i in 1..ranks.last loop if p_new = ranks(i) then newRank := i; elsif p_old = ranks(i) then oldRank := i; end if; end loop; return newRank - oldRank; end; begin if
diff(:new.rank,:old.rank) != 1 then raise_application_error(-20001,'Rank is out of sequence'); end if; end; /
Trigger created. SQL> select * 2 from scout; ID RANK 1
Star Scout
313
314
Глава 6
SQL> update scout 2 set rank = 'Eagle Scout'; update scout * ERROR at line 1: ORA-20001: Rank is out of sequence ORA-06512: at "SCOTT.TBUR_SCOUT", line 26 ORA-04088: error during execution of trigger
'SCOTT.TBUR_SCOUT'
Генерация суррогатного ключа С точки зрения программиста есть только два способа создания идентификаторов записей в базе данных: либо пользователь вводит значение, либо оно генерируется приложением. В последнем случае часто используются триггеры базы данных для получения числа из генератора последовательности и вставки его во вновь созданную строку таблицы. Следующий код демонстрирует этот прием. Триггер BEFORE INSERT для таблицы WORKJDRDERS использует генератор последовательности для заполнения ключевого столбца wo_m SQL> create table work_orders 2 (wo_id number); Table created.
create or replace trigger trg_wo_id before insert on work_orders for each row when (new.wo id is null) begin select wo_seq.nextval into :new.wo_id from dual; end;
Этот триггер пригодится, когда мы не сможем контролировать текст оператора INSERT. Но более эффективно будет генерировать последовательный номер в самом операторе INSERT, если это возможно, что и продемонстрирует сеанс RUNSTATS.
Следующий код оценивает относительную производительность заполнения последовательных значений с помощью триггерам непосредственно в SQL-операторе. В тесте runl (таблица WORK_ORDERSI) используется показанный ранее триггер BEFORE INSERT. В тесте шп2 (таблица WORK_ORDERS2) выполняется тот же объем действий с помощью SQL-оператора INSERT без триггера. SQL> exec
runStats_pkg.rs_start;
PL/SQL procedure successfully completed. SQL> insert into 2 work orders1
Триггеры 3 4 5
315
select null from all_objects where rownum < 1000;
999 rows created. SQL> exec runStats_pkg.rs_middle; PL/SQL procedure successfully completed. SQL> 2 3 4 5
insert into work_orders2 select wo_seq.nextval from all_objects where rownum < 1000;
999 rows created. SQL> exec runStats_pkg.rs_stop(1500); Runl ran in 188 hsecs Run2 ran in 103 hsecs run 1 ran in 182.52% of the time Name STAT.. .db block changes STAT.. .calls to get snapshot s STAT.. .consistent gets LATCH.shared pool LATCH.library cache pin alloca STAT.. .session logical reads LATCH.library cache pin LATCH.cache buffers chains LATCH. library cache STAT.. .redo size
Runl 3,203 3,156 7,317 4,398 4,256 9,012 8,485 22,769 17,906 326,312
Run2 1,212 159 4,320 1,391 258 5,005 2,495 11,772 3,873 112,612
Diff -1,991 -2,997 -2,997 -3,007 -3,998 -4,007 -5,990 -10,997 -14,033 -213,700
Runl latches total versus runs — - difference and pet Runl Run2 Diff Pet 70,659 30,979 -39,680 228.09%
TnMiTPnki Ш /
View created. SQL> select * 2 from dept_sal; DNAME ACCOUNTING RESEARCH SALES
AVGSALARY 2916. 67 2175, 00 1566. 67
Пусть необходимо уменьшить среднюю зарплату в бухгалтерии на два процента (пример, очевидно, гипотетический). SQL> update dept_sal 2 set avgsalary = avgsalary * .98 3 where dname = 'ACCOUNTING1 4 / update dept_sal ERROR at line 1: ORA-01732: data manipulation operation not legal on this view
Это представление неизменяемое. Сервер Oracle не может определить, какому сотруднику (или сотрудникам) надо изменить зарплату, чтобы изменилась средняя зарплата в отделе. Триггер INSTEAD OF может реализовать соответствующий алгоритм. Например, можно распределить уменьшение зарплаты равномерно среди всех сотрудников отдела. -- Триггер instead of для изменения таблиц emp и dept — при изменении представления dept_sal create or replace trigger dept_sal_trg instead of update on dept_sal begin if (nvl(:new.avgsalary,-1) nvl(:old.avgsalary,-1)) then update emp set sal = (:new.avgsalary/:old.avgsalary) * sal where deptno - (select deptno from dept where dname = :new:old.dname);
Триггеры
set where
317
dept dname = :new.dname dname » :old.dname;
end; Предупреждение Столбец DNAME используется для доступа к строке dept. Во избежание проблем столбец DNAME должен быть объявлен уникальным. — Строки имеют уникальные имена отделов SQL> update dept 2 s e t dname='ACCOUNTING1; update dept * ERROR a t l i n e 1: ORA-00001: unique constraint (SCOTT.DNAME_UK) violated) Триггер INSTEAD OF подобен строчному триггеру DML тем, что срабатывает для каждой строки и имеет доступ к значениям : OLD И : NEW. SQL> update dept_sal 2 set avgsalary = avgsalary * .98 3 where dname = 'ACCOUNTING'; 1 row updated. SQL> select * 2 from dept_sal; DNAME ACCOUNTING RESEARCH SALES
AVGSALARY 2858.33 2175.00 1566.67
Поскольку мы создали триггер на изменение, а не на операторы INSERT ИЛИ DELETE, мы не сможет выполнять вставки и удаления из этого представления. 1 i n s e r t i n t o dept_sal 2* values ('HR',6000) SQL> / insert into dept_sal * ERROR at line 1: ORA-01732: data manipulation operation not legal on this view К триггерам INSTEAD OF мы еще вернемся при обсуждении таблиц с поддержкой версий.
318
Глава 6
Мутирующие таблицы Строчный триггер DML работает с таблицей по ходу выполнения оператора, вызвавшего его срабатывание. В течение этого времени таблица называется мутирующей. На мутирующие таблицы накладываются определенные ограничения, которые мы опишем сейчас на примере системы бронирования билетов. Пассажирский поезд состоит из переменного количества пассажирских вагонов. При бронировании билетов на поезд агент с помощью компьютерной системы продажи билетов проверяет наличие свободных мест. Если имеющихся мест недостаточно, агент добавляет к поезду еще один вагон и тем самым увеличивает количество свободных мест. Проблема в том, что для того, чтобы добавить еще один вагон, агент должен перейти к другому терминалу и использовать другую программу. Вы, потенциальный пассажир, остаетесь "висеть" на телефонной линии, пока не появятся дополнительные места. Мы автоматизируем этот процесс. Точнее, вместо изменения нескольких программ продажи билетов мы собираемся создать в базе данных триггер DML, проверяющий доступность мест при бронировании. Триггер будет считать количество свободных мест в поезде и, если их слишком мало, программа будет добавлять нагон. Агентам больше не придется добавлять вагоны вручную. Примечание При работе вручную возможна ситуация, когда два агента по бронированию билетов добавят два новых вагона к одному поезду примерно в одно и то же время, хотя нужен только один. Это не проблема, и мы не будем пытаться ее исправить. Локомотив легко потянет лишний вагон.
Таблица по имени TRAIN_RIDES содержит строку для каждого места в поезде. Все столбцы, кроме номера бронирования, заранее заполнены. SQL> d e s c r i b e Name
train_rides Null?
TRAIN_NO TRAVEL_DATE COACH_NO SEATJJO RESERVATION_NO
Type NUMBER DATE NUMBER NUMBER NUMBER
Столбец RESERVATION_NO изменяется при продаже места. SQL> SQL> 2 3 4 5 6
-- продать любые имеющиеся места update train_rides reservation_no = :г set where train_no = :a and travel_date = :b and reservation_no i s null and and rownum :nbr of seats
available);
Триггеры
319
Новое приложение основано на следующем триггере. Он добавляет новый вагон к поезду, когда остается менее 50 свободных мест. — Триггер для автоматического добавления вагона, когда остается мало — свободных мест create or replace trigger check_free_space_on_train before update of reservation_no on train_rides for each row declare free_seats number; begin select into from where
count(*) free_seats train_rides reservation_no is null and train_no = :new.train_no and travel_date = :new.travel_date; if free_seats < 50 then add_a_coach(:new.train_no,:new.travel_date); end if;
end;
Теперь давайте посмотрим, что происходит при срабатывании триггера. update train_rides ERROR at line 1: ORA-04091: table RR.TRAIN_RIDES is mutating, trigger/function may not see it ORA-06512: at "RR.CHECK_FREE_SPACE_ON_TRAIN", line 5 ORA-04088: error during execution of trigger 'RR.CHECK_FREE_SPACE_ON_TRAIN'
Ошибка времени выполнения возникает потому, что строчному триггеру не разрешается читать данные из таблицы, на которой он основан. Интуитивно понятно, что триггер пытается читать таблицу, часть строк в которой изменена, а часть — нет. Это не допускается. Таблицы являются мутирующими только в контексте триггеров DML. В ходе обычных действий в базе данных каждый из одновременно работающих с ней пользователей получает согласованное представление данных и не видит незафиксированных изменений других пользователей по ходу их выполнения. Триггер, однако, должен видеть выполненные им ранее изменения. Поскольку ограничение мутирующей таблицы применяется только к строчным триггерам, то, чтобы избежать проблем, достаточно сохранять информацию в строчном триггере, а фактические действия выполнять в операторном триггере AFTER. Реализация этого решения потребует также создания третьего триггера — операторного триггера BEFORE — для инициализации переменных и очистки остатков прерванного выполнения. Мы будем называть это решение отсроченной, или отложенной, обработкой. Часто, чтобы избежать ошибки мутирующей таблицы, также советуют использовать в триггере автономную транзакцию. Автономная транзакция не видит мутиру-
320
Глава 6
ющую таблицу; она видит таблицу в том состоянии, в котором та была до начала выполнения оператора, вызвавшего срабатывание триггера, так что сообщение об ошибке выдаваться не будет. Но тот факт, что автономная транзакция не видит выполненные изменения, обычно означает, что в ней нельзя реализовать необходимый нам алгоритм работы. Мы рассмотрим этот вопрос далее.
Решение на базе отложенной обработки Решение на базе отложенной обработки переносит чтение таблицы из строчного триггера в операторный триггер AFTER. Строчный триггер сохраняет идентификатор каждой затронутой строки в массиве. Операторный триггер AFTER выбирает каждый сохраненный идентификатор строки и использует его для обращения к таблице, которая к этому времени уже не изменяется. Следующий код демонстрирует конкретное решение описанной ранее проблемы. Процедура S E T _ I N I T I A L _ S T A T E вызывается операторным триггером BEFORE. Процедура SAVE_TRAIN_NO вызывается строчным триггером. Процедура CHECK_FREE_SPACE_ON_TRAIN, которая и выполняет фактическую обработку, вызывается операторным триггером AFTER. — Пакет для обхода ошибки мутирующей таблицы create or replace package train_ride_package is procedure set_initial_state; procedure save_train_no (trainid number,dateid date); procedure check_free_space_on_train; end; / create or replace package body train_ride_package is — Это тело пакета создает процедуры, — которые будут выполняться триггерами type train_table is table of date index by pls_integer; position train_table; empty train_table; — инициализируем ассоциативный массив procedure set_initial_state is begin position := empty; end; — сохраняем значения traineld и travelDate procedure save_train_no (trainid number, dateid date) is begin position(trainid) : = dateid; end; • — выбираем значения traineld и travelDate -- проверяем наличие мест — если надо, добавляем вагон procedure check_free_space_on train
Триггеры IS
trainid_in number; free seats number; begin trainid_in := position.FIRST; while trainid_in is not null loop select count(*) into free_seats from train_rides where reservation_no is null and train_no = trainid_in and travel_date = position(trainid_in); if free_seats < 50 then add_a_coach(trainid_in,position(trainid_in)) ; end if; position.delete(trainid_in); trainid_in : = position.next(trainid_in); end loop; end; end;
Вот триггеры, необходимые для реализации отложенной обработки: — Операторный триггер BEFORE — Инициализируем массив пакета create or replace trigger trg_tr_bs before update on train_rides begin train_ride_package.set_initial_state; end; — Строчный триггер — сохраняем информацию о строке в массиве пакета CREATE OR REPLACE TRIGGER trg_tr_AR after update on train_rides FOR EACH ROW begin train_ride_package.save_train_no (:new.train_no,:new.travel_date); end; -- Операторный триггер AFTER — Выполняет обработку, читая массив пакета CREATE OR REPLACE TRIGGER trg_tr_AS After update on train rides begin train_ride_package.check_free_space_on_train; end; 11 Зак. 348
321
322
Глава 6
Прежде чем мы сможем протестировать это решение, надо будет создать процедуру ADDACOACH. Ниже представлена подобная процедура, очевидно, непригодная для реального использования, но она позволит нам продолжить работу. — Добавляем дополнительный вагон к поезду c r e a t e or replace procedure add_a_coach (id in number, dt in date) as begin insert into train_rides (train_no,travel_date,coach_no,seat_no) select train_no,trunc(sysdate),coach_no + l,rownum from train_rides where rownum < 101 and train_no = 1; end;
Для тестирования мы зарезервируем все свободные места. SQL>— считаем свободные места SQL> select count (*) 2 from train_rides 3 where reservation_no is null; COUNT(*) 100 SQL> 2
update train_rides set reservation_no = 1;
100 rows updated. SQL> select count(*) 2 from train_rides; COUNT(*) 200 SQL> select count(*) 2 from train_rides 3
where
reservation_no is null;
COUNT(*) 100
Тут возникает проблема безопасности одновременной работы, которую мы до сих пор игнорировали. Проблема состоит в том, что два агента могут одновременно зарезервировать все места в поезде так, что ни одна из транзакций не добавит вагон. Эта проблема не связана с мутирующими таблицами как таковыми — это побочное
Триггеры
323
следствие принятой в Oracle модели многоверсионной согласованности. Каждый агент/триггер читает данные из состояния, зафиксированного в базе данных. Каждый агент/каждая транзакция видит свободные места и работает, исходя из предположения, что данные не меняются. Универсальное решение подобных проблем состоит в проектировании приложения так, чтобы оно гарантировало сериализацию, т.е. приложение должно допускать только поочередные изменения. Этого можно добиться, выделяя все места следующим SQL-оператором: — Обеспечиваем сериализацию update train_rides set reservation_no = :Ы where reservation_no is null and rownum select count(*) 2 from train_rides 3 where reservation_no is null; COUNT(*) 100 SQL> update train_rides 2 set reservation_no = 1; 100 rows updated.
В соответствии с требованиями сервер должен заметить, что в поезде остается свободных мест меньше допустимого, и добавить новый вагон, но это не происходит. После выполнения оператора UPDATE свободных мест в поезде вообще не остается. SQL> select count(*) 2 from train_rides 3
where
reservation_no is null;
COUNT(*) 0 SQL> select count(*) 2 from train_rides; COUNT(*) 100
Объясняет сбой триггера то, что хотя оформленный как автономная транзакция триггер и срабатывал при изменении каждой строки (100 раз), он ни разу не увидел ни одного из выполненных изменений. "С его точки зрения", каждый раз в поезде были все те же 100 свободных мест. Сравните это с отложенной обработкой, представленной ранее. Отложенная обработка происходит после выполнения оператора, и сделанные оператором изменения видны в триггере.
Еще об ошибке мутирующей таблицы Есть и другие, менее типичные ситуации, которые могут приводить к выдаче сообщений об ошибке мутирующей таблицы. Один из сценариев возникает при задании действий для ограничений, например, ON DELETE CASCADE. ЕСЛИ ДЛЯ ограниче-
Триггеры
325
ния задано действие, триггер для главной таблицы не может запрашивать подчиненную таблицу. alter table emp add constraint fk_deptno foreign key (deptno) references dept on delete set null; CREATE OR REPLACE TRIGGER deptAR after delete ON dept FOR EACH ROW DECLARE v_ename varchar2(30); begin select ename into v_ename from emp where deptno = :old.deptno and rownum=l; —
выполняем определенные действия
exception when no_data_found then null; end; SQL> delete from dept 2 where deptno=10; delete from dept * ERROR at line 1: ORA-04091: table ALEX.EMP is mutating, trigger/function may not see it ORA-06512: at "ALEX.DEPTAR", line 4 ORA-04088: error during execution of trigger 'ALEX.DEPTAR'
Ошибка мутирующей таблицы может быть связана с простой ошибкой проектирования (или кодирования). Предположим, для таблицы А задан триггер, изменяющий таблицу в. Но для таблицы в также задан триггер, и этот триггер изменяет таблицу с. Наконец, для таблицы с задан триггер, замыкающий круг и изменяющий таблицу А. update A ERROR at line 1: ORA-04091: table SAMPLE.A is mutating, trigger/function may not see it ORA-06512: at "SAMPLE.T_C", line 2 ORA-04088: error during execution of trigger 'SAMPLE.T_C ORA-06512: at "SAMPLE.T_B", line 2 ORA-04088: error during execution of trigger 'SAMPLE.T_B' ORA-06512: at "SAMPLE.T_A", line 2 ORA-04088: error during execution of trigger 'SAMPLE.T_A'
326
Глава 6
Полезно создать запрос, который может выявлять циклические ссылки в триггерах. SQL> 2 3 4 5 6 7 8 9
select name as "Trigger", table_name as "Base Table", referenced_name as "References TableName" from user_dependencies,user_triggers where type='TRIGGER' and referenced_type = 'TABLE' and table_name != referenced_name and name = trigger_name order by 1;
Trigger
Base Table
References TableName
T_A
A В С
с
T В T С
В А
Аудит данных Типичное требование — аудит изменений значений данных — требует сохранения изменений данных приложения. Запись изменений (журнал аудита) содержит значения :OLD И : NEW ДЛЯ каждого оператора INSERT, UPDATE И DELETE. Сервер Oracle обеспечивает возможность аудита с помощью оператора AUDIT, НО значения : OLD И : NEW при этом не отслеживаются. Для реализации аудита данных можно использовать триггеры, если создать теневую таблицу. Теневая таблица — это клон таблицы, для которой выполняется аудит, но с несколькими дополнительными столбцами, идентифицирующими время последнего изменения строки и пользователя, выполнившего это изменение. Вот код для создания нашей теневой таблицы: create table dept$audit ( deptno number(2,0), dname varchar2(14), loc varchar2(13), change_type varchar2(1), changed_by varchar2(30), changed_date date >;
Для изменений можно создать в журнале аудита две строки или сохранять только значения : NEW либо : OLD. Затем можно будет выполнить запрос к таблице аудита, сортируя записи по дате и уникальному ключу, чтобы реконструировать полную хронологию изменений данных. — Триггер для создания журнала аудита CREATE OR REPLACE TRIGGER auditdeptar after INSERT or UPDATE or DELETE on dept for each row
Триггеры 327 declare my DEPT$audit%ROWTYPE; begin if inserting then my.change_type := 'I'; elsif updating then my.change_type :='U'; else my.change_type := 'D'; end if; my.changed_by := user; my.changed_time := sysdate; case my.change_type when 'I' then my.DEPTNO := :new.DEPTNO; my.DNAME := :new.DNAME; my.LOC := :new.LOC; else my.DEPTNO := :old.DEPTNO; my.DNAME := :old.DNAME; my.LOC := :old.LOC; end case; insert into DEPT$audit values my; end;
Генерация триггеров для обеспечения аудита данных Создавать вручную триггеры, подобные представленному выше, скучно. Эту работу может автоматически выполнить процедура. Следующая процедура создаст только что представленный триггер. Обратите внимание, что теневую таблицу она не создает. — Получает имя таблицы и создает простой триггер для аудита — Теневая таблица (по имени $audit) должна уже существовать Procedure generateTrigger(p_tableName in varchar2) authid current user — as b varchar2(4000); cursor cl is select column_name from user_tab_columns where table name = p tableName order by column_id; procedure appendx (destination in out varchar2, /archar2, string in varchar2)
328
Глава 6
is begin destination := destination!|string_in||chr(10); end; begin — строим оператор create trigger appendx(b,'create or replace trigger '||p_tableName||'ar'); appendx(b,'after update or insert or delete on 'I|p_tableName|I' ' ) ; appendx(b,'for each row'); appendx(b,'declare'); appendx(b,'my 'I|p_tableNameI|'$audit%ROWTYPE;'); appendx(b,'begin ' ) ; appendx(b,'if inserting then my.change_type := ' ' I 1 ' ; ' ) ; appendx(b,'elsif updating then my.change_type :=''U'';'); appendx(b,'else my.change_type := ''D'';'); appendx(b,'end if;'); appendx(b,'my.changed_by : = user;'); appendx(b,'my.changed_time := sysdate;'); appendx(b,'case my.change_type'); appendx(b,'when ' 'I'' then'); for x in cl loop appendx(b,'my.'||x.column_name|Г end loop; appendx(b,'else');
:= :new.'||x.column_name||';');
for x in cl loop appendx(b,'my.'||x.column_name||' := :old.'||x.column_name|I';'i; end loop; appendx(b,'end case; ') ; appendx(b,'insert into '||p_tableName||'$audit values my;'); appendx(b,'end;'); —
создаем триггер execute immediate b;
end;
Многие разработчики испытывают объяснимый дискомфорт при необходимости для обеспечения аудита данных создавать многочисленные теневые таблицы и уникальные триггеры, как было только что описано, даже если соответствующий код можно сгенерировать. Oracle9/ предоставляет стандартные пакеты, обеспечивающие аудит данных без обязательного написания разработчиком большого объема кода. Есть два метода: первый — использовать возможности поддержки многоверсионности таблиц, которые обеспечивает Oracle Workspace Manager, а второй — возможности "захвата и применения изменений в одной базе данных" технологии Oracle Streams.
Триггеры 329
Многоверсионность таблиц Таблица с поддержкой версий — это таблица, в которой хранятся, кроме текущего, прежние состояния данных. При изменении строки в таблице с поддержкой версий оператор UPDATE преобразуется в INSERT. Исходная строка остается без изменений, а новая строка добавляется. При удалении строки из таблицы она удаляется логически, но никакие данные не удаляются. Поскольку таблица с поддержкой версий содержит текущие и прежние данные, легко выполнить аудит всех изменений, произведенных в этой таблице. После использования следующего простого вызова процедуры для запуска этой поддержки все изменения в таблице ЕМР будут сохранены. SQL> begin 2 dbms_wm. enableversioning('ЕМР•,'VIEW_WO_OVERWRITE'); 3 end; 4 / PL/SQL procedure successfully completed.
Теперь попробуем выполнить несколько изменений. SQL> 2 3 4
update emp set job = 'ARTIST1 where job = 'CLERK' and ename = 'MILLER';
1 row updated. SQL> delete from emp 2 where ename = 'MILLER1; 1 row deleted.
Каждая таблица с поддержкой версии имеет соответствующее представление хронологии изменений, содержащее столбцы таблицы, имя пользователя, выполнившего изменение, и время создания, изменения или удаления строки. Теперь можно просматривать хронологию изменений информации о сотрудниках. SQL> 2 3 4
select user_name,job,type_of_change,createtime,retiretime from emp_hist where ename='MILLER' order by createtime;
USER NAME —
JOB
T CREATETIME
RETIRETIME
SCOTT SCOTT SCOTT
CLERK ARTIST ARTIST
и 25-AUG-03
I 25-AUG-03 20:52 20:57 D 25-AUG-03 20:58
25-AUG-03 20:57 25-AUG-03 20:58
Когда для таблицы включается поддержка версий, за кадром происходят несколько вещей.
330
Глава 6
1. Таблица переименовывается — таблица х становится таблицей X_LT. Корпорация Oracle не описала, что значит суффикс _LT, но он может быть сокращением от LOCKED_TABLE (заблокированная таблица). Заблокированная таблица включает все столбцы исходной таблицы, а также шесть дополнительных столбцов. Первичный ключ таблицы состоит из исходного первичного ключа, номера версии и кода статуса. 2. Создается представление с тем же именем, что и исходная таблица. Представление дает конечным пользователям интересующую их версию строки (обычно — последнюю). 3. Для представления создается несколько триггеров INSTEAD OF для реализации алгоритмов работы с версиями. Любые изменения, выполняемые с представлением, преобразуются во вставки в заблокированную таблицу. В заблокированной таблице каждое изменение сохраняется как новая отдельная строка. 4. Создаются также и другие представления, включая представление с хронологией изменений, которое упоминалось ранее. Вот какое интересное действие можно выполнить с таблицами, для которых поддерживаются версии. Мы удалили строку по условию ENAME = MILLER; МОЖНО установить время до этого удаления и посмотреть, как тогда выглядела таблица. SQL> 2
select * from emp where ename='MILLER';
no rows selected SQL> b e g i n 2 dbms_wm.gotodate(to_date('25-AUG-03 20:57:02','dd-mon-yy h h 2 4 : m i : s s ' ) ) ; 3 end; SQL> select empno,ename,job 2 from emp 3 where ename = 'MILLER'; EMPNO 7934
ENAME
JOB
MILLER
ARTIST
Отключить поддержку версий для таблицы тоже легко. execute dbms_wm.DisableVersioning('EMP')
Как только для таблицы включена поддержка версий, с ней надо работать внимательно. Например, если включить поддержку версий для таблицы ЕМР, ее нельзя будет экспортировать саму по себе, ЕМР станет представлением, а экспортировать представления нельзя. Придется также перенести любые триггеры таблицы ЕМР на таблицу EMP_LT с учетом того, что удалений из таблицы EMP_LT не бывает.
Триггеры
331
Примечание Подробнее о работе с таблицами с поддержкой версий см. в руководстве Application Developer's Guide — Workspace Manager Release 2".
"Oracle9i
Технология Oracle Streams Для тех, кто хочет централизовать функцию аудита данных, чтобы создать универсальную процедуру аудита, скорее всего, подойдет решение на базе потоков Oracle Streams. Мы хотим создать единую таблицу аудита, используемую в качестве репозитория для всех изменений, которые выполняются в базе данных. Эта единая для всей базы данных таблица аудита затем будет использоваться для вставки несколькими процессами, что вызывает ряд других проблем производительности. Их описание выходит за рамки нашей книги, но эти проблемы можно преодолеть, настроив параметры хранения для объекта, связанные со списками свободных блоков. Централизованный журнал аудита данных будет состоять из основной и детализирующей таблиц, которые мы назовем AUDIT_TRAIL_I И AUDIT_TRAIL_2. Таблица AUDIT_TRAIL_1 имеет следующую структуру: create table audit_trail_l (change_id number, command_type varchar2(l), table_name varchar2(30), user_name varchar2(30), change_date date);
Таблица AUDIT_TRAIL_2: create table audit_trail_2 (change_id number, column_name varchar2(30), actual_data_new sys.anydata, actual_data_old sys.anydata);
Потоки используют тип SYS .ANYDATA для хранения значений столбцов. (Столбец типа ANYDATA содержит экземпляр неких данных и описание соответствующего типа данных). Мы будем использовать этот же тип данных в нашей таблице аудита. Простая конфигурация Streams состоит из двух процессов. Процесс захвата выбирает изменения, выполненные операторами DML, из журналов повторного выполнения и записывает их в очередь базы данных. Процесс применения выбирает изменения из очереди и передает их процедуре обработки DML, которую мы напишем сами. Процедура обработки DML будет записывать изменения в журнал аудита данных. Прежде чем можно будет использовать Streams, необходимо выполнить ряд предварительных требований. Нужно использовать редакцию Enterprise сервера Oracle, а база данных должна работать в режиме архивирования журналов. Надо установить несколько параметров инициализации, создать и сконфигурировать учетную запись администратора Streams. Демонстрация каждого из этих шагов уведет нас слишком
332
Глава 6
далеко от рассматриваемой темы, но мы представим достаточно деталей, чтобы вы прочувствовали, как работает аудит на базе потоков Streams. Потоки Streams конфигурируются с помощью вызовов PL/SQL-процедуры. begin dbms_streams_adm.set_up_queue( queue_table => 'streams.queue_table', queue_name => 'streams_queue', queue_user => 'streams'); end;
Конфигурируем процессы захвата и использования для операторов DML, применяемых к конкретной таблице. Это примерно эквивалентно включению аудита. begin DBMS_STREAMS_ADM.ADD_TABLE_RULES( t a b l e _ n a m e => 'SCOTT.EMP\ s t r e a m s _ t y p e => ' c a p t u r e ' ) ; DBMS_STREAMS_ADM.ADD_TABLE_RULES( t a b l e _ n a m e => 'SCOTT.EMP1, streams_type => 'apply'); end;
Мы добавляем дополнительную информацию в журналы повторного выполнения так, чтобы каждое изменение в потоке включало первичный ключ строки. a l t e r table emp add supplemental log group log_group_emp_pk (empno) always;
Мы связываем пользовательскую процедуру с процессом применения. Это будет делаться также для операторов INSERT И DELETE. begin DBMS_APPLY_ADM.SET_DML_HANDLER( object_name => 'scott.emp', object_type => 'TABLE', operation_name => 'UPDATE', error_handler => false, user_procedure => 'streams.dml_handler', apply_database_link => NULL); end; DML_HANDLER — это программа, которую мы напишем для вставки данных в таблицы AUDIT_TRAIL. Это универсальная программа, которую можно использовать для любой таблицы. Процесс захвата Streams читает изменения из журналов повторного выполнения. Эти изменения не включают имя пользователя, их выполнившего, или дату изменения, но эта информация нужна нам для журнала аудита. Можно предоставить ее процедуре DML_HANDLER следующим образом:
Триггеры
333
CREATE OR REPLACE TRIGGER empar after delete or update or insert ON emp FOR EACH ROW DECLARE old_scn number; begin old_scn := dbms_flashback.get_system_change_number; insert into streams.audit_temp values (old_scn,sysdate,user) ; end;x
Этот триггер выбирает номер SCN нашей транзакции, связывает его с именем пользователя и временной отметкой и сохраняет эти три значения в таблице. (Нам придется периодически удалять строки из этой таблицы.) Процесс применения Streams теперь имеет всю необходимую информацию для построения полного журнала аудита. Он будет выбирать сохраненные имя пользователя и временную отметку по значению SCN И добавлять эту информацию в журнал аудита. Вот код процедуры DML_HANDLER: — Универсальный код для сохранения изменений в любой таблице — и записи в журнал аудита данных PROCEDURE dml handler(in any IN SYS.ANYDATA) IS lcr SYS.LCR5 ROW RECORD; PLS INTEGER; re oldlist SYS.LCR$_ROW_LIST; SYS.LCR$_ROW_LIST ; newlist varchar2(32); command varchar2(30) ; tname v scn number; v user varchar2(30) ; v date date; v change id number; newdata sys.AnyData; BEGIN —
Обращаемся к LCR = in_any.GETOBJECT(lcr); re command = lcr.GET_COMMAND_TYPE() ; - lcr.GET_OBJECT_NAME; tname = lcr.GET_SCN; v_scn oldlist = lcr.GET_VALUES('old'); newlist = lcr.get_values('new'); — ищем информацию, которую мы связали с соответствующим sen, -- и вставляем ее в audit_trail_l insert into audit_trail_l (change_id, command_type, table_name, user_name,change_date) select data_audit_seq.nextval,substr(command,1,1), tname,scn user,scn date
334
Глава 6 from where
audit_temp sen = v_scn;
—
записываем в audit_trail_2 IF command = 'DELETE' then FOR i IN 1..oldlist.COUNT LOOP insert into audit_trail_2 (change_id, column_name,actual_data_old) values (data_audit_seq.currval, oldlist(i).column_name,oldlist(i).data); END LOOP; ELSIF command - 'INSERT' then FOR i IN 1..newlist.COUNT LOOP insert into audit_trail_2 (change_id, column_name,actual_data_new) values (data_audit_seq.currval, newlist(i).column_name,newlist(i).data); END LOOP; ELSIF command = 'UPDATE' then FOR i IN 1..oldlist.COUNT LOOP newdata := lcr.get_value('new',oldlist(i).column_name); if newdata is null then newdata := oldlist(i).data; end if; insert into audit_trail_2 (change_id, column_name,actual_data_new,actual_data_old) values (data_audit_seq.currval, oldlist(i).column_name,newdata,oldlist(i).data); END LOOP; END IF; END;
Вот пример данных аудита, полученных с помощью потоков: SQL> update scott.emp 2 set job = 'ENGINEER', 3 deptno = 1 0 4
where
ename='JONES';
1 row updated. SQL> commit; Commit complete. SQL> select * 2
from audit_trail_l;
CHANGE_ID С TABLE_NAME USER_NAME CHANGE_DA 24 U EMP
SCOTT
28-Aug-03
Триггеры 335 SQL> select change_id,column_name, disp_any(actual_data_old) as old, 2 disp_any(actual_data_new) as new 3 from audit_trail_2 4 where change_id = 24; CHANGE_ID COLUMN_NAM OLD 24 EMPNO 24 JOB
NEW
7566 ARTIST
7566 ENGINEER
Учтите, что в журнале аудита мы видим фактические изменения и первичный ключ, но не все столбцы таблицы. Вот пример оператора DELETE: SQL> delete from emp 2 where empno • 7566; 1 row deleted. SQL> commit; Commit complete.
Если необходимо увидеть данные столбцов в обычном порядке, можно соединить журнал аудита с представлением TAB_COLUMNS. SQL> 2 3 4 5 6 7 8
s e l e c t change_id,a.column_name, disp_any(actual_data_old) disp_any(actual_data_new) as new from audit_trail_2 a,all_tab_columns u where change_id = 25 and a.column_name = u.column_name and u.table_name - 'EMP1 and u.owne r='SCOTT• order by column_id;
CHANGE I D COLUMN NAM
25 EMPNO 25 ENAME 25 JOB 25 25 25 25 25
MGR HIREDATE SAL COMM DEPTNO
OLD
NEW
7566 JONES ENGINEER 7839 02-APR-81 2975 10
SQL> select * 2 from audit_trail_l 3
where
change_id=25;
CHANGE_ID С TABLE_NAME USER_NAME 25 D EMP
STREAMS
CHANGE_DA 28-Aug-03
as old,
336
Глава 6
Вас может интересовать функция DISP_ANY, которая использовалась для показа типов данных SYS . ANYDATA. ВОТ какие данные хранятся в столбце SYS . ANYDATA: —
Выдает значения, хранящиеся в столбце SYS.ANYDATA
function disp_any(data IN SYS.AnyData) return varchar2 IS str VARCHAR2(4000); return_value varchar2(4000); chr CHAR(255); num NUMBER; dat DATE; res number; begin if data is null then return_value := null; else case data.gettypename when 'SYS.VARCHAR2' then res := data.GETVARCHAR2(str); return_value :- str; when 'SYS.CHAR' then res := data.GETCHAR(chr); return_value := chr; when 'SYS.NUMBER' THEN res := data.GETNUMBER(num); return_value := num; when 'SYS.DATE' THEN res := data.GETDATE(dat); return_value := dat; else return_value := data.gettypename()||' ????'; end case; end if; return return_value; end;
Процессы Streams могут автоматически выполнять программы (грубо говоря) на основе события. Событием является процесс захвата Streams, который может срабатывать на все изменения DDL и DML, записанные в журнале повторного выполнения базы данных. Для создания потрясающего приложения Streams придется программировать не так уж много.
Очередь заданий (триггеры на временные события) Стандартный пакет DBMS_JOB позволяет выполнять процедуру в указанный момент времени или через заданный интервал (например, каждый час или каждый день). Он используется для планирования "пакетных" заданий, т.е. заданий, работающих без взаимодействия с пользователем. Например, если необходимо регулярно
Триггеры
337
обмениваться данными с другой системой, выгружая или создавая текстовые файлы, и вы хотите выполнить задание сегодня в полночь, а затем ровно через семь суток, то вы можете послать задание следующим образом: declare jobno number; begin dbms_job.submit(job = > jobno, what => 'AnotherDataLoad;', next_date => trunc(sysdate) + 1, interval => 'trunc (sysdate) + 7'); commit; end;
Параметр JOB передается в режиме IN OUT И получит уникальный порядковый номер задания, используемый для его идентификации. Параметр WHAT — исходный текст анонимного PL/SQL-блока или имя PL/SQL-процедуры, которая будет выполнена, a NEXT_DATE и INTERVAL — параметры, задающие расписание выполнения задания. Для посылки задания необходима только привилегия выполнения процедур в пакете DBMS_JOB. С помощью процедур пакета DBMS_JOB МОЖНО добавлять задания в очередь, изменять и удалять их. Можно также помечать задание как разрушенное так, чтобы оно не выполнялось. (Чтобы задания выполнялись, параметр инициализации JOB_QUEUE_PROCESSES должен иметь значение больше нуля.) Вы можете проверять свои собственные задания в очереди, чтобы определить, в частности, когда задание будет выполняться в следующий раз и когда оно последний раз было выполнено. SQL> select job,what,last_date,last_sec, 2 next_date,next_sec 3 from user_jobs 4* order by job JOB WHAT 5 dataload;
LAST_DATE LAST_SEC
NEXT_DATE NEXT_SEC
03-Aug-03 04:00:00
03-Aug-04 04:00:00
Планирование заданий Расписание выполнения определяется параметрами NEXT_DATE И INTERVAL. NEXT_DATE — это параметр типа DATE. ЕСЛИ значение NEXT_DATE не задано или указанная дата уже прошла, задание будет выполнено немедленно. Параметр INTERVAL имеет тип VARCHAR2. ОН не представляет собой интервал между двумя моментами времени, как можно было ожидать. Вместо этого он содержит выражение типа DATE, которое станет новым временем выполнения задания (NEXT_DATE). Значение должно соответствовать моменту времени в будущем или быть неопределенным (NULL). Если параметру INTERVAL установлено значение NULL (или оно просто не задано), задание будет выполнено только один раз.
338
Глава 6
Например, для выполнения задания в начале каждого часа можно задать интервал следующим образом: interval
=> ' t r u n c ( s y s d a t e ,
I r
hh24'')
+
1/24'
Для запуска задания каждую пятницу в 6 вечера interval => 'next_day(trunc(sysdate), ''friday'1) + 18/24'
Для выполнения задания в 4 утра третьего дня месяца interval -> 'trunc(last_day(sysdate)+3) + 4/24'
Эти интервалы задают время следующего выполнения абсолютно; выражение не основано на текущем времени. Использования текущего времени обычно избегают потому, что задания часто выполняются немного позже, чем указанный момент NEXT_DATE, в зависимости от того, когда пробуждается процесс обработки очереди заданий и насколько он загружен. Если значение NEXT_DATE зависит от текущего времени, интервал начнет смещаться. Следующий пример показывает задание, запланированное для выполнения каждые пять минут, с помощью интервала SYSDATE + .003742. — задание будет выполняться через 0.003472 дня, или через 5 минут begin dbms_job.submit( :job,'anotherJob;', sysdate, 'sysdate + .003472'); commit; end;
Далее показаны моменты времени, когда задание фактически выполняется и когда оно должно было выполняться в идеале. Во втором случае это должно быть ровно через пять минут после предыдущего идеального времени, но реальные моменты выполнения отличаются от идеальных. Job Schedule Slide Actual Ideal Time Time 16:42:57 16:48:02 16:53:06 16:58:11 17:03:15 17:08:19
16:42:57 16:47:57 16:52:57 16:57:57 17:02:57 17:07:57
Задания и триггеры DML Кроме использования для пакетных заданий, очередь заданий может также применяться для расширения функциональных возможностей других триггеров. Пусть вашему приложению необходимо посылать сообщение по электронной почте при вставке новой строки в таблицу. Можно создать триггер, посылающий сообщение, но при вставке пользователем любой строки триггеру придется ждать завершения
Триггеры
339
процесса посылки сообщения, а пользователю — завершения работы триггера. Ситуация усложняется еще и тем, что пользователь может откатить транзакцию, но сообщение уже будет отправлено в любом случае. Нет механизма двухфазной фиксации, поддерживающего согласование базы данных и сервера электронной почты. Обе эти проблемы могут быть решены, если триггер передаст часть действий, связанных с посылкой электронной почты, в очередь заданий. В этом случае сообщение посылается, только если вызвавший срабатывание триггера оператор (исходная вставка) фиксируется. Лишь после этого запрос добавляется в очередь заданий. Если происходит откат, запись в очередь заданий также откатывается. Кроме того, пользователю не придется ждать завершения посылки сообщения, поскольку оно посылается асинхронно. — Триггер, посылающий PL/SQL задание create or replace trigger worknotification after insert on work_orders for each row declare jobno number; begin dbms_j ob.submit( job => jobno, what => 'email('''I I:new.recipient||''');' ); end;
Задания и разделяемый пул Хотя представленный триггер работает, он неэффективен. Проблема в том, что каждое задание выполняет новую команду. При посылке заданий вида e m a i l ( ' I s a b e l l e i s e r v e r l . c o m ) , emaiI('
[email protected]) И
email (' Jean@server4. com) мы просим процесс обработки заданий выполнить три отдельные команды. Сервер сохраняет каждую уникальную команду в разделяемом пуле, как показано далее: SQL> select sql_text 2 from v$sqlarea 3 where sql text like '%server%'; — SQL TEXT DECLARE job BINARY_INTEGER := :job; next_date DATE := :mydate; broken BOOLEAN :- FALSE; BEGIN email('
[email protected]'); :mydate := next_date; IF broken THEN :b := 1; ELSE :b := 0; END IF; END; DECLARE job BINARY_INTEGER := :job; next date DATE := :mydate; broken
340
Глава 6
BOOLEAN := FALSE; BEGIN email('
[email protected]'); :mydate := next_date; IF broken THEN :b := 1; ELSE :b := 0; END IF; END; DECLARE job BINARY_INTEGER := :job; next_date DATE := :mydate; broken BOOLEAN := FALSE; BEGIN email(rtheodorosSserver2.com'); :mydate := next_date; IF broken THEN :b := 1; ELSE :b := 0; END IF; END;
Если в нашей таблице есть много уникальных получателей, в разделяемом пуле будет много уникальных операторов, и это отрицательно повлияет на производительность системы в целом. Решить эту проблему можно способом, описанным ниже. Помните, что мы говорим о строчном триггере, который срабатывает при иставке строки в таблицу. Мы знаем, что один из столбцов таблицы содержит адрес электронной почты получателя. Еще один из столбцов будет использоваться для хранения номера задания. Строчный триггер BEFORE, помимо посылки задания на отправку электронной почты, будет записывать этот номер задания. create or replace trigger worknotbr before insert on work_orders for each row declare jobno number; begin dbms_job.submit(job => jobno, what => 'email( job );'); :new.email_jobNo:= jobno; end;
Один из специальных параметров, которые воспринимает система управления заданиями, — это параметр JOB, передаваемый в режиме IN И идентифицирующий номер текущего задания. Процедура посылки электронной почты будет использовать этот номер задания для выборки адреса электронной почты получателя. procedure email (job in number) is lv_recipient work_orders.recipient%type; begin select recipient into lv_recipient from work_orders where email_jobNo = job; send_email(lv_recipient); end;
Мы избежали заполнения разделяемого пула, посылая одну команду, которая может совместно использоваться любым количеством отдельных получателей.
Триггеры
341
Ошибки при выполнении задания Если задание не срабатывает из-за необработанной исключительной ситуации, сервер записывает трассировочный файл в каталог дампа фоновых процессов и сообщение — в журнал сообщений сервера. Процесс обработки очереди заданий попытается повторно выполнить сбойное задание. Первая попытка делается минутой позже, вторая — через две минуты, третья — через четыре, потом — через восемь, и т.д. Интервал между попытками выполнения растет, но не может стать больше значения исходного интервала для задания. Если исходный интервал выполнения был установлен равным пяти минутам, повторные попытки произойдут через минуту, две, четыре, потом — через пять, еще раз через пять и т.д. После 16-ти попыток задание помечается как неработоспособное и больше на запускается. Если, как в случае сценария посылки электронной почты в предыдущем примере, это "однократное" задание, максимального интервала нет, и интервал между попытками растет очень быстро. Десятая попытка будет сделана через 1024 минуты (примерно через 17 часов). Мы можем изменить это поведение и сделать так, чтобы несработавшее задание повторно ставило себя на выполнение примерно через 15 минут, хотя для исходного задания интервал и не задавался. Возможно (снова возвращаясь к предыдущему примеру), сервер электронной почты временно не работает, так что мы хотели бы попытаться посылать сообщение еще 16 раз в течение следующих трех часов (а не 16 раз в течение следующих двух месяцев). Параметр NEXTDATE может помочь нам решить эту проблему, потому что его можно использовать для установки времени следующего выполнения. Однако изменение параметра NEXT_DATE выполняется, только если задание завершается успешно. Если происходит сбой задания, параметр NEXT_DATE игнорируется. Сравните следующие две процедуры, которые были посланы без указания интервала. Для первой планирование будет выполняться обычным образом, и она будет помечена как неработоспособная после 16-ти попыток. Вторая будет выполняться каждые пять минут, но никогда не будет помечена как неработоспособная. Она будет оставаться в очереди, пока не будет удалена вручную. -- Задание, которое будет всегда завершаться ошибкой -- изменение следующей даты будет игнорироваться procedure fail_a_job_l (next_date in out date) as begin next_date := sysdate + .00347; raise_application_error(-20000,'fail this job'); end; -- Задание никогда не будет завершаться ошибкой -- Задан обработчик исключительных ситуаций — Будет устанавливаться следующая дата — Задание будет работать "вечно", пока его не удалят из очереди procedure fail_a_job_2 (next_date in out date) as begin raise_application_error(-20000,'fail this job');
342
Глава 6
exception when others then next_date := sysdate + .00347 — 5 минут; end;
Таким образом, очередь заданий позволяет программам автоматически выполняться в указанный момент времени или с заданным интервалом. Расписание выполнения задания можно рассматривать как события, вызывающие срабатывание задания. Однократные задания пригодятся для распределения нагрузки между процессами; если задание не срабатывает, расписание выполнения для него меняется. Для постановки заданий на выполнение используется пакет DBMS_JOB.
Триггеры DDL Операторы DDL (языка определения данных) составляют небольшую часть кода прикладных программ, но они очень часто используются администраторами баз данных для настройки или изменения среды. Предположим, вы — АБД, который должен выполнить сценарий, создающий новые таблицы. Кроме того, вы должны предоставить привилегии для каждой новой таблицы. Правда, хорошо было бы, если бы можно было автоматизировать предоставление привилегий? Следующий триггер пытается предоставить роли BI_ROLE привилегию SELECT на только что созданную таблицу. — Этот триггер не будет работать create or replace trigger SystemGrantSelect after create on database begin if ora_dict_obj_type='TABLE' then execute immediate('grant s e l e c t ' I I' on 'I | ora_dict_obj_owner||'.'|| ora_dict_obj_nameI|' to b i _ r o l e ' ) ; end if; end;
К сожалению, этот триггер не работает. create table customized_items ( ERROR at line 1: ORA-00604: error occurred at recursive SQL level 1 ORA-30511: invalid DDL operation in system triggers ORA-06512: at line 3
Давайте попробуем решить другую задачу. Предположим, приложение создает учетные записи пользователей, но не задает для них стандартное табличное пространство. Мы хотели бы изменить учетную запись пользователя следующим образом: create or replace trigger SystemAlterUser after create on database
Триггеры 343 begin if ora_dict_obj_type = 'USER' then execute immediate ( 'alter user ' I Iora_dict_obj_name|| ' default tablespace users'); end if; end;
Но снова create user milton identified by xxxx; * ERROR at line 1: ORA-00604: error occurred at recursive SQL level 1 ORA-30511: invalid DDL operation in system triggers ORA-06512: at line 5
Большинство операторов DDL не работает в триггерах DDL. Поддерживаются только действия с таблицами (вроде операторов CREATE TABLE И DROP TABLE) И действия операторов ALTER COMPILE. МЫ можем обойти это ограничение, заставив триггер DDL посылать задание в очередь заданий PL/SQL, аналогично тому, как было представлено в разделе "Задания и триггеры DML" ранее в этой главе. Сначала мы создадим процедуру, принимающую имя пользователя и изменяющую для пользователя стандартное табличное пространство. procedure AlterUser (usernameln in varchar2) is begin execute immediate ( ' a l t e r user ' |Iusernameln|| ' default tablespace u s e r s ' ) ; end;
Затем мы перепишем триггер для посылки задания, выполняющего эту процедуру. create or replace trigger SystemAlterUser after create on database declare jobno number; begin if ora_dict_obj_type = 'USER' then dbms_job.submit(job => jobno, what => 'alter user('''IIora_dict_obj_nameI I''');'); end if; end;
Триггер для обеспечения целостности операторов DDL Пользователь Oracle автоматически имеет привилегии для объектов своей схемы, включая привилегию на удаление любого из этих объектов. Бывают случаи, когда хотелось бы сделать пользователей менее могущественными. Как можно предотвратить удаление пользователями их собственных объектов?
344
Глава 6
create or replace trigger prevent_drop before drop on alex.schema begin raise_application_error(-20000,'Invalid command: DROP ' ) ; end; SQL> drop table b; drop table b * ERROR at line 1: ORA-00604: error occurred at recursive SQL level 1 ORA-20000: Invalid command: DROP ORA-06512: at line 2 Триггеры DDL могут быть созданы для схемы или базы данных. Здесь мы создали триггер для схемы ALEX.
Триггер журнала аудита DDL Том Кайт на сайте http://asktom.oracle.com предлагал следующее решение для реализации журнала аудита операторов DDL после того, как кто-то задал ему вопрос: "Если разработчик изменяет функцию, существует ли способ узнать, как эта функция выглядела изначально, т.е. узнать, как она выглядела до изменения?Иными словами, нет ли способа поддерживать журнал аудита изменений функций ?" Его решение (немного измененное здесь) использует две таблицы журнала аудита и следующий триггер DDL: — Сохранить код перед любыми его изменениями c r e a t e or replace t r i g g e r save_old_code before c r e a t e on database begin i f ora_dict_obj_type in ( •PACKAGE','PACKAGE BODY','PROCEDURE','FUNCTION' insert values
)
then
into old_source_header (username,change_ id,change_date) (ora_login_user,source_seq.nextval,sysdate);
i n s e r t i n t o old_source_detail s e l e c t source_seq.currval,dba_source.* from dba_source where owner = ora_dict_obj_owner and name = ora_dict_obj_name and type = ora_dict_obj_type; end end;
if;
Две таблицы журнала аудита содержат текст хранимой процедуры, поскольку он существовал до изменения.
Триггеры 345 SQL> desc old_source_header Name Null?
Type
USERNAME CHANGE_ID CHANGE_DATE
VARCHAR2(30) NUMBER DATE
QL> desc olc1 source detail Name Null?
Type
CHANGE ID OWNER NAME TYPE LINE TEXT
NUMBER VARCHAR2(30) VARCHAR2(30) VARCHAR2(12) NUMBER VARCHAR2(4000)
SQL> 2
select * from
DSERNAME ALEX SQL> 2 3 4 Here
old_source_header; CHANGE_ID 10
CHANGE_DATE 28-Aug-03 10:20:44
select text as "Here is how it looked before" from old_source_detail where change_id=10 order by line; is how it looked before
procedure monitor_db_size as begin insert into dbresults (runtime,instance,parameter_id,result) select sysdate,instance_name,'3', round(sum(bytes)/(1024*1024),2) from dba segments,v$instance; — end;
Чтобы вам было понятно, что привело к созданию этого приложения, посмотрите, как выглядит эта процедура сейчас. SQL> select text 2 from dba_source 3 where name = 'MONITOR_DB_SIZE'; TEXT procedure monitor_db_size as
346
Глава 6
begin null; end;
Триггеры на события базы данных Триггеры на события базы данных срабатывают при возникновении нескольких событий. Мы рассмотрим некоторые из них.
Триггеры на регистрацию Один триггер на событие регистрации был представлен в начале главы. Хотя триггеры на события базы данных очень полезны, вот триггер, который практически бесполезен: Create or replace t r i g g e r loginCheckTrg a f t e r logon on database declare m_count number; begin s e l e c t count(*) into m_count from v$session where audsid=sys_context('userenv','sessionid') and program like '%MSQRY32.EXE%'; if m_count > 0 then raise_application_error(-20000,'Please try again later'); end if; end;
Триггер проверяет, какую программу вы выполняете, и если это MSQRY32, триггер выдает сообщение об ошибке и не разрешает зарегистрироваться. (Мы пытаемся предотвратить использование офисных приложений для доступа к базе данных.) Проблема с триггерами этого типа состоит в том, что пользователи зачастую полностью контролируют имена программ на своей машине. Достаточно легко скопировать выполняемый файл, переименовать его, запустить и обойти ограничения этого триггера на событие регистрации. Триггеры на регистрацию могут использоваться для инициализации сеансов базы данных различными способами, например, для записи файла SQL_TRACE ИЛИ ДЛЯ сохранения статистической информации сеанса, или для изменения текущей схемы. Причина изменения текущей схемы связана с архитектурой многих сред баз данных, в которых общая схема приложения совместно используется схемами многих пользователей приложения. Обычно пользователи обращаются к централизованным таблицам приложения через общедоступные или приватные синонимы, либо уточняя каждое имя таблицы именем схемы приложения. Каждая подобная среда подвержена одной проблеме. Общедоступные синонимы снижают производительность. Приватные синонимы сложно поддерживать, а явное указание имен схем делает приложение намного менее гибким. Лучше изменять се-
Триггеры 347 анс пользователя так, чтобы он ссылался на схему приложения. Триггер на регистрацию позволяет этого добиться: CREATE OR REPLACE TRIGGER change_schema AFTER logon ON DATABASE begin execute immediate('alter session set current_schema=app_master'); end;
Триггер на ошибку сервера Триггер на ошибку сервера обеспечивает централизованный метод перехвата ошибок в базе данных, включая те ошибки пользователей, которые обычно не записываются в журнале сообщений базы данных. Предположим, пользователь выполняет запрос, но ему не хватает временного пространства. Можно перехватить ошибку, включая код SQL-оператора, который выполнялся пользователем, а затем определить причину проблемы. — Сохраняем информацию обо всех ошибках create or replace trigger log_errors after servererror on database declare sql_text ora_name_list_t; msg varchar2(2000) := null; stmt varchar2(2000):= null; begin for i in 1 .. ora_server_error_depth loop msg := msgl|ora_server_error_msg(i); end loop; for i in 1..ora_sql_txt(sql_text) loop stmt := stmt||sql_text(i); end loop; insert into our_user_errors (error_date,username,error_msgr error_sql) values (sysdate,ora_login_user,msg, stmt); end; SQL> desc user errors Name ERROR_DATE USERNAME ERROR MSG ERROR SQL SQL> select * 2 from user errors; ERROR_DAT USERNAME ERROR_MSG
Null?
Type DATE VARCHAR2(30) VARCHAR2(2000) VARCHAR2(2000)
348
Глава 6
ERROR_SQL 20-MAY-03 SAMPLE ORA-01652: unable to extend temp segment by 64 in tablespace TEMP_1 ORA-27059: skgfrsz: could not reduce file size OSD-04005: SetFilePointer() failure, unable to read from file O/S-Error: (OS 112) There is not enough space on the disk. select nunavut.userlisting, nunavut.phone_number, woodbury.phone_number from nunavut_phone_book nunavut, woodbury_phone_book woodbury
В этом примере пользователь не задал конструкцию WHERE, так что неудивительно, что ему не хватило временного пространства. Можно также использовать триггер на событие приостановки, чтобы помочь устранить нехватку пространства без прерывания запроса, как будет описано далее.
Триггер на событие приостановки Триггер на событие приостановки используется для анализа ситуаций с нехваткой свободного пространства. Обычно, когда при выполнении SQL-оператора возникает ошибка, связанная с нехваткой пространства или несоответствием между параметрами хранения объекта и необходимым ему пространством на диске, SQLоператор не срабатывает, и выполненные им изменения откатываются. Однако, если сеанс восстанавливаемый (resumable), SQL-оператор приостанавливается и ждет заданный период времени. Когда проблема будет устранена, выполнение оператора продолжится. В противном случае по истечении периода ожидания приостановленный SQL-оператор выдаст исходное сообщение об ошибке, связанной с нехваткой пространства. С помощью триггера на событие приостановки вы можете перехватить SQL-оператор (почти так же, как это было сделано в предыдущем примере триггера на ошибку сервера) или выполнить любую соответствующую обработку. Разработчик Alex выполнил следующий SQL-оператор в надежде застать врасплох ничего не подозревающего дежурящего по ночам АБД и заставить его выделить больше дискового пространства под свои объекты (увеличить квоту). a l t e r session enable resumable name 'Помогите, мне действительно нужно больше пространства на д и с к е ' ;
Но АБД, однако, ожидал такого развития событий и подготовил следующий триггер на событие приостановки: create or replace trigger respond_to_resumable_session after suspend on database declare jobno number; begin i f o r a _ l o g i n _ u s e r = 'ALEX' t h e n r a i s e _ a p p l i c a t i o n _ e r r o r ( - 2 0 0 0 0 , ' A l e x , I t o l d you, you s h o u l d n ' t ' | l ' b e d o i n g t h i s . T h e r e ' ' s n o way y o u ' ' r e g e t t i n g more s p a c e . ' ) ; else dbms_j o b . s u b m i t ( j o b => j o b n o ,
Триггеры 349 what => 'space_shortage_notification;' end;
Вот что пользователь Alex увидит на следующее утро: insert into * ERROR at line 147: ORA-00604: error occurred at recursive SQL level 1 ORA-20000: Alex, I told you, you shouldn't be doing this. There's no way you're getting more space. ORA-06512: at line 164 ORA-01653: unable to extend table ALEX.EMP_HOURS_BACKUP
Если заданию любого другого пользователя не хватит пространства на диске, АБД получит уведомление и сможет немедленно отреагировать, выделив дополнительное пространство или изменив параметры хранения. После этого оператор пользователя возобновит работу и выполнится успешно.
Ошибки и триггеры на события базы данных При работе с триггерами на события базы данных необходимо предоставить привилегию ADMINISTER DATABASE TRIGGER непосредственно пользователю, которому принадлежит триггер. В противном случае может быть выдано сообщение об ошибке. (table char) ERROR at line 2: ORA-04 045: errors during recompilation/revalidation of SYSTEM.LOG_ERRORS ORA-01031: insufficient privileges ORA-00904: invalid identifier
Вот описание действий, после которых было получено это сообщение об ошибке. Пользователь создавал таблицу и попытался использовать слово table (недопустимый идентификатор) в качестве имени столбца. Общесистемный триггер на событие ошибки сервера был создан, но в тот момент оказалось, что триггер этот неработоспособен. Сервер Oracle попытался перекомпилировать триггер, но скомпилировать его не удалось потому, что владелец триггера получил привилегию ADMINISTER DATABASE TRIGGER Через рОЛЬ.
Ошибки в триггерах на события базы данных имеют серьезные последствия. Если ошибка возникает в триггере на регистрацию, это, вероятно, не позволит непривилегированным пользователям зарегистрироваться. Если триггер на событие ошибки сервера регистрирует все ошибки в таблице и эта таблица заполняется (или при работе с ней возникает другая ошибка), любой сеанс, вызвавший возникновение ошибки, зависнет.
Не надо изобретать велосипед Бывают ситуации, когда сложно определить, надо ли создавать триггеры. Многие требования, которые можно удовлетворить с помощью триггеров, можно выпол-
350
Глава 6
нить с помощью встроенных средств сервера Oracle. Имеет смысл внимательно изучить требования, прежде чем активно браться за создание специализированных триггеров. Рассмотрим простой пример.
Отчет об использовании базы данных Нам необходимо ежемесячно формировать итоговый отчет для каждого пользователя базы данных. Исходные данные имеют следующий вид: USERID AUDSID LOG_ON SCOTT TEST TEST
439 440 441
03/11/03 11:10 03/11/03 11:43 03/11/03 11:44
LOG_OFF 03/11/03 11:41 03/11/03 11:44 03/11/03 11:47
CPUJJSED 231 30 108
Данные будут суммироваться по месяцам и помогут понять, сколько процессорного времени использует каждый отдел. Для создания такого приложения достаточно одной таблицы и пары триггеров. — таблица для хранения информации о сеансах create table usage_log (user_id varchar2(30), audsid varchar2(30), log_on date, log_off date, cpu_used number); — триггер на регистрацию в базе данных create or replace trigger usage_start after logon on database begin insert into system.usage_log (user_id,audid,log_on,log_off,cpu_used) values (sys_context('userenv','session_user'), sys_context('userenv','sessionid'), sysdate, null, null); end; — триггер на завершение сеанса create or replace trigger usage stop before logoff on database begin update system.usage_log set log_off = sysdate, cpu_used = (select value from v$mystat s,v$statname n where s.statistic# = n.statistic! and n.name like 'CPU used by this session1) where sys_context('USERENV1, 'SESSIONID') = audid and sys_context('userenv','session_user') = user_id and log_off is null; end;
Триггеры
351
Ничего неправильного в этой идее нет, и триггеры на события базы данных будут работать, но, включив аудит (AUDIT SESSION), ВЫ будете получать те же отчеты, которые будут сохраняться в стандартном журнале аудита базы данных. SQL> select 2 3 4 5 from USERNAME
username, sessionid, to_char(timestamp,'mm/dd/yy hh24:mi') timestamp, to_char(logoff_time,'mm/dd/yy hh24:mi') logoff_time, session_cpu dba_audit_trail;
SESSIONID
SCOTT TEST TEST
439 440 441
TIMESTAMP
LOGOFFJTIME
03/11/03 11:10 03/11/03 11:43 03/11/03 11:44
03/11/03 11:41 03/11/03 11:44 03/11/03 11:47
SESSION_CPU 232 31 109
Отличие между версией на базе триггеров и использованием команды аудита проявляется при остановке сервера способом, требующим восстановления при запуске (SHUTDOWN ABORT), или при принудительном прекращении работы пользовательского процесса (ALTER SESSION). В обоих случаях триггер на завершение сеанса вообще не срабатывает. Поэтому решение на базе триггеров приведет к возникновению строк со временем начала сеанса, но без соответствующего времени его завершения. Журнал аудита базы данных, напротив, будет содержать время завершения сеанса и даже соответствующий комментарий, в котором будет сказано, что отключение было выполнено в процессе очистки. С другой стороны, запись журнала аудита системы имеет фиксированное количество полей. В ней регистрируется только ограниченный набор статистических показателей (логические операции чтения и записи, использованное процессорное время, количество взаимных блокировок). Если вам нужна другая информация, скорее всего, придется создавать триггеры. Следует избегать создания триггеров, реализующих ограничения целостности ссылок или ограничения проверки. Всегда лучше использовать аналогичные декларативные ограничения, поддерживаемые сервером.
Резюме Триггеры — это программы, которые сервер выполняет каждый раз при возникновении определенного события. Триггеры на операторы DML и триггеры INSTEAD OF связаны с событиями изменения данных, триггеры DDL связаны с созданием и удалением объектов базы данных, а триггеры на события базы данных срабатывают при возникновении определенных событий в базе. В этой главе мы рассмотрели примеры триггеров DDL и триггеров на события базы данных. Был представлен способ, позволяющий избежать ошибок мутирующих таблиц, а также рассмотрены несколько примеров триггеров на операторы DML. Мы описали возможности механизма поддержки версий таблиц и технологии Oracle Streams; они могут использоваться для обеспечения аудита изменений данных. Мы описали механизм поддержки очередей заданий PL/SQL и объяснили, как обеспечить выполнение этих заданий в определенные моменты времени или с оп-
352
Глава 6
ределенным интервалом. Эти задания можно использовать вместе с триггерами для выполнения слишком продолжительных (для триггера) действий, а также для выполнения действий, которые в триггере непосредственно выполнять нельзя. Наконец, мы обсудили, почему не нужно использовать триггеры в ситуациях, когда можно удовлетворить вьщвинутые требования с помощью стандартных средств сервера. В этой главе представлено много новых идей и использовано много новых понятий. Проверьте, как вы поняли то, о чем в ней говорилось, дав определения следующим терминам: триггер, состояние триггера, состояние объекта, триггер DML, ошибка мутирующей таблицы, операторные и строчные триггеры, журналы аудита данных, поддержка нескольких версий таблицы, технология Oracle Streams, задания и очередь заданий, интервал и следующая дата выполнения задания, триггер DDL, триггеры для схемы и базы данных, триггер на событие базы данных, триггер на ошибку сервера и триггер на событие приостановки.
Г™
™7
I /VClDCl / •
Пакеты АБД Если вы отвечаете за сопровождение базы данных Oracle, то уже знаете, что ваша работа состоит из задач двух типов: > разовых задач, включая создание пользователей или объектов базы данных, предоставление привилегий, изменение параметров хранения и т.д.; > регулярных действий, выполняемых в рамках текущего сопровождения системы, таких, как контроль и настройка производительности, резервное копирование и восстановление, мониторинг базы данных для отслеживания уже произошедших и потенциально возможных сбоев. Оба аспекта вашей работы можно выполнять эффективнее, если вы будете использовать PL/SQL. Для задач первого типа, например, для сброса пароля, как было продемонстрировано в главе 5, можно создать утилиты, обеспечивающие более точный уровень контроля, чем стандартная привилегия Oracle ALTER USER. Аналогично: как только подобные средства будут снабжены интерфейсом PL/SQL, вы сможете использовать их в Web-среде с помощью набора инструментальных средств OWA Web. как описано в главе 9. Только ваше воображение ограничивает круг административных задач, при решении которых использование средств PL/SQL позволит получить дополнительный контроль и гибкость. В этой главе мы остановимся на второй группе — на регулярных действиях, необходимых для сопровождения базы данных Oracle. Конечно, определенные аспекты регулярных действий стоит автоматизировать, поскольку это поможет стандартизировать решение задач, упростить работу администратора и сделать ее более эффективной. Мы обсудим использование языка PL/SQL для создания автоматизированных, стандартизованных решений для контроля базы данных. Расскажем, как нужно следить за свободным пространством в базе данных и в каталоге архивных журналов. Мы будем проверять, успешно ли завершено резервное копирование. Будем также собирать информацию о росте и использовании базы данных. Мы продемонстрируем, как анализировать файл сообщений Oracle, чтобы найти сообщения об ошибках, и как посылать уведомления при выявлении новых сообщений. В этой главе будут представлены четыре пакета: > пакет для контроля и управления файлом сообщений Oracle; > пакет уведомления для посылки сообщений по электронной почте; > пакет мониторинга для процедур проверки состояния резервных копий и мониторинга свободного пространства, которые позволяют предотвращать потенциальные проблемы; 12 Зак. 348
354
Глава 7
> пакет для обработки хронологических данных, содержащий процедуры для вычисления размера базы данных, подсчета количества сеансов и оценки использования ключевых ресурсов в базе данных. Полный код этих пакетов можно найти в разделе загрузки на Web-сайте издательства Apress (http: //apress .com).
Пакет для работы сфайлом сообщений В ходе работы сервер Oracle записывает сообщения в файл сообщений (который часто называют журналом сообщений). Обычно журнал сообщений находится в каталоге $0RACLE_BASE/admin/MMH_6a3bi/bdump, но вообще он может находиться где угодно. Его местонахождение задается параметром инициализации BACKGROUND_DUMP_DEST, так что его можно определить с помощью команды SQL> show parameter background_dump_dest NAME
TYPE
VALUE
background_dump_dest
string
C:\oracle\admin\oratest\bdump
Начиная с версии Oracle9/, имя файла сообщений ALERT_SID. LOG на платформах Unix и Windows стандартизовано. Если в этом файле появляется сообщение об ошибке, предполагается, что АБД быстро отреагирует. В частности, записи в файле сообщений могут свидетельствовать о возникновении проблемы, которая может иметь очень серьезные последствия, такие, как сбой базы данных. Своевременный контроль над файлом сообщений позволяет не только выявить проблемы, но и избежать их. В общем случае эти сообщения об ошибках описывают проблемы, влияющие на всю базу данных. Речь идет, например, об ошибках фоновых процессов, внутренних ошибках ORA-600, ошибках, связанных со свободным пространством, повреждениями блоков и т.д. Вот как может выглядеть фрагмент файла сообщений: Fri Jun 27 11:49:53 2003 Thread 1 advanced to log sequence 16386 Current log# 2 seq# 16386 mem# 0: C:\ORACLE\ORADATA\REP9\REDO02.LOG Fri Jun 27 11:49:53 2003 ARC1: Evaluating archive log 1 thread 1 sequence 16385 ARC1: Beginning to archive log 1 thread 1 sequence 16385 Creating archive destination LOG_ARCHIVE_DEST_1: 'C:\ORACLE\ORAARCHIVES\A16385.ARC ARC1: Completed archiving log 1 thread 1 sequence 16385 Fri Jun 27 12:04:02 2003 Errors in file c:\oracle\admin\rep9\udump\rep9_j000_640.trc: ORA-12012: error on auto execute of job 147 ORA-00376: file 8 cannot be read at this time ORA-01110: data file 8: 'C:\ORACLE\ORADATA\REP9\TOOLS01.DBF' ORA-06512: at "SCOTT.ROUTINEJOB", line 5 ORA-06512: at line 1
1
Пакеты АБД
355
Структура пакета Наш пакет для работы с файлом сообщений будет контролировать файл, подсчитывать количество новых сообщений и посылать всем заинтересованным сторонам письмо по электронной почте — текст сообщения об ошибке. Спецификация пакета следующая: SQL> CREATE OR REPLACE PACKAGE ALERT_FILE is 2 /* 4 5 6 7 8
* * * * *
Q
*
Проект: Описание: Влияние на БД: Операторы Commit: Операторы Rollback:
Обработка файла сообщений Обеспечивает мониторинг и управление файлом сообщений читает внешнюю таблицу нет нет
ю v 11 12 13
procedure monitor alert file; end; /
Package created.
Как видите, в пакете есть одна точка входа — процедура MONITOR_ALERT_FILE. Эта общедоступная процедура вызывает несколько приватных процедур в теле пакета: > READ_ALERT_FILE — эта процедура обрабатывает файл сообщений; > UPDATE_SKIP_COUNT — процедура гарантирует, что мы не будем перечитывать старые сообщения из файла; >• RENAME_ALERT_FILE — процедура, которая ежедневно переименовывает файл сообщений; > REVIEW_ALERT_FILE — посылает по электронной почте копию ежедневного файла сообщений соответствующим получателям. Код для всего пакета свободно доступен для загрузки с Web-сайта h t t p : // www. apress. com. В следующих разделах мы рассмотрим код каждой из перечисленных процедур, но сначала надо разобрать структуру файла сообщений, с которым мы будем работать.
Структура файла сообщений Более детально рассмотрев фрагмент файла сообщений, можно увидеть строку, начинающуюся с ORA-12012. Она свидетельствует об ошибке при выполнении задания PL/SQL. Где начинается и где заканчивается сообщение об ошибке? Этот вопрос не существенен, если файл просматривает человек, но он важен для автоматизации процесса поиска и уведомления об ошибках. Понятно, что сообщение начинается на строке, предшествующей строке с текстом ORA-12012. МОЖНО поинтересоваться, сообщения о скольких ошибках содержатся в представленном фрагменте. Очевидно, что только об одной, хотя текст ORA-XXXX повторяется в нем несколько раз.
356
Глава 7
Сообщение об ошибке в файле сообщений всегда содержит как минимум две строки. Оно начинается с одной строки, содержащей отметку даты и времени, например, FRI JUN 27 12:01:04 2003. Сообщение продолжается на одной или нескольких строках и завершается перед следующей отметкой даты/времени или в конце файла. Фактически файл сообщений состоит из последовательности таких сообщений. Это можно выразить формулой Бэкуса-Наура (БНФ) следующим образом: файл_сообщений ::= {строка_даты {строка_сообщения}}
Наша процедура MONITOR_ALERT_FILE должна учитывать структуру файла сообщений. Она должна распознавать многострочные сообщения, чтобы была возможность посылать правильное уведомление. Было бы ошибкой для программы мониторинга посылать пять уведомляющих сообщений (в соответствии с пятью строками фрагмента, которые начинаются с ORA-). БЫЛО бы также неверно не указывать дату сообщения об ошибке и информацию о трассировочном файле, которая также входит в сообщение об ошибке. В итоге фрагмент файла сообщений содержит три сообщения. Есть сообщение, связанное с переключением журнала. Fri Jun 27 11:49:53 2003 Thread 1 advanced to log sequence 16386 Current log# 2 seq# 16386 mem# 0: C:\ORACLE\ORADATA\REP9\REDO02.LOG
Второе сообщение связано с состоянием архивирования неактивного файла журнала повторного выполнения. Fri Jun 27 11:49:53 2003 ARCl: Evaluating archive log 1 thread 1 sequence 16385 ARC1: Beginning to archive log 1 thread 1 sequence 16385 Creating archive destination LOG_ARCHIVE_DEST_1: 'С:\ORACLE\ORAARCHIVES\A16385.ARC' ARCl: Completed archiving log 1 thread 1 sequence 16385
И, наконец, есть сообщение, свидетельствующее, что задание базы данных не сработало, потому что файл был отключен. Fri Jun 27 12:04:02 2003 Errors in file c:\oracle\admin\rep9\udump\rep9_j000_640.trc: ORA-12012: error on auto execute of job 147 ORA-00376: file 8 cannot be read at this time ORA-01110: data file 8: 'C:\ORACLE\ORADATA\REP9\TOOLS01.DBF' ORA-06512: at "SCOTT.ROUTINEJOB", line 5 ORA-06512: at line 1 •
Следует отметить, что корпорация Oracle не публикует спецификацию файла сообщений. У нас нет гарантии, что Oracle согласится с нашим описанием файла. Можно только сказать, что наше описание файла сообщений кажется подходящим, хотя в будущем структура файла может измениться. Примечание В разделе "Проблемы" мы рассмотрим несколько случаев, когда сообщения об ошибках не соответствуют описанной выше структуре или просто не будут записываться в файл сообщений.
Пакеты АБД 357
Файл сообщений как внешняя таблица Простой способ прочитать файл сообщений в PL/SQL — связать его с внешней таблицей, данные которой находятся вне базы данных. Внешние таблицы позволяют запрашивать данные из текстовых файлов. Примечание Внешние таблицы — новая возможность Oracle 9/.
Внешние таблицы полагаются на объект DIRECTORY ДЛЯ задания местонахождения файла, в который мы будем записывать, так что сначала необходимо создать этот объект. Как мы уже видели ранее, каталог задается параметром инициализации BACKGROUND DUMP DEST. SQL> Create or replace directory alert_dir 2 as 'c:\oracle\admin\rep9\bdump'; Directory created.
После создания каталога (как и любого объекта базы данных) мы можем получить его определение из словаря данных с помощью пакета DBMS_METADATA. SQL> s e l e c t d b m s _ m e t a d a t a . g e t _ d d l 2 ('DIRECTORY','ALERT_DIR') 3 from d u a l ; DBMS_METADATA.GET_DDL('DIRECTORY','ALERT_DIR') CREATE OR REPLACE DIRECTORY "ALERT_DIR" AS 'c:\oracle\adrain\rep9\bdump'
Полное описание синтаксиса1 для создания внешней таблицы выходит за рамки нашей книги, но, как видите, он похож на смесь стандартного оператора CREATE TABLE и управляющего файла SQL Loader. CREATE TABLE "ALERT_FILE_EXT" M ( MSG_LINE" VARCHAR2(1000) ) ORGANIZATION EXTERNAL ( TYPE ORACLE_LOADER DEFAULT DIRECTORY "ALERT_DIR" ACCESS PARAMETERS ( RECORDS DELIMITED BY NEWLINE CHARACTERSET US7ASCII nobadfile n o l o g f i l e n o d i s c a r d f i l e skip 0 READSIZE 1048576 FIELDS LDRTRIM REJECT ROWS WITH ALL NULL FIELDS
' Полное описание синтаксиса оператора создания внешних таблиц см. в руководстве "SQL Reference" документации Oracle 9.2.
358
Глава 7 MSG_LINE (1:1000) CHAR(IOOO)
LOCATION ( 'alert_rep9.log'
REJECT LIMIT UNLIMITED /
Мы создали внешнюю таблицу ALERT_FILE_EXT, которая содержит данные из файла операционной системы A L E R T _ R E P 9 . L O G , находящейся в каталоге с: \oracle\admin\rep9\bdump (этот сервер базы данных работает на машине под управлением Microsoft Windows), как указано нашим объектом-каталогом ALERT_DIR. Если понадобится получить определение внешней таблицы, можно использовать пакет DBMS_METADATA. Каждая запись внешней таблицы завершается символом новой строки; максимальный размер поля для внешней таблицы — 1000 символов. Если файл журнала содержит строку длиннее 1000 символов, она не появится в результатах запроса ко внешней таблице (самая длинная строка в файле сообщений, которую мне приходилось видеть, была около 140 символов в длину). Теперь можно запрашивать данные из внешней таблицы так, как из обычной. Например, чтобы увидеть первые девять строк из файла сообщений, можно выполнить запрос: SQL> select * 2 from alert file ext — — 3
where rownum < 10;
MSG_LINE Fri Jun 27 11:49:53 2003 Thread 1 advanced to log sequence 16386 Current log# 2 seq# 16386 mem# 0: C:\ORACLE\ORADATA\REP9\REDO02.LOG Fri Jun 27 11:49:53 2003 ARC1: Evaluating archive log 1 thread 1 sequence 16385 ARC1: Beginning to archive log 1 thread 1 sequence 16385 Creating archive destination LOG_ARCHIVE_DEST_1: 1 С:\ORACLE\ORAARCHIVES\A16385.ARC' ARC1: Completed archiving log 1 thread 1 sequence 16385 Fri Jun 27 12:01:04 2003 9 rows selected.
Мы предполагаем, что при запросе из внешней таблицы без конструкции ORDER BY строки из файла сообщений выбираются последовательно, хотя это и не гарантировано. На техническом форуме Oracle я нашел следующее описание (которое исходило от анонимного члена группы разработчиков ядра Oracle): При ПОСЛЕДОВАТЕЛЬНОМ доступе ко внешней таблице вы получаете записи (которые были выбраны, а не отвергнуты) в том же порядке, в котором они нахо-
Пакеты АБД 359 дятся в текстовом файле. Я (пока) не вижу реального преимущества такого поведения. В отличие от SQL*Loader, где процесс должен вставлять данные вполне определенным образом, внешние таблицы просто предоставляют ДОСТУП ко внешним данным, но не контролируют их последующее использование. Последующее использование определяется SQL-оператором, и фактически любой порядок результатов SQL-оператора гарантируется только указанием соответствующей конструкции ORDER BY... При последовательном доступе мы сейчас сохраняем порядок. При параллельном доступе порядок не сохраняется.
Иными словами, при последовательном запросе ко внешней таблице сегодня строки возвращаются в том же порядке, что и в текстовом файле, но это может измениться. Если корпорация Oracle действительно внесет какие-то изменения в последующие версии, мы все равно сможем предложить решение, гарантирующее, что строки будут возвращаться последовательно, с помощью конвейерной функции для чтения файла сообщений. Как мы уже видели в предыдущих главах, конвейерная функция должна возвращать набор, поэтому создадим набор для хранения строк файла сообщений. SQL> create or replace type varchar2_list 2 as table of varchar2(1000); 3 / Type created.
Теперь создадим функцию для чтения каждой строки из файла сообщений. "Строка" определяется как набор символов между последовательными переводами строк, т.е. символами с ASCII-кодом 10. Таким образом, мы просто открываем файл сообщений как объект типа BFILE И ищем в файле переводы строк, направляя по ходу дела строки по конвейеру. SQL> create or replace 2 function alert_log return varchar2_list pipelined is 3 v_alert_log bfile := bfilename('ALERT_DIR','alert_rep9.log'); 4 v_prev_chrlO number := 1; 5 v"tnis"chrlO number; 6 chrlO raw(4) := utl_raw.cast_to_raw(chr(10)); 7 begin 8 dbms_lob.fileopen( v_alert_log ); 9 loop 10 v_this_chrl0 := dbms_lob.instr( v_alert_log, chrlO, w *v_prev_chrl0, I ) ; 11 exit when (nvl(v_this_chrl0,0) = 0); 12 pipe row ( utl_raw.cast_to_varchar2( 13 dbms_lob.substr( v_alert_log, 14 v_this_chrl015 v_prev_chrl0+l, 16 v_prev_chrlO ) ) ) ;
360
Глава 7 17 18 19 20 21 22
v_prev_chrlO : = v_this_chrlO+l; end loop; dbms_lob.fileclose(v_alert_log); return; end; /
Function created.
Затем мы можем выполнять запросы из функции с помощью знакомой конструкции TABLE для получения содержимого журнала сообщений. Поскольку файл читается нашей функцией последовательно, строки будут идти в нужном порядке. SQL> s e l e c t
* from
table(alert_log);
MSG_LINE F r i Jun 27 11:49:53 2003 Thread 1 advanced to log sequence 16386 Current log# 2 seq# 16386 mem# 0: C:\ORACLE\ORADATA\REP9\REDO02.LOG F r i Jun 27 11:49:53 2003 ARCl: Evaluating a r c h i v e log 1 t h r e a d 1 sequence 16385 ARCl: Beginning t o a r c h i v e log 1 t h r e a d 1 sequence 16385 C r e a t i n g a r c h i v e d e s t i n a t i o n LOG_ARCHIVE_DEST_1: 1 С:\ORACLE\ORAARCHIVES\A16385.ARC' ARCl: Completed a r c h i v i n g log 1 t h r e a d 1 sequence 16385 F r i Jun 27 12:01:04 2003
Таким образом, реализуем ли мы внешнюю таблицу или читаем непосредственно из файла сообщений, обращаясь к нему как к большому внешнему объекту, мы сможем выполнять SQL-запросы к нему, что позволяет перейти к следующему разделу — обработке результатов этого запроса.
Обработка файла сообщений Далее представлена процедура пакета ALERT_FILE, обрабатывающая файл сообщений. Она находит строку с датой и затем выбирает каждое сообщение, пока не найдет следующую строку с датой или больше не останется строк. Как только сообщение выбрано, в нем легко найти подстроку ORA-, чтобы классифицировать его как сообщение об ошибке. procedure read_alert_file
(error_msg_arry out monitor.msgs, linecount out integer)
as — Читает файл сообщений — Сохраняет любые сообщения об ошибках в ассоциативном массиве cursor ci is select msg_line from alert_file_ext; l_buffer varchar2 (1000); l_msg text varchar2(32767) ;
Пакеты АБД 361 error_count binary_integer :=0; . begin -- открыть курсор open cl; —
прочитать строку fetch cl into 1 buffer; while cl%FOUND loop сохранить строку даты l__msg_text := l_buffer; прочитать первую строку текста сообщения fetch cl into l_buffer; while (l_buffer not like '_ _ : :_ __' and cl%FOUND) loop l_msg_text := l_msg_text|Ichr(10)||l_buffer; fetch cl into l_buffer; end loop; проверить, не ошибка ли это if (instr(l_msg_text,'ORA-') > 0) then error__count := error_count + 1; error_msg_arry(error_count) := l_msg_text; end if;
end loop; linecount := cl%ROWCOUNT; close cl; end;
По завершении процедуры наша переменная типа набора (ERROR_MSG_ARRY) будет содержать список сообщений из журнала с отметками даты. Если мы изменим код так, чтобы распечатывать записи в наборе, то увидим следующий результат: Sat Sep 20 23:44:00 2003 alter tablespace example offline Sat Sep 20 23:44:01 2003 Completed: alter tablespace example offline Sat Sep 20 23:50:39 2003 Errors in file c:\oracle\admin\rep9\udump\rep9_j000_1772.trc: ORA-12012: error on auto execute of job 244 ORA-20000: Testing... ORA-06512: at "SCOTT.TEST", line 4 ORA-06512: at line 1 Sat Sep 20 23:50:39 2003 ARC1: Evaluating archive log 1 thread 1 sequence 17474 ARC1: Beginning to archive log 1
362
Глава 7
thread 1 sequence 17474 Creating archive destination LOG_ARCHIVE_DEST_1: •C:\ORACLE\ORAARCHIVES\ARCH_17474.ARC' ARC1: Completed archiving 1
Мы построили решение на языке PL/SQL. Но, как уже видели в главе 1, мы должны задаться вопросом: а нельзя ли решить задачу с помощью чистого языка SQL? Хотя нет одного простого SQL-оператора, который позволил бы нам выбрать отдельное сообщение, при творческом использовании некоторых аналитических функций можно создать представление, строки которого будут соответствовать записям в только что представленном наборе ERROR_MSG_ARRY. — Представление, содержащее сообщения об ошибках из файла create or replace view alert_log_errors as select * from ( select * from ( select lineno, msg_line, thedate, max( case when ora_error like 'ORA-%' then rtrim(substr (ora_error, l,instr (ora_error, ' ')-].),':') else null end ) over (partition by thedate) ora_error from ( select lineno, msg_line, max(thedate) over (order by lineno) thedate, lead(msg_line) over (order by lineno) ora_error from ( select rownum lineno, substr( msg_line, 1, 132 ) msg_line, case when msg_line like ' : : ' then to_date( msg_line, 'Dy Mon DD hh24:mi:ss yyyy' ) else null end thedate from alertfileext
where ora_error is not null order by thedate
Строки в представлении соответствуют сообщениям об ошибках в файле сообщений, хотя мы также применили функцию RTRIM К коду ошибки Oracle, поскольку записи в журнале сообщений о некоторых сообщениях об ошибках (например, ORA-
Пакеты АБД 363 4031) завершаются двоеточием (:). Если в представлении нет строк, значит, в файле сообщений сейчас ошибок нет. Это представление позволяет просматривать файл сообщений различными способами. Можно выполнять запрос по коду ошибки. SQL> select ora_error, msg_line 2 from alert_log_errors 3 where ora error = 'ORA-1652' ORA ERROR
MSG LINE
ORA-1652: ORA-1652:
Tue Sep 23 12:54:54 2003 ORA-1652: unable to extend temp segment by 8 in tablespace TOOLS
Можно также запрашивать по содержимому столбца MSG_LINE ИЛИ ПО времени (столбец THEDATE). SQL> 2 3 4 5
select thedate,msg_line from alert_log_errors where thedate between to_date('21-Sep-03 11:15','dd-mon-yy hh24:mi') and to_date('21-Sep-03 11:30','dd-mon-yy hh24:mi') /
THEDATE
MSG LINE
21-SEP-03
Sun S e p 2 1 1 1 : 1 5 : 0 6 2 0 0 3
21-SEP-03
ORA-000060: Deadlock detected. More info in f i l e •*c:\oracle\admin\dev92
21-SEP-03
При использовании этого представления следует учитывать одну особенность. Если два сообщения в файле имеют одинаковую дату и время, представление смешает оба сообщения, даже если только одно из них является сообщением об ошибке. SQL> select ora_error,msg_line from alert_log_errors; ORA ERROR
MSG LINE
ORA-20000: ORA-20000: ORA-20000: ORA-20000: ORA-20000: ORA-20000: ORA-20000: ORA-20000: ORA-20000: ORA-20000:
Sat Sep 20 23:50:39 2003 Errors in file c:\oracle\admin\rep9\udump\rep9_j000_1772.trc: ORA-12012: error on auto execute of job 244 ORA-20000: Testing... ORA-06512: at "SCOTT.TEST", line 4 ORA-06512: at line 1 Sat Sep 20 23:50:39 2003 ARC1: Evaluating archive log 1 thread 1 sequence 17474 ARC1: Beginning to archive log 1 thread 1 sequence 17474 Creating archive destination LOG_ARCHIVE_DEST_1: 'С:\ORACLE\ORAARCHIVES\ARCH_17474.ARC' ORA-20000: ARC1: Completed archiving log 1 thread 1 sequence 17474
Как мы уже говорили в главе 1, при запросах к большому журналу сообщений это представление может обеспечивать более высокую производительность по сравне-
364
Глава 7
нию с PL/SQL-решением. Однако когда файл сообщений становится очень большим, у вас, вероятно, появятся проблемы поважнее, чем производительность представления. Прежде всего надо разобраться, откуда взялись эти ошибки. В оставшейся части данного раздела мы будем использовать решение на базе PL/SQL — это ведь книга по языку PL/SQL. Поэтому мы легко сможем расширить код для удовлетворения любых возникающих потребностей. Например, если в какой-то момент мы захотим контролировать сообщения, не начинающиеся с ORA- (допустим, ошибки CHECKPOINT NOT COMPLETE), очень легко будет добавить эту возможность к процедуре на языке PL/SQL. Тем не менее, мы хотели снова продемонстрировать, как SQL-операторы позволяют весьма элегантно реализовывать достаточно сложный алгоритм.
Исключительные ситуации Как в процедурной программе, так и при запросах к представлению могут возникнуть ошибки. Если файл сообщений находится не там, где выдумаете, вы получите сообщение об ошибке следующего вида: SQL> select * 2 from alert_file_ext; select * ERROR at line 1: ORA-29913: error in executing ODCIEXTTABLEOPEN callout ORA-29400: data cartridge error KUP-04040: file alert_rep9.log in ALERT_DIR not found ORA-065I2: at "SYS.ORACLE_LOADER", line 14 ORA-06512: at line 1
Файл не найден в указанном каталоге. Правильная реакция на эту ошибку зависит от ситуации. В стабильной среде, где файл сообщений часто "прокручивается" (файл сообщений переименовывается и фактически при этом удаляется; подробнее об этом — чуть позже), может быть короткий период времени, когда файла сообщений не будет. Суть в том, что временное отсутствие файла сообщений вполне допустимо. С другой стороны, в большой базе данных с несколькими АБД или с недалеким АБД, возможно, кто-то перенесет файл сообщений, и наша программа будет искать "не в том" каталоге. Это, несомненно, проблема, и наша программа контроля файла должна выдать сообщение об ошибке. Можно различить эти ситуации, сравнивая значение каталога для дампов фоновых процессов (реальное местонахождение файла сообщений) с местонахождением (полным именем каталога) объекта DIRECTORY, который мы создали для указания местонахождения внешней таблицы. — проверяем местонахождение файла сообщений s e l e c t count(*) from v$parameter where name = 'background_dump_dest' and value = (select directory_path
Пакеты АБД
365
from dba_directories where directory_name = 'ALERT_DIR') Если
местонахождение, описываемое параметром инициализации совпадает с задаваемым объектом ALERT_DIR, запрос вернет значение 0. Кто-то перенес файл сообщений, поэтому должно появиться сообщение об ошибке. С другой стороны, если каталоги совпадают, мы знаем, что ищем файл в соответствующем месте. В процедуре MONITOR_ALERT_FILE, которую мы представим немного позже (она также доступна в файле с кодом на сайте), вы увидите, что мы объединили этот запрос с запросом, который выбирает имя экземпляра. Вместо подсчета количества строк мы будем использовать обработчик исключительной ситуации NO_DATA_FOUND, которая возбуждается, когда объект-каталог указывает не на тот каталог. BACKGROUND__DUMP_DEST, не
—
проверяем местонахождение файла сообщений select instance_name into l_instance_name from v$parameter,v$instance,dba_directories where directory_name = 'ALERT_DIR' and name='background_dump_dest' and value = directory_path; — здесь идет другой код exception when no_data_found then raise_application_error(-20000,'ALERT_DIR is not current 1 );
В ОС Windows вы потенциально можете создать каталог для дампов фоновых процессов по имени "D:\oracle.. ." и объект-каталог " d : \ o r a c l e . . .". При сравнении этих двух имен возникнет проблема, поскольку буквы набраны в разных регистрах. Лучший способ избежать этой ситуации — быть последовательным при именовании каталогов. Однако можно изменить запрос так, чтобы регистр символов игнорировался. s e l e c t count{*) from v$parameter where name » 'background_dump_dest' and upper(value) = (select upper(directoryjpath) from dba_directories where directory_name = 'ALERT_DIR') Процедура также подвержена другой проблеме: она собирает сообщения, добавляя последовательные строки в буфер, определенный как: l_msg_text varchar2(32767);
Если размер собранного сообщения превысит размер буфера, при конкатенации будет возбуждена исключительная ситуация. ORA-20000: ORA-06502: PL/SQL: numeric or value error ORA-06512: at "DBMON.ALERT_FILE", line 174 ORA-06512: at line 2
366
Глава 7
Но это весьма маловероятно. Практически все сообщения в файле сообщений состоят из одной-двух строк. Сообщения об ошибках могут состоять из нескольких строк, но их длина все равно составляет несколько сотен символов. Самое длинное сообщение, которое мне приходилось видеть, выдается при запуске сервера При этом сервер выдает все параметры инициализации с нестандартными значениями как одно длинное сообщение, но и оно намного короче 32767 символов. Если вы столкнетесь с этой проблемой, надо будет урезать сообщение об ошибке. Мы включили соответствующую проверку в наш код. — задать верхний предел длины сообщения if length(l_msg_text)+length(l_buffer) < 32765 then l_msg_text := l_msg_text||chr(10)||l_buffer; else l_msg_text := substr(l_msg_text,1,length(l_msg_text)) I Ichr(10) I I 1 * * Message truncated'; end if;
Жизненный цикл уведомления В текущем виде наш процедурный код читает все строки во внешней таблице. Большая часть этих строк — информационные сообщения, не связанные с ошибками. В сообщения об ошибках входит строка ORA-. if instr(l_msg_text,'ORA-') > 0 then error_count_out := error_count_out + 1; end if;
При выявлении сообщения об ошибке в файле сообщений мы будем уведомлять АБД. Однако после посылки этого уведомления не хотелось бы повторно сообщать о той же ошибке — мы должны посылать сообщения только о тех ошибках, которые произошли с момента последней проверки файла. Поэтому уведомлять АБД нужно лишь при наличии изменений в файле сообщений, т.е. если после прошлого запуска программы в файл было записано новое сообщение об ошибке. Для этого нужно изменить определение внешней таблицы. Если файл сообщений, например, был длиной 1000 строк при последней проверке, в следующий раз мы хотим начать со строки 1001. Этого можно добиться, изменяя значение SKIP ДЛЯ внешней таблицы. SQL> select access_parameters 2 from user_external_tabies 3 where table_name - 'ALERT_FILE_EXT'; ACCESS
PARAMETERS
RECORDS DELIMITED BY NEWLINE CHARACTERSET nobadfile nologfile nodiscardfile skip 0 READSIZE 1048576 FIELDS LDRTRIM REJECT ROWS WITH ALL NULL FIELDS
US7ASCII
Пакеты АБД 367 MSG_LINE (1:1000) CHAR(IOOO) )
Значение ACCESSPARAMETERS внешней таблицы указывает, что мы пропускаем ноль записей перед чтением файла сообщений. Так, мы создали внешнюю таблицу первоначально. Но после чтения внешнего файла (а мы будем читать его приблизительно один раз в десять минут) мы хотели бы изменить количество пропускаемых записей, чтобы начинать читать с того места, где закончили в последний раз. procedure update_skip_count(p_count in number default 0, r e s e t boolean default false) as — изменить параметры доступа к внешней таблице i number; j number; adj number := p_count; begin for x in (select replace(access_parameters,chr(10)) param from user_external_tables where table_name = 'ALERT_FILE_EXT') loop i := owa_pattern.amatch(x.param,1,'.*skip',i); j := owa_pattern.amatch(x.param,1,'.*skip \ d * ' ) ; для сброса счетчика (в ноль) if r e s e t then adj := -1 * to_number(substr(x.param,i,j-i)); end if; execute immediate ' a l t e r table alert_file_ext access parameters substr (x.param, 1, i) | | (to_number(substr(x.param,i,j-i))+ a d j ) | | substr(x.param,j)||')';
('II
end loop; end;
Мы используем PL/SQL-пакет сопоставления с образцом, OWA_PATTERN, вместо того чтобы пытаться решить задачу с помощью комбинации функций INSTR И SUBSTR. Функция АМАТСН возвращает позицию во входной строке, где завершается фрагмент, соответствующий образцу. Два последовательных вызова функции АМАТСН дают нам начальную и завершающую позиции параметра SKIP nn. Мы обновляем количество пропускаемых строк, а затем изменяем определение внешней таблицы. Интересно, что подобное изменение внешней таблицы не делает недействительными зависящие от нее объекты.
Прокрутка файла сообщений Несколько лет назад я работал со службой поддержки Oracle над одной проблемой, когда они попросили меня проверить файл сообщений. Я попытался открыть файл, пока представитель службы поддержки ждал "на телефоне". Я не знал точно,
368
Глава 7
насколько большим был файл сообщений, но он оказался размером в несколько сотен мегабайтов. Я не помню, какой редактор использовал (и в какой ОС работал), но помню, как пришлось ждать, пока редактор медленно загружал содержимое (райла сообщений в свой буфер, а примерно через 15 минут то ли произошел сбой сервера Oracle, то ли служба поддержки переключила мое внимание на что-то другое. Сервер Oracle постоянно добавляет информацию в файл сообщений. Если не трогать этот файл, он будет постоянно расти. Очень большой файл занимает дисковое пространство, которое можно было бы использовать для других целей. Это приводит, вероятно, к тому, что операции записи замедляются, и файл практически невозможно открыть в некоторых редакторах. Несомненно, не следует допускать, чтобы файл сообщений увеличивался настолько, что его невозможно было бьг открыть. Чтобы избежать такой ситуации, можно периодически удалять файл. Но лучше использовать более систематический подход к проблеме и переименовывать файл сообщений в PL/SQL-процедуре. Вот листинг каталога дампа фоновых процессов, который управляется заданием PL/SQL: 06/26/2003 06/27/2003 06/28/2003 06/29/2003 06/30/2003 07/01/2003 07/02/2003 07/02/2003
05:45р 05:27а 05:13а 05:59а 05:05а 05:16а 05:15а 09:56а
32,345 6,673 5,207 10,787 5,551 5,551 20,910 1,281
alert_rep9Wed.log alert_rep9Thu.log alert_rep9Fri.log alert_rep9Sat.log alert_rep9Sun.log alert_rep9Mon.log alert_rep9Tue.log alert_rep9.log
Вы видите набор файлов сообщений за неделю. Завтра утром задание на языке PL/SQL переименует текущий файл ALERT_REP9.LOG В ALERT_REP9WED.LOG И перепишет старый файл с тем же именем. Мы используем процедуру переименования файла FRENAME стандартного PL/SQLпакета UTL_FILE. Процедура FRENAME принимает в качестве параметров имя исходного каталога, имя исходного файла, имя целевого каталога и имя целевого файла. Мы переименовываем файл сообщений ALERT_SID.LOG, где значение вычисляется как SYSDATE - 1. Мы задаем местонахождение файла с помощью уже созданного объекта-каталога. Procedure rename_alert_file (l_instance_name in varchar2) as - - Предположим, журнал сообщений называется alert_OK3EMIUIHP>. log - - Переименуем его в а1ег1;_День. log alert_file_does_not_exist EXCEPTION; PRAGMA exception_init(alert_file_does_not_exist, -29283); begin - - переименовать файл сообщений utl_file.frename ( src_location => 'ALERT_DIR', src_filename ~> ' a l e r t _ ' | I l _ i n s t a n c e _ n a m e | | ' . l o g ' , dest location => 'ALERT DIR',
Пакеты АБД
369
dest_filename «> 'alert_'||l_instance_nameI I to_char(sysdate - 1,'Dy')| Г . l o g ' , overwrite => true); exception when alert_file_does_not_exist then null; end;
При прокрутке файла сообщений таким образом надо не забыть сбросить параметры доступа внешней таблицы (количество пропускаемых строк). Есть еще одна причина ежедневной прокрутки журнальных файлов. Многие АБД привыкли регулярно просматривать весь файл сообщений (не только ошибки в нем). Эту задачу намного проще планировать и решать, если файл сообщений не слишком большой. Мы рассмотрим это далее в текущей главе, при обсуждении процедуры REVIEW_ALERT_FILE.
Конечно, не в каждой организации необходимо прокручивать файл сообщений ежедневно. Может потребоваться недельный или месячный цикл. В некоторых случаях, даже если переименовывать файл каждый день, может потребоваться именовать его по другим принципам. Значит ли это, что мы не можем использовать PL/SQL-решение? Конечно, нет. За счет небольшого усовершенствования мы можем создать более универсальную процедуру поддержки файла сообщений, представленную далее. create or replace procedure rename_alert_file (l_instance_name in varchar2, p_format_mask varchar2 default 'DD', p_filename_mask varchar2 default 'Dy') as alert_file_does_not_exist EXCEPTION; PRAGMA exception_init(alert_file_does_not_exist, -29283); v_prev_date date := trunc(sysdate,p_format_mask); begin utl_file.frename ( src_location => 'ALERT_DIR', src_filename => 'alert_'||l_instance_name||'.log1, dest_location => 'ALERT_DIR', dest_filename => 'alert_'|Il_instance_name|| to_char(v_prev_date - l,p_filename_mask)||'.log', overwrite => true); exception when alert_file_does_not_exist then null; end;
Если эта процедура вызывается только с именем версии, как и в предыдущей версии, файл сообщений будет переименован в ALERT_SID . LOG, как раньше. Однако два новых параметра, P_FORMAT_MASK И P_FILENAME_MASK, МОЖНО использовать для достижения намного большей гибкости, как показано в таблице 7.1. С помощью маски формата можно создать задание для переименования файла сообще-
370
Глава 7
ний в соответствии с вашими требованиями. Например, если вы хотите сохранять информацию журнала сообщений за неделю, используйте маску формата iw и запускайте процесс раз в неделю. Таблица 7.1. Варианты именования хронологии файла сообщений PFORMATMASK
PFILENAMEMASK
Желаемое влияние на файл сообщений, сохраняемый (например) 4 августа
DD
DY
Файл сообщений прокручивается ежедневно: сегодняшний файл получит имя A L E R T _ S I D _ M O N . L O G , завтрашний —
ALERT_SID_TUE.LOG. Таким образом, файлы переписываются через семь дней. DD
MMDD
Файл сообщений прокручивается ежедневно: сегодняшний файл получит имя ALERT_SID_0804.LOG, завтрашний — A L E R T _ S I D _ 0 8 0 5 . L O G . Таким образом, файлы переписываются через 365 дней.
ММ
ММ
Файл сообщений прокручивается ежемесячно: сегодняшний файл получит имя A L E R T _ S I D _ 0 8 . L O G , файл сообщений за следующий месяц — A L E R T _ S I D _ 0 9 . L O G . Таким образом, файлы переписываются через 12 месяцев.
IW
YYYVMMDD
Файл сообщений прокручивается еженедельно: сегодняшний файл получит имя ALERT_SID_20030804.LOG, завтрашний — ALERT_SID_20030811.LOG. Таким образом, файлы не переписываются никогда.
Прежде чем продолжать дальше, следует заметить: если в вашей системе принципы работы отличаются и используется большой файл сообщений, то применение средств множественной выборки, описанных в главе 4, позволит повысить производительность.
Планирование и одновременный доступ Процедура MONITOR_ALERT_FILE ("задание мониторинга") срабатывает в очереди заданий PL/SQL с 10- или 15-минутным интервалом. Процедура RENAME_ALERT_FILE ("задание прокрутки") срабатывает раз в день. Надо проследить, чтобы эти задания не конфликтовали. Рассмотрим следующий сценарий: в 5:55 утра фоновый процесс сервера записывает сообщение об ошибке в файл сообщений. В 6:00 утра задание прокрутки срабатывает и переносит файл сообщений в ALERT . LOG. В 6:01 утра срабатывает задание мониторинга, но оно не видит сообщения об ошибке. Оно, вероятно, не найдет файл сообщений. Если новый файл сообщений и найдется, он не будет содержать сообщение, записанное в 5:55. Можно использовать таблицу для записи времени срабатывания каждой из процедур, чтобы контролировать их выполнение, или использовать пакет DBMS_LOCK для обеспечения последовательного доступа к файлу сообщений. Однако более простым способом решения проблемы будет интеграция программы прокрутки в процедуру мониторинга файла сообщений вместо составления для нее отдельного расписания.
Пакеты АБД 371 Как только мы проверили наличие новых ошибок в файле сообщений, выполняем следующую проверку: — Если сейчас от 6 до 6:20 утра, переименовываем файл сообщений if to_char(sysdate,'hh24')= '06' and to_char(sydate,'mi') < '20' then rename_alert_file; end if;
Чтобы гарантировать, что мы не прокрутим файл сообщений дважды в один день, мы также проверяем минимальный размер файла перед вызовом процедуры переименования. Конечно, если вы решили запускать процедуру мониторинга файла сообщений по расписанию с помощью пакета DBMS_JOB, МОЖНО просто обеспечить выполнение только один раз в день путем запроса к представлению USER_JOBS ДЛЯ определения времени последнего выполнения процесса. Ниже представлена процедура MONITOR_ALERT_FILE. Она должна объединить вместе все, что мы рассмотрели до сих пор. (Обратите внимание, что в коде используется процедура уведомления, которую мы еще не рассмотрели, но вскоре рассмотрим.) procedure monitor_alert_file as error_msg_arry monitor.msgs; lcount integer; l_instance_name varchar2(16); exists_p boolean; flength number; bsize number; alert_file_does_not_exist exception; PRAGMA EXCEPTION_INIT(alert_file_does_not_exist, -29913); begin — Проверить местонахождение файла сообщений select instance_name into l_instance_name from v$parameter,v$instance,dba_directories where directory_name = 'ALERT_DIR' and name='background dump dest' and value = directory_path; —
Проконтролировать файл сообщений и сохранить информацию об ошибках read_alert_file(error_msg_arry,lcount);
—
Изменить параметры доступа к внешней таблице update_skip_count(lcount);
—
Послать любые обнаруженные сообщения об ошибках if error_msg_arry.last > 0 then monitor.notify(l_instance_name, error_msg_arry, 'Alert file error messages'); end if;
372
Глава 7
—
Не пора ли переименовывать файл сообщений? if to_char(sysdate,'hh24')= '06' and to_char(sysdate,'mi') < '19' then Проверить размер файла сообщений utl_file.fgetattr ( location => 'ALERT_DIR', filename => 'alert_'| |l_instance_nameI I'.log1, fexists => exists_p, file_length => flength, block size => bsize);
Переименовать файл сообщений if flength > 3000 then update_skip_count(reset=>true); review_alert_file(l_instance_name); rename_alert_file(l_instance_name); end if; end if; exception when alert_file_does_not_exist then null; when no_data_found then raise_applicaticn_error(-20000,'ALERT_DIR is not current'); end;
Проблемы при использовании файла сообщений Мы уже очень близки к созданию полной системы мониторинга журнала сообщений Oracle; несомненно, она справится с подавляющим большинством задач обработки файла сообщений, с которыми вы можете столкнуться. К сожалению, есть несколько случаев, когда структура записываемых сообщений об ошибках создает трудности или когда сообщения об ошибках вообще не записываются в файл сообщений.
Ошибки ORA-01S55 Начиная с версии 9/, сервер Oracle записывает сообщения об ошибках ORA-01555 ("snapshot too old") в файл сообщений, как показывает следующий фрагмент файла: Mon Sep 29 13:18:32 2003 ORA-01555 caused by SQL statement below (Query Duration=0 sec, SCN: 0x0000.0083568e): Mon Sep 29 13:18:32 2003 select empno,ename, sal from emp as of timestamp (SYSTIMESTAMP - INTERVAL 4 ' HOUR)
В соответствии с нашим определением сообщения, этот фрагмент содержит два сообщения: первое описывает ошибку, ORA-01555, а второе содержит текст SQLоператора. Но при проверке оказывается, что второе сообщение зависит от первого
Пакеты АБД 373 и в идеале должно быть его частью. Проблема состоит в том, что наша программа контроля ошибок перехватит первое сообщение, но проигнорирует второе. В результате программа уведомит АБД об ошибке ORA-01555, НО не включит в уведомление SQL-оператор. Достаточно немного изменить PL/SQL-код процедуры MONITOR_ALERT_FILE, чтобы поддержать и такого рода сложные сообщения. При выявлении ошибки ORA-01555 мы просто устанавливаем флаг, показывающий, что следующее вхождение строки с датой надо игнорировать. Тот факт, что в Oracle не совсем "строго" соблюдается структура журнала сообщений, означает, что, к сожалению, наш код становится несколько менее элегантным. Однако он решает проблему. Измененный код имеет следующий вид (изменения выделены полужирным): procedure read_alert_file (error_msg_arry out monitor.msgs, linecount out integer) as — Прочитать файл сообщений -- Сохранить любые сообщения об ошибках в ассоциативном массиве cursor cl is select msg line — from alert_file_ext; l_buffer varchar2(1000); l_msg_text varchar2(32767); error_count binary_integer :=0; date_to_be_skipped boolean := false; begin — открыть курсор open cl; —
—
прочитать строку наперед fetch cl into l_buffer; while cl%FOUND loop сохранить строку даты l_msg_text := l_buffer; прочитать первую строку тела сообщения fetch cl into l_buffer; loop exit when ( ( l_buffer like ' : and not date_to_be_skipped ) or cl%NOTFOUND ) ;
:
'
l_msg_text := l_msg_text||chr(10)||l_buffer; fetch cl into l_buffer; date_to_be_skipped := ( instr(l_msg_text,'ORA-01555') > 0 ) and not ( date_to_be_skipped and l_buffer like ' : : ') ; end loop;
374
Глава 7
—
проверить, не ошибка ли это if (instr(l_msg_text,'ORA-') > 0) then error_count := error_count + 1; error_msg_arry(error_count) : = l_msg_text; end if;
end loop; linecount : = cl%ROWCOUNT; close cl; end;
Ошибки, сообщения о которых не записываются Есть несколько случаев, когда об ошибке или проблеме вообще ничего не записывается в файл сообщений. Вот один пример из трассировочного файла процессамонитора очереди (QMNO): *** 2003-09-21 00:10:04.000 *** SESSION ID:(22.13) 2003-09-21 00:10:04.000 kwqicaclcur: Error 376 Cursor Session Number : 21 Cursor Session Serial : 63 Cursor Pin Number : 10 Error 37 6 in Queue Table QS.QS_ORDERS_SQTAB kwqitmmsgs: error 604 error 604 detected in background process OPIRIP: Uncaught error 447. Error stack: ORA-00447: fatal error in background process ORA-00604: error occurred at recursive SQL level 1 ORA-00376: file 5 cannot be read at this time ORA-01110: data file 5: 'C:\ORACLE\ORADATA\DEV92\EXAMPLE01.DBF' Dump file c:\oracle\admin\dev92\bdump\dev92_qmnO_1116.trc
Проблема в этом выдуманном примере состоит в том, что необязательный фоновый процесс Oracle Advanced Queuing потерпел сбой. Сообщение об ошибке (ORA00376) показывает, что, вероятнее всего, табличное пространство отключено. Но эти сообщения об ошибках не появляются в файле сообщений; они есть только в соответствующем трассировочном файле процесса-монитора очереди. Примечание Трассировочные файлы (как и файл сообщений) записываются в каталог дампа для фоновых процессов. Они могут именоваться по-разному, в зависимости от того, какой процесс Oracle записывает трассировочную информацию. Часто фоновый или серверный процесс Oracle, записывающий в трассировочный файл, также вносит сообщение в файл сообщений, но, как показывает этот пример, бывают и исключения.
Есть ситуации, которые не считаются ошибками, хотя мы знаем, что они свидетельствуют о проблемах, которые можно устранить. Рассмотрим, например, такое сообщение в файле: Fri Sep 26 14:39:22 2003 Thread 1 cannot allocate new log, sequence 17746
Пакеты АБД 375 Checkpoint not complete Current log* 2 seq# 17745 meral 0: C:\ORACLE\ORADATA\REP9\REDO02.LOG
Сообщение CHECKPOINT NOT COMPLETE не считается ошибкой. С ним не связан код ошибки ORA-. Наша программа контроля ошибок в текущем виде не заметит его, хотя мы можем легко изменить проверку в коде if (instr(l_msg_text, 'ORA-') > 0) then error_count := error_count + 1; error_msg_arry(error_count) := l_msg_text; end if;
на if (instr(l_msg_text, 'ORA-') > 0)
or
(instr(l_msg_text, 'Checkpoint not complete') > 0) error_count := error_count + 1; error_msg_arry(error_count) := l_msg_text; end if;
then
Наконец, есть ошибки, которые сервер Oracle не считает достаточно серьезными, чтобы включать их в файл сообщений или в трассировочный файл. (Ошибка ORA1555: SNAPSHOT TOO OLD относилась к этой категории в прежних версиях Oracle. Сейчас она регистрируется, как обсуждалось ранее.) При выявлении непригодного к использованию индекса, например, ошибка возникает только в сеансе. SQL> select * 2 from emp_history 3 where empno = 7934; select * ERROR at line 1: ORA-01502: index 'SCOTT.EMPHIX' or partition of such index is in unusable state
Мы не хотели бы слишком далеко отходить от нашей темы контроля файла сообщений, но даже ошибки этого типа можно перехватывать с помощью триггера на системное событие ошибки сервера.
Просмотр содержимого файла сообщений Помимо получения информации об ошибках, явно указанных в журнале сообщений, многие администраторы баз данных проверяют все содержимое файлов сообщений каждое утро. Возможно, в файле сообщений обнаружится нечто интересное, достойное внимания, или выявится некая тенденция, свидетельствующая о наступающей проблеме. Наше автоматизированное решение для контроля помогает такому просмотру тем, что файл сообщений ежедневно переименовывается. Но можно сделать еще больше. Мы можем частично автоматизировать процесс просмотра, посылая копию ежедневного файла сообщений по электронной почте или генерируя ежедневный отчет и помещая его в доступное место. Код для обеспечения просмотра файла сообще-
376
Глава 7
ний путем посылки его по электронной почте очень прост (и тоже использует пакет уведомления, который мы рассмотрим в следующем разделе). -- Посыпка содержимого файла сообщений по электронной почте procedure review_alert_file ( p_instance_name_in in varchar2) is alert_msg_arry notification.msgs; begin select msg_line bulk collect into alert_msg_arry from alert file ext; — — notification.notify(instance_name_in => p_instance_name_m, msgs_in => alert_msg_arry, subject_in => 'Review Alert File', email_p «•> true, db_p => false); end;
Файл сообщений: итоги Файл сообщений Oracle требует особого внимания. Мы просматриваем этот файл в поисках ошибок сервера Oracle и для получения общего представления о состоянии базы данных. Периодически мы будем удалять или переименовывать файл, чтобы сэкономить дисковое пространство. В этом разделе мы продемонстрировали, как PL/SQL-пакет может автоматизировать решение этих задач. Пакет для работы с файлом сообщений позволяет контролировать появление сообщений об ошибках в этом файле. Этот пакет посылает сообщения по электронной почте, когда выявляет новые сообщения об ошибках, а также посылает вам все содержимое файла сообщений (или его начало, если файл слишком большой) каждое утро, чтобы упростить просмотр содержимого данного файла. Администраторам баз данных все больше необходимы стандартизированные процедуры, упрощающие администрирование. Управление файлом сообщений — важный шаг в этом направлении.
Пакет уведомления Используемый нами пакет уведомления содержит две процедуры: одну — для посылки сообщений, другую — для сохранения информации в таблице базы данных. Без пакета уведомления не удалось бы создать систему мониторинга. Мы не смогли бы проинформировать АБД о новой ошибке в файле сообщений или о выполнении любого другого условия, которое мы проверяем. Наш клиент электронной почты, по сути, является централизованной консолью — именно в нем мы будем получать сообщения от всех наших сценариев контроля. Процедура посылки электронной почты помещена в отдельный пакет, а не включена в пакет для работы с файлом сообщений, который мы только что рассмотрели.
Пакеты АБД 377 Это более нормализованный подход. Алгоритмы работы с файлом сообщений и алгоритмы посылки сообщений по электронной почте не зависят друг от друга. Некоторую информацию о системе мы хотели бы собирать и сохранять долго. В пакете уведомления есть процедура SAVE_IN_DB, которая позволяет это сделать. Вот как выглядит спецификация пакета уведомления: SQL> 2 3 4 5 6 7 8 9 10 11 12 13 14
create or replace package notification is type msgs is table of varchar2(4000) index by binary_integer; type recipients is table of varchar2(255); procedure notify (instance_name_in in varchar2, msgs_in in msgs, subject_in in varchar2 default null, result_in in number default null, email_p in boolean, db_p in boolean, recip recipients default null); end; /
Package created.
Список адресатов электронной почты передается как набор типа RECEPIENTS, a содержимое сообщения строится из массива строк, переданных как набор типа MSGS. Два булевых входных параметра в общедоступной процедуре NOTIFY используются для определения того, надо ли посылать сообщение по электронной почте и/или сохранять его в базе данных. procedure notify (instance_name_in in varchar2, msgs_in in msgs, subject_in in varchar2 default null, result_in in number default null, email_p in boolean, db_p in boolean, recip recipients default null); is begin if email_p = true then send_email (instance_name_in, msgs_in, subject in, recip); end if; if db_p = true then save_in_db ( instance_name_in, msgs_in, subject_in, result_in); end if; end;
Процедура SEND_EMAIL Стандартный пакет UTL_SMTP разработан для посылки электронной почты по протоколу Simple Mail Transfer Protocol. Мы передаем содержание сообщения в массиве. Мы также передаем процедуре посылки электронной почты имя экземпляра и
378
Глава 7
тему сообщения как входные параметры. За счет использования HTML для форматирования сообщения его можно выдавать в более удобном для восприятия виде. Есть ограничение на максимальную длину текста, выдаваемого процедурой UTL_SMTP.WRITE_DATA, — приблизительно 1000 символов, так что размеры наших сообщений должны этому ограничению соответствовать (или надо разбить их на меньшие части). Мы просто задаем список адресатов (первый адресат — отправитель сообщения). Вот процедура SEND_EMAIL: — Посылка сообщения по SMTP procedure send_email (instance_name_in varchar2, msgs_in msgs, subject_in varchar2, recip recipients) is sender
constant varchar2(60) := '
[email protected]';
smtp_server constant varchar2(255) := 'SERVER100'; local_domain constant varchar2(255) := 'foo.com'; — Форматируем дату сообщения электронной почты lvDate varchar2(30) := to_char(sysdate,'mm/dd/yyyy hh24:mi'); lvBody varchar2(32000); с utl_smtp.connection; recipient_list varchar2(1000) := sender; — Локальная процедура для уменьшения избыточности procedure write_header (name in varchar2, header in varchar2) is begin utl_smtp.write_data(c, name |I ': ' |I header I| utl_tcp.CRLF); end; begin for i in 1 .. recip.count loop recipient_list := ','| | recip (i) ; end loop; — Подключаемся по протоколу SMTP с := utl_smtp.open_connection(smtp_server); — Выполняем обмен начальными сообщениями с сервером SMTP после — подключения utl_smtp.helo(с, local_domain ); — Инициируем почтовую транзакцию utl_smtp.mail(с, sender); — Задаем адресатов utl_smtp.rcpt(с, sender); for i in 1 .. recip.count loop utl_smtp.rcpt(c, recip(i)); end loop; — Посылаем команду DATA серверу SMTP utl_smtp.open_data(c) ;
Пакеты АБД 379 -- Выдаем заголовок сообщения write_header('Date',lvDate); 1 write_header('From ,sender); write_header('Subject',instance_name_in| |' 'IIsubj ect_in); write_header('To',recipient_list); write_header('Content-Type', 'text/html;'); — Форматируем тело сообщения с помощью HTML lvbody := ' BODY, P, li, {font-family: courier-new,courier; font-size : 8pt;} '; — Тело сообщения состоит из массива входных сообщений for i in 1.. msgs_in.last loop lvbody := lvbodyI|''|| replace(msgs_in(i),chr(10),''); end loop; lvbody := lvbody||''; — Вьщаем тело сообщения -- Пишем не более 1000 символов за раз for х in 1 .. (length(lvbody)/800 + 1) loop utl_smtp.write_data(c, utl_tcp.CRLF || substrdvBody, (x-l)*800 +1,800)); end loop; — Завершаем сообщение электронной почты utl_smtp.close_data(с) ; — Отключаемся от сервера SMTP utl_smtp.quit(с); exception when others then utl_smtp.quit(с); raise; end;
Сохранение сообщений в базе данных Пакет уведомления также включает процедуру для сохранения сообщений в базе данных. Как мы уже говорили, наш пакет для работы с файлом сообщений позволяет сохранять файлы сообщений, скажем, неделю, прежде чем они будут переписаны. Однако для целей отчетности может понадобиться сохранять некоторые данные дольше. (Например, вас могут попросить сообщать о количестве ошибок сервера за месяц, произошедших из-за увольнения ведущего АБД.) Это будет возможно, только если мы сохраним сообщения об ошибках. Создадим для этого следующую таблицу: CREATE TABLE ALERTS ( EVENT_DATE DATE, EVENT_TYPE VARCHAR2(16) NOT NULL ENABLE,
380
Глава 7 RESULT NUMBER, RESULTJTEXT VARCHAR2(4000), INSTANCE_NAME VARCHAR2(16), CONSTRAINT EVENT_TYPE_IN CHECK (event_type in ('ALERT FILE', 1 BACKUPS', 'STORAGE','ARCHIVE', 'GROWTH', 'SESSIONS', 'RESOURCE LIMIT')) ENABLE );
Процедура SAVE_IN_DB предполагает, что каждое сообщение начинается со строки, которая является датой в определенном формате. Это соглашение создано по аналогии с сообщениями в файле сообщений, и этому стандарту полезно следовать. Мы также позволяем классифицировать сообщения пчо полю темы в таблице, которое называется EVENT_TYPE. Текст процедуры представлен далее. Поскольку мы можем создавать сразу несколько записей в таблице, для обеспечения оптимальной производительности использованы средства множественного связывания, описанные в главе 4. - - Сохранение уведомляющих сообщений в базе данных procedure save_in_db(instance_in varchar2, msgs_in msgs, subject_in varchar2, result_in number) i s begin forall i in 1.. msgs_in.last insert into a l e r t s (event_date, instance__name, event_type, result, result_text) values (to_date(substr(msgs_in(i),1,24), 'Dy Mon dd hh24:mi:ssyyyy'), instance_in,subject_in, result_in,msgs_in(i)); end;
Вот пример сохраненного сообщения. Это сообщение не относится к сообщениям об ошибках ORA- из файла сообщений. Оно сгенерировано процедурой презентивного контроля дискового пространства (по имени CHECK_FREE_SPACE), которая представлена в следующем разделе. SQL> select instance_name,event_date,result_text 2 from alerts 3 where event_type = 'STORAGE'; INSTANCE
EVENT_DATE
RESULTJTEXT
TOTO
04-JUL-03
Fri Jul 04 13:40:35 2003 Tablespace TOOLS is 80.4% full.
Уведомления: итоги Мы описали пакет для уведомления, состоящий из двух процедур: одной — для посылки сообщений по электронной почте, другой — для сохранения информации в базе данных. Эти процедуры соответствуют двум типам уведомлений: одни требу-
Пакеты АБД
381
ют быстрого реагирования, а на другие вообще не нужно отвечать. Сохраненные данные могут использоваться для создания отчетов о ходе сопровождения. В процедуре посылки электронной почты нам пришлось явно указать ряд значений: имя почтового сервера и т.п. Если предполагается использовать это решение в масштабах предприятия, имеет смысл сохранить эти значения в таблице базы данных или передавать их как параметры.
Пакет превентивного контроля В этом разделе мы рассмотрим пакет, который применяется для выявления потенциальных проблем. В отличие от пакета для работы с файлом сообщений, позволяющего контролировать уже произошедшие ошибки, пакет превентивного контроля ищет потенциальные проблемы до того, как они возникнут. Это возможно благодаря тенденции в последних версиях Oracle включать много дополнительной общесистемной информации в словарь данных, например, информации о состоянии резервных копий базы данных и состояния каталога, в который архивируются журналы. Основываясь на информации этого типа, сервер Oracle 10g предоставил множество средств автоматического контроля состояния, таких, как монитор автоматической диагностики базы данных (Automatic Database Diagnostic Monitor — ADDM). Мы рекомендуем изучить его возможности при переходе на эту версию сервера. Тем временем много чего можно проверить в пакете превентивного контроля, и он позволит вам понять, как работают подобные автоматические средства. Займемся тремя проверками: > успешно ли прошло резервное копирование; > не близок ли к заполнению каталог, в который помещаются архивные журналы; > достаточно ли в базе данных свободного пространства. Вот спецификация пакета
PROACTIVE:
package proactive xs /** * Проект: * Описание: * * * * * *
мониторинг базы данных это пакет для процедур превентивного данных. При выявлении проблемы можно уведомление по электронной почте или сообщение в базе данных Влияние на БД: минимальное Фиксация внутри:нет Откат внутри: нет _. ,
контроля базы послать сохранить
.__.
*/ procedure checkStatusOfLastBackup (p_instance_name_in in varchar2); procedure checkArchiveDestination (p_instance_name_in in varchar2); procedure checkFreeSpace (p_instance_name_in in varchar2); end;
382
Глава 7
Резервные копии АБД должен контролировать корректность процессов резервного копирования базы данных. Точнее, АБД должен проверять, что последнее резервное копирование прошло успешно. Если в ходе последнего резервного копирования произошел сбой (или, что более важно, и раньше при резервном копировании были сбои), АБД стоит готовиться к проблемам. При горячем резервном копировании и резервном копировании с помощью RMAN (recovery manager — диспетчер восстановления) информация о резервных копиях и состоянии сохраняется в словаре данных. Информация RMAN обычно сохраняется в наборе представлений v$. Эти представления выдают информацию из управляющего файла. Мы будем использоззать представление V$BACKUP_SET, которое содержит запись для каждого завершенного процесса резервного копирования RMAN. Если вы используете RMAN с каталогом восстановления, информация будет сохраняться в представлениях RC. Пусть мы выполняем полное резервное копирование базы данных в оперативном режиме каждую ночь (без каталога восстановления). RMAN> backup database;
Нас интересуют только два столбца в представлении V$BACKUP_SET: столбец содержит время завершения резервного копирования, а в столбце BACKUP_TYPE указано, было ли резервное копирование полным или инкрементным. При полном резервном копировании в столбце BACKUP_TYPE будет значение D (вероятно, сокращение от "Database backup"). Вместо поля BACKUPJTYPE МОЖНО использовать поле INCREMENTAL_LEVEL (инкрементный уровень). Полные резервные копии имеют инкрементный уровень 0. Как мы уже упоминали, мы выполняем полное резервное копирование базы данных каждую ночь, так что проверяем время завершения полного резервного копирования или, точнее, проверяем возраст последней полной резервной копии. Если последняя резервная копия сделана больше суток назад, можно заключить, что самое последнее резервное копирование завершилось неудачно. COMPLETION_TIME
Procedure checkStatusOfLastBackup (p_instance_name_in in varchar2) is — Проверяем, что за последние сутки (threshold = 1) была сделана полная — оперативная резервная копия базы данных с помощью RMAN notes notification.msgs; THRESHOLD number(4,2) := 1; Begin -- Создаем сообщение, содержащее дату последнего полного резервного — копирования с помощью RMAN select to_char(sysdate,'Dy Mon dd hh24:mi:ss yyyy') I |chr(10) | | 'Most recent full backup was ' I I extract(day from (systimestamp - completion_time))|| 1 Days and ' | | extract(hour from (systimestamp - completion_time)) | | ' hours ago.' bulk c o l l e c t into notes from v$backup set a
Пакеты АБД 383 where
completion_time= (select max(completion_time) from v$backup_set where nvl(incremental_level,10) = nvl(a.incremental_level, 10) and backup_type - a.backup_type) and backup_type='D' and incremental_level is null and sysdate - completion_time > THRESHOLD;
— Если есть что посылать, — посылаем if notes.last > 0 then notification.notify(instance_name_in => p_instance_name_in, msgs_in => notes, subj ect_in => 'BACKUPS', email_p => false, db_ P => true); end if; end;
Если запрос в этой процедуре не возвращает строк, это значит, что за последние 24 часа была успешно создана резервная копия (или никогда не было создано ни одной копии!). Если запрос возвращает строку, значит, резервное копирование прошлой ночью не выполнялось или закончилось неудачно, и мы увидим сообщение следующего вида: SQL> select event_date,result_text 2 from alerts 3 where event_type = 'BACKUPS'; EVENT_DATE 03-OCT-03
RESULTJTEXT Fri Oct 03 10:10:10 2003 Most recent full backup was 2 days and 10 hours ago.
Свободное место в каталоге архивных журналов Одна из наиболее важных задач превентивного контроля, которую вы можете решить, это контроль свободного пространства в каталоге архивных журналов (предполагается, что сервер работает в режиме ARCHIVELOG). ЕСЛИ каталог архивных журналов заполнится, сервер зависнет. Точнее, если сервер не сможет переписать файл журнала повторного выполнения, поскольку журнал еще не был заархивирован, а заархивировать журнал он не может потому, что каталог архивных журналов заполнен, то новые подключения будут невозможны, и никакие изменения не будут выполняться. Программа контроля файла сообщений в этот момент тоже не поможет, поскольку ни одного задания из очереди, вероятно, выполнить уже не получится. Нет ни одного стандартного пакета PL/SQL, позволяющего определять объем свободного пространства на диске, но пользователи Oracle 9/ Enterprise Edition могут воспользоваться атрибутом Q U O T A _ S I Z E параметра инициализации
384
Глава 7
который, несомненно, поможет. Задавая квоту для вы указываете максимальный объем дискового пространства, которое может использоваться процессами архивирования. После этого сервер Oracle будет динамически отслеживать пространство в каталоге архивных журналов. Соответствующую информацию можно получить из представления V$ARCHIVE_DEST. LOG_ARCHIVE_DEST_N, LOG_ARCHIVE_DEST_N,
SQL> select destination,quota_size,quota_used 2 from v$archive_dest 3* where destination is not null; DESTINATION c:\oracle\OraArchives
QUOTA_SIZE
QUOTAJJSED
18192
5509
Следующая процедура проверяет доступное свободное пространство в каталоге архивных журналов, сравнивая значение в столбце QUOTA_USED СО значением QUOTA_SIZE. Если процент использования квоты превышает заданный порог, процедура будет посылать сообщение электронной почты и сохранять уведомление в таблице Oracle. — Проверка свободного пространства в каталоге архивных журналов procedure checkArchiveDestination (p_instance_name_in in varchar2) is — Проверяем, сколько пространства использовано в archivel — (порог - 80%) THRESHOLD number(4,2):= 80.0; notes notification.msgs; begin — Строим сообщение, идентифицирующее каталог(и), заполненные, — по крайней мере, на 80% select to_char(sysdate,'Dy Mon dd hh24:mi:ss yyyy')I|chr(10)|| 'Archive: 'I|dest_name|I' 'I|destination|| 1 is 'II to_char(round(1 - (quota_size-quota_used)/(quota_size),4)*100 ) | | '% full. ' bulk collect into notes from v$archive_dest where round(1 - (quota_size-quota_used)/(quota_size),4)*100 > THRESHOLD and schedule = 'ACTIVE'; — — — if
Если предыдущий запрос обнаружил строки, сообщения будут содержаться в массиве notes. Если массив не пустой, послать сообщение по электронной почте notes.last > 0 then notification.notify(instance_name_in => p_instance_name_in, msgs_in => notes, subject_in => 'ARCHIVE', email_p => true, db_p => true); end if; end;
Пакеты АБД 385 Если процедура выявляет проблему, она будет генерировать сообщение, подобное следующему: SQL> select event date,result text — — 2 from alerts 3 where event_type = 'ARCHIVE'; EVENT_DATE
RESULTJTEXT
03-OCT-03
Fri Oct 03 10:31:48 2003 Archive: LOG_ARCHIVE_DEST_1 c:\oracle\OraArchives is 81.4% full.
Контроль свободного пространства в базе данных Одна из наиболее типичных "привычек" АБД — контролировать наличие свободного пространства в базе данных. Кто-то может сказать, что проверка свободного пространства не нужна, потому что всегда можно создать (или изменить) файл данных так, чтобы он автоматически увеличивался при необходимости (AUTOEXTEND ON). Поэтому теоретически в табличном пространстве всегда должно быть свободное пространство, но, несомненно, все АБД должны знать о росте любых объектов в базе данных. С другой стороны, неформатированные устройства не могут автоматически увеличиваться и в любом случае с практической точки зрения объем доступного свободного пространства зависит не от атрибутов файла данных, а от возможностей оборудования. Зачем задавать максимальный размер 50 Гбайтов при создании файла данных размером 2 Гбайта, если на диске не осталось свободного пространства или если в базе данных уже есть 24 файла данных размером по 2 Гбайта каждый? Если вы используете автоматическое увеличение файлов данных, имеет смысл задавать обоснованный максимальный размер файла, такой, который заведомо может быть достигнут, а также оставить часть пространства свободной для исключительных ситуаций. Иными словами, включение автоматического расширения упрощает сопровождение, но не решает реальные проблемы нехватки пространства. И если свободного пространства не хватает, лучше тщательно его контролировать. Следующая процедура контролирует свободное пространство во всех табличных пространствах. При вычислениях она учитывает текущий размер файла, а также размер, до которого файл может расти с учетом параметров автоматического увеличения. Сравнивая размер файла с объемом свободного пространства в табличном пространстве, она может определить, не превышает ли процент использованного пространства установленное пороговое значение. procedure checkFreeSpace (p_instance_name_in in varchar2) is — Проверка использования табличных пространств THRESHOLD number(4,2):= 80.0; notes notification.msgs; begin
13 Зак. 348
386
Глава 7 — — —
Строим сообщение после проверки свободного и использованного пространства, если доля использованного пространства больше определенного порога select to_char(sysdate,'Dy Mon dd hh24:mi:ss yyyy')I|chr(10)I| 'Tablespace 'I I a.tablespace_nameI| ' is 'II to_char(round(1 - (free+potential)/(allocated+potential),4)*100) I I '% full.' bulk collect into notes from (select tablespace_name,sum(bytes) free from dba_free_space group by tablespace_name) f, (select tablespace_name,sum(bytes) allocated, sum(maxbytes) potential from dba_data_files group by tablespace_name) a where a.tablespace_name = f.tablespace_name (+) and round(l - (free+potential)/(allocated+potential),4)*100 > THRESHOLD; — Если представленный выше запрос вернул строки, послать уведомление if notes.last > 0 then notification.notify(instance_name_in => p_instance_name_in, msgs_in => notes, subject_in => 'STORAGE', email p => false, — db p => true); — end if; end;
Если эта процедура выявит табличные пространства, заполненные сверх указанного предела, она будет выдавать сообщение следующего вида: SQL> select event_date,result_text 2 from alerts 3 where event_type = 'STORAGE'; EVENT DATE — 03-OCT-03
RESULT TEXT — Fri Oc.t 03 11:46:42 2003 Tablespace TOOLS is 84.75% full.
Превентивный контроль: итоги Мы описали пакет превентивного контроля с тремя процедурами: для контроля состояния резервных копий, для контроля свободного пространства вне базы данных, в каталоге архивных журналов, и для проверки свободного пространства в базе данных. В каждой процедуре мы задаем пороговое значение. Если оно превышено, процедура записывает соответствующую информацию в таблицу ALERTS С ПОМОЩЬЮ пакета NOTIFICATION.
Пакеты АБД 387
Пакет для поддержки хронологических данных Предполагается, что, помимо выявления проблем, АБД также должен накапливать все возможные сведения о своей базе данных. Растет база данных или уменьшается? Если растет, то насколько быстро? Когда закончится свободное место на диске? Сколько сеансов в среднем поддерживает база данных? Стала ли она использоваться больше после установки нового приложения? На подобного рода запросы иногда можно ответить, выполнив запрос к словарю данных, но в словаре данных хранится не так уж много хронологической информации. Чтобы достичь поставленной цели, обычно надо самому накапливать данные в хронологическом порядке. Пакет для поддержки хронологических данных сохраняет информацию по экземплярам и по датам (хотя он не анализирует эту информацию). Мы рассмотрим три процедуры: > вычисление размера базы данных; > подсчет количества сеансов; > сохранение набора показателей использования ресурсов в базе данных. package history is /** .X.
* * * * * *
_
_
_
мониторинг базы данных содержит процедуры для сбора хронологической информации о базе данных минимальное Влияние на БД: Фиксация внутри: нет нет Откат внутри:
Проект: Описание:
*/ procedure databaseSize (p_instance_name_in in varchar2); procedure databaseSessions (p_instance_name_in in varchar2); procedure resourceLimit (p_instance_name_in in varchar2); end;
Пакет для поддержки хронологических данных отличается от предыдущих двух пакетов тем, что не посылает уведомления по электронной почте. Дело в том, что он не выявляет проблемы — он накапливает данные, которые в дальнейшем могут использоваться для анализа хронологии событий. Хронологические данные могут использоваться для выявления потенциальных проблем, но сами проблемы на момент сбора данных неизвестны. Еще одна уникальная особенность пакета для поддержки хронологических данных состоит в том, что процедуре уведомления, помимо строки сообщения, он передает числовое значение. Числовое значение (параметр RESULT) сохраняется в таблице ALERTS и используется в хронологических отчетах и запросах.
388
Глава 7
Размер базы данных В некоторых базах данных объем используемого дискового пространства стабилен — его размеры варьируются в известных пределах в соответствии с ежемесячным или ежегодным вводом и удалением данных. Другие же базы данных постоянно растут. В любом случае (но особенно тогда, когда база данных растет) имеет смысл сохранять показатели размера базы данных. Это имеет смысл даже при наличии большого объема свободного пространства и отсутствии угрозы его нехватки в обозримом будущем. Основная причина сбора показателей размера базы данных состоит в том, что эти хронологические данные позволяют интерпретировать события, связанные с дисковым пространством. Предположим, вы обратили внимание, что в табличное пространство добавилось несколько сотен мегабайтов (возможно, автоматически увеличился один из его файлов данных). Единственный способ узнать, является ли это изменение поводом для беспокойства, — это сравнить текущее увеличение размера с прежней скоростью роста этого табличного пространства в базе данных. Если база данных так никогда не увеличивалась, вероятно, имеет смысл выяснить причины изменений. Еще одна причина сбора и сохранения информации об изменении размера базы данных со временем — это необходимость прогнозирования того, возможна ли нехватка свободного пространства для базы данных и когда с этим придется столкнуться. Если база данных ежемесячно увеличивается на определенный объем, можно экстраполировать статистическую информацию о росте и определить, когда свободного пространства не останется. Следующая процедура, DATABASESIZE, вычисляет общий размер базы данных, запрашивая количество байтов в сегментах из представления DBA_SEGMENTS. Ее можно выполнять один раз в месяц или раз в неделю. — Получить размер базы данных, суммируя размеры всех объектов procedure databaseSize (p_instance_name_in in varchar2) is notes notification.msgs; begin — Получить текстовое сообщение о размере базы данных (в Мбайтах) 1 select to_char(sysdate,'Dy Mon dd hh24:mi:ss yyyy ) I|chr(10) I I 'DB Size(Mb) '|| to_char(round(sum(bytes)/(1024*1024) ,2)) bulk collect into notes from dba_segments; — Извлечь размер из предыдущего сообщения и сохранить его как result in notification.notify(instance_name_in => p_instance_name_in, msgs_in => notes, subj ect_in => 'GROWTH', result_in => substr(notes (1),37), email_p => false, db_p => true); end;
Пакеты АБД 389 Следующий SQL-запрос выбирает размер базы данных из накопленных хронологических данных. Столбец RESULT переименован в SIZE (MB) . SQL> SQL> 2 3 4
col result heading "Size(Mb)" select event_date,result,result_text from alerts where event_type='GROWTH' and 1 event_date > '01-OCT-03 ;
EVENT DATE 03-ОСТ-ОЗ
Size (Mb) RESULT TEXT 281.38 Fri Oct 03 13:37:00 2003 DB Size(Mb) 281.38
Регистрируемое значение (размер базы данных) — простой показатель. Он не описывает точно, сколько пользовательских данных содержится в базе данных. Но если размеры объектов базы данных не сильно завышены, вычисленное значение можно использовать для ответа на интересные вопросы, которые были представлены в начале этого раздела. Предположим, мы собрали следующие показатели: SQL> 2 3 4 5 6
select trunc(event_date,'mm') EVENT_DATE, round(max(result),0) result from alerts where event_type = 'GROWTH' group by trunc(event_date,'mm') order by 1;
EVENT DATE 01-JAN-03 01-FEB-03 01-MAR-03 01-APR-03 01-MAY-03 01-JUN-03 Ol-JUL-03 01-AUG-03 01-SEP-03 01-OCT-03
Size (Mb) 31,714 32,545 33,257 34,040 36,204 36,808 37,494 39,019 39,406 40,046
Мы можем вычислить ежемесячный рост этой базы данных (с помощью аналитических функций) таким вот образом: SQL> select trunc(event_date,'mm') event_date, 2 max(result) result, 3 max(result) - lag(max(result),1) over 4 (order by trunc(event_date,'mm')) as growth 5 from alerts 6 group by trunc(event_date,'mm');
390
Глава 7
EVENT DATE 01-JAN-03 01-FEB-03 01-MAR-03 01-APR-03 01-MAY-03 01-JUN-03 01-JUL-03 01-AUG-03 01-SEP-03 01-OCT-03
Result(Mb)
Growth(Mb)
31,714 32,545 33,257 34,040 36,204 36,808 37,494 39,019 39,406 40,046
831 712 783 2164 604 686 1525 387 640
Эта база данных (в среднем) увеличивается на 1000 Мбайтов в месяц. При наличии достаточных данных можно вычислить и увеличение размера в год.
Сеансы базы данных Еще один показатель, за которым имеет смысл следить, — это количество сеансов в базе данных в определенные моменты времени каждый день. Эти значения могут помочь определить, насколько интенсивно используется экземпляр, а также могут учитываться при определении количества необходимых лицензий Oracle или приложения, в зависимости от того, как пользователи базы данных получают к ней доступ. Если каждый пользователь создает отдельный сеанс базы данных, подсчитать количество сеансов можно простым запросом к представлению V$SESSION. — Получение текущего количества пользовательских сеансов procedure databaseSessions (p_instance_name_in in varchar2) is notes notification.msgs; begin -- Получить количество сеансов select to_char(sysdate,'Dy Mon dd hh24:mi:ss yyyy')I|chr(10)|| 'Sessions '||count(*) bulk collect into notes from v$session where type = 'USER'; — Выбрать количество сеансов из полученного выше текста и сохранить — как result_in notification.notify(instance_name_in => p_instance_name__in, msgs_in => notes, subject_in => 'SESSIONS', result_in => substr(notes(1),34), email_p => false, db_p => true); end;
Пакеты АБД 391 В результате в таблице хронологических данных получаем запись следующего вида: SQL> col result heading "Sessions" SQL> select event_date,result,result_text 2 from alerts 3 where event_type = 'SESSIONS' and 4 event_date > '01-OCT-03'; EVENT DATE
SESSIONS
03-OCT-03
134
RESULT TEXT Fri Oct 03 15:12:51 2003 Sessions 134
Вот запрос, вычисляющий среднее количество "сеансов" в месяц. Показатели, на основании которых строится среднее, брались в один и тот же момент времени каждый день. Для получения достоверного результата выходные и праздники мы исключили. SQL> select trunc(event date,'MON') event date, round(avg(result),0) avgSessions from alerts 4 where event_type = 'SESSIONS' 5 group by trunc(event date,'MON'); EVENT_DATE
AVGSESSIONS
01-DEC-02 01-JAN-03 01-FEB-03 01-MAR-03 01-APR-03 01-MAY-03 01-JUN-03
192 208 227 217 207 203 207
Ограничения ресурсов Запись количества подключенных сеансов в определенный момент времени, как показано выше, — полезный способ отслеживания тенденций пользовательской активности в базе данных. Однако не менее важно знать, насколько "близко к пределу" находится база данных с точки зрения различных ресурсов. Эти данные можно получить из представления V$RESOURCE_LIMIT, как продемонстрировано в следующем запросе: SQL> select resource_name, max_utilization, limit_value 2 from v$resource limit; RESOURCE NAME
MAX UTILIZATION
LIMIT VALUE
processes sessions enqueue_locks
11 10 18
150 170 2230
392
Глава 7
enqueue_resources ges_procs ges_ress ges_locks ges_cache_ress ges_reg_msgs ges_big_msgs ges_rsv_msgs gcs_resources gcs_shadows dml_locks temporary_table_locks transactions branches cmtcallbk sort_segment_locks max_rollback_segments max_shared_servers parallel_max_servers
11 0 0 0 0 0 0 0 0 0 29 1 4 0 1 1 11 0 0
UNLIMITED 0 UNLIMITED UNLIMITED UNLIMITED UNLIMITED UNLIMITED 0 2500 2500 UNLIMITED UNLIMITED UNLIMITED UNLIMITED UNLIMITED UNLIMITED 38 20 6
22 rows selected.
Для многих ресурсов допустимый предел указан как UNLIMITED, ЧТО практически означает, что вы ограничены только возможностями оборудования. Однако для других ресурсов имеет смысл регистрировать уровень максимального использования со временем, чтобы удостовериться, что вы не столкнетесь в ближайшее время с нехваткой ресурсов. Для этого предназначена процедура RESOURCELIMIT. — Определяем максимальный объем использования ресурсов procedure resourceLimit (p_instance_name_in in varchar2) is notes notification.msgs; begin select to_char(sysdate,'Dy Mon dd hh24:mi:ss yyyy') I|chr(10) I I rpad(resource_name,30)||' Max: '|IIpad(max_utilization,10) bulk collect into notes from v$resource_limit where trim(limit_value) != 'UNLIMITED'; — сохраняем сообщение об использовании ресурсов notification.notify(instance_name_in => p_instance_name_in, msgs_in => notes, subject_in => 'RESOURCE LIMIT1, result_in => -1, emailj? => false, db_jp => true) ; end;
Пакеты АБД 393
Хронологические данные: итоги Мы описали пакет для поддержки хронологических данных. Он содержит процедуры для вычисления размера базы данных, подсчета количества сеансов и оценки использования ключевых ресурсов базы данных. Каждая из этих процедур добавляет результаты вычисления в таблицу базы данных. Эти накопленные результаты обеспечивают основу для создания запросов и отчетов, помогающих выявлять тенденции в базе данных.
Резюме Можно написать и другие сценарии. Можно проверять состояние базы данных, находя отключенные триггеры или недействительные объекты. Можно искать пользователей с избыточными привилегиями, т.е. пользователей с административными или другими привилегиями, не нужными им. Можно искать объекты, которые не должны находиться в табличном пространстве SYSTEM, а также измерять объем сгенерированных данных повторного выполнения. Мы представили примеры, которые были полезны нам. Представленные процедуры используют пакет уведомления, основанный на стандартном пакете PL/SQL для посылки электронной почты. Выполнение представленных процедур по расписанию обеспечивается очередью заданий PL/SQL, которая также поддерживается стандартным пакетом Oracle. Это сочетание (выполнение по расписанию и посылка сообщений по электронной почте) позволяет создать автоматическую систему контроля. Контролировать базу данных Oracle можно по-разному. Можно выполнять интерактивный контроль от случая к случаю, хотя очевидно, что такой подход не отличается систематичностью и плохо масштабируется. Можно купить готовое средство мониторинга. Можно создать сценарии контроля и запускать их автоматически с помощью планировщика заданий. В этом случае сценарии контроля и планировщик заданий могут быть как внешними по отношению к базе данных, так и работать в ней, на PL/SQL-машине. Мы продемонстрировали, как использовать пакеты PL/SQL для контроля базы данных Oracle. Для общения с внешним миром PL/SQL может читать и записывать файлы операционной системы и посылать сообщения по электронной почте. В базе данных PL/SQL позволяет легко обращаться к информации, которую хранит сервер Oracle, в том числе к информации о ходе резервного копирования, свободном пространстве и блокировках, которые мешают работе других сеансов. Средства контроля на языке PL/SQL легко создать и поддерживать. Ничего не нужно покупать. Не надо изучать новый язык программирования, выполнять сложные процедуры установки и инициализации, задавать пароли и привилегии пользователей или создавать новый репозитарий в базе данных. Вследствие природы языка PL/SQL и способа выполнения программ на нем на сервере есть ряд задач контроля, которые с его помощью решить нельзя. Сценарий PL/SQL не может выявить, что сервер не работает или к нему нельзя подключиться. (Можно контролировать PL/SQL-кодом на одном экземпляре состояние базы дан-
394
Глава 7
ных другого экземпляра с помощью связей базы данных, но такое решение слишком сложно, чтобы его рекомендовать.) По сути, эта проблема касается любого контролирующего ПО, поскольку всегда есть риск, что контролирующее ПО не сможет сделать то, что мы потребовали. С другой стороны, при использовании решения на языке PL/SQL, если сервер останавливается по любой причине, контролирующее приложение не будет выдавать бесполезные предупреждения и уведомления. Механизм контроля на базе PL/SQL можно расширять по мере изучения базы данных и уточнения требований к тому, что надо контролировать. Решение на языке PL/SQL также будет работать на любой платформе, на которой работает сервер Oracle (NT, Unix и любая другая ОС). PL/SQL-код можно скрыть, так что можно создать защищенный набор средств контроля внешним клиентам. В итоге решение на базе пакетов PL/SQL легко позволяет стандартизировать и упростить контроль базы данных.
Глава 8
Пакеты для защиты Почему защита так важна? Вопрос сам по себе кажется смешным, но фактически большинство разработчиков не думает о защите при создании приложений баз данных. Хотя многие оценивают успешность приложения по его производительности, способности масштабироваться или простоте использования, итоговый успех или неудача может на самом деле зависеть от того, можно ли взломать приложение и, как следствие, базу данных. Если знать, какие средства защиты доступны и как их правильно использовать, разрабатывать и создавать безопасные приложения на языке PL/SQL не только возможно, но и сравнительно просто. В этой главе мы рассмотрим различные аспекты защиты при программировании на языке PL/SQL для сервера Oracle. > Проблемы проектирования: как правильно строить программы, чтобы обеспечить максимальную защиту. > Триггеры базы данных: реализация различных аспектов защиты. > Защита исходного кода: возможные варианты.
Вопросы проектирования Решения, принятые на этапе проектирования, принципиально влияют на общую производительность и защиту любого PL/SQL-приложения. Поскольку PL/SQLпроцедуры могут делать многое — от поддержки бизнес-правил до обеспечения целостности данных, — нет универсального решения для любой проблемы. Поэтому правильное проектирование принципиально важно. Следует учитывать правильность моделей, организацию кода и взаимосвязи между схемами, объектами, пользователями и привилегиями.
Обзор выполнения с правами создателя и вызывающего Для эффективного обеспечения защиты при проектировании и создании PL/SQLприложений важно глубоко понимать модель прав доступа, используемую в базе данных Oracle. Подходящий для вас режим выполнения будет зависеть от требований конкретного приложения. Далее мы рассмотрим несколько проверенных приемов, но пока давайте представим обзор имеющихся режимов выполнения и их особенностей. Сервер Oracle обеспечивает два режима работы хранимых процедур, особенности которых подытожены в таблице 8.1.
396
Глава 8
Режим работы с правами создателя — стандартный режим работы, когда сервер Oracle использует привилегии и разрешает ссылки на объекты от имени создателя процедуры. Эта модель была традиционной при разработке приложений многие годы. Режим работы с правами вызывающего — появившийся в версии Oracle 8.1.5, этот режим в ходе компиляции программы не отличается от работы с правами создателя. Однако во время выполнения сервер использует привилегии и разрешает ссылки на объекты от имени пользователя, вызывающего процедуру. Таблица 8.1. Режимы выполнения хранимых процедур Oracle Права создателя Компиляция
Выполнение
Права вызывающего Компиляция
Выполнение Вызывающего
Разрешение объектов
Создателя
Создателя
Создателя
Привилегии
Создателя
Создателя
Создателя
Вызывающего
Роли
Отключены
Отключены
Отключены
Включены
При использовании прав создателя, как при компиляции, так и при выполнении, сервер будет использовать набор привилегий и объекты создателя процедуры. Важно учитывать, что роли базы данных при этом отключены. При использовании прав вызывающего в ходе выполнения сервер использует набор привилегий и объекты пользователя, вызывающего процедуру (схемы, привилегии которой действуют в ходе определенного сеанса). В отличие от работы с правами создателя, роли при выполнении учитываются. Это утверждение про роли важно. Непонимание происходящего часто вызывает разочарование и лишние обращения в службу поддержки. Простой пример позволит продемонстрировать, что мы имеем в виду. Одна из наиболее типичных проблем, с которыми сталкиваются разработчики, использующие PL/SQL, иллюстрируется следующим примером. Мы хотим создать функцию, возвращающую имя модуля (или программы) для сеанса текущего пользователя. Имя модуля получается путем соединения представлений V$SESSION И V$PROCESS. Чтобы обеспечить доступ к этим объектам, мы создадим функцию от имени пользователя SYSTEM (ЭТОТ ПОДХОД не рекомендуется использовать, но люди часто его применяют). Пользователю SYSTEM предоставлена роль DBA, позволяющая обращаться к представлениям v$. Прежде всего мы создадим анонимный блок для проверки нашей логики. Анонимный блок, в отличие от процедуры с правами создателя, работает с учетом ролей. system@KNOX10g> s e t serveroutput on system@KNOX10g> declare 2 l_module varchar2(48); 3 begin 4 select b.module into l_module 5 from v$process a, v$session b 6 where a.addr = b.paddr 7 and b.audsid = sys_context('userenv', 'sessionid'); 8 dbms_output.put_line('Current Program is ' I| l_module); 9 end;
Пакеты для защиты
397
10 / Current Program is SQL*Plus PL/SQL procedure successfully completed. •
Анонимный блок работает отлично. Теперь, используя копирование и вставку, мы поместим этот запрос в функцию, чтобы можно было легко выполнить его, когда понадобится данная информация. system@KNOX10g> create or replace function get_my_program 2 return varchar2 3 as 4 l_module varchar2(48); 5 begin select b.module into l_module 6 from v$process a, v$session b 7 where a.addr = b.paddr 8 and b.audsid » sys_context('userenv','sessionid') 9 return l_module; 10 11 end; 12 / Warning: Function created with compilation errors. system@KNOX10g> sho errors Errors for FUNCTION GET_MY_PROGRAM: LINE/COL
ERROR
6/3 7/23
PL/SQL: SQL Statement ignored PL/SQL: ORA-00942: table or view does not exist
Это не ошибка. Функция не компилируется, поскольку роль DBA отключена. Поэтому представления v$ недоступны в (именованной) профаммной единице PL/SQL. Можно изменить код, встроив запрос в динамический SQL. Это позволит скомпилировать процедуру, но при выполнении процедура снова не сработает, и будет выдано то же сообщение об ошибке. При работе с правами создателя именованные профаммные единицы PL/SQL компилируются и работают только с непосредственно предоставленными привилегиями. Любые привилегии, предоставленные ролям как вызывающего процедуру, так и ее создателя, недоступны. Мы можем проверить это, выдавая действующие роли в именованной профаммной единице. Сначала мы покажем, что все стандартные роли доступны, а затем выполним профамму и сравним результаты. system@KNOX10g> — показать роли system@KNOX10g> s e l e c t * from session_roles; ROLE DBA
SELECT_CATALOG_ROLE HS ADMIN ROLE
398
Глава 8
EXECUTE_CATALOG_ROLE DELETE_CATALOG_ROLE EXP_FULL_DATABASE IMP_FULL_DATABASE GATHER_SYSTEM_STATISTICS SCHEDULER__ADMIN WM_ADMIN_ROLE JAVA_ADMIN JAVA_DEPLOY XDBADMIN OLAP_DBA AQ_ADMINISTRATOR_ROLE MGMT_USER 16 rows selected. system@KNOX10g> — показать роли в процедуре system@KNOX10g> create or replace procedure show_privs 2 as 3 begin 4 dbms_output.put_line('ROLES:') ; 5 for rec in (select * from session_roles) 6 loop 7 dbms_output.put_line(rec.role) ; 8 end loop; 9 end; 10 / Procedure created. system@KNOX10g> set serveroutput on system@KNOX10g> exec show_privs ROLES: PL/SQL procedure successfully completed.
Результат четко показывает, что в процедуре SHOW_PRIVS все роли отключены. Следовательно, все привилегии, предоставленные ролям пользователя, тоже не действуют в процедуре. Не удивляйтесь. Не посылайте сообщение об ошибке или запрос на расширение функциональности. Все именно так и было задумано. Зная, что в программах с правами вызывающего роли действуют, вы можете тут же перейти в этот режим, чтобы решить проблему с функцией GET_MY_PROGRAM. Помните, что работа с правами вызывающего не всегда позволяет легко решить проблему ролей, отключенных при работе с правами создателя. Если мы пересоздадим функцию GET_MY_PROGRAM с правами вызывающего (далее мы подробно опишем, как это делается) и с использованием динамического SQL, проблема покажется решенной. system@KNOX10g> c r e a t e or replace function get_my_program 2 return varchar2 3 authid current user
Пакеты для защиты
399
4 as 5 ljnodule varchar2(48); 6 l_query varchar2(500); 7 begin 8 l_query := 'select b.module ' || 9 'from v$process a, v$session b ' |I 10 'where a.addr = b.paddr 'I| 11 'and b.audsid = sys_context(''userenv'',''sessionid'')'; 12 execute immediate l_query into l_module; 13 return l_module; 14 end; 15 / Function created. system@KNOX10g> select get_my_program from dual; GET_MY_PROGRAM
SQL*Plus
К сожалению, проблема решена только для пользователя SYSTEM. Любой пользователь без роли DBA или привилегий на запросы из представлений v$ не сможет выполнить эту функцию. Реальным решением проблемы будет предоставление привилегии SELECT на представления V$SESSION И V$PROCESS непосредственно схеме, которой принадлежит функция GET_MY_PROGRAM (на самом деле это не должна быть схема SYSTEM). Затем надо скомпилировать функцию с правами создателя. Работа с правами создателя обеспечивает эффективное и безопасное выполнение и фактически является предпочтительным режимом работы в большинстве случаев. В этом режиме программные единицы PL/SQL принадлежат схеме, в которой они работают, а привилегии на выполнение предоставляются по мере необходимости. Это обеспечивает естественную и эффективную инкапсуляцию методов доступа и изменения базовых таблиц в схеме. Конечно, это не означает, что использовать права вызывающего нет смысла (вскоре мы это продемонстрируем). Как уже неоднократно утверждалось, в таком случае корпорация Oracle не стала бы их реализовывать. Просто не следует рассматривать программные единицы с правами вызывающего как средство обхода проблемы ролей.
Использование прав создателя Если раньше вы писали программы на языке PL/SQL, то, вероятно, вы хорошо знакомы с особенностями работы с правами создателя. Надеемся, вас не сбивала с толку ситуация, аналогичная только что описанной. Давайте подытожим прежние знания и немного дополним их: > по умолчанию именованные программные единицы в базе данных работают с правами создателя. Если не указать режим при создании программы, будет установлен режим работы с правами создателя; > роли отключены;
400
Глава 8
> права владельца процедуры используются при компиляции и выполнении процедур; > имена объектов разрешаются в схеме владельца процедуры. Третий пункт гласит, что сервер Oracle использует привилегии владельца (а, значит, и создателя) процедуры как при компиляции PL/SQL-кода, так и при его выполнении. Например, пусть у нас есть пользователь SCOTT, который создает процедуру UPDATE_SAL, изменяющую таблицу ЕМР. Эта процедура будет всегда выполняться с базовыми привилегиями пользователя SCOTT, даже если ее выполняет другой пользователь. Для создания и успешного компилирования процедуры пользователю SCOTT потребуются привилегии на таблицу ЕМР, предоставленные ему непосредственно (подробнее об этом — чуть позже). Любому пользователю, выполняющему эту процедуру, привилегия на изменение этой таблицы не нужна — достаточно привилегии на выполнение процедуры. Обратите внимание, что привилегия на выполнение процедуры может быть получена косвенно, через роль. Последний пункт гласит, что неуточненные имена объектов разрешаются в схеме создателя. Если наша процедура UPDATE_SAL содержит оператор Update ЕМР s e t
s a l = 5000;
сервер будет обрабатывать оператор как Update SCOTT.ЕМР s e t s a l = 5000;
Эти же принципы разрешения имен действуют и для других объектов, таких, как процедуры и синонимы. Пример использования прав создателя Продолжим рассмотрение процедуры UPDATE_SAL. Эта процедура будет использовать модель прав создателя. Она прекрасно иллюстрирует продуманную организацию защиты. Пользователи не имеют непосредственного доступа к объектам данных. Вместо этого они получают доступ косвенно, через хранимые процедуры. Процедуры могут осуществлять проверки бизнес-правил, ограничений целостности данных и требований защиты, прежде чем изменять данные, чтобы гарантировать, что все происходит так, как должно. Если доступ к данным надо ограничить, например, если вы не доверяете пользователям или нужно вести аудит их действий, использование модели прав создателя — хорошее начало. Рассмотрим дополнительное требование: изменение данных должно происходить только с 9 утра до 5 вечера с понедельника по пятницу. Процедура может проверять, что текущее время входит в диапазон рабочего времени, и только в этом случае разрешать выполнение изменений. В противном случае процедура должна отвергнуть попытку выполнения изменений. В результате получаем отличную модель защиты, позволяющую разработчику или АБД контролировать дальнейший доступ к данным. Мы начнем с таблицы, принадлежащей пользователю SCOTT. Пользователь SCOTT хочет, чтобы пользователь BLAKE МОГ изменять значение столбца SAL В таблице ЕМР. Однако пользователь SCOTT не хочет, чтобы BLAKE ВЫПОЛНЯЛ непосредственные изменения, поэтому он создает процедуру UPDATE_SAL, которая будет выполнять изменения.
Пакеты для защиты
401
scott8KNOX10g> CREATE PROCEDURE update_sal (p_empno in number, 2 p sal in number) 3 AS 4 BEGIN 5 /* 6 Можно выполнять проверки данных и/или требований защиты. 7 Примеры: 8 p_sal > sal; 9 Изменения в ходе "обычного" рабочего времени. 10 Пользователи, выполняющие процедуру, изменяют 11 только зарплату. 12 */ 13 update EMP set sal - p_sal where empno = p_empno; 14 END; 15 / Procedure created. В комментариях указаны дополнительные способы обеспечения защиты и целостности данных. Затем пользователь SCOTT разрешает пользователю BLAKE ВЫПОЛНЯТЬ процедуру UPDATE_SAL и выбирать данные из таблицы. scott@KNOX10> grant execute on update_sal t o blake; Grant succeeded. scott@KNOX10> — разрешаем пользователю видеть данные scott@KNOX10> grant s e l e c t on EMP t o blake; Grant succeeded. Теперь пользователь BLAKE, который по-прежнему не может обновлять таблицу непосредственно, может менять зарплаты с помощью процедуры UPDATE_SAL. Фактически любой пользователь с привилегией EXECUTE на процедуру UPDATE_SAL сможет изменить таблицу ЕМР пользователя SCOTT. ЭТОТ процесс проиллюстрирован на рис. 8.1. TABLE KING SCOTT BLAKE SMITH JAMES JONES MILLE
10 20 30 20 30 20 10 SCOTT.EMP
BLAKE 5000 3000 2850
800 950 2975 1300
Выполнить PROCEDURE SCOTT.UPDATE_SAL scott@KNOX10> scott@KNOX10> Connected. blake@KNOX10> blake@KNOX10>
— проверяем от имени blake (учетная запись которого -- создана для этого примера) connect blake/blake — получаем данные select ename, empno, sal from SCOTT.EMP where ename = USER;
ENAME
EMPNO
SAL
BLAKE
7698
2850
blake@KNOX10> — показываем, что непосредственное изменение невозможно blake@KNOX10> update SCOTT.emp set sal = sal * 1.1; update SCOTT.emp set sal = sal * 1.1 ERROR at line 1: ORA-01031: insufficient privileges blake@KNOX10> — показываем, что вызов процедуры срабатывает blake@KNOX10> execute SCOTT.update_sal(p_empno => 7698, p_sal => 3000) PL/SQL procedure successfully completed. blake@KNOX10> —
получаем данные
blake@KNOX10> select ename, empno, sal from SCOTT.EMP where ename = USER; ENAME
EMPNO
SAL
BLAKE
7698
3000
Этот пример показывает, насколько эффективно работа с правами создателя может гарантировать безопасную среду. Помните также о разрешении объектов. В процедуре упоминается неуточненное имя таблицы ЕМР, а не SCOTT.EMP. ЭТО сработало, поскольку при работе с правами создателя неуточненные имена объектов разрешаются в схеме создателя PL/SQL-кода. Непосредственный запрос, выполненный от имени BLAKE, должен использовать полностью уточненное имя объекта, иначе сервер выдаст сообщение об ошибке, как продемонстрировано в следующем примере, где использованы два запроса. Первый запрос использует неуточненное имя объекта.
Пакеты для защиты 403 blake@KNOX10> select ename from emp where ename like 'B%'; select ename from emp ERROR at line 1: ORA-00942: table or view does not exist blake@KNOX10> select ename from SCOTT.EMP where ename like 'B%'; ENAME BLAKE
Когда использовать работу с правами создателя Некоторые варианты использования требуют работы с правами создателя. Одно из реальных преимуществ такого подхода — производительность. SQL-операторы в процедуре с правами создателя можно кешировать как разделяемые, что повышает производительность, как было показано в главе 5, "Методы оптимизации PL/SQL". Поэтому разработчики используют права создателя для максимизации совместного использования SQL-операторов. С точки зрения защиты часто оказывается, что работа с правами создателя оптимально соответствует принципу наименьших привилегий. Этот принцип гласит, что пользователь должен иметь только те привилегии, которые ему необходимы для выполнения работы, и не более. Например, пусть необходимо создать процедуру, позволяющую пользователю завершать свой сеанс. Это создает проблему, поскольку привилегия ALTER SYSTEM, необходимая для решения задачи, позволит пользователю прекращать любой сеанс, а также делать многое другое. Решение состоит в создании процедуры с правами создателя в схеме с привилегией ALTER SYSTEM. При получении запроса на прекращение сеанса процедура может проверять, что сеанс принадлежит пользователю, вызывающему процедуру. Вот ряд случаев, когда надо выбирать работу с правами создателя: > необходимо предотвратить непосредственные запросы и изменения данных в таблице пользователями; > пользователи совместно используют учетную запись базы данных, так что привилегии можно предоставлять непосредственно этой схеме; > будет подключено много пользователей, и нужно максимально использовать разделяемые SQL-операторы. Совместное использование SQL-операторов важно для обеспечения высокой производительности, поскольку позволяет повторно использовать результаты работы сервера по разбору и оптимизации. Это возможно потому, что результаты разбора и оптимизации, выполненных одним сеансом, могут повторно использоваться другим сеансом1; ' Подробнее см. в книге Тома Кайта 'Expert One-on-One Oracle" (Apress, 2003). Перевод на русский язык под названием "Oracle для профессионалов"вышел в 2003году в издательстве "ДиаСофт". — Прим. науч. ред.
404
Глава 8
> необходимо максимизировать производительность PL/SQL-кода за счет того, что все проверки привилегий и разрешение имен выполняются при компиляции; > не предполагается использовать роли для управления привилегиями пользователя (помните, что привилегии на выполнение процедур с правами создателя можно предоставлять через роли). Поскольку режим работы с правами создателя существует дольше, он намного популярнее и используется чаще, чем работа с правами вызывающего. Почему роли отключены? В отношении работы с правами создателя возникает два вопроса. 1. Почему отключены роли? 2. Не является ли это существенным ограничением работы с правами создателя? Для ответа на первый вопрос лучше всего описать действия сервера при компиляции и выполнении программы. С точки зрения защиты, весь доступ к данным надо проверять. Рассмотрим, например, процедуру, изменяющую таблицу. Сервер должен проверить, что у соответствующего пользователя есть привилегия на изменение этой таблицы. Как известно, привилегии можно предоставлять ролям. Роли можно предоставлять другим ролям практически бесконечным количеством способов. Наконец, роль или роли предоставляются пользователю. При компиляции программы север должен проверить привилегии и сохранить зависимости. Зависимости отслеживаются, чтобы обеспечить целостность с точки зрения доступа. Если привилегия на изменение таблицы отбирается, процедура должна стать недействительной. Если учитывать роли, отмена привилегии любой из ролей почти наверняка сделает недействительными все программы, обращающиеся к объектам. Сервер тратил бы существенную часть времени на перекомпиляцию и проверку программ, а это, естественно, негативно сказалось бы на производительности. В итоге режим работы с правами создателя работает именно так, как нужно для обеспечения высокой производительности и защиты программных единиц PL/SQL. Ответ на второй вопрос прост: "Нет, это не является ограничением режима работы с правами создателя". Работу с правами создателя можно использовать для выполнения многих реальных требований обработки и защиты. Однако, если не понимать или забыть, что происходит с ролями в программах с правами создателя, сомнения и потраченное на обращения в службу поддержки время могут замедлить реализацию. Как было показано в окончательном варианте реализации функции GET_MY_PROGRAM, отключенные роли не являются непреодолимым препятствием. Однако это может повлиять на проект или, по крайней мере, на способ предоставления привилегий.
Использование прав вызывающего В режиме прав вызывающего при выполнении PL/SQL-процедура работает строго наоборот по сравнению с процедурой с правами создателя. Иными словами, сервер
Пакеты для защиты
405
использует привилегии пользователя, вызывающего процедуру, и разрешает объекты в его схеме. Проверка привилегий и разрешение объектов при компиляции происходит так же, как и в режиме создателя. Т.е. роли при компиляции отключены, а разрешение объектов происходит в схеме создателя (процедуры с правами вызывающего). Работа с правами вызывающего позволяет создавать весьма полезные проекты. Чтобы код работал в режиме вызывающего, надо просто добавить конструкцию AUTHID CURRENT_USER после определения и перед конструкцией AS/IS. Давайте пересоздадим нашу предыдущую процедуру SHOW_PRIVS ДЛЯ работы с правами вызывающего. scott@KNOX10> c r e a t e or replace procedure show_privs 2 authid current_user 3 as 4 begin 5 dbms_output.put_line('ROLES:'); 6 for rec in (select * from session_roles) 7 loop 8 dbms_output.put_line(rec.role); 9 end loop; 10 end; 11
/
Procedure c r e a t e d . scott@KNOX10> e x e c u t e show_privs ROLES: CONNECT RESOURCE PL/SQL p r o c e d u r e s u c c e s s f u l l y completed.
Мы не только видим роли, но действуют также и все их привилегии. Пример использования прав вызывающего При работе с правами вызывающего во время компиляции используются привилегии и объекты создателя. Нельзя просто взять код, не компилировавшийся с правами создателя, добавить конструкцию AUTHID CURRENT_USER И надеяться, что он перекомпилируется. blake@KNOX10> CREATE PROCEDURE update_sal (p_empno in number, p_sal in number) 2 authid current_user 3 AS 4 BEGIN 5 update SCOTT.EMP set sal = p_sal where empno = p_empno; 6 END; 7 / Warning: Procedure created with compilation errors.
406
Глава 8
blake@KNOX10> show errors Errors for PROCEDURE UPDATE_SAL: LINE/COL ERROR 5/5 5/18
PL/SQL: SQL Statement ignored PL/SQL: ORA-01031: insufficient privileges
Есть несколько решений этой проблемы. Во-первых, можно предоставить создателю доступ к объектам. Если у вас нет возможности это сделать, можно использовать динамический SQL: blake@KNOX10> CREATE OR REPLACE procedure update_sal 2 ( 3 p_empno in number, 4 p_sal in number 5 ) 6 authid current_user 7 as 8 begin 9 execute immediate 'update SCOTT.EMP set ' || 10 'sal = :x ' || 11 'where empno = :y' using p_sal, p_empno; 12 end; 13 / Procedure created. blakeSKNOXIC» exec update_sal (7698,5000); PL/SQL procedure successfully completed. blake@KNOX10> select ename, empno, sal from SCOTT.EMP where ename = USER; ENAME
EMPNO
SAL
BLAKE
7698
5000
Во-вторых, можно создать объект-шаблон. Объект-шаблон — это объект, похожий на реальный (имена столбцов и типы данных совпадают), но его единственное назначение — дать возможность сопоставить типы данных SQL и PL/SQL. Таким образом, если у нас есть процедура, изменяющая таблицу ЕМР, надо создать временную таблицу ЕМР той же структуры. blake@KNOX10> —
создаем шаблон таблицы ЕМР
blake@KNOX10> create table EMP as select * from SCOTT.EMP where 1=2; Table created. blahe@KNOX10> CREATE OR REPLACE PROCEDURE update_sal 2 ( 3 p_empno in number, 4 p sal in number
Пакеты для защиты 5 ) 6 7 8 9
407
authid current_user AS BEGIN update EMP set sal • p_sal where empno = p_empno;
10 END; 11 / Procedure created.
Для шаблона не нужны индексы или данные — просто надо, чтобы серверу было на чем проверять. Это позволит успешно скомпилировать процедуры. Процедура скомпилируется и с правами создателя; основное отличие состоит в том, что при выполнении процедура с правами вызывающего будет работать с другим набором объектов. Обратите внимание, что в только что показанном примере имя таблицы не уточнено полностью. Имя ЕМР будет разрешаться во время выполнения для каждого вызывающего. При каждом вызове ЕМР может оказаться другим объектом, а может использоваться и один объект во всех вызовах. Следовательно, модель работы с правами вызывающего, вероятно, подойдет для организаций, предоставляющих услуги хостинга. У каждой компании-клиента может быть свой набор таблиц (структуры которых во всех схемах одинаковы) со своими данными. Но поддерживать хотелось бы только один экземпляр кода. Можно создать код с правами вызывающего один раз и, благодаря соответствующему разрешению объектов и привилегиям, можно быть уверенным, что данные компании А попадут в таблицы компании А, а данные компании Б — в таблицы компании Б. Более того, компании смогут вставлять данные только в собственные таблицы, но не в таблицы другой компании. Кроме этого, можно будет использовать роли для управления привилегиями пользователя. Когда использовать работу с правами вызывающего Процедуры с правами вызывающего, как и процедуры с правами создателя, могут выполнять любые необходимые дополнительные проверки — обеспечивать выполнение бизнес-правил, целостность данных, защиту, так что эти процедуры тоже могут обеспечивать корректный доступ к данным. Что еще можно получить за счет работы с правами вызывающего? Большинство разработчиков пришло к выводу, что права вызывающего допустимо использовать только для утилит. Это не преуменьшает значимость работы с правами вызывающего. Утилиты могут быть очень ценными. Многие из PL/SQL-пакетов DBMS_* скомпилированы с правами вызывающего. В качестве примера полезной утилиты, которая будет прекрасно работать с правами вызывающего, давайте рассмотрим программу, которая принимает произвольный SQL-оператор и возвращает отчет, сформатированный определенным образом. Эта утилита может быть создана одним пользователем, а затем сделана доступной многим. При использовании прав вызывающего процедура пишется один раз, а затем при выполнении сервер проверяет привилегии и разрешает объекты автоматически. Классический пример универсальной утилиты с правами вызывающего — процедура PRINTJTABLE, написанная Томом Кайтом (подробнее см. в книге "Expert One-on-One Oracle", Apress, ISBN 1590592433.
408
Глава 8
Ее перевод по названием "Oracle для профессионалов" вышел в 2003 году в издательстве "ДиаСофт". — Прим. ред.). Мы создаем эту процедуру в непривилегированной схеме UTILS. utils@ORA817> create or replace 2 procedure print_table( p_query in varchar2 ) 3 AUTHID CURRENTJJSER 4 is 5 l_theCursor integer default dbms_sql.open_cursor; 6 l_columnValue varchar2(4000); 7 l_status integer; 8 l_descTbl dbms_sql.desc_tab; 9 l_colCnt number; 10 begin 11 dbms_sql.parse( l_theCursor, p_query, dbms_sql.native ) ; 12 dbms_sql.describe_columns( l_theCursor, l_colCnt, l_descTbl); 13 14 for i in 1 .. l_colCnt loop 15 dbms_sql.define_column(l_theCursor, i, l_columnValue, 4000) 16 end loop; 17 18 l_status := dbms_sql.execute(l_theCursor); 19 20 while ( dbms_sql.fetch_rows(l_theCursor) > 0 ) loop 21 for i in 1 .. l_colCnt loop 22 dbms_sql.column_value( l_theCursor, i, l_columnValue ) ; 23 dbms_output.put_line( rpad( l_descTbl(i).col_name, 30 )
24
I I ': ' I I
25 l_columnValue ) ; 26 end loop; 27 dbms_output.put_line ( ' ' ); 28 end loop; 29 exception 30 when others then 31 dbms_sql.close_cursor( l_theCursor ) ; 32 RAISE; 33 end; 34 / Procedure created. utils@ORA817> utils@ORA817> grant execute on print_table to public; Grant succeeded.
Можно пойти еще дальше и фактически "заблокировать" учетную запись UTILS, отобрав у нее привилегию CREATE SESSION. utils@ORA817> connect tkyte/tkyte utils@ORA817> revoke create session, create procedure
Пакеты для защиты 2
409
from utils_acct;
Revoke succeeded.
Однако после этого вы сможете зарегистрироваться как пользователь SCOTT И свободно использовать процедуру PRINTJTABLE ДЛЯ получения красиво сформатированного результата. scott@ORA817> exec utils.print_table('select * from scott.dept'); 10 DEPTNO ACCOUNTING DNAME NEW YORK LOC DEPTNO DNAME
LOC
20 RESEARCH DALLAS
Работа с правами вызывающего может оказаться предпочтительной также в случаях, когда: > необходимо поддерживать один код для нескольких схем. Этот случай описан в примере с компанией, предоставляющей услуги хостинга; > вы уже используете роли базы данных. Трудности возникают, когда много пользователей применяют средства создания произвольных запросов и обращаются к Web-приложению в той же базе данных. Средства создания произвольных запросов обеспечивают непосредственное выполнение запросов через роли, а Web-приложение подключается как конечный пользователь и вызывает PL/SQL-процедуры для выполнения операторов DML (чаще всего так происходит при использовании Web-интерфейса modplsql). Для получения максимальной производительности придется использовать связываемые переменные и полностью уточненные имена объектов. Это требование кажется несколько надуманным, но мы сталкивались с ним много раз. Ограничения при использовании прав вызывающего Есть также несколько случаев, когда использование прав вызывающего — не лучший выбор. Например, когда: > вы создаете триггер или представление. Извините, но для представлений и триггеров можно использовать только права вызывающего; > производительность является определяющим требованием. При выполнении серверу придется разрешать неоднозначные ссылки на объекты и проверять привилегии пользователя. Также при работе с правами вызывающего затрудняется совместное использование SQL, поскольку операторы могут ссылаться на разные объекты; > требуется гарантированная поддержка зависимости. У проверки при выполнении есть веские причины. Однако при использовании прав вызывающего нет способа проверить, что код будет успешно работать для всех пользователей. Он может работать для пользователя Alice, но не для пользователя Bob.
410
Глава 8
Берегитесь смешанных режимов! Будьте внимательны при смешивании режимов. Оно может казаться необходимым, но будет сбивать с толку. В частности, разрешение объектов может оказаться нетривиальным. Процедура с правами создателя, вызывающая процедуру с правами вызывающего, приводит к тому, что она работает с правами создателя вызывающей процедуры. Запутались? Хорошо. Будьте внимательны при проектировании. Мы считаем, что лучше выбрать единый режим при проектировании и придерживаться его. Любые отклонения надо тщательно продумывать и описывать в документации, чтобы следующий (несчастный), которому придется просматривать код, мог понять, почему вы сделали именно так. Также имеет смысл явно указывать используемый режим. При использовании прав вызывающего вы задаете конструкцию AUTHID, а при использовании прав создателя — обычно нет. Если вы предполагаете использовать оба режима, указывайте AUTHID DEFINER для процедур с правами создателя. Таким образом, вы покажете, что сделали обдуманный выбор того или иного режима.
Построение пакетов Следующий важный элемент проектирования — построение пакетов. Как обсуждалось в главе 2, "Объедините все в пакет", пакеты состоят из двух частей: спецификации и тела. Помимо двух частей, есть две возможности доступа: общая и приватная. Очень важно помнить, что пользователь с привилегией EXECUTE для пакета имеет также привилегии EXECUTE ДЛЯ всех общедоступных процедур и функций в пакете. Привилегия также дает доступ на чтение-запись к любым переменным, объявленным в спецификации. Будьте внимательны при построении пакета и убедитесь, что все, кто получил привилегию EXECUTE, нуждается в привилегии EXECUTE на все общедоступные процедуры и функции. Спецификация пакета определяет, что доступно при выполнении или описании PL/SQL-пакета пользователям. Все, что не указано в спецификации, считается приватным. Команда DESCRIBE ДЛЯ пакета выдает только процедуры и функции в спецификации пакета. scott@KNOX10> CREATE OR REPLACE package sample_package 2 as 3 pv_this_is_public varchar2(6) := 'Hello'; 4 function a_function return varchar2; 5 procedure ajprocedure; 6 end; 7 / Package created. scott@KNOX10> scott@KNOX10> describe sample_package FUNCTION A_FUNCTION RETURNS VARCHAR2 PROCEDURE A PROCEDURE
Пакеты для защиты
411
Обратите внимание, что переменные пакета, объявленные в спецификации, не представлены в результатах команды DESCRIBE. ИХ, однако, можно увидеть при просмотре кода2. scott@KNOX10> s e l e c t t e x t from user_source 2 where name = 'SAMPLE_PACKAGE' order by l i n e ;
TEXT package sample_package as pv_this_is_public varchar2(6) := 'Hello'; function a_function return varchar2; procedure a_procedure; end; б rows selected.
Как правило, переменные должны быть приватными. Если с ними надо работать, часто лучше создать соответствующие функции для "получения" и "установки" значений. Любую процедуру, которую не будут вызывать непосредственно, не следует объявлять в спецификации пакета. Это позволяет скрывать детали реализации и особенности функционирования защиты от пользователей. Они не смогут увидеть или даже узнать о существовании ваших вспомогательных функций, если только не выполнят запрос к словарю данных, но, к счастью, для этого им необходимо владеть пакетом или иметь системную привилегию EXECUTE ANY PROCEDURE. В общем, с точки зрения защиты, мы всегда пытаемся получить среду с минимальными привилегиями: пользователь имеет только те привилегии, которые необходимы для выполнения его работы, и не более. Однако во многих случаях, помимо защиты, существенны и другие факторы, такие, как производительность или управляемость. С точки зрения производительности и управляемости, имеет смысл попытаться большую часть обработки поместить в пакет. Проблема с пакетами состоит в том, что как только у пользователя есть привилегия EXECUTE ДЛЯ пакета, он имеет привилегию EXECUTE ДЛЯ всех входящих в него процедур. Следовательно, надо убедиться, что все процедуры и функции в спецификации пакета необходимы для всех пользователей с привилегиями EXECUTE. Иначе нарушается модель минимальных привилегий. Это, очевидно, противоречивое требование, но часто существуют две группы пользователей, одной из которых требуется один набор процедур, другой — некоторые процедуры из этого набора и еще несколько новых. Было бы неправильно создать один пакет со всеми этими процедурами, а затем предоставить привилегии EXECUTE на него обеим группам пользователей. Итак, при проектировании приходится учитывать количество и состав пакетов. Разбиение кода на пакеты и распределение привилегий часто фактически можно выполнить несколькими способами. В следующем примере один пакет нужен груп2
Подробнее об этом см. в разделе "Защита исходного кода" настоящей главы.
412
Глава 8
пе пользователей Alpha. Отдельной группе пользователей, Beta, нужен доступ к части процедур, необходимых пользователям Alpha, а также к ряду других процедур. system@KNOX10> create user alpha identified by a; User created. system@KNOX10> create user beta identified by b; User created. system@KNOX10> system@KNOX10> conn scott/tiger Connected. scott@KNOX10> create or replace package alpha_package 2 as 3 PROCEDURE PI; 4 PROCEDURE P2; 5 PROCEDURE P3; 6 FUNCTION Fl return number; 7 FUNCTION F2 return number; 8 end; 9 / Package created. scott@KNOX10> grant execute on alpha_package to alpha; Grant succeeded. scott@KNOX10> scott@KNOX10> create or replace package beta_package 2 as 3 PROCEDURE P3; 4 PROCEDURE P4; 5 PROCEDURE P5; 6 FUNCTION Fl return number; 7 FUNCTION F3 return number; 8 end; 9 / Package created. scott@KNOX10> grant execute on betajoackage to beta; Grant succeeded. Реализация пакета BETA_PACKAGE может просто делегировать свои вызовы пакету ALPHA_PACKAGE. scott@KNOX10> c r e a t e or replace package body betajpackage 2 3
as PROCEDURE P3
Пакеты для защиты 4 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
as
413
begin ALPHA_PACKAGE.P3; end;
FUNCTION Fl return number as begin return ALPHA_PACKAGE.Fl; end; PROCEDURE P4 as begin null; end; PROCEDURE P5 as begin null; end; FUNCTION F3 return number as begin return 1; end; end; /
Package body created.
Этот проект проиллюстрирован на рис. 8.2. Alpha_Package
Пользователи Alpha Рис. 8.2. Реализация пакетов Alpha и Beta
Beta_Package
audit execute on scott.beta_package by access;
Audit succeeded.
Помимо прочего, представленная реализация должна дополняться следующими требованиями: > не дублируйте код в обоих пакетах. В противном случае любые изменения в процедуре РЗ И функции F1 придется выполнять в обоих пакетах. Это плохой стиль программирования; > не предоставляйте привилегии на оба пакета любому или всем пользователям. Это нарушает принцип минимальных привилегий; > не создавайте единый пакет со всеми процедурами из обоих пакетов. Это тоже нарушает принцип минимальных привилегий. Учтите, что вложенные вызовы могут понизить производительность, так что не заходите слишком далеко, не протестировав сначала проект!
Схемы, везде схемы... Один из наиболее важных вопросов — какие объекты и в какие схемы помещать. Вам потребуется принять решение о том, куда помещать: > объекты данных; > процедуры; > пользователей. Эти решения принципиально важны для успешной (с точки зрения защиты) реализации. Было бы неправильным, например, собирать все вместе в одной схеме. Иными словами, не помещайте таблицы данных и процедуры, которые их обрабатывают, в схемы, к которым подключаются пользователи. В противном случае есть риск, что пользователи, случайно или намеренно, сделают с данными или процедурами то, что не следует делать. Мы весьма часто встречаем этот неправильный подход в Web-приложениях.
Пакеты для защиты
415
Связанное с предыдущим и очень важное требование, которое часто не выполняется, — пользователи не должны регистрироваться или подключаться от имени любой из учетных записей *SYS*: SYS, SYSTEM, CTXSYS, MDSYS, OLAPSYS, WKSYS И Т.Д. Это не пользовательские учетные записи — привилегии слишком широки. Такое требование распространяется и на другие привилегированные учетные записи приложений. Приложению может быть необходимо выполнять много действий. В связи с этим достаточно часто разработчики предоставляют приложению (которое в этом случае представлено схемой) роль DBA. Затем, в приступе борьбы с защитой, разработчики приложения позволяют пользователям подключаться к этой привилегированной схеме приложения. Проектировщик может думать, что приложение будет контролировать защиту, так что риска нет, но это неверная и очень плохая практика. При этом, с точки зрения защиты базы данных, возникает слишком много риска. Каждый пользователь, работающий с приложением, работает с привилегиями DBA. Следует задаться вопросом: "Как пользователи подключаются к базе данных?" Если они подключаются от своего имени к своим схемам — отлично; если же нет, может иметь смысл разобраться получше, чтобы убедиться, не сделал ли кто-то ошибку, позволив пользователям подключаться к учетной записи базы данных с избыточными привилегиями. Помните, что вы добиваетесь среды с минимальными привилегиями. Она часто теряется при создании приложений. Далее даны советы по созданию такой среды.
Трехуровневый подход Можно поместить объекты данных в одну схему, пользователей — в другую, а процедуры для обработки данных — в третью, как показано на рис. 8.3. Это очень консервативный подход, требующий наибольшего объема сопровождения (для него нужно создать больше всего схем), но он обеспечивает и наибольшую гибкость.
Рис. 8.3. Трехуровневый подход к проектированию схем
-р
update user_info 2 set image_url = 'http://funnypictures.com/img/tomCrusie.jpg' 3 where username = 'SCOTT 1 ; 1 row updated.
420
Глава 8
Когда пользователь SCOTT пытается взломать приложение и изменить фотографию пользователя BLAKE, триггер выявляет рассогласование и возбуждает исключительную ситуацию. scott@KNOX10> — триггер возбуждает ошибки для сеанса пользователя SCOTT scott@KNOX10> update user_info 2 set image_url = 'http://funnypictures.com/img/chewbacca.lg.jpg' 3 where username = 'BLAKE'; update user_info ERROR at line 1: ORA-20001: Unauthorized update! ORA-06512: at "SCOTT.USER_IMG_UPDATE_CHECK", line 5 ORA-04088: error during execution of trigger 1 SCOTT.USER_IMG_UPDATE_CHECK'
Будет легко дополнить показанный триггер так, чтобы при попытке взлома, подобной только что выполненной, соответствующему человеку посылалось уведомление по электронной почте. Можно использовать в триггере пакет уведомления вроде описанного в главе 7. Наш триггер может иметь приблизительно такой вид: CREATE OR REPLACE TRIGGER user_img_update_check BEFORE UPDATE OF image_url ON user info FOR EACH ROW DECLARE v_notification_msg varchar2(1000); BEGIN IF (sys_context('USERENV\ 'SESSIONJJSER1) != :new.username) then v_notification_msg := 'Unauthorized update to image URL from ' || sys_context('USERENV','SESSIONJJSER') || ' on ' || :new.username; send_mail(p_to=>'securityAdmin.yourCompany.com', p_message=>v_notification_msg); raise_application_error(-20001,'Unauthorized update!'); END IF; END; /
Теперь, когда м ы снова выполним изменение, вызывается процедура SEND E M A I L и посылается сообщение С текстом Unauthorized update to image URL from SCOTT on BLAKE.
Аудит Многим приложениям требуются средства аудита для отслеживания изменений данных и контроля действий пользователей. У сервера есть много стандартных и расширенных средств аудита, но иногда они не подходят для поставленных задач. Лучший пример — когда аудит необходим только в нерабочее время. Многие взломы происходят в нерабочее время, потому что при этом вероятность того, что хаке-
Пакеты для защиты
421
ра поймают по ходу, меньше. Нельзя сконфигурировать сервер так, чтобы аудит выполнялся только после пяти вечера по рабочим дням и в выходные. Однако можно создать собственные средства аудита, учитывающие время, контекст или данные, используя триггеры. Кроме того, можно выполнять аудит, вставляя данные в таблицу AUDIT И параллельно записывая их в файл в файловой системе. Не забывайте, что триггеры могут быть отключены и снова включены4, так что этот метод хотя и эффективен, но не на 100 процентов надежен. Может иметь смысл создать триггер как автономную транзакцию. Автономная транзакция позволяет выполнять транзакцию в транзакции. Обычно вставка, выполненная в триггере, будет отменена вместе с транзакцией, в которой она выполнена. Используя эту особенность, опытные взломщики могут получить необходимую информацию. Например, пользователь без привилегии SELECT на таблицу может косвенно получить информацию, не выполняя операторы SELECT. Следующий оператор, UPDATE, показывает, что у четырех сотрудников зарплата выше, чем у сотрудника BLAKE. Транзакция откатывается, чтобы не осталось следов, поскольку записанные триггером данные аудита тоже будут откачены. blake@KNOX10g> update emp set s a l = 0 2 where sal > 3 (select sal from emp 4 where ename = 'BLAKE'); 4 rows updated. blake@KNOX10g> — замести следы blake@KNOX10g> rollback; Rollback complete.
В следующем примере мы записываем данные аудита в таблицу с помощью триггера на изменение. Давайте сначала создадим нашу таблицу аудита. scott@KNOX10> create t a b l e aud_emp_tab ( 2 username varchar2(30), 3 action varchar2(6), 4 column_name varchar2(255), 5 empno number(4), 6 old_value number, 7 new_value number, 8 action_date date 9 ) 10 / Table created.
Затем создадим триггер для аудита изменений зарплат. Сделаем это с помощью автономных транзакций, что позволит отделить выполняемый в триггере аудит от остальной транзакции, вызвавшей срабатывание триггера. 4
Триггеры могут быть отключены только авторизованными пользователями.
422
Глава 8
scott@KNOX10> CREATE OR REPLACE TRIGGER aud_emp 2 BEFORE UPDATE OF SAL 3 ON EMP 4 FOR EACH ROW 6 DECLARE 7 PRAGMA. AUTONOMOUS_TRANSACTION; 8 9 BEGIN 10 INSERT into aud_emp_tab 11 values (sys_context('USERENV','SESSION_USER'), 12 'UPDATE', 13 'SAL', 14 :old.empno, 15 :old.sal, 16 :new.sal, 17 SYSDATE ); 18 COMMIT; 19 END; 20 / Trigger created.
Возвращаясь к нашей модели защиты Web-приложения, мы предполагаем, что приложение позволяет специалисту отдела кадров (HR) изменять значения зарплат и что сотрудник BLAKE работает в отделе кадров. Поэтому мы должны позволить сотруднику BLAKE изменять эти значения. scott@KNOX10> grant update(sal) on emp to blake; Grant succeeded.
Однако BLAKE не должен менять значение своей собственной зарплаты. К сожалению сотрудника BLAKE, приложение обеспечивает такую защиту. Однако если BLAKE успешно взломает Web-приложение (код здесь не показан), может быть выполнен следующий SQL-оператор. blake@KNOX10> update SCOTT.EMP set sal = 5000 where empno=7698; 1 row updated.
Автономная транзакция гарантирует, что запись аудита останется, даже если транзакция будет отменена. Это ценный способ выявления ситуаций, когда кто-то пытался что-то делать, но либо испугался, либо у него это не получилось. Мы продолжаем наш пример, откатывая транзакцию и проверяя таблицу аудита. blake@KNOX10> — заметаем следы blake@KNOX10> rollback; Rollback complete. blake@KNOX10> @conn scott/tiger
Пакеты для защиты
423
scott@KNOX10> col username format a8 scott@KNOX10> col column_name format al2 scott@KNOX10> select username, empno, old_value, new_value, 2 to_char(action_date, 'Mon-DD-YYYY HH24:MI:SS') Time 3
from aud_emp_tab;
USERNAME
EMPNO
BLAKE
7698
OLD_VALUE 2850
NEW_VALUE
TIME
5000 Oct-03-2003 18:23:34
1 row selected.
Реализация работает лучше всего, если код триггера вызывает ранее определенные процедуры. Это упрощает тестирование и отладку, поскольку не придется изменять таблицу, чтобы вызвать срабатывание кода. Кроме того, как уже отмечалось ранее, код будет лучше масштабироваться, поскольку SQL-операторы в процедуре кешируются, а в триггере — нет. Учтите также, что триггеры (и представления) работают только с правами создателя, так что любые необходимые в теле триггера привилегии должны быть предоставлены пользователю непосредственно, а не получены через роли. Может также иметь смысл записывать данные в файлы ОС, что поможет защитить их от (привилегированных) пользователей базы данных. Пакет UTL_FILE позволяет решить эту задачу, но это поможет только тогда, когда пользователи, от которых вы пытаетесь защитить записи аудита, не имеют доступа к каталогу ОС, указанному параметром UTL_FILE_DIR.
Наконец, замечание об операторах TRUNCATE: ОНИ не вызывают срабатывание триггеров. Поэтому можно легко удалить данные в обход триггера на удаление. Однако можно организовать аудит операторов TRUNCATE сервером.
Детальный аудит Хотя могут быть редкие случаи, когда стандартный или детальный аудит (finegrained auditing — FGA) не подходит, намного больше случаев, когда он является подходящим средством (вместо триггеров). При использовании механизма FGA, который появился в Oracle9/, мы просто используем пакет DBMS_FGA ДЛЯ установки условий аудита таблицы. Можно задать условие на определенный столбец таблицы, что позволяет контролировать весьма специфические запросы. При поступлении запроса, соответствующего условию, он регистрируется. Например, если мы хотим контролировать изменения в столбце SALARY таблицы ЕМР, МЫ можем просто использовать механизм FGA для автоматического получения требуемого результата. Кроме того, механизм FGA позволяет нам вызвать обработчик событий при наступлении события аудита. В следующем примере мы включаем детальный аудит любых операторов INSERT или UPDATE, затрагивающих столбец SAL (ЭТОТ пример будет работать только в версии 10g, поскольку механизм FGA в версии 9/ поддерживается только для оператора SELECT). system@KNOX10> begin 2 DBMS FGA.ADD POLICY(object schema => 'SCOTT',
424
Глава 8 3 4 5 6 7 8 9 10 11 end; 12 /
object_name => 'EMP', policy_name => 'EMP_INS_UPD', audit_condition => '1=1', audit_column =>'SAL', handler_schema => NULL, handler_module => NULL, enable => TRUE, statement_types=> 'INSERT,UPDATE');
PL/SQL procedure successfully completed.
Пользователь BLAKE выполняет то же самое изменение, что и раньше, включая оператор отката. blake@KNOX10> update SCOTT.EMP set sal = 5000 where empno=7698; 1 row updated. blake@KNOX10> — заметаем следы blake@KNOX10> rollback; Rollback complete.
Выполнив запрос к таблице аудита, мы можем получить определенную информацию о выполненном действии. system@KNOX10> s e l e c t db_user, statement_type action, 2 object_schema schema, object_name object, 3 to_char(timestamp, 'Mon-DD-YYYY HH24:MI:SS') Time, 4 SQL_TEXT 5 FROM s уs.dba_fga_audi t_t rail; DB_USE ACTION
SCHEMA OBJE TIME
SQL_TEXT
BLAKE UPDATE SCOTT EMP Oct-03-2003 18:58:36 update SCOTT.EMP set sal = 5000 where empno=7698
Обратите внимание, что, как и в примере с автономной транзакцией, действие было отражено в таблице аудита, хотя пользователь выполнил откат. Единственное отличие — в информации, которую позволял получить триггер по сравнению с механизмом детального аудита: новое и старое значения в строке. Если бы механизм аудита регистрировал старые и новые значения, журналы аудита, вероятно, заполнились бы за несколько минут. Для получения этой информации с помощью механизма аудита записи аудита надо скомбинировать со старыми и новыми значениями из журналов повторного выполнения. Встроенный механизм аудита имеет много преимуществ. Во-первых, не требуется дополнительный код. Во-вторых, этот режим аудита менее подвержен ошибкам (при использовании аналогичного решения на базе триггера кто-то может, например, отключить триггер для отладки, а затем забыть снова включить его). Наконец,
Пакеты для защиты
425
хотя это и не реализовано в нашем примере, механизм детального аудита позволяет вызывать обработчик событий, а значит, как и в прежнем примере с триггером, мы можем поднять тревогу и послать сообщение по электронной почте при выявлении любых подозрительных действий.
Триггеры на регистрацию: первая линия защиты Мы кратко представили триггеры на регистрацию в главе 6 и еще раз более детально рассмотрим их здесь, поскольку это — один из наиболее интересных типов триггеров с точки зрения защиты. Одним из важнейших свойств триггеров на регистрацию является их прозрачность для любого приложения, обращающегося к базе данных. Эти триггеры имеют различные приложения в области защиты, и мы изучим некоторые наиболее популярные.
Содействие защите Триггеры на регистрацию широко используются как удобное средство инициализации или установки определенных значений в базе данных. С точки зрения защиты, триггеры на регистрацию обязаны своей популярностью, в основном, предоставляемой ими возможности настройки контекста приложения, который может затем использоваться в других местах. Сервер Oracle позволяет пользователям создавать контексты приложений. Они, определяемые пользователем, представляют собой пары имя-значение, которые могут устанавливаться в памяти для отдельных сеансов. Манипулировать контекстом приложения можно только одной PL/SQLпрограммой, которая задается при создании контекста приложения. Обращаться к контексту приложения могут несколько пользователей, и каждый получит отдельные значения для своего сеанса. Затем мы можем использовать эту информацию в журналах аудита (или для реализации специализированного контроля доступа для каждого сеанса). Обычно при регистрации триггер срабатывает и устанавливает контекст приложения для пользователя, вызывая процедуру DBMS_SESSION.SET_CONTEXT. Эта процедура позволяет устанавливать различные компоненты информации о пользователе, включая регистрационное имя, имя приложения и т.д. Сервер Oracle также предоставляет стандартный контекст приложения. Он включает пространство имен USERENV. Значения большинства атрибутов автоматически устанавливаются сервером. Мы применяем один из тех атрибутов, которые автоматически не устанавливаются. В нашем примере будет использован этот стандартный контекст приложения, который включает атрибут CLIENT_DENTIFIER. При регистрации мы будем записывать в атрибут CLIENT_DENTIFIER определенную информацию о среде с помощью процедуры DBMS_SESSION.SET_IDENTIFIER. DBMS_SESSION.SET_IDENTIFIER(client_id VARCHAR2);
Эта процедура принимает строку типа VARCHAR2 , для которой нет значения по умолчанию. Его можно получить в любой момент с помощью функции SYS_CONTEXT. SYS_CONTEXT('userenv', 'client_identifier')
Значение CLIENT_IDENTIFIER регистрируется в журналах аудита, так что с помощью этого атрибута мы можем расширить возможности аудита сервера. В этом при-
426
Глава 8
мере наш триггер на регистрацию будет добавлять IP-адрес и имя клиентской программы В CLIENT_IDENTIFIER. sys@NH101> c r e a t e o r r e p l a c e t r i g g e r s e t _ c l i e n t _ i d 2 a f t e r logon on d a t a b a s e 3 DECLARE 4 ljrrodule v$session.module%type;
5 BEGIN 6 select upper(module) into l_module 7 from v$process a, v$session b 8 where a.addr = b.paddr 9 and b.audsid = userenv('sessionid'); 10 dbms_session.set_identifier(sys_context('userenv','ip_address') 11 I I ':' I I 12 nvl(l_module,'No Module Specified')); 13 END; 14 / Trigger created.
Этот триггер создан пользователем SYS, поскольку он запрашивает защищенные представления, которые доступны пользователю SYS ИЛИ роли DBA. Поскольку триггер работает с правами создателя, роль DBA отключена, поэтому пользователь SYSTEM не мог бы создать этот триггер, не получив от SYS привилегии SELECT непосредственно. Предупреждение Как IP-адрес, так и имя клиентской программы может быть подделано профессиональным хакером. Это не означает, что мы использовали неправильный метод, просто его (как и практически все остальные) может обойти целеустремленный и опытный атакующий
Затем мы добавляем правила детального аудита для регистрации действий с таблицей ЕМР. Используется механизм FGA, поскольку мы будем анализировать столбец SQL_TEXT в журнале аудита, который не поддерживается стандартным механизмом аудита Oracle 9/. Аудит обычно выполняется привилегированной учетной записью, поэтому следующий пример выполняется от имени учетной записи SYSTEM. system@NH101> begin 2 DBMS_FGA.ADD_POLICY(object_schema => 'SCOTT 1 , 3 object_name => 'EMP', 4 policy_name => 'EMP_TRIG_AUD', 5 audit_condition => '1=1', 6 audit_column => NULL, 7 handler_schema => NULL, 8 handlerjnodule => NULL, 9 enable => TRUE, 10 statement_types => 'SELECT,INSERT,UPDATE,DELETE'); 11 end; 12 / PL/SQL procedure successfully
completed.
Пакеты для защиты
427
Было выполнено три запроса к таблице ЕМР ИЗ трех разных программ, работающих на трех разных машинах, после чего мы запрашиваем журнал аудита. Мы используем для форматирования результата утилиту PRINTJTABLE Тома Кайта, которая работает с правами вызывающего. system@NITEHAWK> declare 2 l_aud_str varchar2(256) ; 3 begin 4 l_aud_str := 'select db_user, client_id, ' || 5 'userhost, substr(sql_text,1,50) SQL, 'II 6 'timestamp day, to_char(timestamp,''HH24:MI:SS'') time 7 'from sys.dba_fga_audit_trail ' || 8 'where object_schema = ''SCOTT'1 and ' || 9 'obj ect_name = ''EMP' " ; 10 print table(l_aud_str); 11 end; 12 / SCOTT DBJJSER CLIENT_ID 141.144.98.80:EXCEL.EXE USERHOST US-ORACLE\DKNOX-PC SQL E M P . J O B , EMP. MGR, EMP DAY 09-NOV-03 TIME 14:57:52 DBJJSER CLIENT_ID USERHOST SQL DAY TIME
SCOTT 127.0.0.1:SQLPLUS@NIGHTHAWK (TNS VI-V3) nighthawk select ename from emp order by sal desc 09-NOV-03 14:57:31
DBJJSER CLIENT_ID USERHOST SQL DAY TIME
SCOTT 141.144.98.80:TOAD.EXE US-ORACLE\DKNOX-PC SELECT rowid, "SCOTT"."EMP".* FROM "SCOTT"."EMP" 09-NOV-03 14:57:44
PL/SQL procedure successfully completed.
Результаты запроса показывают не только, кто выполнил запрос, но также когда, как и откуда это было сделано. Обратите внимание, что возможность контролировать операторы INSERT, UPDATE И DELETE С ПОМОЩЬЮ механизма детального аудита появилась только в сервере Oracle 10g. Журнал аудита может помочь определить, произошел ли "инцидент" из-за ошибки приложения, ошибки пользователя или имел место продуманный и намеренный взлом. Помните, что поскольку права на выполнение пакета DBMS_SESSION предоставлены роли PUBLIC, есть риск, что пользователь мог переустановить идентификатор
428
Глава 8
клиента. Вы можете защититься от этого, придерживаясь принципа минимальных привилегий и скрывая исходный код, как будет вскоре описано.
Предотвращение регистрации
Если вы писали когда-то триггер на регистрацию, то уже на собственном печальном опыте наверняка знаете, что если триггер на регистрацию завершается сбоем, пользователь не может зарегистрироваться. Это согласуется с действием табличных триггеров, в которых в случае сбоя текущая транзакция откатывается. Исключение из этого правила составляют пользователи с ролью DBA или SYSDBA, которые будут зарегистрированы. Обычные пользователи, не администраторы, будут отключены. Хотя сбой обычно происходит из-за ошибки программирования или в результате непредусмотренной ситуации, например, при вызове в коде процедуры, которая была удалена, это простое правило может помочь при реализации защиты. Вот простой пример. Мы хотим гарантировать, что пользователи не подключаются к базе данных из среды Excel. Нам надо отсекать не только EXCEL.EXE;, НО И программу MSQRY32, поскольку она используется Excel при организации ODBC-соединения. Если пользователь регистрируется с помощью Excel, мы вызывает н триггере сбой и отключаем пользователя. Следующий триггер не скомпилируется, если только пользователь не получил непосредственно (не через роль) привилегии на представления V$PROCESS И V$SESSION. ДЛЯ простоты этот пример выполняется от имени пользователя SYS. sys@NH101> create or replace trigger user_logon_module_check 2 after logon on database 3 DECLARE 4 l j n o d u l e v$session.module%type; 5 BEGIN 6 select upper(module) into l_module from v$process a, v$session b 7 where a.addr = b.paddr 8 and b.audsid = userenv('sessionid'); 9 IF ( ljnodule = 'EXCEL.EXE' OR 10 l_module - 'MSQRY32.EXE') THEN 11 raise_application_error(-20001,'Unauthorized Application'); 12 END IF; 13 END; 14 / Trigger created.
При попытке регистрации из Excel происходит ошибка. Пользователь может, однако, подключиться из другого приложения, такого, как SQL*Plus. Благодаря прозрачности триггеров на регистрацию, они стали естественным методом инициализации идентификатора клиента и пользовательских контекстов приложений.
Защита исходного кода При компиляции PL/SQL в базе данных сервер сохраняет как исходный код, так и скомпилированный, или объектный, код. Сохраненный исходный код можно использовать двумя способами:
Пакеты для защиты
429
> для перекомпиляции кода, если он окажется недействительным; > для получения исходного кода, чтобы выяснить, что делает выполняемый код. Второе может пригодиться при исправлении и проверке кода. Однако существует множество причин, по которым вы можете не хотеть, чтобы кто-то видел ваш исходный код. Среди наиболее важных причин — права интеллектуальной собственности и коммерчески ценная информация. Попадание кода в руки конкурента в таком случае нежелательно. Это также может снизить вашу значимость в организации, когда все поймут, как реализованы ваши алгоритмы защиты или обработки данных! В других случаях может быть желательно просто предотвратить изменение вашего кода другими пользователями. Хотя их намерения могут быть хорошими, они могут (и часто будут) ломать код, а затем обращаться в службу поддержки с утверждениями, что "код не работает". Конечно, эта проблема становится критичной, если на языке PL/SQL решаются какие-то задачи, связанные с защитой. Большинство из вас согласится, что доступ к исходному коду может помочь кому-то разобраться, что делает код, и это позволит найти способ обмана или обхода защиты. Есть несколько способов защиты PL/SQL-кода. Во-первых, и, прежде всего, сервер не дает пользователям просматривать исходный код, который они не имеют права выполнять. Проблема в том, что когда пользователь получает привилегии EXECUTE, он может выполнить запрос к представлению ALLSOURCE ДЛЯ получения исходного кода процедур и функций. С учетом этого давайте теперь обсудим, как скрыть PL/SQLкод, чтобы предотвратить его просмотр.
Просмотр исходного кода процедур и функций Прежде всего надо побеспокоиться о процедурах и функциях. Если пользователь имеет привилегию EXECUTE ДЛЯ процедуры или функции, он может получить весь ее исходный код. С пакетами ситуация другая, поскольку привилегии EXECUTE ДЛЯ пакетов позволяют пользователю просматривать только их спецификации. Следующий пример продемонстрирует потенциальные риски. Простая процедура создана в одной схеме, а привилегия EXECUTE предоставлена другому пользователю. scott@KNOX10> create or replace procedure MY_PROC 2 as 3 v_local_var varchar2(30) := 'This is a secret string'; 4 begin 5 — Это исходный код. Хорошо бы сделать так, 6 — чтобы пользователи не видели исходный код. 7 null; — Это — секретная часть 8 end; 9 / Procedure created.
430
Глава 8
scott@KNOX10> grant execute on my_proc to blake; Grant succeeded.
Подключаясь как BLAKE, МЫ ВИДИМ, ЧТО ЭТОТ пользователь не только может успешно выполнить процедуру, но и получить ее исходный код из словаря данных. blake@KNOX10> exec scott.my_proc PL/SQL procedure successfully completed. blake@KNOX10> col t e x t format a65 blake@KNOX10> s e l e c t t e x t from all_source where name='MY_PROC' order by line; TEXT
procedure MY_PROC as v_local_var varchar2(30) := 'This is a secret string'; begin — Это исходный код. Хорошо бы сделать так, — чтобы пользователи не видели исходный код. null; — Это — секретная часть end; rows selected.
Как видите, надо внимательно следить за тем, какая информация доступна в процедурах и функциях. Помните также, что комментарии в коде, хотя помогают и обычно рекомендуются, могут выдавать слишком много информации, если обрабатываются секретные данные.
Сокрытие кода Одно простое решение проблемы — поместить вызов процедуры, выполняющей секретные действия, в другую процедуру, к которой у пользователя есть доступ. В следующем примере надо защитить содержимое процедуры MY_SECRET PROC. КОД защищается или скрывается процедурой MY_PROC. Процедура MY_PROC просто делегирует свои действия процедуре MY_SECRET_PROC. scott@KNOX10> create or replace procedure MY_SECRET_PROC 2 as 3 v_local_var varchar2(30) := 'This is a secret string'; 4 begin 5 — Это исходный код. Хорошо бы сделать так, 6 — чтобы пользователи не видели исходный код. 7 null; -- Это — секретная часть 8 end; 9 / Procedure created.
Пакеты для защиты
431
scott@KNOXlO> scott@KNOX10> create or replace procedure MY_PROC 2 as 3 begin 4 MY_SECRET_PROC; — вызываем реальный код 5 end; 6 / Procedure created.
Процедура MY_PROC создана в режиме прав создателя, чтобы предотвратить просмотр исходного кода, который мы пытаемся скрыть, выполняющим код пользователем. Пользователь BLAKE выполняет секретную процедуру через процедуру MY_PROC. blake@KNOX10> exec scott.my_proc PL/SQL procedure successfully completed.
Поскольку он может ее выполнять, то может и просматривать исходный код. blake§KNOX10> col text format a65 blake@KNOX10> select text from all_source where name='MY_PROC' order by line; TEXT procedure MY_PROC as begin MY SECRET PROC; end!
—
вызываем реальный код
5 rows selected.
Однако он больше не может получить секретную информацию, и исходный код процедуры MY_SECRET_PROC защищен. blake@KNOX10> select text from all_source where name='MY_SECRET_PROC' order by line; no rows selected
В качестве альтернативы этому методу сокрытия кода можно создать процедуру как часть пакета. При этом пользователи с привилегиями EXECUTE ДЛЯ пакета не смогут просматривать его реализацию. Относясь к сокрытию кода таким способом с уважением, следует учитывать два важных момента: > код не скрывается от всех. Если вас беспокоят права интеллектуальной собственности, этот способ ваши проблемы не решит; > вызов процедур из других процедур снижает производительность. Требуется больше памяти и процессорного времени.
432
Глава 8
Исходный текст пакета Как уже обсуждалось, пакеты состоят из двух частей: спецификации и тела пакета. Спецификация, или заголовок пакета, как его иногда называют, определяет, что общедоступно для пользователей, которые выполняют или получают информацию о PL/SQL-пакете. Все, что не указано в заголовке, считается приватным. Приватные процедуры, функции и переменные нельзя выполнять или использовать. Пользователи с привилегиями выполнения для пакетов могут запрашивать исходный код этих пакетов из словаря данных. Существенное отличие здесь в том, что они не получат исходный код тела пакета. Это принципиально важно и помогает поддерживать безопасную среду. Единственное исключение делается для пользователей с привилегией EXECUTE ANY PROCEDURE. Часто в качестве вспомогательного метода обеспечения защиты используют специально "неочевидные" имена процедур, пакетов, функций, параметров и переменных. Хотя этот метод и менее эффективен, чем другие приемы, он подходит для использования в случаях, когда известно, что исходный код будет доступен пользователям, но необходимо усложнить понимание особенностей его работы по интерфейсу. Я встречал даже случаи, когда имена и комментарии намеренно были неправильными и сбивающими с толку, чтобы направить пользователей по ложному пути. К этому методу имеет смысл прибегать только после реализации всех базовых принципов защиты, таких, как минимальность привилегий, хорошее моделирование и т.д. Проблема в том, что усложняется сопровождение кода кем-либо, кроме исходного разработчика.
PL/SQL-утилита Wrap Корпорация Oracle давно поняла важность защиты исходного кода и предоставила для решения этой проблемы утилиту WRAP. Эта утилита весьма просто скрывает код (а не создает для него "обертку"), преобразуя его в вид, который (на первый взгляд) кажется бессмысленным. Однако сервер может прочитать этот скрытый код, скомпилировать и выполнить его. При просмотре исходного кода в словаре данных ваши пользователи, вероятно, не смогут понять, что в действительности делает код. В документации Oracle, в руководстве "PL/SQL User's Guide and Reference 9.2", перечислены следующие ограничения:
"Строковые литералы, числовые литералы, имена переменных, таблиц и столбцов остаются в измененном файле в явном виде. Сокрытие текста процедуры с помощью wrap помогает скрыть алгоритм и предотвратить его воспроизведение, но этот способ не подходит для сокрытия паролей или имен таблиц, которые необходимо сохранить в тайне". Это означает, что определенные задачи этой утилитой не решаются. Хотя код и изменяется (не путайте с пакетом DBMS_OBFUSCATION_TOOLKIT, который фактически выполняет шифрование), строковые литералы в нем встречаются. Например, давайте рассмотрим код процедуры MY_PROC. Изначально он будет загружаться из текстового файла с SQL-кодом.
Пакеты для защиты 433 scott@KN0X10> @my_proc_wrap_demo.sql Procedure created.
Просмотр исходного кода от имени привилегированного владельца позволяет получить все. scott@KNOX10> s e l e c t t e x t from user_source where name='MY_PROC' order by
TEXT procedure MY_PROC as v_local_var varchar2(30) := 'This is a secret string'; begin — Это исходный код. Хорошо бы сделать так, — чтобы пользователи не видели исходный код. null; — Это — секретная часть end; 8 rows selected.
Чтобы скрыть этот код, мы используем упомянутую выше утилиту командной строки WRAP, предлагаемую корпорацией Oracle. Она по указанному нами входному файлу строит скрытый выходной файл. Мы применим две версии этой утилиты — из Oracle 9 г и Oracle lOg. С:> wrap iname=my_proc_wrap_demo.sql PL/SQL Wrapper: Release 9.2.0.4.0 - Production on Mon Oct 06 19:00:51 2003 Copyright (c) Oracle Corporation 1993, 2001. All Rights Reserved. Processing my_proc_wrap_demo.sql to my_proc_wrap_demo.plb
Выходной файл создан автоматически. Его имя можно задать явно с помощью параметра ОЫАМЕ= @my_proc_wrap_demo.plb Procedure created. scott@KNOX10> select text from user_source where name='MY_PROC' order by ^•line; TEXT procedure MY_PROC wrapped 0 abed
434
Глава 8
abed abed abed abed abed abed abed abed abed abed abed abed abed abed 3 7 9200000 1 4 0 5 2 :e: 1MY_PROC: 1V_LOCAL_ VAR: 1VARCHAR2 130: IThis is a secret string: 0 0 0 17 2 0 9a b4 55 6a a3 aO 51 a5 lc 6e 81 bO 4f b7 a4 bl 11 68 4f Id 17 b5 17 2 0 3 17 18 lc 3e 24 28 2b 2c 34 39 23 20 45 47 4b 4d 59 5d 5f 60 69 17 2 0 b 0 :2 1 3 f 18 17 f If f 3 4 :2 3 :7 1 17 4 010:2 1 :8 3 7 :2 4 :7 1 6b
Пакеты для защиты
435
:3 О 1 :а О 12 1 : 8 0 2 : 2 0 12 1 3 :3 О е 0 5 3 :3 0 4 :2 О 3 6 8 :б 0 5 :4 0 с 9 а 10 0 2 :б О 7 11 :3 0 11 9 11 10 е f :б 0 12 :2 О 1 3 11 15 :3 0 14 12 16 :8 О Ь 4 :3 О 1 7 1 5 1 d 1 Ь 1 4 О 15 О 1 14 1 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 5 10 10 1 о
1 row selected.
Интересно, что из базы возвращена в итоге одна строка. Вы можете увидеть значение строковой переменной типа VARCHAR2. ЭТО может быть плохо не только для переменных, но и при использовании динамического SQL. Как утверждает документация, можно также увидеть имена других процедур, функций и таблиц. Это исправлено в версии Oracle 10g, как демонстрирует следующий пример. С:>wrap iname=my_proc_wrap_demo.sql PL/SQL Wrapper: Release 10.1.0.1.0- Beta on Mon Oct 06 19:05:00 2003 Copyright (c) 1993, 2003, Oracle.
All rights reserved.
Processing my_proc_wrap_demo.sql to my_proc_wrap_demo.plb
436
Глава 8
После загрузки полученного файла просмотр исходного кода дает нам следующий результат: scott@KNOX10> @my_proc_wrap_demo.plb Procedure created. scott@KNOX10> select text from user_source where name='MY_PROC' order by '••line;
TEXT
p r o c e d u r e MY_PROC wrapped aOOOOOO b2 abed abed abed abed abed abed abed abed abed abed abed abed abed abed abed 7 80 aa RcGafTHY40C+bHa3WU170KbmIFMwg5nnm7+fMr2ywFyl8F8opsBTjo521jeiVDTQnQOMrs91 C8jhL9oSVzmI3gT9EcVUMcVNB5GRb9WmzFHmUifAzCf051Khzl4nHRUUqaYy3Cpiw:LMhA+yHo D03Q0CplKfzB4Nempi4EHPM=
1 row
selected.
Строка теперь скрыта! Утилита wrap замечательна, поскольку скрывает код от любопытных глаз нечестных пользователей. Учтите, что если потребуется изменить исходный код, придется менять оригинал, снова скрывать его с помощью wrap и снова загружать скрытую версию в базу данных.
Резюме Окончательный успех или провал приложения может зависеть и от того, будет ли оно взломано. Неважно, насколько быстро работает приложение, как легко его использовать или какую ценность оно представляет для лиц, принимающих решения.
Пакеты для защиты
437
Если эти же возможности оно предоставит и хакеру, приложение могут счесть неудачным. Защита начинается с проекта. Для ее обеспечения необходимо понимать взаимосвязи между пользователями, программными единицами PL/SQL и объектами базы данных. Если следовать основным требованиям защиты, таким, как принцип минимальных привилегий, создать надежную и безопасную среду будет проще. Контроль доступа к PL/SQL-коду включает два аспекта. Первый — привилегии на его выполнение. Второй, не менее важный, — защита исходного кода. Комбинируя защиту с приемами, представленными в других главах этой книги, вы сможете создавать PL/SQL-приложения, успешные по всем критериям оценки.
I /\CXDCX
Zf
Пакеты для Web-приложений Разработчики могут выбирать для создания HTML-приложений язык Java, Active Server Pages, PHP, Perl и многие другие. Обычно эти HTML-приложения представляют динамическую информацию, извлекаемую из базы данных. А какой может быть лучший способ представления информации из базы данных, как ни средствами непосредственно сервера базы данных? Именно эту возможность и обеспечивает сервер Oracle с помощью набора инструментальных средств PL/SQL Web Toolkit. Это набор сложных PL/SQL-пакетов, которые можно использовать для генерации HTML-страниц в реальном времени. В этой главе мы рассмотрим следующие темы: > краткую историю развития и архитектуру PL/SQL Web Toolkit; > > > > >
базовые возможности PL/SQL Web Toolkit; использование "ключиков" (cookies) в Web-приложении; выгрузку файлов из браузера и их получение в приложении; управление таблицей через Web-приложение; выполнение HTTP-запросов из базы данных.
Основы PL/SQL Web Toolkit Набор инструментальных средств PL/SQL Web Toolkit ведет свою историю с начала 1990-х годов, когда один консультант Oracle придумал способ генерации динамических Web-страниц непосредственно из базы данных Oracle. Корпорация Oracle взяла эту раннюю работу и выпустила на ее основе программный продукт под названием Oracle Web Agent. Во всех последующих реализациях, включая Oracle Web Server 2.x, Web Applications Server 3.x, Oracle Application Server 4.x, Oracle9/ Application Server и Oracle Application Server lOg, цели и архитектура PL/SQL Web Toolkit остались теми же. С практической точки зрения фундаментальное назначение набора инструментальных средств PL/SQL Web Toolkit — обеспечить простую генерацию динамических Web-страниц из базы данных Oracle. Он используется вместе с HTTP-сервером Oracle и компонентом HTTP-сервера, который называется MOD_PLSQL И вызывает хранимые процедуры в базе данных Oracle, направляя результаты запрашивающему клиенту. Тогда как технологии создания сценариев, такие, как Active Server Pages, PHP и Perl, будут выбирать данные из базы данных Oracle и формировать страницы в отдельном процессе, PL/SQL Web Toolkit уникален тем, что может выполнять доступ к данным и формировать страницу полностью на сервере Oracle.
440
Глава 9
HTTP-сервер Oracle входит в состав сервера Oracle 9/, сервера Oracle lOg, а также в состав серверов приложений Oracle 9г Application Server и Oracle Application Server 10*.
Архитектура Архитектура, используемая при создании Web-страниц с помощью PL/SQL Web Toolkit, на удивление проста, как показано на рис. 9.1.
Рис. 9 . 1 . Архитектура PL/SQL Web Toolkit
Web-браузер HTML HTTP-сервер Oracle
Процесс генерации Web-страниц с помощью PL/SQL Web Toolkit можно описать следующим образом: 1. клиент выполняет запрос к HTTP-серверу Oracle из своего Web-браузера; 2. HTTP-сервер Oracle вызывает хранимую процедуру в базе данных Oracle от имени клиента; 3. хранимая процедура (или процедуры) заполняет внутренний буфер HTML-кодом и другими данными; 4. HTTP-сервер Oracle переправляет результаты из внутреннего буфера обратно клиенту, а на их основании клиентский Web-браузер будет отображать страницу. Откуда HTTP-сервер Oracle знает, что запрос требует обращения к определенной хранимой процедуре и в какой базе данных Oracle эта процедура находится? HTTPсервер Oracle и сервер приложений Oracle, основанные на HTTP-сервере Apache, используют модуль MOD_PLSQL для обмена данными между HTTP-сервером и базой данных Oracle. Примечание В прежних версиях набора инструментальных средств Web Toolkit, когда он входил в состав Web-сервера Oracle, отдельный модуль был связан с HTTP-сервером и назывался Webагентом или Web-''картриджем".
Модуль MOD_PLSQL может определить, к какой базе данных и как подключаться с помощью дескриптора аутентификации базы данных (Database Authentication Descriptor — DAD). Дескриптор DAD используется для поддержки информации о конфигурации, в том числе задает имя пользователя и пароль для подключения, строку TNS подключения к базе данных, тип аутентификации (простая аутентификация базы данных, единая регистрация и т.д.), управление состоянием сеанса PL/SQL, настройки для таблицы выгрузки/загрузки документов и другие.
Пакеты для Web-приложений
441
При выполнении запроса из модуля MOD_PLSQL К базе данных инициализируется буфер в памяти, который заполняется данными при выполнении процедуры, а в конце запроса модуль MOD_PLSQL читает содержимое этого буфера и направляет его обратно запрашивающему клиенту. Важное последствие такой архитектуры состоит в том, что данные не начинают направляться обратно клиенту, пока запрос не будет выполнен. Поэтому если вы вызываете хранимую процедуру, которая выполняется две минуты и возвращает 4 Мбайта данных, поток данных от модуля MOD_PLSQL запрашивающему клиенту вообще не начнет передаваться, пока буфер в памяти не будет заполнен четырьмя мегабайтами данных и не завершится запрос к серверу. Это не плохо, но, несомненно, неэффективно. Будем надеяться, что проблема будет решена в будущих версиях модуля MOD_PLSQL.
Конфигурирование дескрипторов DAD Конфигурирование дескриптора DAD может быть выполнено через интерфейс браузера к HTTP-серверу Oracle в составе сервера базы данных или сервера приложений Oracle, либо вручную путем редактирования файлов конфигурации HTTPсервера Oracle в части конфигурации модуля MOD_PLSQL. Имена, местонахождение и синтаксис этих файлов для HTTP-сервера Oracle в составе сервера приложений и сервера базы данных различны. Полные инструкции можно найти в руководстве "Oracle HTTP Server Administration Guide".
Синтаксис типичного запроса при использовании MOD_PLSQL имеет вид: http://:/pls//[.]
Компонент PLS в запросе сигнализирует HTTP-серверу Oracle, что запрос должен быть выполнен модулем MOD_PLSQL. Затем будет прочитана информация, соответствующая указанному дескриптору DAD, и установлено подключение к базе данных с использованием указанного имени пользователя и пароля. После этого будет выполнена PL/SQL-процедура, а ее результаты — отправлены запрашивающему клиенту, и HTTP-подключение к клиенту прекратится.
Резюме по пакетам Набор инструментальных средств PL/SQL Web Toolkit состоит из следующих PL/SQL-пакетов: > НТР — гипертекстовые процедуры: запись данных и HTML-тегов в выходной буфер; > HTF — гипертекстовые функции: то же, что и процедуры пакета НТР, НО вместо записи значения в выходной буфер они возвращают его; > OWA_COOKIE — утилиты для работы с "ключиками" HTTP: упрощение посылки и выборки значений ключиков у запрашивающего клиента; > OWA_IMAGE — управление картами изображений HTML: подпрограммы этого пакета помогают создавать и использовать карты изображений в HTML; > OWA_OPT_LOCK — утилиты для оптимистического блокирования: позволяют выявлять и предотвращать потерянные изменения в Web-приложениях;
442
Глава 9
>
OWA_PATTERN — утилиты для работы с шаблонами строк: обеспечивают поддержку регулярных выражений и других функций сопоставления строк; > OWASEC — средства защиты: позволяют создавать специализированные средства аутентификации; > OWA_TEXT —- различные строковые утилиты: особенно полезны при посылке произвольного количества элементов из HTML-формы; > OWAJJTIL — утилиты общего назначения: включают функции для получения значений из среды CGI, а также посылки значений в HTTP-заголовке.
Из перечисленных пакетов в этой главе мы рассмотрим пакеты НТР, HTF, OWA_COOKIE И OWA_UTIL.
Если вы установили PL/SQL Web Toolkit как часть HTTP-сервера Oracle по ходу установки сервера базы данных Oracle9/, спецификации и тела пакетов можно найти в каталоге $ORACLE_HOME/APACHE/MODPLSQL/OWA. При установке PL/SQL Web Toolkit соответствующие объекты обычно создаются в схеме SYS. Кроме того, привилегия EXECUTE предоставляется роли PUBLIC, И ДЛЯ каждого пакета в наборе PL/SQL, Web Toolkit создается общедоступный синоним. Очень важно, чтобы в каждом экземпляре базы данных было не более одного экземпляра пакетов PL/SQL Web Toolkit. Реализация и архитектура PL/SQL Web Toolkit такова, что при наличии нескольких экземпляров пакетов могут выдаваться некорректные результаты. Когда набор PL/SQL Web Toolkit установлен, любой пользователь базы данных, обладающий привилегией создания процедур и пакетов, сможет выполнить все примеры, приведенные в этой главе. Демонстрационного пользователя базы данных, SCOTT, который обычно есть в большинстве баз данных Oracle, более чем достаточно для выполнения всех примеров.
Тестирование из среды SQL*Plus Прежде чем мы начнем изучать базовые возможности набора инструментальных средств PL/SQL Web Toolkit, надо разобраться с парой процедур, которые будут интенсивно использоваться в примерах этой главы. Первая процедура — OWA_UTIL. SHOWPAGE. Помните, что обычно модуль MOD_PLSQL работает через процесс прослушивания HTTP-сервера Oracle и непосредственно отправляет данные запрашивающему клиенту. К счастью, есть процедура OWA_UTIL. SHOWPAGE, которая пригодится как для демонстрации, так и для отладки функциональных возможностей Web Toolkit, поскольку она позволяет просмотреть буфер памяти, используемый набором инструментальных средств Web Toolkit, и выдать его содержимое с помощью SQL*Plus. Эта процедура выдает содержимое буфера с помощью пакета DBMS_OUTPUT, поэтому для получения результата сначала надо выполнить команду- SET SERVEROUTPUT ON. Процедура OWA_UTIL. SHOWPAGE не имеет аргументов. Еще один PL/SQL-пакет, о котором надо знать, это пакет OWA. PL/SQL-пакет OWA используется для поддержки внутреннего буфера памяти, из которого читает данные модуль MOD_PLSQL. Однако перед использованием надо сначала проинициализировать переменные среды общего шлюзового интерфейса (Common Gateway Interface —
Пакеты для Web-приложений 443 CGI) для пакета. HTTP-сервер Oracle и модуль MOD_PLSQL автоматически инициализируют среду CGI для каждого запроса, поэтому как конечный разработчик вы вряд ли когда-либо будете вызывать методы этого пакета непосредственно. Однако при вызове пакетов Web Toolkit напрямую из среды SQL*Plus нам надо выполнять эту инициализацию самостоятельно. Следующая процедура OWAINIT ПОЗВОЛИТ сделать это (примеры в данной главе предполагают, что эта процедура в базе данных создана): create or replace procedure owainit as l_cgivar_name owa.vc_arr; l_cgivar_val owa.vc_arr; begin htp.init; l_cgivar_name(l) : - 'REQUEST_PROTOCOL'; l_cgivar_val(1) := 'HTTP'; owa. init_cgi_env ( num_params => 1, param_name => l_cgivar_name, param_val => l_cgivar_val ); end;
Кроме того, процедура инициализирует среду для настройки параметров протокола HTTP для клиентского запроса. Некоторые интересные последствия этого мы вскоре продемонстрируем. Если по какой-то причине вы не проинициализируете среду CGI для PL/SQLпакета OWA, ВЫ можете получить следующее сообщение об ошибке: SQL> exec htp.p('Oracle'); BEGIN htp.p('Oracle'); END;
ERROR at line 1: ORA-06502: PL/SQL: numeric or value error ORA-06512: at "SYS.OWA_UTIL", line 323 ORA-06512: at "SYS.HTP", line 860 ORA-06512: at "SYS.HTP", line 975 ORA-06512: at "SYS.HTP", line 993 ORA-06512: at line 1
Пакеты НТР и HTF Основное назначение процедур пакета НТР и функций пакета HTF — непосредственная запись данных в буфер Web Toolkit. Следующая процедура, НТР . Р, используется для записи значения первого аргумента во внутренний буфер и завершения этого значения символом новой строки, \п. SQL> set serveroutput on SQL> exec owainit;
444
Глава 9
PL/SQL procedure successfully completed. SQL> begin 2 htp.p('Ohio State'); 3 htp.p('Buckeyes'); 4 end; 5 / PL/SQL procedure successfully completed. SQL> exec owa_util.showpage; Content-type: text/html Content-length: 20 Ohio State Buckeyes PL/SQL procedure successfully completed.
После инициализации среды мы сделали два вызова НТР . р для печати содержимого аргумента в выходной буфер. Затем просмотрели результаты так, как их получил бы Web-браузер, но в SQL*PIus, с помощью процедуры OWAJJTIL. SHOWPAGE. Первая строка результата — его MIME-тип. Поскольку мы не задали MIME-тип, был сгенерирован стандартный, TEXT/HTML. Следующая строка содержит длину результата HTTP, после чего идут выданные нами строки. Примечание В версиях Web-сервера до появления Oracle HTTP-сервера строки Content-type и Contentlength генерировались и выдавались самим Web-сервером, а не пакетами PL/SQL Web Toolkit. Поэтому вы можете не увидеть эти строки при выполнении примеров в среде SQL*Plus.
PL/SQL-пакеты НТР И HTF содержат широкий набор различных методов генерации HTML-разметки в результатах. Предлагаются методы для создания HTML-таблиц и управления ими, форматирования текста, генерации фреймов, списков, форм, ссылок на изображения, якорей и многого другого. Для большинства типов HTML-тегов есть высокоуровневые процедуры в пакетах НТР и HTF. Например, следующий листинг показывает три анонимных PL/SQLблока, генерирующие один и тот же результат HTML (помните, что пробельные символы для HTML не имеют значения). — Пример 1 begin htp.tableOpen; htp.tableRowOpen; htp.tableHeader( cvalue => 'TheHeader', calign => 'left' ); htp.tableRowClose; htp.tableRowOpen; htp.tableData( cvalue => 'DataVal' ) ; htp.tableRowClose; htp.tableClose;
Пакеты для Web-приложений 445 end; exec owa_util.showpage; — Пример 2 declare l_str varchar2(32000); begin l_str = '
'; l_str - l_Str I I ''; l_str = l_str || 'TheHeader | '; l_str • l_str || '
'; l_str - l_str I I ''; l_str = l_str N 'DataVaK/TD>'; l_str = l _ s t r | | ' |
'; 1 str = 1 s t r | | '
'; htp.p( l _ s t r
);
end;
exec owa util.showpage; -- Пример 3 begin h t p . p (' TheHeader') ; h t p . p ('DataVal') • end; / exec
owa_util.showpage;
Вас может интересовать, зачем использовать методы пакетов нтр и HTF ДЛЯ генерации HTML? He будет ли более эффективно вызвать одну PL/SQL-процедуру вместо нескольких для генерации одного и того же результата? Да. Но различие по времени выполнения между этими двумя методами минимально даже при тысячах вызовов в одном запросе модуля MOD_PLSQL. Основное преимущество методов НТР И HTF — простота чтения и неявно простота сопровождения. Если мы захотим изменить значение данных в таблице в только что представленных примерах, будет очень легко изменить первый пример и весьма сложно — третий. Этот момент часто упускают из виду, но он существенно важнее незначительного повышения производительности.
Использование переменных среды В Web-приложении часто приходится использовать переменные среды. Их называют переменными CGI и используют для передачи информации между Web-сервером и серверным CGI-сценарием. При использовании PL/SQL Web Toolkit и модуля MOD_PLSQL стандартные переменные среды CGI 1.1 непосредственно доступны в программной единице PL/SQL (за исключением переменной QUERY_STRING). ДЛЯ получения значения определенной переменной CGI используется функция owa util.get_cgi env( param_name in varchar2 ) return varchar2
446
Глава 9
Функция для выдачи списка всех переменных среды CGI: owa_util.print_cgi_env return varchar2
Поэтому простая процедура PRINTENV, которая выдает все переменные среды CGI, будет иметь следующий вид: SQL> 2 3 4 5 6
create or replace procedure printenv as begin owa_util.print_cgi_env; end; /
Procedure created.
Обычные переменные среды CGI не инициализируются при вызове пакетов непосредственно из среды SQL*Plus. Поэтому процедура PRINTENV выдаст два весьма разных результата при вызове из SQL*Plus и из Web-браузера. Вот результаты вызова этой процедуры непосредственно из среды SQL*Plus: SQL> exec owainit; PL/SQL procedure successfully completed. SQL> exec printenv; PL/SQL procedure successfully completed. SQL> exec owa_util.showpage; REQUEST_PROTOCOL = HTTP PL/SQL procedure successfully completed.
Выдана только переменная среды CGI REQUEST_PROTOCOL. He случайно это именно та переменная среды, которая была установлена вручную при вызове процедуры OWAINIT, определенной ранее. При вызове той же процедуры через Web-браузер и подключении через дескриптор DAD от имени пользователя SCOTT выдается принципиально другой результат: REMOTEJJSER = scott WEB_AUTHENT_PREFIX = DAD_NAME - scott_dad DOC_ACCESS_PATH DOCUMENT_TABLE = PATH_INFO = /printenv SCRIPT_NAME = /pls/scott_dad PATH_ALIAS = REQUEST_CHARSET = WE8MSWIN1252 REQUEST_IANA_CHARSET = WINDOWS-1252 SCRIPT_PREFIX = /pis PLSQL GATEWAY = WebDb
Пакеты для Web-приложений 447 GATEWAY_IVERSION - 2 SERVER_SOFTWARE = Oracle HTTP Server Powered by Apache/1.3.22 (Win32) ^* PHP/4.2.3 mod_plsql/3.0.9.8.3b mod_ssl/2.8.5 OpenSSL/0.9.6b ^*mod_fastcgi/2.2.12 mod_oprocmgr/l.0 mod_perl/l.25 GATEWAY_INTERFACE = CGI/1.1 SERVER_PORT = 8 0 SERVER_NAME = jkallman-home REQUEST_METHOD = GET REMOTE_ADDR = 127.0.0.1 SERVER_PROTOCOL = HTTP/1.1 REQUEST_PROTOCOL = HTTP HTTP_USER_AGENT = Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; "••YComp 5.0.0.0) HTTP_HOST = 127.0.0.1 HTTP_ACCEPT » image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */* HTTP_ACCEPT_ENCODING = gzip, deflate HTTP_ACCEPT_LANGUAGE = en-us,de;q=0.8,ko;q=0.6,zh;q=0.4,ja;q=0.2
Здесь мы видим, что инициализируется полная среда CGI для каждого запроса через MOD_PLSQL. Вот некоторые из наиболее интересных переменных среды: > REMOTE_USER — имя пользователя, по которому аутентифицируется сеанс. В приведенном выше примере дескриптор DAD был сконфигурирован так, чтобы подключение к базе данных всегда выполнялось от имени пользователя SCOTT;
> REQUEST_METHOD — имя метода для текущего запроса, обычно — GET ИЛИ POST; > HTTP_USER_AGENT — подробная информация о браузере, с которого выполнен запрос. Помимо обычных переменных среды CGI/1.1, есть также ряд специфических переменных модуля MOD_PLSQL И Web Toolkit: > DADNAME — имя дескриптора DAD, используемого для текущего запроса; > WEB_AUTHENT_PREFIX — префикс для добавления к имени пользователя текущего сеанса перед аутентификацией. Его имеет смысл использовать, когда необходимо сконфигурировать класс пользователей базы данных исключительно для Web-аутентификации. Если пользователь аутентифицирован как LELLISON и префикс Web-аутентификации установлен равным WEB$, будет выполнена попытка аутентификации пользователя базы данных WEB$LELLISON; > DOCUMENTJTABLE — имя таблицы базы данных, в которую будут непосредственно выгружаться файлы через модуль MOD_PLSQL. Файлы, посланные методом POST через элемент HTML-формы типа , будут сохраняться в вьов-столбце этой таблицы. Обратите внимание, что есть возможность сохранять выгруженные документы в столбце типа LONG, НО использовать этот устаревший тип данных не рекомендуется; > REQUEST_CHARSET — набор символов базы данных Oracle на клиенте. При запросе через модуль MOD_PLSQL набор символов базы данных однозначно определяется конфигурацией дескриптора DAD;
448
Глава 9
> REQUEST_IANA_CHARSET — формальное имя набора символов IANA, полученное от запрашивающего браузера; > REMOTE_ADDR — IP-адрес клиента, выполняющего текущий запрос; > HTTPREFERER — Web-адрес, с которого был получен текущий запрос. Если пользователь щелкает на ссылку с одной Web-страницы на другую, значением переменной среды HTTP_REFERER будет идентификатор URL первой страницы. Возможность обращения к переменным среды CGI в PL/SQL-процедуре может пригодиться во многих ситуациях. Мы можем легко создать простой пакет регистрации использования путем чтения и сохранения значений переменных REMOTE_ADDR, REMOTEJJSER и HTTP_USER_AGENT. Пользователь, изменяющий или подделывающий значение URL, может быть выявлен путем проверки переменной среды HTTP_REFERER. Все переменные среды CGI, включая несколько специфических переменных Oracle, легко доступны с помощью процедуры OWA_UTIL.GET_CGI_ENV. Например, чтобы создать простую процедуру регистрации, которую можно использовать в PL/SQL-программе, надо сначала создать таблицу для сохранения определенной информации, как показано в следующем листинге: SQL> c r e a t e t a b l e web log( 2 log date date, 3 ip address varchar2(255), 4 user agent varchar2(4000), script name varchar2(4000), 5 6 path info varchar2(4000), 7 http_referer varchar2(4000)) / i Type created.
Осталось только создать процедуру LOG_IT, которая получает все значения соответствующих переменных среды CGI и вставляет их в таблицу WEB_LOG. SQL> create or replace procedure log i t 2 as 3 begin 4 insert into web log( 5 log date, 6 ip address, 7 user agent, 8 script name, 9 path_info, 10 http referer ) 11 values( 12 sysdate, 13 owa_util.get cgi env('REMOTE_ADDR' ), 14 owa util.get cgi env('HTTP USER AGENT' 15 owa util.get cgi env('SCRIPT_NAME' ), 1 16 owa util.get cgi env( PATH_INFO' ) , 17 owa util.get cgi env('HTTP_REFERER' ) 18 end;
Пакеты для Web-приложений 449 / Procedure created.
Помните, что процедура OWAINIT инициализирует среду CGI, но не устанавливает ни одну из этих переменных среды. Мы можем доказать это, выполняя следующую процедуру непосредственно из SQL*Plus:
[email protected]> exec owainit; PL/SQL procedure successfully completed.
[email protected]> exec log_it; PL/SQL procedure successfully completed. SQL>column ip_address format SQL>column user_agent format SQL>column script_name format SQL>column path_info format SQL>column http_referer format SQL>set linesize 120 SQL>select * from web_log; LOG_DATE
IP_ADDRESS
al5 alO word_wrapped alO word_wrapped alO word_wrapped alO word_wrapped
USER_AGENT SCRIPT_NAM PATH_INFO
HTTP_REFER
02-AUG-03
Только значение даты, определяемое функцией SYSDATE, заполнено в нашей регистрационной таблице. Все остальные переменные среды CGI при вызове имеют неопределенные значения. Однако, если вызвать процедуру LOG_IT через Oracle HTTP-сервер, мы увидим намного более интересные результаты. Давайте перепишем процедуру PRINTENV так, чтобы она вызывала нашу процедуру LOG_IT следующим образом: SQL> create or replace procedure printenv as 2 begin
3
owa util.print cgi env;
4
loglit;
~
"
5 end; 6 / Procedure created.
Затем мы удалим все прежние строки из таблицы WEB_LOG, вызовем процедуру PRINTENV из Web-браузера и снова выполним запрос к таблице следующим образом: SQL>delete from web_log; 1 row d e l e t e d . SQL>commit; Commit complete. —Вызываем через Oracle HTTP Server, SQL>select * from web_log; 15 Зак. 348
затем запрашиваем
450
Глава 9
LOG_DATE
IP_ADDRESS
02-OCT-03 127.0.0.1
USER_AGENT SCRIPT_NAM PATH_INFO Mozilla/4. /pls/comm 0 (compatibl e; MSIE 6.0; Windows NT 5.0)
HTTP_REFER
/printenv
Ключики Большинство разработчиков Web-приложений с определенным опытом знают, что ключики (cookies) — это данные, читаемые и записываемые в ходе выполнения стандартного HTTP-запроса и ответа. Они могут применяться для различных целей, включая контроль использования ресурса каждым посетителем за определенный период времени, поддержку уникальных идентификаторов для каждого пользователя для сохранения данных сеанса между HTTP-запросами, и даже для проверки того, что пользователь имеет право обращаться к определенному Web-приложению или ресурсу. Основные атрибуты ключика такие: > NAME — имя ключика; >• VALUE — значение ключика; > EXPIRES —дата устарев а нш ключгоа Клкниш мзгуг бьяь действительными продолжительный период времени (например, годы). Если вы хотите, чтобы ключик при запросе становился устаревшим, этот атрибут можно установить равным дате в прошлом; > PATH — полный адрес ресурса, для которого действителен ключик; по умолчанию — полный адрес документа, из которого сделан вызов; > DOMAIN — домен, для которого действителен ключик; по умолчанию — домен документа, из которого сделан вызов; > SECURE — булево значение, задающее безопасность передачи ключика Работа с ключиками в наборе PL/SQL Web Toolkit осуществляется с помощью пакета OWA_COOKIE. В этом пакете есть два основных метода, которые вам нужно знать, в частности, OWA_COOKIE.GET owa cookie.get(name in varchar2) return owa cookie.cookie И OWA_COOKIE.SEND: owa_cookie.send( name in varchar2, value in varchar2, expires in date default NULL, path in varchar2 default NULL, domain in varchar2 default NULL, secure in varchar2 default NULL )
Пакеты для Web-приложений
451
Ключик можно использовать для хранения скалярного значения на компьютере запрашивающего клиента. Если вы хотите сохранять на компьютере клиента несколько значений, можно посылать несколько ключиков или просто сформировать из нескольких значений одну строку и использовать ее в качестве значения одного ключика. Ключики можно применять для различных целей, от контроля использования приложения по поддержки ключика сеанса, показывающего, что текущий пользователь аутентифицирован вашим приложением. Вот простой пример: exec owainit; begin owa_util.mime_header('text/html', FALSE); owa_cookie.send('NAME','Brutus.Buckeye',sysdate+1); owa_util.http^header_close; htp.pCHello world'); end; /
При выполнении этого примера будет выдан следующий результат: SQL> exec owa_util.showpage; Content-type: text/html Set-Cookie: NAME=Brutus.Buckeye; expires=Sunday, 29-Jun-2003 04:11:52 GMT; Content-length: 12 Hello world
Если вы поместите этот код в процедуру и выполните ее через MOD_PLSQL ИЗ Webбраузера, конечный пользователь увидит только строку HELLO WORLD. Однако частью HTTP-заголовка в запросе станет директива SET-COOKIE. ЕСЛИ ТОЛЬКО пользователь может принимать ключики через Web-браузер, ключик с именем NAME И значением BRUTUS . BUCKEYE для стандартного домена и адреса будет записан в клиентский список. Обратите внимание, что MIME-заголовок был явно изменен в этом примере, а затем закрыт после вызова процедуры для работы с ключом. Как требуется НТТР-протоколом, в PL/SQL Web Toolkit все вызовы процедур OWA_COOKIE . GET И OWA_COOKIE . SEND должны выполняться в контексте HTTP-заголовка. Кроме того, HTTP-заголовок должен быть закрыт путем вызова процедуры OWA_UTIL.HTTP_HEADER_CLOSE перед вызовом любых методов пакета НТР. Важно учитывать, что ключики, установленные в HTTP-запросе, не будут доступны при немедленном выполнении процедуры GET В ТОМ же запросе. Метод OWACOOKIE.GET может обращаться к уже установленным у клиента ключикам, но не в том же запросе, в котором они устанавливаются. Значения ключиков, установленные с помощью вызовов OWA_COOKIE.SEND в одном запросе, не будут доступны для OWA_COOKIE . GET, пока не будет выполнен второй запрос.
Управление файлами Даже в утопическом мире Web-приложений и HTML современные пользователи все равно используют файлы для различных целей — от документов офисных при-
452
Глава 9
ложений до контроля исходного кода. Из-за этого многие приложения должны иметь возможность хранить эти файлы и управлять ими. Чаще всего для этого обеспечивается общий доступ к файлам операционной системы по сети, но было бы желательно обеспечить такие возможности средствами простого и эффективного Webприложения. К счастью, модуль MOD_PLSQL И набор инструментальных средств PL/SQL Web Toolkit включают простые в использовании средства для создания приложений для выгрузки и загрузки файлов, которые будут храниться непосредственно в базе данных Oracle. При конфигурировании настроек дескриптора DAD для модуля MOD_PLSQL МОЖНО задать определенные атрибуты таблицы документов, которые позволят использовать эти возможности выгрузки/загрузки. Речь идет о следующих атрибутах модуля MOD_PLSQL, связанных с доступом к документам: > таблица документов — имя таблицы, в которую будут выгружаться документы. Эта таблица должна быть доступна пользователю базы данных Oracle, от имени которого модуль MOD_PLSQL будет подключаться к Oracle. При необходимости это имя таблицы можно уточнить именем схемы, в которой содержится таблица; > путь доступа к документам — виртуальный путь, который будет использован вместе с дескриптором DAD и именем документа при загрузке файла (например, DOCS);
> процедура доступа к документам — PL/SQL-процедура для вызова модулем MOD_PLSQL в ответ на запрос загрузки файла с использованием соответствующего дескриптора; > расширения файлов для загрузки как LONG RAW — список расширений файлов (например, pdf, txt, hqx), которые будут сохраняться в столбце типа LONG RAW в таблице документов. Для новых приложений ни в коем случае не используйте устаревший тип данных LONG RAW. ТИП данных BLOB — более эффективная и функциональная замена для LONG RAW. Таблицей документов не может быть любая обычная таблица со столбцом типа BLOB. Таблица документов должна содержать определенные столбцы с конкретны-
ми именами и типами данных. Дополнительные столбцы, не связанные с выгрузкой/загрузкой файлов, в таблицу документов добавить можно, но, как минимум, следующие столбцы должны существовать, иметь указанные типы данных и минимальную длину: >
NAME: varchar2(256) unique not null;
>
MIME_TYPE: mime_type varchar2(128) ;
>
DOC_SIZE: number;
>
DAD_CHARSET: varchar2(128) ;
>
LAST UPDATED: date;
>
CONTENT_TYPE: varchar2(128);
>
BLOB CONTENT: blob.
Пакеты для Web-приложений
453
Одно из ограничений архитектуры таблицы документов модуля MOD_PLSQL И набора инструментальных средств PL/SQL Web Toolkit состоит в том, что для дескриптора DAD должна быть задана одна и только одна таблица документов. Поэтому если ваш сервер Oracle содержит несколько приложений, использующих один и тот же дескриптор DAD, придется использовать одну таблицу документов, если вы хотите выгружать файлы с помощью модуля MOD_PLSQL. Чтобы обойти это ограничение, придется либо использовать отдельный дескриптор DAD для каждого приложения, которому нужна своя таблица документов (что усложняет администрирование), либо реализовать средства копирования вьов-содержимого из таблицы документов дескриптора в таблицу конкретного приложения. Сначала создадим таблицу документов MYDOCS С ПОМОЩЬЮ операторов DDL. create table mydocs( id number primary key, name varchar2(256) not null, mime_type varchar2(128), doc size number, — dad_charset varchar2(128), last_updated date, content_type varchar2(128), blob_content blob ) create sequence mydocs_seq / create or replace trigger biu_mydocs before insert or update on mydocs for each row begin if :new.id is null then select mydocs_seq.nextval into -.new.id from dual; end if; end;
Затем сконфигурируем дескриптор DAD с MYDOCS В качестве таблицы документов. После этого легко можно будет создать простой Web-интерфейс для выгрузки документов на базе следующих двух листингов: create or replace procedure my_doc_listing( p_name in varchar2 default null ) as beqin htp.htmlOpen; htp.bodyOpen; if p_name is not null then htp.bold('Document ' I I p_name | | ' successfully uploaded!'); end if; htp.tableOpen;
454
Глава 9 htp.tableRowOpen; htp.tableHeader('Name'); htp.tableHeader('Size') ; htp.tableRowClose; for cl in (select id, name, doc_size from mydocs order by name) loop htp.tableRowOpen; htp.tableData( cl.name ); htp.tableDatat cl.doc_size ) ; htp.tableRowClose; end loop; htp.tableClose; htp.anchor('upload_doc','Upload a new document'); htp.bodyClose; htp.htmlClose; end;
Вот второй листинг. create or replace procedure upload_doc as begin htp.htmlOpen; htp.bodyOpen; htp.formOpen(curl => 'my_doc_listing', cmethod => 'POST', cenctype => 'multipart/form-data'); — Процедура для файла в наборе не предоставлена htp.p(''); htp.formSubmit; htp.formClose; htp.anchor('my_doc_listing','Document listing'); htp.bodyClose; htp.htmlClose; end; /
Процедура MY_DOC_LISTING будет генерировать HTML-таблицу, содержащую имена и размеры всех документов, выгруженных в таблицу документов MYDOCS. Процедура UPLOAD_DOC выдает простую HTML-форму для выгрузки файла. На этой странице пользователи могут выбрать файл из своей локальной файловой системы и выгрузить его непосредственно в базу данных, щелкнув на кнопке Submit. Обратите внимание, что цель этой формы — процедура MY_DOC_LISTING, как показано на рис. 9.2. Специальная процедура WPG_DOCLOAD.DOWNLOAD_FILE может использоваться для расширения простого Web-интерфейса так, чтобы он позволял также загружать содержимое таблицы документов.
Пакеты для Web-приложений
455
'Ш http://1 27.0.0.1/pls/scott_dad/upload_doc - Micro so.. File Edit View Favorites Tools Help v |3GO Links
Address llhttp://127.0.0.1/pls/scott_dad/4f)load_doc
4^j
|| Browse... 11 SubmitJ
Рис. 9.2. Форма для выгрузки документов
Document listing
id] Done
|
Ш I Internet
Некоторые разработчики реализуют загрузку файла путем выбора его содержимого в локальную переменную типа BLOB, применения функции UTL_RAW . CAST_TO_VARCHAR2 и процедуры НТР . PRN ДЛЯ чтения содержимого фрагментами, а затем — выдачи в буфер Web Toolkit. Мы не рекомендуем этот подход по двум причинам. Во-первых, как обсуждалось ранее, модуль MOD_PLSQL не отправляет данные клиенту по мере заполнения буфера. Фактически поток из модуля MOD_PLSQL клиенту не пойдет, пока не закончится выполнение вызова на сервере. Таким образом, если вы загружаете файл размером 1 Гбайт, пользователю не предложат выбрать каталог для загрузки, пока весь гигабайт содержимого не заполнит буфер Web Toolkit. Процедура WPG_DOCLOAD.DOWNLOAD_FILE начнет выдавать содержимое немедленно. Во-вторых, и это еще важнее, есть вероятность преобразования набора символов, особенно при работе с файлом, содержащим многобайтовые символы. Для успешной передачи двоичных данных независимо от настроек набора символов для базы данных или дескриптора DAD используйте следующую процедуру WPG_DOCLOAD. DOWNLOAD_FILE: create or replace procedure get_file( p_id in number ) as begin for cl in (select mime_type, blob_content, name from mydocs where id = p_id) loop — Настраиваем заголовки HTTP — owa_util.mime_header( cl.mime_type, FALSE ); htp.p('Content-length: ' || dbms_lob.getlength( cl.blob_content )); htp.p('Content-Disposition: inline ' ); owa_util.http_header_close; -- Затем передаем модулю mod_plsql содержимое BLOB wpg_docload.download_file( cl.blob_content ); — exit; end loop; end; / .
456
Глава 9
По идентификатору документа эта процедура построит MIME-тип в заголовке HTTP, а затем непосредственно направит содержимое клиенту. При наличии этой универсальной процедуры легко изменить исходную, MY_DOC_LISTING, интегрируя возможность загрузки непосредственно в отчет. Вместо выдачи имени документа в ячейке таблицы мы теперь будем генерировать HTML-ссылку для каждого выдаваемого документа. Это дает пользователю простой механизм не только для просмотра списка всех документов, но и для немедленной загрузки документа. create or replace procedure my_doc_listing( p_name in varchar2 default null ) as begin htp.htmlOpen; htp.bodyOpen; if p_name is not null then htp.bold('Document ' II p_name || ' successfully uploaded!'); end if; htp.tableOpen; htp.tableRowOpen; htp.tableHeader('Name'); htp.tableHeader(' S i z e ' ) ; htp.tableRowClose; for cl in
(select id, name, doc_size from mydocs order by name) loop htp.tableRowOpen;
htp.tableData( htf.anchor( 'get_file?p_id=' htp.tableData( cl.doc_size ); htp.tableRowClose; end loop;
||
cl.id,
cl.name));
htp.tableClose; htp.anchor('upload_doc','Upload a new document'); htp.bodyClose; htp.htmlClose; end; /
Используя всего три простые процедуры в базе данных Oracle, мы реализовали систему документооборота "для бедных", обеспечивающую выгрузку и загрузку документов через любой Web-браузер. Помните, что это всего лишь пример, абсолютно не обеспечивающий защиту. Таких "гостеприимных" процедур, как GET_FILE, которая позволяет обратиться к любому документу, хранящемуся в таблице документов, в производственных системах нужно избегать, если только не предприняты другие меры защиты.
Управление таблицами через Web Помимо чтения данных в таблицах Oracle и выдачи отчетов в Web-приложениях, с помощью набора инструментальных средств PL/SQL Web Toolkit также очень лег-
Пакеты для Web-приложений 457 ко создать полный набор процедур для вставки, редактирования и удаления данных. По сути, выполняемые при этом операции не отличаются от посылки пользователем операторов INSERT, UPDATE И DELETE ИЗ среды SQL*Plus. Но обеспечение доступа к такого рода операциям через Web намного упрощает доступ к данным. Давайте начнем с создания таблицы для управления информацией о наших любимых акциях и фондах взаимопомощи. create table my_investments( ticker varchar2(10) name varchar2(4000) type varchar2(20)
primary key, not null, not null );
Затем, как и в примере в предыдущем разделе, создадим простой отчет. create or replace procedure investment_rpt as l_count number := 0; begin htp.htmlOpen; htp.bodyOpen; htp.tableOpen; htp.tableRowOpen; htp.tableHeader('Ticker'); htp.tableHeader('Name'); htp.tableHeader('Type') ; htp.tableRowClose; —
Выдаем информацию о каждой строке в таблице my_investments
for cl in (select ticker, name, type from my_investments order by ticker) loop htp.tableRowOpen; htp.tableData( cl.ticker ) ; htp.tableData( cl.name ); htp.tableData( cl.type ); htp.tableRowClose; l_count := l_count + 1; end loop; htp.tableClose; htp.p( l_count I I ' rows found'); htp.bodyClose; htp.htmlClose; end;
Если выполнить эту процедуру при отсутствии строк в таблице MY_INVESTMENTS, мы получим результат, показанный на рис. 9.3.
458
Глава 9 Ц http://127.0.0. l/pls/scott_dad/investment_rpt - Mic... Е File Edit View Favorites Tools Help Address |Жвдр^127.0.0.1/рк/5СОЙ_с1а 'investment_rpt', cmethod => 'POST' ); —
Включить скрытое поле, указывающее действие при посылке данных
htp.formHidden ( cname=> 'p_action', cvalue=> p_action ); htp.tableOpen; -- Сгенерировать текстовое поле и метку для каждого столбца в таблице htp.tableRowOpen; htp.tableDatat calign => 'RIGHT', cvalue => 'Ticker:'); htp.tableData( calign => 'LEFT', cvalue => htf.formText( cname => 'p_ticker' )); htp.tableRowClose; htp.tableRowOpen; htp.tableData( calign => 'RIGHT', cvalue => 'Name:'); htp.tableData( calign => 'LEFT', cvalue => htf.formText( cname => 'p_name' )); htp.tableRowClose; htp.tableRowOpen; htp.tableDatat calign => 'RIGHT', cvalue => 'Type:'); htp.tableData( calign => 'LEFT', cvalue => htf.formText( cname => 'p_type' )); htp.tableRowClose; — -- Сгенерировать кнопку посылки данных для HTML-формы
462
Глава 9 htp.tableRowOpen; htp.tableData( calign => 'RIGHT', cattributes => 'colspan="2"', cvalue •> htf.formSubmit(cvalue => 'Submit' )); htp.tableRowClose; htp.formClose; htp.bodyClose; htp.htmlClose; end;
Теперь мы можем вызвать процедуру INVESTMENT_MODIFY ИЗ Web-браузера и ввести значения данных, показанные на рис. 9.5. •3 http://127.0.0.1/pls/scott_dad/investment_modify - . . . . . File Edit View Favorites Tools Help Links
A(l cl.ticker) );
463
464
Глава 9 htp.tableData( cl.name ); htp.tableData( cl.type ); htp.tableRowClose; l_count := l_count + 1; end loop; — htp.tableClose; htp.p( l_count || ' rows found'); htp.br; htp.anchor( curl => 'investment_modify?p_action=INSERT', ctext => 'Create New' ) ; htp.bodyClose; htp.htmlClose;
create or replace procedure investment_modify( p_ticker in varchar2 default null, p_action in varchar2 default 'INSERT' ) as l_count number := 0; l_row my_investments%rowtype; begin — —
Если указано действие update, запросить изменяемые значения из таблицы
if p_action = 'UPDATE' then select * into l_row from my_investments where ticker = p_ticker; end if; htp.htmlOpen; htp.bodyOpen;
— —
Открыть HTML-форму, которая будет передавать данные методом POST нашей основной отчетной процедуре
htp.formOpen( curl => 'investment_rpt', cmethod => 'POST' );
—
Включить скрытое поле, указывающее действие при посылке данных
htp.formHidden ( cname=> 'p_action', cvalue=> p_action ); htp.tableOpen; —
Сгенерировать текстовое поле и метку для каждого столбца в таблице
htp.tableRowOpen;
Пакеты для Web-приложений
465
1
htp.tableData( calign => 'RIGHT , cvalue => 'Ticker:'); htp.tableData( calign => 'LEFT', cvalue => htf.formText( cname => 'p_ticker', cvalue => l_row.ticker ) ) ; htp.tableRowClose; htp.tableRowOpen; htp.tableData( calign => 'RIGHT 1 , cvalue => 'Name:'); htp.tableData( calign => 'LEFT', cvalue => htf.formText( cname => 'p_name', cvalue => l_row.name ) ) ; htp.tableRowClose; htp.tableRowOpen; htp.tableData( calign => 'RIGHT', cvalue => 'Type:'); htp.tableData( calign => 'LEFT', cvalue => htf.formText( cname => 'p_type', cvalue => l_row.type ) ) ; htp.tableRowClose;
—
Сгенерировать кнопку посылки данных для HTML-формы
htp.tableRowOpen; htp.tableData( calign => 'RIGHT 1 , cattributes => 'colspan="2"', cvalue => htf.formSubmit(cvalue => 'Submit' ) ) ; htp.tableRowClose; htp.formClose; htp.bodyClose; htp.htmlClose; end; /
Если выполнить эти процедуры в браузере и добавить несколько значений, страница будет выглядеть так, как показано на рис. 9.7. p _ u r l ) ) ; end;
Эту процедуру PRINT_WEB_SITE МОЖНО использовать для выборки содержимого любого адреса URL и выдачи его запрашивающему клиенту. Если вы хотите вызывать ее из браузера, то, предполагая, что процедура эта создана в схеме, для которой есть соответствующий дескриптор DAD, для ее вызова надо указать адрес URL http://:/pls//print_web_site?p_url=www.yahoo.com.В следующем примере мы получим страницу с популярного Web-сайта непосредственно из среды SQL*Plus. SQL> exec owainit; PL/SQL procedure successfully completed. SQL> exec print_web_site( p_url •> 'www.yahoo.com' PL/SQL procedure successfully completed. SQL> exec owa_util.showpage; Content-type: text/html
);
468
Глава 9
Content-length: 1998 Yahoo!...
Если вы выполните этот пример, то увидите, что HTML-страница с этого сайта не полна, обрезана. Это — ограничение функции request. Она читает только первых 2000 байтов выбранного содержимого. Чтобы гарантировать получение всех запрошенных данных, придется использовать функцию UTL_HTTP.REQUEST_PIECE.3 СО следующей спецификацией: utl_http.request_pieces( url in varchar2, max_pieces in natural default 32767, proxy in varchar2 default null, wallet_path in varchar2 default null, wallet_password in varchar2 default null ) return html_piece£;;
Функция REQUEST_PIECES почти идентична функции UTL_HTTP. REQUEST, но вместо 2000 байтов она вернет PL/SQL-таблицу, каждая запись которой будет содержать до 2000 байтов. Вот пример использования функции UTL_HTTP.REQUEST_PIECES, которая позволяет загружать с указанного адреса URL очень большие документы: create or replace procedure print_web_site( p_url in varchar2 ) is l_pieces utl_http.html_pieces; begin l_pieces :=» utl_http.request_pieces( url => p_url ); for x in 1..l_pieces.count loop htp.p( l_pieces(x) ); end loop; end;
С помощью переписанной процедуры PRINT_WEB_SITE МОЖНО выбрать содержимое любого URL объемом до 62,5 Мбайт. Понятно, что при наличии PL/SQL-таблицы со всем содержимым можно использовать всю мощь языка PL/SQL для быстрого преобразования его в объект CLOB, анализировать с помощью XDB и т.д.
Клиент Web-службы на базе пакета UTL_HTTP Простой протокол доступа к объектам (Simple Object Access Protocol — SOAP) — это основанный на XML протокол, который обычно упоминается в том же контексте, что и Web-службы. По сути, SOAP — всего лишь стандартный протокол для описания запроса и результатов этого запроса. Запросы SOAP чаще всего передаются по протоколу HTTP (хотя это и не обязательно). С помощью протокола SOAP вы можете легко выполнять запросы в стиле удаленных вызовов процедур (Remote Procedure Call — RPC) в пределах офиса или в пределах страны. При наличии основанного на стандартах SOAP-сервера в качестве интерфейса к источникам данных существенно упрощается решение проблемы интеграции возможностей широкого спектра систем. В этом разделе показано, насколь-
Пакеты для Web-приложений
469
ко легко обратиться к Web-службе непосредственно из хранимой процедуры на языке PL/SQL, используя только пакет UTL_HTTP. Мы будем использовать следующие процедуры и функции пакета UTL_HTTP: > SET_HEADER — установка значений в заголовке HTTP-запроса; > BEGIN_REQUEST — инициализация нового HTTP-запроса; > WRITEJTEXT — запись текста в тело HTTP-запроса; > GETRESPONSE — получение HTTP-ответа; > READJTEXT — чтение текста в теле HTTP-ответа; > END_RESPONSE — закрытие HTTP-ответа. Цель следующего примера — создать хранимую процедуру, которая по почтовому индексу США будет выдавать температуру в градусах Фаренгейта на соответствующей территории. В сети есть множество общедоступных SOAP-серверов, которые реализуют эту возможность. Наша задача — подключить базу данных Oracle к одной из этих Web-служб. SOAP-конверт для этого запроса такой: 43065
В этом полном XML-документе единственный компонент, который когда-нибудь будет меняться, — почтовый индекс (элемент zipcode). Если не придется менять используемую Web-службу, все остальное в этом SOAP-конверте остается неизменным. Этот SOAP-конверт надо послать как запрос SOAP-серверу. SOAP-сервер реализует соответствующую Web-службу, и наш запрос будет послан по протоколу HTTP этому серверу. Затем мы прочитаем ответ с того же сайта и выберем требуемые результаты. create or replace procedure temp_from_zip( p_zip in varchar2) as l_soap_envelope varchar2(4000); l_http_request utl_http.req; l_http_response utl_http.resp; l_piece utl_http.html_pieces; l_response varchar2(4000);
470
Глава 9
begin —
Создать SOAP-конверт, содержащий переданный почтовый индекс
l_soap_envelope : = ' '; l_soap_envelope := l_soap_envelope || p_zip; l_soap_envelope := l_soap_envelope || ' ' ; __ —
Начать новый запрос к целевому SOAP-серверу и послать ему запрос
l_http_request := utl_http.begin_request( url => 'http://services.xmethods.net:80/soap/servlet/rpcrouter', method => 'POST' ) ; utl_http.set_header(l_http_request, 'Content-Type', 'text/xml'); utl_http.set_header(l_http_request, 'Content-Length', length(l_soap_envelope)); utl_http.set_header(l_http_request, 'SOAPAction', 'getTempRequest');
—
Выдать конверт как часть запроса
utl_http.write_text(l_http_request, l_soap_envelope);
—
Сразу же получить ответ на запрос —
l_http_response := utl_http.get_response(l_http_request); utl_http.read_text( l_http_response, l_response ) ; utl_http.end_response(l_http_response); htp.p( l_response ) ; end; /
Весь процесс — достаточно простой. Просто берете XML-документ, посылаете его с требуемыми значениями аргументов соответствующему серверу, читаете ответ и завершаете HTTP-запрос. Назначение последнего вызова нтр. р в процедуре — выдать результаты, полученные от SOAP-сервера.
Пакеты для Web-приложений
471
Выполнение следующего кода в среде SQL*Plus дает интересные результаты. SQL> exec owainit; PL/SQL procedure successfully completed. SQL> exec temp_from_zip(43065) ; PL/SQL procedure successfully completed. SQL> exec owa_util.showpage; Content-type: text/html Content-length: 466 echo msgtxt rdbms ora 1476 | o r a t c l s h DBSNMP for 32-bit Windows: Version 9.2.0.1.0 - Production on 28-OCT-2003 18:25:23 Copyright
(c) 2002 Oracle Corporation.
All r i g h t s reserved.
o r a t c l s h [ l ] ~ ora-1476: d i v i s o r i s equal t o zero oratclsh[2]-
Примечание Вы можете найти свободно распространяемый сценарий Perl в сети, который имитирует команду OERR в Unix и Linux, выдавая также информацию о причинах и возможных действиях.
Если вы хотите увидеть все предопределенные исключительные ситуации Oracle, посмотрите спецификацию пакета STANDARD В файле $ORACLE_HOME/rdbms/admin/ stdspec.sql.
478
Глава 10
Пользовательские исключительные ситуации Теперь, когда мы увидели, как сервер Oracle определяет исключительные ситуации, можно попытаться определить собственные: declare my_exception exception: begin If some_condition = really_bad then raise my_exception: end if; exception when my_exception then — исправляем ситуацию end;
Теперь, после объявления исключительной ситуации, вы можете возбуждать ее в любом месте блока, в котором она объявлена. Можно определить исключительные ситуации в спецификации пакета, а затем ссылаться на них по имени PACKAGE .EXCEPTION_NAME так же, как на глобальные переменные из спецификации пакета. Я бы посоветовал применять пользовательские исключительные ситуации осторожно. Уверен, что исключительные ситуации должны использовать не как оператор GOTO, а только при возникновении в коде ситуации, требующей специальной обработки. Если вы столкнетесь с кодом, где пользовательские исключительные ситуации используются для выхода из циклов или вложенных процедур, то знайте: я считаю такой код неудачным. Поток управления в коде в основном должен логично идти от начала к концу, а переход на специальный обработчик должен происходить только тогда, когда происходит нечто неожиданное. Речь не идет о том, чтобы вообще никогда не использовать пользовательские исключительные ситуации; применяйте их только в тех случаях, когда нет другого способа создать необходимый код. Как уже упоминалось, сервер Oracle связывает определенные исключительные ситуации с кодами типичных ошибок, возникающих при выполнении. Вы можете при желании связать ваши пользовательские исключительные ситуации с кодами ошибок с помощью прагмы EXCEPTION_INIT: declare table_space_exists exception: pragma exception_init( table_space_exists, -1534 ); begin execute immediate 'create tablespace system....'; exception when table_space_exists then — обработать ошибку end;
Здесь мы определили исключительную ситуацию TABLE_SPACE_EXISTS И связали ее с кодом ошибки 1543, состоящей в следующем: [clbeck@clbeck-tecra clbeck]$ oerr ora 1543 01543, 00000, "tablespace '%s' already exists"
Отладка PL/SQL 479 // *Cause: Tried to create a tablespace which already exists // *Action: Use a different name for the new tablespace
Теперь при наличии процедуры, принимающей имя табличного пространства и пытающейся создать его в PL/SQL-коде, вы можете сначала проверить, что такое табличное пространство не существует. Есть вероятность, что между моментом проверки и моментом создания табличного пространства кто-то другой успеет создать табличное пространство с тем же именем. Такое маловероятно, но возможно. Это прекрасный пример того, когда надо использовать исключительные ситуации. Используйте их для обработки маловероятных событий.
Процедура RAISE_APPUCATIONJRROR() Так же, как вы можете связать пользовательскую исключительную ситуацию с кодом ошибки Oracle, можно задать сообщения об ошибках для возбуждаемых исключительных ситуаций. С помощью процедуры RAISE_APPLICATION_ERROR МОЖНО возбуждать исключительные ситуации и задавать для них осмысленные сообщения об ошибке. Для вызова этой процедуры используется следующий синтаксис: raise_application_error(номер_ошибки,
сообщение[, {TRUE | FALSE}]);
Параметры процедуры RAISE_APPLICATION_ERROR следующие: > номер_ошибки — любое отрицательное число в диапазоне от -20000 до -20999; > сообщение — сообщение об ошибке, которое может быть длиной до 2048 символов; >• необязательный булев параметр — принятое по умолчанию значение FALSE приводит к замене всех предыдущих ошибок в стеке ошибок. Если задано значение TRUE, ошибка добавляется к стеку ошибок. Процедуру RAISE_APPLICATION_ERROR можно использовать для возбуждения исключительных ситуаций так же, как оператор RAISE . Например: begin if salary < expenses then raise_application_error( -20001, 'Мне нужно больше денег' ) ; end if; end;
Разработчик может обработать эти исключительные ситуации только при использовании описанной ранее прагмы EXCEPTION_INIT ДЛЯ СВЯЗИ пользовательской исключительной ситуации с номером ошибки, указанным в процедуре RAISE APPLICATION ERROR. — —
Обработчик исключительных ситуаций OTHERS Бывают ситуации, когда вы просто не знаете, что может произойти (или вам это неважно!). Вы знаете только, что если произойдет что-то неожиданное, вам надо это обработать. До сих пор все представленные обработчики задавались для именованных исключительных ситуаций. Обработчик исключительных ситуаций OTHERS pa-
480
Глава 10
ботает как ловушка для любой исключительной ситуации, которая может произойти В блоке BEGIN-END. declare l_num number; l_char varchar2(10) ; begin select coll/col2, col3 into l_num, l_char from my_table where id = 1; exception when NO_DATA_FOUND then — обработать ситуацию, когда строки не найдены when TOO_MANY_ROWS then — обработать ситуацию, когда найдено больше одной строки when OTHERS then — обработать все остальные ошибки end;
Здесь важно учесть два важных момента. Во-первых, блок обработки исключительных ситуаций может обрабатывать несколько исключительных ситуаций, для каждой из которых задана отдельная конструкция WHEN . Во-вторых, универсальный обработчик WHEN OTHERS обработает любую другую исключительную ситуацию, которая может возникнуть и не будет обработана другим обработчиком явно указанной исключительной ситуации. В нашем примере мы явно обрабатываем стандартные исключительные ситуации NO_DATA_FOUND и TOO_MANY_ROWS, о которых разработчики обычно задумываются, используя операторы SELECT . . . INTO. НО В нашем случае могут возникнуть еще две исключительные ситуации. Мы можем получить исключительную ситуацию ZERO_DIVIDE при делении coii/coi2, а также исключительную ситуацию VALUE_ERROR, если длина со13 превзойдет 10 символов. Конечно, правильный способ избежать последней потенциальной ошибки — использовать при объявлении переменной атрибут %TYPE, но суть в том, что могут возникнуть разные исключительные ситуации. Учтите, что неправильное использование обработчика WHEN OTHERS может помешать отладке. Если используются только обработчики WHEN OTHERS, будет сложно понять, где именно в коде возникает проблема. Заданный не там, где нужно, обработчик WHEN OTHERS может перехватить исключительную ситуацию, которую иы отдельно обрабатываете во внешнем блоке BEGIN-END, тем самым делая эту обработку бесполезной. Исключительные ситуации — очень мощная и полезная конструкция языка PL/SQL. Они позволяют разработчикам легко перехватывать и обрабатывать непредвиденные ошибки. Разработчики могут также прекращать обработку и возбуждать собственные исключительные ситуации с информативными сообщениями. Правильная обработка исключительных ситуаций существенно облегчает процесс отладки. Однако неправильное или избыточное использование некоторых возможностей, а именно — обработчиков WHEN OTHERS И пользовательских исключительных ситуаций,
Отладка PL/SQL
481
может усложнить отладку. В любом случае используйте исключительные ситуации, но делайте это корректно.
Снабжение кода средствами трассировки и отладки Отладочная информация существенна для разработчика, который пытается исправить неверный фрагмент кода. Чем больше информации ему доступно, тем больше шансов быстро найти и исправить соответствующий код. Часто, однако, когда код относят к "производственному", отладочные и трассировочные вызовы из него убираются. Объясняют это тем, что при удалении всего отладочного кода повышается производительность. Мы считаем, что наличие этого кода — ключ к получению эффективного и нормально работающего производственного кода, который можно будет отладить без изменений при возникновении проблемы. Конечно, не хотелось бы записывать отладочную информацию в ходе нормальной работы приложения, но, определенно, хотелось бы иметь возможность включить ее запись при возникновении ошибки. Если вы не можете этого сделать, вам остается пробовать воспроизвести ситуацию, вызвавшую ошибку, в производственной среде, что не всегда получается. Все это время ваше производственное приложение не будет работать, а его пользователи будут злиться еще больше. Эти соображения применимы для всех разработчиков, а не только для тех, кто использует язык PL/SQL. Сервер Oracle, настроенный для максимально эффективной работы, имеет возможность записывать при необходимости разнообразную отладочную информацию. Хотя сервер и проходит многочисленные тесты, чтобы гарантировать стопроцентное отсутствие ошибок, непредвиденные ошибки все же возникают. С помощью службы поддержки Oracle вы можете включить выдачу разнообразнейшей отладочной информации. Она принципиально важна для обеспечения ускорения работы вашего сервера. Представьте себе, что было бы, если бы служба поддержки пыталась сымитировать среду каждого клиента, чтобы воспроизвести все ошибки, возникающие по ходу работы сервера. Это просто нереально. Далее в этой главе мы обсудим, как можно реализовать такие средства отладки и трассировки в PL/SQL, чтобы по ходу нормальной работы отладочная информация не генерировалась, но с помощью простого переключателя вы могли бы сгенерировать всю потенциально необходимую отладочную информацию.
Документация Я не знаю ни одного разработчика, которому нравилось бы писать документацию. Часто приходится слышать, как разработчики в шутку говорят: "Мой код — самодокументированный" или "Я намеренно не документирую свой код, поскольку это гарантирует мне сохранение работы". Я знаю, что написание документации — неприятное занятие, но я также знаю, насколько это важно. Не помню, сколько раз я вынужден был пересматривать фрагмент кода, написанный мною или, что еще хуже, кем-то другим много месяцев назад. Когда это происходило, половина времени уходило на почесывание затылка и вопросы: "Что делается в этой процедуре?" или "О чем думал тот, кто это написал?". Таких ситуаций можно избежать, если потратить ,6 З а , 348
482
Глава 10
время на документирование кода. Достаточно краткого разъяснения, нескольких строк, объясняющих код по-человечески. Конечно, чем больше документации, тем лучше, и в идеальном мире все было бы полностью описано. В реальном мире, однако, временные ограничения и сроки (а иногда и элементарная лень) мешают разработчикам документировать свой код. Но учтите следующее: потратив несколько минут на краткое объяснение вашего кода, вы сэкономите в будущем часы и даже дни, потраченные на трассировку кода.
Инструментальные средства Теперь, когда мы рассмотрели некоторые методы сведения времени отладки кода к минимуму, давайте рассмотрим средства, которые помогут вам в том случае, если придется-таки заняться отладкой.
Пакет DBMS_OUTPUT DBMSOUTPUT — это стандартный пакет Oracle, позволяющий разработчикам вы-
давать текстовые сообщения любому клиенту, знающему о существовании пакета DBMS OUTPUT.
Примечание Можете просмотреть файл $ORACLE HOME/rdbms/admin/dbmsotpt.sql, содержащий спецификацию этого пакета.
Считайте что этот пакет является аналогом функции printf () в языке С или system, out. p r i n t l n () в Java. Это самый простой способ отладки PL/SQL-кода, но у него есть определенные ограничения.
Спецификация Если запросить описание пакета DBMSOUTPUT, МОЖНО заметить, что в нем всего лишь несколько общедоступных процедур: SQL> desc dbms_output PROCEDURE DISABLE PROCEDURE ENABLE Argument Name
Type
In/Out Default?
BUFFER_SIZE PROCEDURE GET_LINE Argument Name
NUMBER(38)
IN
Type
In/Out Default?
LINE STATUS PROCEDURE GET_LINES Argument Name
VARCHAR2 NUMBER (38)
OUT OUT
Type
In/Out Default?
LINES NUMLINES PROCEDURE NEW_LINE PROCEDURE PUT
TABLE OF VARCHAR2(255) NUMBER(38)
OUT IN/OUT
DEFAULT
Отладка PL/SQL Argument Name
483
Type
In/Out Default?
A PROCEDURE PUT Argument Name
VARCHAR2
IN
Type
In/Out Default?
A PROCEDURE PUT_LINE Argument Name
NUMBER
IN
Type
In/Out Default?
A PROCEDURE PUT_LINE Argument Name
VARCHAR2
IN
Type
In/Out Default?
NUMBER
IN
A
Пакет DBMS_OUTPUT подходит только для сравнительно небольших задач отладки, поскольку буфер (в котором кешируются текстовые сообщения) имеет длину 1000000 байтов, а длина каждой строки не может превышать 255 символов. Пакет буферизует ваши текстовые сообщения в массив строк VARCHAR2 (255). Затем клиент, знающий о существовании пакета DBMS_OUTPUT (например, используемая нами утилита SQL*Plus), выдаст эту информацию после завершения работы программной единицы PL/SQL. Утилите SQL*Plus надо "сказать", что вы хотите получать эти сообщения, поэтому придется выполнить следующую команду: SET
SERVEROUTPUT ON [SIZE ]
Без этой команды ваши сообщения будут оставаться в кеше, и вы их не увидите. Со временем кеш заполнится, и вы окажетесь в странной ситуации, когда отладочный код вызывает ошибку в основном коде!
Использование пакета DBM5_OUTPUT в среде SQL*Plus Давайте рассмотрим несколько примеров использования пакета DBMS_OUTPUT. Первый — элементарный. В любом месте, где вы хотите выдать сообщение, просто вызовите процедуру PUT_LINE, И сообщение будет выдано: SQL> set serveroutput on SQL> begin 2 dbms output.put lxne( 'foo' ); — — 3 dbms_output.put_line( 'bar' );
5 Г"*'"
foo bar
PL/SQL procedure successfully completed.
Если вы хотите программно управлять кешированием сообщений для выдачи, можно использовать процедуры ENABLE И DISABLE.
484
Глава 10
SQL> set serverout on SQL> begin dbms_output.disable; 2 dbms_output.put_line( 1 one' ) ; 3 dbms_output.put_line( 'two' ) ; 4 dbms_output.enable; 5 dbms_output.put_line( ' t h r e e ' ) 6 7 end; 8 three PL/SQL procedure successfully completed.
Обратите внимание, что первые два сообщения были проигнорированы пакетом DBMS_OUTPUT, и только сообщение THREE выдано. Использование процедур ENABLE И DISABLE позволит вам снабдить код средствами отладки и оставить в нем отладоч-
ные сообщения при переносе в производственную среду. Когда надо будет отлаживать код, просто выполните процедуру DBMS_OUTPUT . ENABLE перед запуском приложения, и отладочные сообщения "чудом" появятся. Учтите, что, как демонстрирует следующий пример, кешированные сообщения будут потеряны при вызове DBMS_OUTPUT.DISABLE: SQL> begin 2 dbms_output.disable; 3 dbms_output.put_line( 4 dbms_output.put_line( 5 dbms_output.enable; б dbms_output.put_line( 7 dbms_output.disable; 8 dbms_output.put_line( 9 dbms_output.put_line( dbms_output.enable; 10 dbms_output.put_line( 11 12 end; 13 six
'one' ); 'two' ); 'three' ), — сбрасывает и отключает кеш
'four' ); 'five' ); 'six' ;
— включает кеширование сообщений — единственное выдаваемое сообщение
PL/SQL procedure successfully completed.
Выдаются только сообщения, кешированные после последнего вызова DBMS_OUTPUT.ENABLE.
Проблемы При использовании пакета DBMSJOUTPUT есть пара особенностей, о которых вам следует знать; их мы опишем в следующих разделах. Клиенты, не знающие о существовании пакета DBMSJOUTPUT Сообщения можно получить, только если подпрограмма вызвана из клиента, знающего о существовании пакета DBMS_OUTPUT. В противном случае вам придется "выбирать" сообщения программно, из приложения. Это означает, что если ваша
Отладка PL/SQL 485 процедура выполняется как часть Web-приложения (или приложения на языках Рго*С или Java), то сообщения пакета DBMS_OUTPUT не будут выдаваться, пока вы явно не запрограммируете их выборку. Ограничение на количество символов, выдаваемых процедурами PUT и PUT_LINE Процедуры PUT и PUT_LINE имеют жесткое ограничение длины строки — 255 символов. SQL> exec dbms output.put line ( rpad( '*', 255, '*' ) );
PL/SQL procedure successfully completed. SQL> exec dbms_output.put_line( rpad( '*', 256, '*' ) ); BEGIN dbms_output.put_line( rpad( '*', 256, •*' ) ); END; * ERROR at line 1: ORA-20000: ORU-10028: line length overflow, limit of 255 chars per line ORA-06512: at "SYS.DBMSJXJTPUT", line 35 ORA-06512: at "SYS.DBMS_OUTPUT", line 133 ORA-06512: at line 1
Поэтому, если вам надо выдать много текста, придется проверить, чтобы в каждой строке было не более 255 символов. Одно из решений этой проблемы — использовать функцию SUBSTR для отсечения первых 255 символов из любых сообщений, посылаемых процедурам пакета DBMSOUTPUT. SQL> exec d b m s _ o u t p u t . p u t _ l i n e ( s u b s t r ( rpad( ' * ' , 500, ' * ' ) , 1, 255 ) ) ;
PL/SQL p r o c e d u r e s u c c e s s f u l l y
completed.
Можно также написать процедуру-обертку, которая будет разбивать ваше сообщение на строки длиной в 255 символов, а затем посылать каждый фрагмент через DBMS_OUTPUT. Затем для выдачи всех сообщений надо вместо DBMSJDUTPUT . PUT_LINE использовать такую процедуру: SQL> create or replace 2 procedure my_put_line( p_line varchar2 ) as 3 l_offset number := 1; 4 begin 5 loop 6 exit when substr(p_line, l_offset, 255 ) is null; 7 dbms_output.put_line( substr(p_line, l_offset, 255 ) ); 8 l_offset := l_offset + 255; 9 end loop;
486
Глава 10 10 end my_put_line; 11 / Procedure created. SQL> exec my_put_line( rpad( '*', 500, '*' ) ) ;
*************** ******************************************************************************** ***** PL/SQL procedure successfully completed.
Ограничение на размер буфера Пакет DBMS_OUTPUT сохраняет ваши сообщения в буфере фиксированного размера. Этот буфер может быть размером до 1000000 символов. По умолчанию используется буфер размером 2000 символов. Вы можете увеличить его до 1000000 символов с помощью команды SET SERVEROUTPUT ON SIZE 1000000
или программно с помощью вызова DBMS OUTPUT.ENABLE(1000000) ;
Эта установка делается для каждого сеанса, поэтому придется повторять ее в каждом новом сеансе, в котором вы занимаетесь отладкой. Если вы попытаетесь поместить в буфер больше символов, чем он может вместить, то получите сообщение об ошибке: SQL> set serveroutput on size 1000000 SQL> begin 2 for i in 1 .. 5000 loop 3 dbms_output.put_line('line: •M i l l 4 e n d loop; 5 end; 6 / line: 1
Д Д л
л л Д л л Д л л л
г* л д д л
л л
л л л л
гС л д
л л" д д
л л д л
л л" д д
л д
п д л д *к д л
1
' ' || r p a d C * ,
л л д л
line: 2
< Пропущен большой объем результатов line: 3500
>
л
л л" д
л д л л
л д д д
л д л
240, '*'));
л л" л
л' л" д
л1 д д* 7\ л
л д "л
Отладка PL/SQL 487 begin ERROR at line 1: ORA-20000: ORU-10027: buffer overflow, limit of 1000000 bytes ORA-06512: at "SYS.DBMS_OUTPUT", line 35 ORA-06512: at "SYS.DBMS_OUTPUT", line 198 ORA-06512: at "SYS.DBMS_OUTPUT", line 139 ORA-06512: at line 3
He полагайтесь при генерации отладочных сообщений только на пакет DBMS_OUTPUT. Ограничение на длину строки и сравнительно небольшой размер бу-
фера делают его не самым лучшим решением для отладки крупномасштабных приложений. Однако для простых небольших процедур и быстрого получения сообщений в среде SQL*Plus он отлично подходит.
Встроенные функции SQLCODE и SQLERRM SQLCODE и SQLERRM — встроенные функции, возвращающие код ошибки и сообщение об ошибке для последней возбужденной исключительной ситуации. Эти две функции крайне полезны при использовании универсального обработчика WHEN OTHERS, если необходимо зарегистрировать точную причину возникновения ошибки. Давайте рассмотрим пример того, как эти функции можно использовать: SQL> set serverout on SQL> declare 2 n number; 3 begin 4 n : = ' a' ; 5 exception 6 when OTHERS then 7 dbms output.put line ( 'SQLCODE = 'I 1 SQLCODE ) ; 1 8 dbms output.put line( SQLERRM = ' I 1 SQLERRM ) ; 9 raise; 10 end; 11 / SQLCODE = -6502 SQLERRM = ORA-06502: PL/SQL: numeric or value error: character to number conversion error declare * ERROR at line 1: ORA-06502: PL/SQL: numeric or value error: character to number conversion terror ORA-06512: at line 9
Обратите внимание, что функции SQLCODE И SQLERRM выдают тот же код и сообщение об ошибке, что и утилита SQL*Plus. Вместо их выдачи можно регистрировать ошибку в таблице или файле. Примеры подобной регистрации ошибок вы увидите далее.
488
Глава 10
Функция DBMS_UTILITY.FORMAT_CALL_STACK Эту функцию мы рассматривали в разделе "Упрощение трассировки" главы 2, а здесь мы лишь кратко напомним о ней. По сути, функция DBMS_UTILITY . FORMAT_CALL_STACK возвращает сформатированную строку текста на основе текущего стека вызовов. Эту информацию можно использовать для того, чтобы узнать точно, как вы попали на выполняемую строку кода. Давайте посмотрим эту функцию в действии. Создадим три процедуры — А, в и с. SQL> create or replace 2 procedure a as 3 begin 4 dbms_output.put_line( dbms_utility.format_call_stack ); 5 end a; 6 / Procedure created. SQL> create or replace 2 procedure b as 3 begin 4 a; 5 end b; 6 / Procedure created. SQL> create or replace 2 procedure с as 3 begin 4 b; 5 end c; 6 / Procedure created.
Процедура А вызывает функцию DBMS_UTILITY.FORMAT_CALL_STACK И выдает ее результат. Процедура в просто вызывает процедуру А, а процедура с вызывает процедуру в. При выполнении процедур А и с мы увидим следующие результаты: SQL> SQL> 2 3 4
s e t serverout on begin a; end; / PL/SQL Call Stack object line object handle number name 66C50624 3 procedure CLBECK.A 66BB7EB8 2 anonymous block
PL/SQL procedure successfully completed. SQL> begin 2 c;
Отладка PL/SQL 3 end; 4 / PL/SQL Call Stack object line object handle number name 66C50624 3 procedure 66BC5E20 3 procedure 66BC230C 3 procedure 66B9E6C4 2 anonymous
CLBECK. A CLBECK. В CLBECK. С block
PL/SQL procedure successfully
completed.
489
Обратите внимание, что результаты хорошо сформатированы и легко читаются. Нетрудно определить, что обе процедуры были вызваны из анонимного блока, но во втором случае есть дополнительный шаг — процедура с вызывает процедуру в, которая затем вызывает процедуру А. Эта информация пригодится при отладке большой PL/SQL-программы.
Пакет DBMS_APPUCATION_INFO DBMS_APPLICATION_INFO — стандартный пакет Oracle, позволяющий разработчи-
кам записывать информацию о ходе выполнения процедур и функций. Эта информация затем доступна для других сеансов базы данных, без фиксации изменений текущим сеансом. Помните, что любая информация, выдаваемая с помощью пакета DBMSJOUTPUT, появляется на экране только после завершения процесса. Информация, выдаваемая с помощью пакета DBMS_APPLICATION_INFO, доступна для просмотра сразу же. Рассмотрим несколько примеров. Примечание Можно просмотреть спецификацию пакета DBMS_APPLICATION_INFO в файле $ORACLE_HOME/ rdbms/admin/dbmsapin.sql.
В этом пакете надо обратить внимание на три основные процедуры: > SET_MODULE; > SET_ACTION; >
SET_CLIENT_INFO.
Следующий простой пример демонстрирует их использование. SQL> create or replace 2 package PKG is 3 rprocedure DO— WORK; 4 end; 5 / Package created. SQL> create or replace 2 package body PKG is
490
Глава 10 3 procedure DO_WORK is 4 v_emp_cnt number; 5 begin 6 dbms_application_infо.set_module(module_name=>'PKG.DO_WORK', 7 action_name=>'commencing count'); 8 select count(*) 9 into v_emp_cnt 10 from emp; 11 dbms_application_info.set_action('finished c o u n t ' ) ; 12 for i in 1 . . 1000 loop 13 dbms application_info.set_client_info('Deleting employee ' | | i ) ; 14 d e l e t e from emp where empno = i ; 15 end loop; 16 end; 17 end; 18 /
Package body created.
Какое значение имеют параметры MODULE, ACTION И CLIENT_INFO? ПОЧТИ никакого. Они просто представляют три символьных поля, которым вы задаете значения при выполнении PL/SQL-программ. Параметр MODULE может быть длиной до 48 символов, параметр ACTION — до 32 символов, а параметр CLIENT_INFO — до 64. В предыдущем примере мы записали имя процедуры DO_WORK В качестве имени модуля, а также некий осмысленный текст в качестве описания действия, которое должно выполняться. Аналогично, по мере удаления 1000 сотрудников, мы отслеживали его ход с помощью процедуры SET_CLIENT_INFO. Но настоящая прелесть пакета DBMS_APPLICATION_INFO СОСТОИТ В ТОМ, ЧТО МОЖНО посмотреть значения параметров MODULE, ACTION И CLIENT_INFO. Все три значения можно получить из фиксированного представления V$SESSION. SQL> desc V$SESSION Name
Null?
Type
SADDR SID SERIAL*
RAW(4) NUMBER NUMBER
MODULE ACTION CLIENT_INFO
VARCHAR2(48) VARCHAR2(32) VARCHAR2(64)
Таким образом, от имени любого пользователя, имеющего привилегии на запрос к этому представлению, можно следить за ходом выполнения процедуры PKG . DO_ WORK через представление V$SESSION. SQL> select module, action, client_info 2 from v$session 3 where client info is not null;
Отладка PL/SQL 491 MODULE
ACTION
CLIENT_INFO
PKG.DO WORK
finished count
Deleting employee 1000
Аналогично значения MODULE И ACTION также записываются в представлении V$SQL. Таким образом, можно сопоставлять выполненные сервером SQL-операторы со значениями MODULE И ACTION. Например, после запуска процедуры PKG.DO_WORK можно увидеть, какие SQL-операторы были выполнены в этой процедуре, если знать, что столбец MODULE имеет значение PKG . DO_WORK. SQL> select sql_text, buffer_gets, executions 2 from v$sql 3 where module = 'PKG.DO_WORK'; SQL TEXT BUFFER GETS EXECUTIONS SELECT count (*) from emp DELETE from emp where empno = :bl
3 3000
1 1000
Помните, что информация из этого пакета доступна в представлении сразу после ее установки. Рассмотрим следующую процедуру: SQL> create or replace 2 procedure testl as 3 begin 4 dbms_application_info.set_action( 'Procedure TEST1' ); 5 for i in 1 .. 10 loop 6 dbms_application_info.set_client_info( 'Loop number ' || i ); 7 dbms_lock.sleep( 5 ); 8 end loop; 9 end testl; 10 / Procedure created.
Примечание Может потребоваться предоставить пользователю привилегию на выполнение пакета DBMS LOCK.
Теперь, пока предыдущий сеанс работает, начните второй сеанс и выполните следующий запрос. Если вы — единственный пользователь на сервере, вы должны увидеть следующее: С:\temp>sqlplus clbeck/clbeck SQL*Plus: Release 9.2.0.1.0 - Production on Mon Aug 18 20:43:05 2003 Copyright (c) 1982, 2002, Oracle Corporation. All rights reserved. Connected to: Oracle9i Enterprise Edition Release 9.2.0.1.0 - Production With the OLAP and Oracle Data Mining options JServer Release 9.2.0.1.0 - Production
492
Глава 10
SQL> SQL> SQL> SQL> SQL> 2 3 4
col module for alO col action for a20 col client_info for a20 select module, action, client_info from v$session where module is not null /
MODULE
ACTION
CLIENT_INFO
SQL*Plus SQL*Plus
Теперь вернемся в первый сеанс и выполним процедуру TESTI. SQL> begin 2 testi; 3 end; 4 /
Эта процедура не закончится немедленно, поскольку на каждой итерации цикла происходит задержка на 5 секунд. Теперь переключитесь обратно на второй сеанс и повторно выполните тот же запрос. Обратите внимание, что теперь вы можете увидеть информацию, записанную первым сеансом, хотя он и не зафиксировал транзакцию. SQL> / MODULE
ACTION
CLIENT INFO
SQL*Plus SQL*Plus
Procedure TESTI
Loop number 1
Подождите 5 секунд и еще раз выполните запрос. Столбец CLIENT_INFO ОПЯТЬ изменился. SQL> / MODULE
ACTION
CLIENT INFO
SQL*Plus SQL*Plus
Procedure TESTI
Loop number 2
А если вы подождете до завершения процедуры, то увидите следующее: SQL> / MODULE
ACTION
CLIENT_INFO
SQL*Plus SQL*Plus
Procedure TESTI
Loop number 10
Отладка PL/SQL
493
Есть и.второе представление, V$SESSION_LONGOPS, которое тоже доступно разработчикам. Оно определено следующим образом: SQL> desc v$session_longops Name
Null?
Type NUMBER NUMBER VARCHAR2(64) VARCHAR2(64) VARCHAR2(32) NUMBER NUMBER VARCHAR2(32) DATE DATE NUMBER NUMBER NUMBER VARCHAR2(512) VARCHAR2(30) RAW(4) NUMBER NUMBER
SID SERIAL# OPNAME TARGET TARGET_DESC SOFAR TOTALWORK UNITS STARTJTIME LAST_UPDATE_TIME TIME_REMAINING ELAPSED_SECONDS CONTEXT MESSAGE USERNAME SQL_ADDRESS SQL_HASH_VALUE QCSID
Можно устанавливать и просматривать информацию в этом представлении так же, как и в представлении V$SESSION, НО ДЛЯ установки значений надо использовать процедуру SET_SESSION_LONGOPS пакета DBMS_APPLICATION_INFO. PROCEDURE SET_SESSION_LONGOPS Argument Name RINDEX SLNO OP_NAME TARGET CONTEXT SOFAR TOTALWORK TARGET_DESC UNITS
Type
In/Out
Default?
BINARY_INTEGER BINARY_INTEGER VARCHAR2 BINARY INTEGER BINARY_INTEGER NUMBER NUMBER VARCHAR2 VARCHAR2
IN/OUT IN/OUT IN IN IN IN IN IN IN
DEFAULT DEFAULT DEFAULT DEFAULT DEFAULT DEFAULT DEFAULT
Это представление имеет смысл использовать для предоставления информации о долго работающих PL/SQL-процессах. Периодически ваше долго работающее приложение или процесс должны обновлять информацию в этом представлении, тем самым позволяя другим сеансам, включая такие средства, как Oracle Enterprise Manager, узнавать о его состоянии. Это помогает в процессе отладки, позволяя разработчикам следить за стадией работы приложения. Но информация, хранящаяся в данном представлении, будет хороша и полезна настолько, насколько об этом позаботится разработчик, создававший приложение.
494
Глава 10
Первые два параметра процедуры S E T _ S E S S I O N _ L O N G O P S пакета DBMS_APPLICATION_INFO — RINDEX и SLNO. Процедура использует параметр RINDEX для определения строки представления V$SESSION_LONGOPS, которую надо изменить. Первоначально параметр RINDEX получает значение константы DBMS_APPLICATION_INFO . SET_SESSION_LONGOPS_NOHINT. Процедура возвращает внутренний идентификатор через параметр в режиме оит, который должен использоваться в последующих вызовах для изменения той же строки. Параметр SLNO предназначен только для внутреннего использования — вы вообще не должны его изменять. Остальные параметры предназначены для передачи информации, которую вы хотите предоставить через представление. Вы можете помещать в них любую информацию при вызове процедуры, если только соблюдаются типы данных и размеры столбцов. Рассмотрим, например, параметр OP_NAME. Рекомендуется в этом поле сохранять имя процедуры или приложения, но ничто не мешает вам записать в него название текущего теста или имя пользователя, выполняющего код. Если это полезная для отладки и важная информация — записывайте все, что хотите. В следующем примере мы выполним 20 итераций цикла, представляющих общий объем работы, которую нам надо сделать. После каждой итерации мы делаем паузу на 3 секунды, чтобы имитировать реальную работу, и так мы можем реально увидеть, как последовательные вызовы процедуры SET_SESSION_LONGOPS ВЛИЯЮТ на значения, которые мы увидим в представлении V$SESSION_LONGOPS. SQL> declare 2 l_rindex binary_integer; 3 l_slno binary_integer; 4 l_totalwork number; 5 begin 6 l_rindex := dbms_application_info.set_session_longops_nohint; 7 l_totalwork := 20; 8 for i in 1 .. l_totalwork loop 9 dbms_application_info.set_session_longops( 10 rindex => l_rindex, 11 slno => l_slno, 12 op_name => 'testing session longops', 13 target => null, 14 context => null, 15 sofar => i, 16 totalwork => l_totalwork, 17 target_desc => 'This is a test showing how longops works', 18 units => 'looping with sleep 3' ); 19 dbms_lock.sleep( 3 ); 20 end loop; 21 end;
Интересно же то, что если мы будем изменять параметры процедуры SET_SESSION_LONGOPS на каждой итерации (и параметры sofar и totalwork будут иметь корректные значения), то в столбце TIME_REMAINING будет вычисленное сер-
вером Oracle предположительное время, оставшееся до завершения работы процедуры. Столбец ELAPSED_SECONDS тоже будет постоянно изменяться — в нем будет представлено общее время выполнения процедуры в секундах.
Отладка PL/SQL
495
Если примерно через 5 секунд после начала выполнения предыдущей процедуры мы выполним следующий запрос в другом сеансе, то получим такой результат: SQL> select opname, 2 username, 3 sofar, 4 elapsed_seconds, 5 time_remaining 6 from v$session_longops; OPNAME
USERNAME
testing session long ops CLBECK
SOFAR ELAPSEDJ3ECONDS TIME_REMAINING 2
3
27
Обратите внимание: в представлении указано, что процесс находится на второй итерации цикла, что он проработал три секунды и завершится через 27 секунд. Мы знаем, что это невозможно. Процесс должен выполнить 20 итераций цикла и останавливаться на 3 секунды на каждой итерации (что дает общее время выполнения 60 секунд). Но помните, что сервер Oracle построил свое предположение лишь по одной итерации цикла. Если выполнить тот же запрос спустя 15 секунд, можно получить следующие результаты: OPNAME
USERNAME
testing session long ops CLBECK
SOFAR ELAPSED_SECONDS TIME_REMAINING 7
18
33
Теперь процесс находится на седьмой итерации, проработал 18 секунд, и сервер Oracle вычислил, что он завершится через 33 секунды. Если учесть, что он проработал лишь 18 секунд, мы снова получили неверный результат, но уже ближе к реальному. Если мы выполним запрос третий раз 15 секунд спустя, результаты могут быть такими: OPNAME
USERNAME
testing session long ops CLBECK
SOFAR ELAPSED_SECONDS 12
34
TIME_REMAINING 23
На этот раз сервер Oracle оценивает, что наш продолжительный процесс будет завешен через 23 секунды и он уже проработал 34 секунды, что дает нам общее время выполнения 57 секунд, а это уже весьма хорошая оценка. Чем дольше работал процесс, тем точнее сервер Oracle оценивал время до его завершения. Представьте себе подпрограмму, изменяющую миллионы строк. Если мы используем процедуру DBMS_APPLICATION_INFO.SET_SESSION_LONGOPS, TO сможем следить не только за ходом выполнения процесса, но и за предполагаемым сервером Oracle на основе уже прошедшего времени после его завершения. Эта информация особенно ценна при отладке таких продолжительных процессов.
Автономные транзакции Мы уже обсуждали автономные транзакции в нескольких главах этой книги, но имеет смысл снова рассмотреть их в контексте отладки. По сути, они позволяют
496
Глава 10
регистрировать всю необходимую информацию в таблице базы данных, что дает возможность немедленно использовать ее в других сеансах и оставлять в таблице, даже если сеанс, зарегистрировавший информацию, откатит сделанные изменения полностью или частично. Представьте, насколько бесполезна была бы регистрационная информация в таблице базы, если бы транзакция, записывающая эту информацию, откатывалась в случае ошибки. Без автономных транзакций регистрация чего-то в таблице является частью изменений, выполненных транзакцией, и все эти изменения откатываются, фактически стирая необходимую отладочную информацию. В очень многих примерах возможность регистрации информации вроде значений, выдаваемых функциями SQLCODE, SQLERRM ИЛИ DBMS_UTILITY . FORMAT_CALL_STACK, упоминалась, но никогда не была полностью реализована. Так что теперь пора рассмотреть, как можно записать отладочную информацию в таблицу базы данных. SQL> create table log_table( 2 d date, 3 message clob 4 ) 5 / Table created. SQL> create table test_table( 2 v varchar2(10) 3 ) 4 / Table created.
Здесь мы создали две таблицы: одну для реальных изменений, другую — для записи регистрационной информации. Затем мы создадим пакет LOG_IT И тестовую процедуру, использующую его. SQL> create or replace 2 package log_it as 3 4 procedure put_line( p_message varchar2 ); 5 6 end log_it; 7 / Package created. SQL> create or replace 2 package body log_it as 3 4 procedure put_line( p_message varchar2 ) is 5 pragma autonomous_transaction; 6 begin 7 insert into log_table 8 values ( sysdate, p message );
Отладка PL/SQL 497 9 commit; 10 end put line; 11 12 end log_it; 13 / Package body created. SQL> create or replace 2 procedure test_log_it p_value varchar2 ) as 3 begin 4 log_it.put_line( 'Starting procedure test_log_it' ); 5 insert into test_table values ( p_value ); 6 log_it.put_line( 'Success' ); 7 commit; 8 exception 9 when others then 10 log_it.put_line( 'Exception when inserting ' || p_value ); 11 log_it.put_line( SQLCODE || ' == ' || SQLEREM ); 12 rollback; 13 end test_log_it; 14 / Procedure created.
Обратите внимание, что мы вызываем процедуру пакета LOG_IT ОДИН раз в начале тестовой процедуры, затем еще раз, после выполнения оператора INSERT. ЕСЛИ возбуждается исключительная ситуация, мы регистрируем ошибку и выполняем откат. Теперь давайте посмотрим, что произойдет при выполнении тестовой процедуры. SQL> exec test_log_it( 'ABCDE' ); PL/SQL procedure successfully completed. SQL> exec test_log_it( 'ABCDEFGHIJKLM' ); PL/SQL procedure successfully completed.
Мы использовали обработчик исключительных ситуаций OTHERS И не возбудили исключительную ситуацию повторно. Похоже, все работает нормально. Однако, хотя можно предположить наличие в таблице TESTJTABLE двух строк, при проверке мы увидим только одну: SQL> select * from test_table; V ABCDE
К счастью, мы записали определенную информацию в таблицу LOG_TABLE, поэтому есть возможность разобраться, что произошло. SQL> s e l e c t to_char(d,'DD-MON-YYYY HH24:MI:SS') d, message 2 from l o g _ t a b l e SQL> /
17 Зак. 348
498
Глава 10 D 18-AUG-2003 18-AUG-2003 18-AUG-2003 18-AUG-2003 18-AUG-2003
MESSAGE 21:36:38 21:36:38 21:38:55 21:38:55 21:38:55
Starting procedure test_log_it Success Starting procedure test_log_it Exception when inserting ABCDEFGHIJKLK -1401 == ORA-01401: inserted value too large for column
Хотя при втором вызове процедуры была сделана ошибка, которая привела к откату всех выполненных изменений, регистрационная информация осталась в таблице, поскольку она записывалась в автономной транзакции и не была затронута откатом родительской транзакции. Этот метод отладки весьма полезен. Так можно записать любую необходимую отладочную информацию, и она останется в базе данных даже в случае отката основной транзакции. Дополнительное преимущество такого подхода состоит в том, что вызовы LOG_IT.PUT_LINE можно оставить в производственном коде. Достаточно заменить реализацию процедуры LOG_IT. PUT_LINE пустым оператором (NULL), И при ее вызове ничего не будет выполняться. Поскольку процедура входит в пакет, и вы всего лишь перекомпилируете тело пакета, весь код останется действительным. Затем в любой момент вы сможете снова скомпилировать полную версию реализации процедуры LOG_IT . PUT_LINE в базе данных и начать снова собирать отладочную информацию. Это пример снабжения кода средствами отладки. Оставьте возможность для простой отладки кода — просто отключите ее, пока она не понадобится.
Пакет UTLJILE UTLFILE — это еще один очень полезный стандартный пакет Oracle. Он позво-
ляет записывать в файл операционной системы из хранимой процедуры на языке PL/SQL. Как и при использовании автономной транзакции для записи в таблицу базы данных, как только строка будет записана в файл, фиксация или откат транзакции на нее уже не повлияют. Я обычно предпочитаю выдавать отладочную информацию в файл по нескольким причинам. Во-первых, просматривать и читать строки в файле проще, чем столбцы. Во-вторых, можно просматривать последние строки в отладочных файлах по мере отладки, получая отладочные сообщения в реальном времени с помощью команды tail -£ debugfile.txt
Если бы отладочная информация выдавалась в таблицу, пришлось бы повторно выполнять запрос для получения новых строк. Примечание У тех, кто работает в среде Windows, команды t a i l может не быть, но простой поиск в Web позволяет найти много реализаций t a i l для Windows. Мы рекомендуем установить одну из них, поскольку эта утилита существенно упрощает контроль над журнальными файлами.
Отладка PL/SQL
499
Команда t a i l выдает на экран х последних строк из файла. Опция -F требует продолжить выдачу последних строк по мере их добавления в файл. Примечание Спецификацию пакета UTL_FILE см. в файле $ORACLE_HOME/rdbms/admin/utlf i l e . s q l .
У выдачи отладочной информации в файл есть один недостаток. Oracle может создавать файл только на сервере базы данных, а разработчики часто не имеют к этому серверу доступа. В этих случаях, вероятно, имеет смысл выдавать отладочную информацию в таблицу.
Открытие файла Для открытия файла в файловой системе используется функция FOPEN (). FUNCTION fopen(location filename openjnode max_linesize file_type;
IN IN IN IN
VARCHAR2, VARCHAR2, VARCHAR2, BINARY_INTEGER DEFAULT NULL) RETURN
Чтобы задать файл, который надо открыть, передаются следующие параметры: > LOCATION — задает каталог, в котором находится файл. Надо указать либо имя объекта-каталога, уже созданного с помощью оператора CREATE DIRECTORY, либо каталог файловой системы. Если указывается каталог файловой системы, он должен быть задан в файле параметров инициализации в качестве значения параметра UTL_FILE_DIR; > FILENAME — имя файла, который надо открыть; > OPEN_MODE — строка, определяющая режим открытия файла: запись (w), чтение (R) И добавление (А); > MAX_LINESIZE — необязательный параметр, определяет максимальное количество символов в строке, включая символы новой строки. Допускаются значения от 1 до 32767. Если вызов функции FOPEN завершился успешно, будет возвращен дескриптор файла. Этот дескриптор используется для доступа к файлу и управления им.
Запись строки При наличии дескриптора файла можно записывать в файл строки с помощью процедуры PUT_LINE. PROCEDURE put_line(file IN file_type, buffer IN VARCHAR2, autoflush IN BOOLEAN DEFAULT FALSE);
При этом передаются следующие параметры: > FILE — дескриптор файла, полученный при вызове функции FOPEN; > BUFFER — информация, которую надо записать в файл;
500
Глава 10
> AUTOFLUSH — указывает серверу Oracle, надо ли сбрасывать буфер в файл сразу или нет. Стандартное значение — FALSE. После открытия файла можно вызывать процедуру PUT_LINE многократно. Открывать файл перед каждой записью не нужно.
Закрытие файла Наконец, когда запись в файл полностью завершена, надо закрыть его, освобождая любые ресурсы, которые использовались для поддержки открытого файла. PROCEDURE fclose ( f i l e IN OUT file_type);
Чтобы закрыть файл, просто передайте использованный дескриптор файла.
Использование пакета UTL_FILE Давайте рассмотрим пример использования пакета UTL_FILE ДЛЯ отладки. Мы изменим существующий пакет LOG_IT. SQL> create or replace 2 directory TEMP_DIR as 'c:\temp' 3 / Directory created. SQL> create or replace 2 package body log_it as 3 4 procedure put_line( p_message varchar2 ) is 5 l_file utl_file.file_type; 6 begin 7 l_file : = utl_file.fopen( 'TEMP_DIR', 8 'debugfile.txt', 9 'a 1 , 10 32767 ); 11 utl_file.put_line( l_file, 12 to_char( sysdate, 'DD-MON-YYYY HH24:MI:SS' ) 13 ' ' I I p_message ); 14 utl_file.fclose( l_file ); 15 exception 16 when others then 17 null; 18 end put_line; 19 20 end log_it; 21 / Package body created.
Теперь снова выполним два вызова процедуры TEST2. SQL> delete from test_table; 1 row deleted. SQL> commit; Commit complete.
II
Отладка PL/SQL
501
SQL> exec test_log_it( 'ABCDE' ); PL/SQL procedure successfully completed. SQL> exec test_log_it( 'ABCDEFGHIJKLM1); PL/SQL procedure successfully completed. SQL> select * from test_table; V ABCDE
На этот раз отладочные сообщения были записаны в файл с: \temp\debugf i l e . txt. В другом окне мы выполняли команду TAIL - F ДЛЯ отладочного файла и получили следующий результат: С:\temp>tail -f debugfile.txt 18-AUG-2003 22:20:16 Starting procedure test_log_it 18-AUG-2003 22:20:16 Success 18-AUG-2003 22:20:37 Starting procedure test_log_it 18-AUG-2003 22:20:37 Exception when inserting ABCDEFGHIJKLM 18-AUG-2003 22:20:37 -1401 == ORA-01401: inserted value too large for ^column
Теперь все наши сообщения появляются в этом окне в реальном времени, а также сохраняются в отладочном файле для последующего изучения.
Отладка в реальном времени с помощью конвейерных функций Как мы уже обсуждали, при использовании пакета DBMS_OUTPUT результат недоступен, пока не завершится весь процесс. Годами разработчики испытывали разочарование при тестировании кода со средствами отладки на базе пакета DBMS_OUTPUT, поскольку: > результат не выдавался до завершения вызова; > немногие средства, кроме SQL*Plus, поддерживают выдачу этого результата. В предыдущем разделе, посвященном пакету UTL_FILE, МЫ обсуждали использование команды TAIL для получения потока отладочных сообщений из файла в "реальном времени". Альтернативный способ получения этих отладочных сообщений в реальном времени основан на использовании конвейерных функций (которые детально обсуждаются в главе 5). Поскольку конвейерные функции направляют свой поток строк (или свои результаты) обратно в вызывающую среду в "реальном времени", можно будет отправлять так и отладочную информацию разработчику. Рассмотрим процедуру, выполняющую в цикле пять важных задач и выдающую по завершению каждой задачи определенное сообщение. procedure NIGHTLY_BATCH is begin
502
Глава 10
dbms_output.put_line('Starting TASK_1; dbms_output.put_line('Starting TASK_2; dbms_output.put_line('Starting TASK_3; dbms_output.put_line('Starting TASK_4; dbms_output.put_line('Starting TASK_5; end;
task 1 ' ) ; task 2 ' ) ; task 3 ' ) ; task 4 ' ) ; task 5 ' ) ;
Сначала необходим набор для сохранения результатов, поскольку конвейерные функции должны возвращать наборы. Для простоты мы будем выдавать большие СТРОКИ ТИПа VARCHAR2: SQL> create or replace 2 type output is table of varchar2(1000) ; 3 / Type created. Затем создадим рад процедур, имитирующих задачи в процедуре NIGHTLY_BATCH — в нашем случае они будут просто "засыпать" на 5 секунд. Мы используем динамический SQL для генерации пяти процедур, от TASK_I ДО TASK_5. SQL> begin 2 for i in 1 .. 5 loop 3 execute immediate 4 'create or replace procedure TASK_'||i|| 5 ' is begin dbms_lock.sleep(5); end;'; 6 end loop; 7 end; 8 / PL/SQL procedure successfully completed. SQL> 2 3 4
select object_name from user_objects where object_name like 'TASK%' /
OBJECT NAME TASK_1 TASK_2 TASK_3 TASK_4 TASK_5 Теперь процедуру NIGHTLY_BATCH МОЖНО переписать как конвейерную функцию, а вызовы DBMS OUTPUT заменить командами PIPE ROW.
Отладка PL/SQL SQL> 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
503
create or replace function NIGHTLY_BATCH return output pipelined is begin pipe row ('Starting task I 1 ) ; TASK_1; pipe row ('Starting task 2') ; TASK_2; pipe row ('Starting task 3'); TASK_3; pipe row ('Starting task 4'); TASK_4; pipe row ('Starting task 5'); TASK_5; ' return; end; /
Function created.
При наличии конвейерной функции для получения результатов достаточно выполнить к ней запрос. К сожалению, передать распечаткой то, что дальше произойдет, несколько сложно, но чтобы вызвать функцию NIGHTLY_BATCH И получить результат, мы выполнили оператор SQL> select * from table (nightlyjoatch); COLUMN VALUE Starting Starting Starting Starting Starting
task task task task task
1 2 3 4 5
Результат не был получен до завершения процедуры! С кодом все в порядке, проблему вызывает утилита SQL*Plus. По умолчанию в SQL*Plus установлен размер буфера 15 (выборка массивом полезна в большинстве случаев, как уже было продемонстрировано многими примерами в этой книге), поэтому утилита SQL*Plus не выдаст никаких результатов, пока процедура NIGHTLY_BATCH не отправит ей по конвейеру 15 строк. Если уменьшить размер массива до одной строки, проблема решается. SQL> select * from table(nightly_batch); COLUMN VALUE Starting task 1 Starting task 2 Starting task 3
504
Глава 10
Starting task 4 Starting task 5
Это лучшее, что можно сделать, в связи с оптимизацией предварительной выборки, которая рассматривалась в главе 3, но это намного лучше, чем решение на базе DBMSJDUTPUT. Тогда как длина строки при использовании пакета DBMSJDUTPUT ограничена 255 символами, единственное ограничение при использовании набора — воображение разработчика. Например, в клиентском приложении, написанном в Oracle Forms, можно использовать конвейерную функцию для посылки "индикаторов хода выполнения" обратно в вызывающую среду по ходу работы продолжительного процесса. Конечно, при более реалистичном сценарии наши процедуры TASK_n будут делать намного больше, чем приостановка на 5 секунд. Давайте изменим эти процедуры так, чтобы они выполняли операторы DML над таблицей т, и проверим, работает ли наша выдача результатов по конвейеру так же, как и раньше. SQL> create table T ( х ) as select 1 from dual; Table created. SQL> begin 2 for i in 1 . . 5 loop 3 execute immediate 4 'create or replace procedure TASK_'||ill 5 ' is begin update t set x = x + 1; end;'; 6 end loop; 7 end; 8 / PL/SQL procedure successfully completed.
А теперь давайте повторно выполним конвейерную функцию NIGHTLY_BATCH: SQL> select * from table(nightly_batch); COLUMN_VALUE Starting task 1 ERROR: ORA-14551: cannot perform a DML operation inside a query ORA-06512: at " TASK_1", line 1 ORA-06512: at " NIGHTLY_BATCH", line 4
Проблема не связана собственно с конвейерной функцией; дело в том, что нельзя выполнять операторы DML при выполнении оператора SELECT. Решение состоит в использовании автономной транзакции, как уже обсуждалось ранее, чтобы функция NIGHTLY_BATCH работала в отдельной транзакции. SQL> create or replace 2 function NIGHTLY_BATCH return output pipelined is 3 pragma autonomous_transaction; 4 begin
Отладка PL/SQL 5 5 7 8 9 10 11 12 13 14 15 16 17 18
pipe row TASK_ 1; pipe row TASK_ 2; pipe row TASK_ 3; pipe row TASK_ 4; pipe row TASK_ 5; commit; return; end; /
505
С Starting task 1" С Starting task 2' С Starting task 3'
сStarting
task 4'
сStarting
task 5'
Function created.
Теперь, поскольку функция NIGHTLY_BATCH (И все выполняемые ею задачи) работает как отдельная от вызывающей среды транзакция, ошибок нет. SQL> select * from table(nightly_batch); COLUMN_VALUE Starting Starting Starting Starting Starting
task task task task task
1 2 3 4 5
Специализированная утилита DEBUG До сих пор я описывал стандартные средства и утилиты сервера Oracle. Я рассказывал о различных стандартных пакетах и встроенных функциях, а также об их использовании. Теперь давайте немного сместим акценты и поговорим о специально написанном пакете для отладки. Этот пакет очень помог мне и моим коллегам при отладке наших приложений на языке PL/SQL. Я назвал его DEBUG. В этом разделе я кратко опишу организацию пакета и ключевые процедуры. Полное описание пакета DEBUG вместе с листингами представлено в приложении А. Здесь же мы можем сконцентрироваться на эффективном использовании этого пакета. Для выполнения примеров в данном разделе просто загрузите и установите утилиту DEEUG из раздела Downloads сайта издательства Apress ( h t t p : / / www. apress. com), а также выполните сценарий DEBUG_DB ДЛЯ создания необходимых объектов базы данных. Утилита DEBUG использует многие возможности и методы, описанные ранее в этой главе. Он выдает отладочную информацию в файл, хотя можно также создать регистрационную таблицу с опцией NOLOGGING, чтобы не загружать сервер лишними записями повторного выполнения. Реальная мощь утилиты DEBUG связана с ее возможностью отладки отдельных процедур или пакетов в приложении.
506
Глава 10
Требования Представьте на секундочку, что у вас есть PL/SQL-приложение с большим количеством процедур, функций и пакетов, и возникла ошибка. Если вы следовали приведенным в этой главе рекомендациям, то снабдили код вызовами процедуры отладки. Эти отладочные операторы могут порождать многие тысячи строк результатов, когда выдача разрешена. С точки зрения разработчика, эти строки могут оказаться непонятными и иногда сложными для восприятия. Но вы, как разработчик, вероятно, хорошо представляете, где может быть ошибка, или, что еще лучше, вы локализовали ошибку до конкретного PL/SQL-пакета или процедуры. Поэтому вас интересуют отладочные сообщения только из этого конкретного пакета или процедуры. Все описанные ранее методы работают по принципу "все или ничего". Либо вы получаете всю отладочную информацию, либо вообще ничего. И либо каждый прогон приложения выдает отладочную информацию, либо ни один не выдает. Основная идея разработки пакета DEBUG — дать разработчикам намного более детальный контроль над тем, кто генерирует сообщения и какая информация реально генерируется, без каких бы то ни было изменений приложения. На базе этой основной идеи главные требования к утилите DEBUG МОЖНО уточнить следующим образом: > пригодность к использованию в любом PL/SQL-коде; > отсутствие ограничения на объем отладочной информации, которую можно сгенерировать; > отсутствие ограничения на длину строки отладочной информации; > получение результатов в реальном времени; > самодостаточность (т.е. надо, чтобы утилита знала, на какой строке и в каком пакете она вызвана); > возможность простого включения и отключения; > возможность избирательной отладки пакетов/процедур/функций; > возможность работать по-разному в зависимости от того, кто запустил процесс; > сохранение сообщений в файле; > простота использования. Теперь, когда обозначены требования к утилите том, как ее создать.
DEBUG, МЫ начнем разбираться
в
Проектирование и настройка базы данных SQL-сценарий DebugDB. sql содержит весь код для создания объектов базы, необходимых для работы утилиты DEBUG. ЭТОТ сценарий полностью описан в приложении А и свободно доступен для загрузки с сайта издательства Apress. Если коротко, он содержит код для создания: > схемы для размещения утилиты DEBUG;
Отладка PL/SQL 507 > таблицы DEBUGTAB, в которой хранится профильная информация для каждого пользователя DEBUG, так что утилита будет работать в соответствии со сделанными им настройками среды; > триггера BIU_FER_DEBUGTAB ДЛЯ проверки формата данных профилирования; > объекта-каталога, в который утилита DEBUG будет записывать сообщения.
Структура пакета Теперь давайте разберемся, как устроен пакет DEBUG. ОН должен реализовать четыре основные процедуры: > > > >
инициализацию профиля; генерацию отладочных сообщений; выдачу информации о текущем профиле отладки; сброс профиля.
Процедура инициализации I N I T O должна принимать все опции, которые мы можем устанавливать в профиле отладки. procedure init( p_modules p_dir p_file p_user p_show_date p_date_format p_name_len p_show_sesid
in varchar2 default 'ALL', in varchar2 default 'TEMP', in varchar2 default user II '.dbg', in varchar2 default user, in varchar2 default 'YES', in varchar2 default 'MMDDYYYY HH24MISS', in number default 30, in varchar2 default 'NO' );
Процедура F (), которая будет генерировать отладочное сообщение, должна принимать параметризованное сообщение и список переменных для подстановки в сообщение. procedure f( p_message p_argl p_arg2 p_arg3 p_arg4 p_arg5 p_arg6 p_arg7 p_arg8 p_arg9 p_arglO
in in in in in in in in in in in
varchar2, varchar2 default varchar2 default varchar2 default varchar2 default varchar2 default varchar2 default varchar2 default varchar2 default varchar2 default varchar2 default
null, null, null, null, null, null, null, null, null, null ) ;
Мы уверены, что лишь немногие из вас обратили внимание на жесткое ограничение количества подстановок в отладочное сообщение. Обойти это ограничение можно двумя способами. Можно использовать оператор конкатенации (| |) для построения отладочной строки (что дает неограниченное количество подставляемых значений) или процедуру DEBUG . FA () со следующей спецификацией:
508
Глава 10
emptyDebugArgv Argv; procedure fa( p_message in varchar2, p_args in Argv default emptyDebugArgv ) ;
В приложении А мы полностью описываем, что такое тип ARGV И как его использовать. Здесь достаточно сказать, что он снимает ограничение (не более десяти параметров) реализации процедуры F (). Когда профиль настроен, необходима процедура для просмотра его статуса: procedure p_user p_dir p_file
status( in varchar2 default user, in varchar2 default null, in varchar2 default null ) ;
Наконец, необходим способ сброса профиля отладки, когда генерировать отладочные сообщения больше не нужно. procedure p_user p_dir p_file
clear ( in varchar2 default user, in varchar2 default null, in varchar2 default null ) ;
Процедуры STATUS () и CLEAR о принимают параметры P_USER, P_DIR И P_FILE. Помните, что первичный ключ профиля отладки состоит из всех этих значений. Пользователь должен указать их для получения соответствующей записи. Для простоты мы позволяем передавать NULL В качестве значения параметров P_DIR И P F I L E , В этом случае процедуры влияют на все профили указанного пользователя.
Реализация Полностью реализация всех процедур пакета DEBUG описана в приложении А. Помимо четырех описанных ранее процедур в спецификации, в теле пакета DEBUG реализованы также следующие приватные процедуры: > DEBUG_IT() — координирует работу пакета DEBUG. Процедуры F () H F A O вызывают ее, а она, в свою очередь, вызывает четыре следующие приватные процедуры; > WHO_CALLED_ME () — определяет, на какой строке кода какой процедуры вызвана выдача отладочного сообщения. Она использует для получения стека вызовов стандартную функцию Oracle DBMS_UTILITY . FORMAT_CALL_STACK (). описанную ранее в этой главе; > B U I L D I T O — строит и возвращает заголовок отладочного сообщения. Заголовок содержит всю информацию о вызове DEBUG, сформатированную в соответствии с профилем отладки, установленным с помощью процедуры INIT (); > PARSE_IT() — позволяет анализировать и изменять само сообщение. Выполняется также подстановка вместо %s соответствующих значений; > F I L E _ I T ( ) — используется после создания заголовка, анализа, форматирования и подготовки к выдаче сообщения для записи информации в соответствующий файл.
Отладка PL/SQL
509
Основы использования В следующих разделах мы рассмотрим некоторые простейшие особенности и возможности пакета DEBUG, начиная с его инициализации.
Инициализация Первый шаг при использовании утилиты DEBUG (после установки, конечно) — инициализация. PROCEDURE INIT Argument Name
Type
P_MODULES P_DIR P_FILE P_USER P_SHOW_DATE P_DATE_FORMAT P_NAME_LEN P_SHOW_SESID
VARCHAR2 VARCHAR2 VARCHAR2 VARCHAR2 VARCHAR2 VARCHAR2 NUMBER VARCHAR2
In/Out
Default?
IN IN IN IN IN IN IN IN
DEFAULT DEFAULT DEFAULT DEFAULT DEFAULT DEFAULT DEFAULT DEFAULT
Это процедура INIT пакета DEBUG. Ее вызов позволяет настроить соответствующую среду для отладки. Основные параметры, которые представляют сейчас интерес, — P_MODULES И P_DIR: >• P_MODULES — список пакетов/процедур, которые мы хотим отлаживать; > P D I R — имя объекта DIRECTORY, В котором создается файл с отладочной информацией; > P_FILE — имя файла, в который выдается отладочная информация. Теперь мы вызываем процедуру INIT: SQL> execute d e b u g . i n i t ( 'ALL', 'TEMP', 'debug.dbg' ) ; PL/SQL procedure successfully completed.
При просмотре этого файла мы увидим следующее: Debug parameters USER: MODULES: DIRECTORY: FILENAME: SHOW DATE: DATE FORMAT: NAME LENGTH: SHOW SESSION ID:
initialized on 07-SEP-2003 16:03:40 UTILITY ALL TEMP debug.dbg YES MMDDYYYY HH24MISS 30 NO
Это просто значения параметров текущей среды DEBUG. ДЛЯ пользователя UTILITY все отладочные сообщения будут генерироваться и записываться в файл debug.dbg.
Использование процедуры STATUS Для просмотра текущего профиля можно вызывать процедуру STATUS () в командной строке SQL*Plus.
510
Глава 10
SQL> exec debug.status Debug info for UTILITY USER: MODULES: DIRECTORY: FILENAME: SHOW DATE: DATE FORMAT: NAME LENGTH: SHOW SESSION ID:
UTILITY ALL TEMP myDebug.dbg YES MMDDYYYY HH24MISS 30 NO
PL/SQL procedure successfully completed.
Генерация сообщений Теперь давайте выполним вызов DEBUG . F (): SQL> exec debug.f( 'my first debug message' ); PL/SQL procedure successfully completed.
Весьма непонятно, не так ли? Нет никаких признаков выполненных действий. Проверьте строку 8 в файле отладки. 10282001 213953(UTILITY.ANONYMOUS BLOCK
1) my first debug message
Есть! Вот наше сообщение. Но что это за дополнительный мусор? Это информация заголовка, которая была создана и добавлена к сообщению: > первый набор чисел — это дата в формате MMDDYYYY, а второй набор чисел — время в формате HH24MISS. Формат даты соответствует указанному в профиле; > далее указан владелец объекта, из которого был выполнен вызов. Как видите, он был сделан из автономного блока, принадлежащего пользователю UTILITY, и вызов этот был на строке 1 данного объекта. Процедура WHO_CALLED_ME () получила эту информацию из стека вызовов — именно для этого он очень пригодится.
Изменение профиля Давайте изменим профиль и сгенерируем другое сообщение: SQL> begin 2 debug.init( p_dir => 'TEMP', 3 p_file => 'myDebug.dbg', 4 p_date_format => 'HH:MI:SSAM', 5 p_name_len => 20 ); 6 end; 7 / PL/SQL procedure successfully completed. SQL> exec debug.f( 'another message' ); PL/SQL procedure successfully completed.
Отладка PL/SQL
511
Просмотр отладочного файла дает следующие результаты: Debug parameters initialized on 28-OCT-2003 21:51:04 USER: UTILITY MODULES: ALL DIRECTORY: TEMP FILENAME: rayDebug.dbg SHOW DATE: YES DATE FORMAT: HH:MI:SSAM NAME LENGTH: 25 SHOW SESSION ID: NO 09:53:19PM(UTILITY.ANONYMOUS BLOCK
1) another message
Теперь у нас новый формат даты, и длина имени сокращена. Форматирование можно менять, пока не подберете подходящее. Мне лично нравится стандартное, поэтому я хочу его вернуть. SQL> begin 2 debug.init( 'all', 'TEMP1, 'myDebug.dbg' ); 5 end; 6 / PL/SQL procedure successfully completed.
Теперь давайте разберемся, как работает подстановка. Вспомните, что утилита DEBUG позволяет вызывающему передавать строку и значения для подстановки в нее. Вместо каждого вхождения %s в сообщение утилита DEBUG попытается подставить
следующий параметр. Если выполнить вызов SQL> e x e c d e b u g . f (
'%s % s ! \
'hello',
'world'
);
в результате будет получено следующее отладочное сообщение: 10282001 224317
(UTILITY.ANONYMOUS BLOCK
1)
h e l l o world!
Использование процедуры FA() Процедура F () подойдет только при использовании до десяти параметров. Если параметров больше, можно использовать процедуру FA (). Напомню, что эта процедура принимает в качестве параметров сообщение и значение пользовательского типа ARGV. Вот пример ее вызова: SQL> e x e c
debug.fa(
' T h e %s %s % s ' , d e b u g . a r g v (
'quick','brown','fox'
) );
Результат получается следующий: 10282001 225121(UTILITY.ANONYMOUS BLOCK
1)
The q u i c k brown
fox
Примечание Параметр типа ARGV может состоять из любого количества значений, так что теперь у вас есть способ передать неограниченное количество значений для подстановки.
512
Глава 10
Форматирование файла отладки Для форматирования сообщения можно также использовать управляющие последовательности \N и \т. SQL> e x e c debug.f( 'The %s\n%s f o x ' ,
'quick',
'brown'
);
Вот какое сообщение получится в результате: 10282001 225630(UTILITY.ANONYMOUS BLOCK
1) The q u i c k brown fox
Удаление профиля Когда отладка закончена, можно удалить наш профиль с помощью вызова DEBUG.CLEAR() . SQL> exec debug.clear; PL/SQL procedure successfully completed. SQL> exec debug.status Debug info for UTILITY No debug setup. PL/SQL procedure successfully completed.
Любые последующие вызовы процедур DEBUG . F () И DEBUG . FA () не будут генерировать отладочные сообщения для пользователя BOOK.
Предоставление привилегии на выполнение пакета роли PUBLIC Чтобы процедуры пакета DEBUG МОГЛИ ВЫПОЛНЯТЬ все пользователи, выполните следующий оператор: SQL> grant execute on debug to public; Grant succeeded.
Теперь мы подключимся от имени другого пользователя, SCOTT, И используем пакет DEBUG. Необходимо уточнять все ссылки на процедуры пакета DEBUG име нем пользователя и пакета. SQL> connect scott/tiger Connected. SQL> exec utility.debug.init( 'all', 'TEMP•, 'MyDebug.dbg'); PL/SQL procedure successfully completed. SQL> exec utility.debug.status; Debug info for SCOTT USER: MODULES:
SCOTT ALL
Отладка PL/SQL 513 DIRECTORY FILENAME: SHOW DATE: DATE FORMAT: NAME LENGTH: SHOW SESSION ID:
TEMP MyDebug.dbg YES MMDDYYYY HH24MISS 30 NO
PL/SQL procedure successfully completed.
Давайте перейдем к действительно потрясающей возможности пакета DEBUG: ВОЗМОЖНОСТИ избирательно выдавать отладочные сообщения в зависимости от того, из какой процедуры они выдаются.
Избирательная отладка Именно в этом заключается реальная мощь пакета DEBUG. Прежде всего нам понадобится ряд процедур для отладки. Давайте снова подключимся от имени пользователя UTILITY. SQL> create or replace 2 procedure a as 3 begin 4 debug.f( ' AAAAAAA ' ) ; 5 dbms_output.put_line( ' AAAAAAA ' ); 6 end a; 7 / Procedure created. SQL> create or replace 2 procedure b as 3 begin 4 a; 5 debug.f( ' BBBBBBB ' ); 6 dbms_output.put_line( ' BBBBBBB ' ), 7 end b; 8 / Procedure created. SQL> create or replace 2 procedure с as 3 begin 4 b; 5 debug. f ( ' CCCCCCC ' ) ; 6 dbms_output.put_line( ' CCCCCCC ' ), 7 end c; 8 / Procedure created. SQL> create or replace 2 procedure d as 3 begin 4 c;
•
514
Глава 10
5 debug.f ( ' DDDDDDD ' ) ; 6 dbras_output.put_line( ' DDDDDDD ' ) ; 7 end d; 8 / Procedure created. SQL> create or replace 2 procedure e as 3 begin 4 d; 5 debug.f( ' EEEEEEE ' ) 6 dbms_output.put_line( 7 end e; 8 / Procedure created.
EEEEEEE
У нас есть пять процедур, вызывающих друг друга, и в каждой есть отладочный код. Мы включили вызовы как пакета DEBUG, так и пакета DBMS_OUTPUT, так что можем продемонстрировать возможности пакета DEBUG. Обычно при использовании пакета DBMS_OUTPUT МЫ просто включаем SERVERO JTPUT и выдаем отладочные сообщения на экран. SQL> set serverout on size 1000000 SQL> exec E; AAAAAAA BBBBBBB CCCCCCC DDDDDDD EEEEEEE PL/SQL procedure successfully completed.
Как уже объяснялось, при этом вы получите все или ничего — либо весь журнал отладки, либо ни одного сообщения. Что, если мы хотим получить часть отладочной информации? С помощью пакета DEBUG МЫ можем это сделать. Теперь мы инициализируем пакет DEBUG И повторно выполняем процедуру Е. 1
SQL> execute debug.init( 'ALL , 'TEMP', 'debug.dbg' ) ; PL/SQL procedure successfully completed. SQL> exec E; PL/SQL procedure successfully completed.
В отладочном файле мы должны увидеть следующее: 09072003 09072003 09072003 09072003 09072003
160540( 160540( 160540( 160540( 160540(
UTILITY.A UTILITY.В UTILITY.С UTILITY.D UTILITY.E
3) 4) 4) 4) 4)
AAAAAAA BBBBBBB CCCCCCC DDDDDDD EEEEEEE
Пять строк отладочной информации оказались в файле. Вы, возможно, подумаете, что это не слишком впечатляюще — такое можно сделать с помощью пакета
Отладка PL/SQL
515
U T L F I L E . Но обратите внимание на полученную информацию. Она включает дату
и время генерации отладочного сообщения, что очень полезно для отладки. Затем обратите внимание, что указано, какая процедура какому пользователю принадлежит и на какой строке выдано сообщение. Это помогает разобраться, откуда именно оно выдано. И, наконец, выдается само сообщение. Если бы пакет DEBUG ПОЗВОЛЯЛ сделать только это, он уже стал бы существенно лучше пакетов DBMS_OUTPUT И UTL_FILE. Теперь давайте рассмотрим действительно существенные его возможности. Переинициализируем пакет DEBUG, потребовав отладки лишь некоторых процедур. 1
SQL> e x e c u t e d e b u g . i n i t ( 'A,B,D', 'TEMP , ' d e b u g . d b g ' ) ; PL/SQL p r o c e d u r e s u c c e s s f u l l y c o m p l e t e d . SQL> e x e c u t e E; AAAAAAA BBBBBBB CCCCCCC DDDDDDD EEEEEEE
PL/SQL procedure successfully completed.
Результаты пакета DBMS_OUTPUT не отличаются. Но если обратиться к файлу debug.dbg, мы увидим следующее: Debug parameters initialized on 07-SEP-2003 16:14:16 USER: UTILITY MODULES: A,B,D DIRECTORY: TEMP FILENAME: debug.dbg SHOW DATE: YES DATE FORMAT: MMDDYYYY HH24MISS NAME LENGTH: 30 SHOW SESSION ID: NO 09072003 161418 ( 09072003 161418( 09072003 161418(
UTILITY . А UTILITY .В UTILITY .D
3) 4) 4)
AAAAAAA BBBBBBB DDDDDDD
Отладочные сообщения сгенерировали только процедуры А, В И D. ВЫЗОВЫ DEBUG.F, выполненные в процедурах с и Е, не сгенерировали информации. В этом небольшом примере уменьшение объема отладочной информации на две строки не обязательно поможет. Но представьте себе, что ваш код снабжен вызовами DEBUG, F и при необходимости вы можете включить выдачу отладочной информации только из конкретной процедуры. Это очень удобно. Когда отладка закончена, вы можете просто сбросить ее профиль с помощью процедуры DEBUG . CLEAR, и сообщения больше генерироваться не будут. И все это — не меняя ни единой строки производственного кода! Я применял этот метод во многих приложениях, и он всегда работал. Он простой и безвредный. Его легко использовать, а когда выдача отладочной информация не
516
Глава 10
включена, дополнительных расходов ресурсов при работе приложения практически нет. Я получал от пользователей сообщения об ошибках в приложении, которое я написал год назад. Я не помню точно каждую строку кода и общую организацию работы приложения, но я точно знаю, что использовал пакет DEBUG. Я просто включаю выдачу отладочных сообщений для этого пользователя и просматриваю сгенерированный файл, при этом обычно сравнительно быстро нахожу причину ошибки. Помните, что утилита DEBUG генерирует имя процедуры и номер строки, из которой было выдано отладочное сообщение. Этой информации в сочетании с подробными сообщениями должно быть более чем достаточно для начала поиска в нужном направлении.
Отладка производственного кода После завершения процесса разработки, когда необходимо ввести приложение в производственную эксплуатацию, у нас все еще могут быть в коде многие сотни строк для отладки. Хотелось бы избежать лишних расходов ресурсов на вызовы DEBUG, чтобы увеличить производительность. Хотя достаточно найти и удалить каждое отладочное сообщение, есть и другой вариант решения: изменить процедуры F () и FA() , добавив return; в качестве первой строки. При этом в случае вызова любой из этих процедур она немедленно завершает работу, ничего не делая. Этот метод требует определенных ресурсов на вызов, но преимущества превосходят небольшой недостаток. Чтобы оценить время, расходуемое на вызовы процедуры DEBUG . F (), можно использовать простую PL/SQL-процедуру, вроде представленной ниже. В данном случае при тестировании по умолчанию выполняется 1000 вызовов процедуры DEBUG . F () с посылкой трех параметров для подстановки в каждом вызове. Мы делаем это 20 раз, а затем вычисляем среднее время выполнения 1000 вызовов. create procedure debug_timer( p_test_cnt number default 20, p_iterations number default 1000 ) as l_start timestamp; l_end timestamp; l_timer number := 0; begin for i in 1 .. p_test_cnt loop l_start := current_timestamp; for j in 1 .. p_iterations loop debug.f( 'A %s С %s E %s G 1 , 'B', 'D', 'F' ); end loop; l_end := current_timestamp; l_timer := l_timer + to_number( substr ( l_end-l_start, instr( l_end-l_start, ':', -1 )+l ) ) ; end loop; dbms_output.put_line(
'In ' || p test_cnt || ' tests ' );
Отладка PL/SQL
517
dbms_output.put_line( ' i t took an average ' II l_timer/p_test_cnt | I ' seconds' I I ' / ' II p _ i t e r a t i o n s II ' c a l l s to f ( ) . ' ) ; end debug timer; Примечание Эта процедура будет работать только в версиях Oracle9/ и выше. Мы используем новый тип данных, TIMESTAMP, позволяющий измерять время с точностью до долей секунды, со стандартной точностью шесть знаков после запятой.
Оценка времени выполнения отладочных операторов Теперь давайте выполним эту процедуру и посмотрим на результаты: SQL> exec debug_timer In 20 tests it took an average 1.83848415 seconds/1000 calls to f(), PL/SQL procedure successfully completed. Как видите, в данном случае потребовалось в среднем 1,83 секунды для выполнения 1000 вызовов процедуры DEBUG.F(). Конечно, результаты могут отличаться, в зависимости от процессора. Теперь давайте выполним тот же тест с измененной версией пакета DEBUG. М Ы просто добавили строку r e t u r n ; в начале процедур F () и FA () следующим образом: procedure fa( p_message in varchar2, p_args in Argv default emptyDebugArgv ) is begin return; debug_it( p_message, p_args ); end fa; procedure f( p_message in varchar2, p_argl in varchar2 default null, p_arg2 p_arg3 p_arg4 p_arg5 p_arg6 p_arg7 p_arg8 p_arg9 p_arglO begin return; debug_it(
in in in in in in in in in
varchar2 varchar2 varchar2 varchar2 varchar2 varchar2 varchar2 varchar2 varchar2
default default default default default default default default default
null, null, null, null, null, null, null, null, null ) is
p_message, argv( substr( p argl, 1, 4000 ),
518
Глава 10 substr( substr( substr( substr( substr( substr( substr( substr( substr(
p_arg2, 1, 4000 ), p_arg3, 1, 4000 ), p_arg4, 1, 4000 ), p_arg5, 1, 4000 ), p_arg6, 1, 4000 ), p_arg7, 1, 4000 ), p_arg8, 1, 4000 ), p_arg9, 1, 4000 ), p_arglO, 1, 4000 ) ) ) ;
end f;
Теперь давайте повторно выполним тест для оценки времени выполнения и рассмотрим его результаты: SQL> exec debug_timer In 20 tests i t took an average .04530185 seconds/1000 calls to f ( ) . PL/SQL procedure successfully completed.
Сорок пять тысячных секунды в среднем для выполнения 1000 вызовов процедуры DEBUG . F () — не так уж много времени. Поэтому, похоже, что если оставить отладочные вызовы в производственном коде, производительность приложения от этого существенно не пострадает. Примечание Эти значения представлены просто для сравнения и не определяют реальное время на отладку. Значения, которые вы получите при выполнении тестов, естественно, будут зависеть от конфигурации системы.
Дополнительное преимущество состоит в том, что вы можете удалить строку RETURN,- из двух процедур и начать отлаживать производственное приложение в
любой момент. Все мы знаем, что в работающих в производственной среде приложениях возникают ошибки, которые трудно или невозможно воспроизвести в среде разработки. Если оставить вызовы DEBUG В коде, можно будет выявить ошибку в реальном времени, быстро выяснить, в чем проблема, и устранить ее.
Для чего может пригодиться пакет DEBUG? Пакет DEBUG можно использовать во многих случаях. 1. В любом PL/SQL-приложении. Просто добавьте вызовы F () и FA о там, где необходимо. А когда надо будет их активизировать, проинициализируйте профиль отладки. Если отладка приложения не ведется, просто сбросьте профиль, и вызовы процедур пакета DEBUG не будут выдавать никаких результатов. 2. Рассмотрим PL/SQL-приложение или подпрограмму, которая вызывается не из среды SQL*Plus. Как насчет Forms-приложения, вызывающего PL/SQL-код на сервере? Как легко получить информацию о ходе выполнения кода? Никак. Надо использовать пакет DEBUG.
Отладка PL/SQL
519
3. Если вы знакомы с защитой на уровне строк (row-level security — RLS) в Oracle, то знаете о правилах, которые необходимо реализовать в виде процедуры, для включения этой возможности. Но эту процедуру нельзя вызвать непосредственно. Сервер Oracle вызывает ее сам при выполнении оператора SELECT ДЛЯ таблицы с включенной поддержкой RLS. Удачи вам при получении информации из этой процедуры, если вы не используете пакет DEBUG! 4. Триггер — вот еще один пример PL/SQL-кода, который нельзя вызвать непосредственно от имени пользователя или в приложении, а пакет DEBUG прекрасно подходит для контроля действий триггера. 5. Используя временную отметку в каждом отладочном сообщении, вы можете определить, сколько времени понадобилось для перехода в коде от одного отладочного сообщения до другого. Это может помочь выявить в коде узкие места с точки зрения производительности. При наличии следующих вызовов debug.f( 'before calling my_proc' ) ; my_proc; debug.f( 'after calling my_proc' ) ;
если между выдачей сообщений прошло много времени, и вы точно знаете, что процедура MY_PROC () выполняется достаточно долго, может иметь смысл заняться повышением ее производительности. Я рекомендую каждому разработчику, использующему язык PL/SQL, загрузить копию пакета DEBUG И попробовать его в работе. Загрузить пакет можно из раздела Downloads на сайте издательства Apress (http://www.apress.com). Если вас интересует полная информация о возможностях и коде пакета DEBUG, прочтите приложение А.
Резюме Как вы уже поняли, у PL/SQL-разработчика есть очень много средств отладки: от простейшего пакета DBMS_OUTPUT, простой записи в таблицу или в файл и до полнофункциональной утилиты DEBUG. В одной главе невозможно рассмотреть все доступные способы отладки. Например, среда JDeveloper включает полнофункциональный отладчик PL/SQL-кода с возможностью установки в коде точек останова, пошагового, по строкам, выполнения кода и просмотра значений переменных на каждом шаге. Эта среда предназначена не только для Java-разработчиков. Если вы хотите попробовать среду JDeveloper в работе, ее можно свободно загрузить со страницы http://otn.oracle.com/products/jdev/content.html. При разработке на языке PL/SQL мы использовали все методы, описанные в этой главе, и продолжаем их использовать. Каждый метод полезен в определенных ситуациях. Нет единственно правильного способа отладки. Сочетание защитного кодирования, эффективной обработки исключительных ситуаций и применения описанных в этой главе средств отладки позволяет разработчику быстро и эффективно выявлять и устранять проблемы в приложениях.
Приложение А
Создание утилиты DEBUG В этом приложении мы представим код для специализированной утилиты отладки мы использовали в главе 10. Этот код свободно доступен для загрузки с Web-сайта издательства Apress (http: //www.apress .com). DEBUG, которую
Проектирование и настройка объектов базы данных Поскольку DEBUG — PL/SQL-утилита, она находится в базе данных; нужно выбрать схему, которой она будет принадлежать. Это может быть схема любого пользователя с ролями CONNECT, RESOURCE и привилегией CREATE PUBLIC SYNONYM. Я рекомендую создать отдельную схему для пакета DEBUG И предоставить привилегию EXECUTE для пакета DEBUG роли PUBLIC. Таким образом, будет поддерживаться одна копия пакета, совместно используемая всеми. В моей базе данных обычно есть схема UTILITY, в которой находятся утилиты вроде DEBUG, поэтому в примерах я буду использовать схему UTILITY. SQL> create user u t i l i t y identified by u t i l i t y ; User created. SQL> grant connect, resource, create public synonym to u t i l i t y ; Grant succeeded.
Теперь мы готовы создавать объекты схемы, необходимые для поддержки утилиты DEBUG.
Таблицы Сейчас мы хотим настроить пакет DEBUG так, чтобы он работал в соответствии с установками среды каждого пользователя. Вместо того чтобы запоминать соответствующую информацию в среде, мы будем хранить ее в таблице базы данных DEBUGTAB. Код для создания этой таблицы следующий: create table userid dir filename modules show_date date format
debugtab( varchar2(30), varchar2(32), varchar2(1024), varchar2(4000), varchar2 (3), varchar2(255),
522
Приложение А name_length number, session_id varchar2(3), —
Ограничения целостности
constraint debugtab_jpk primary key ( userid, dir, filename ), constraint debugtab_show_date_ck check ( show_date in ( 'YES1, 'NO' ) ), constraint debugtab_session_id_ck check ( session_id in ( 'YES', 'NO' ) )
Как можно понять по именам столбцов, мы храним всю информацию для каждого пользователя, желающего использовать пакет DEBUG. Параметры (например, какие процедуры генерируют отладочную информацию, в каком формате выдается дата/временная отметка, надо ли выдавать идентификатор сеанса) хранятся в этой таблице.
Индексы и ограничения целостности По этой таблице создан единственный индекс по первичному ключу — DEBUGTAB_PK. Он был создан в операторе CREATE TABLE. ДЛЯ уникальной идентификации записи мы используем комбинацию столбцов USERID, DIR, FILENAME. ЭТО может показаться несколько странным, поскольку значения USERID вроде бы должно быть достаточно. Однако рассмотрим приложения, к которым каждый пользователь подключается с тем же именем, а реальная идентификация выполняется по каким-то другим признакам (например, по "ключикам" в Web-приложении). Если бы первичный ключ включал только столбец USERID, ДЛЯ всего приложения пришлось бы использовать один профиль отладки. Это было бы не слишком удобно, особенно когда над приложением одновременно работали бы несколько человек. Ограничения проверки для таблицы DEBUGTAB гарантируют корректность введенных значений, и можно предположить, что при чтении данных из таблицы DEBUGTAB в соответствующих столбцах будут только значения YES ИЛИ NO.
Триггеры Мы используем триггер по таблице DEBUGTAB, чтобы проверять, имеют ли параметры профиля при вводе правильный формат: create or replace trigger biu_fer_debugtab before insert or update on debugtab for each row begin :new.modules : = upper( :new.modules ); :new.show_date := upper( :new.show_date ) ; :new.session_id := upper( :new.session_id ); :new.userid := upper( mew.userid );
Создание утилиты DEBUG 523
Здесь триггер форматирует данные, переводя переданные значения в верхний регистр: declare l_date varchar2(100); begin l_date := to_char( sysdate, :new.date_format ) ; exception when others then raise_application_error( -20001, 1 Invalid Date Format In Debug Date Format' ); end; declare l_handle utl_file.file_type; begin l_handle := utl_file.fopen( location => :new.dir, filename => :new.filename, open_mode => 'a', max_linesize => 32767 ); utl_file.fclose( l_handle ) ; exception when others then raise_application_error( -20001, 'Cannot open debug dir/file ' || :new.dir || '/' II :new.filename ); end; end; /
Эта часть триггера проверяет переданный формат даты. Он пытается создать строку по переданной маске. Если маска недопустима, возбуждается ошибка приложения, и вставка отменяется. Затем триггер проверяет допустимость переданных имен каталога и файла, а также возможность сервера Oracle записывать в указанный файл. Это упрощает реализацию пакета DEBUG, поскольку можно предполагать, что все данные в таблице DEBUGTAB корректны.
Объект DIRECTORY Традиционно пакет UTL_FILE работал с явно указанным именем каталога. В последних версиях сервера (начиная с версии 9) вместо явного указания одного из перечисленных в параметре UTL_FILE_DIR каталогов можно указывать имя объекта DIRECTORY. Эта версия пакета DEBUG была изменена для использования объекта DIRECTORY. Для создания соответствующего объекта необходимо выполнить следующий оператор: create or replace directory TEMP as
'/some/directory/writable/by/Oracle'
524
Приложение А
Структура пакета Давайте рассмотрим, как устроен пакет DEBUG. ОН должен реализовать четыре основные процедуры: > инициализацию профиля; > генерацию отладочных сообщений; > выдачу текущего профиля отладки; > очистку профиля. Процедура инициализации I N I T O должна принимать все параметры, которые можно устанавливать в профиле отладки. procedure init( p_modules p_dir p_file p_user p_show_date p_date_format p_name_len p_show_sesid
in in in in in in in in
varchar2 varchar2 varchar2 varchar2 varchar2 varchar2 number varchar2
default default default default default default default default
'ALL', 'TEMP', user | | '.dbg', user, 'YES', 'MMDDYYYY HH24MISS', 30, 'NO' );
Процедура F (), генерирующая отладочное сообщение, должна принимать сообщение с параметрами и список переменных для подстановки. procedure f( p_message in p_argl in p_arg2 in p_arg3 in p_arg4 in p_arg5 in p_arg6 in p_arg7 in p_arg8 in p_arg9 in p_arglO in
varchar2, varchar2 default varchar2 default varchar2 default varchar2 default varchar2 default varchar2 default varchar2 default varchar2 default varchar2 default varchar2 default
null, null, null, null, null, null, null, null, null, null );
Мы уверены, что лишь немногие из вас обратили внимание на жесткое ограничение количества подстановок в отладочное сообщение. Обойти это ограничение возможно двумя способами. Можно использовать оператор конкатенации (| ) для построения отладочной строки (что дает неограниченное количество подставляемых значений) или процедуру DEBUG.FA() со следующей спецификацией: emptyDebugArgv Argv; procedure fa( p_message in varchar2, p_args in Argv default emptyDebugArgv );
Создание утилиты DEBUG
525
Далее мы расскажем, что такое тип ARGV И как его использовать. Здесь достаточно сказать, что он снимает ограничение (не более десяти параметров) реализации процедуры F(). Когда профиль настроен, необходимо средство для его просмотра — процедура для просмотра статуса: procedure status( p_user in varchar2 default user, p_dxr in varchar2 default null, p_file in varchar2 default null );
Наконец, необходим способ сброса профиля отладки, когда генерировать отладочные сообщения больше не нужно. procedure clear( p_user in varchar2 default user, p_dir in varchar2 default null, p_file in varchar2 default null );
Процедуры STATUS () и CLEAR о принимают параметры P_USER, P_DIRH P_FILE. Помните, что первичный ключ профиля отладки состоит из всех этих значений. Пользователь должен указать их все для получения соответствующей записи. Для простоты мы позволяем передавать NULL В качестве значения параметров P_DIR И P_FILE. В этом случае процедуры влияют на все профили указанного пользователя.
Реализация Теперь пора переходить к кодированию. Необходимо реализовать четыре процедуры из спецификации, но в теле пакета DEBUG есть ряд приватных процедур, которые тоже надо реализовать. Разобьем приватные процессы отладки на логические группы действий. Для этого нужно сделать следующее: > определить, из какого кода вызвана процедура пакета DEBUG — это делает процедура WHO_CALLED_ME() ; > построить заголовок отладочного сообщения — это выполняет процедура BUILD_ITO;
> проанализировать отладочное сообщение и выполнить подстановки — это делает процедура PARSE_IT о ; > выдать отладочное сообщение в файл — для этого предназначена процедура FILE_IT().
Для каждого из этих действий создается приватная процедура/функция. Давайте, однако, начнем с общедоступных интерфейсов, используемых для генерации отладочных сообщений, — с процедур F (), FA () и DEBUG_IT {).
Процедура F() Процедура F () — наиболее часто используемая процедура пакета DEBUG. Ее можно использовать в любом месте кода для генерации сообщения в файл, чтобы можно было (в реальном времени) увидеть, что происходит. Реализация этой процедуры весьма проста:
526
Приложение А 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
procedure f( p_message in varchar2, p_argl in varchar2 default null, p_arg2 in varchar2 default null, p_arg3 in varchar2 default null, p_arg4 in varchar2 default null, p_arg5 in varchar2 default null, p_arg6 in varchar2 default null, p_arg7 in varchar2 default null, p_arg8 in varchar2 default null, p_arg9 in varchar2 default null, p_arglO in varchar2 default null ) is begin debug_it( p_message, argv( substr( p_argl, 1, 4000 ), substr( p_arg2, 1, 4000 ), substr( p_arg3, 1, 4000 ), substr( p_arg4, 1, 4000 ), substr( p_arg5, 1, 4000 ), substr( p_arg6, 1, 4000 ), substr( p_arg7, 1, 4000 ), substr( p_arg8, 1, 4000 ), substr( p_arg9, 1, 4000 ), substr( p_arglO, 1, 4000 ) ) ); end f;
Как видите, она не делает ничего, кроме переупаковки десяти значений p_ARGn в параметр типа ARGV И передачи его процедуре DEBUG_IT ().
Тип ARGV Тип ARGV определен в спецификации пакета DEBUG следующим образом: type argv is table of varchar2(4000);
Мы создали тип ARGV для реализации процедуры FA {). С помощью типа ARGV МЫ можем передать в процедуру любое количество параметров. Рассматривайте его как массив. Процедура F () была добавлена для простоты, а процедура FA о — для полноты. Намного проще набрать l_var varchar2(5) := 'World'; debug.f( 'Hello %s', l_var ) ;
чем следующий вызов: l_var varchar2(5) := 'World'; debug.fa( 'Hello %s', debug.argv( 'World' ) );
Кроме того, с точки зрения способа вызова, процедура DEBUG . F () ближе к вызову функции printf () языка С, чем процедура DEBUG, FA о , а мы пытаемся имитировать функцию printf о .
Создание утилиты DEBUG
527
Если надо передать более десяти переменных для подстановки, можно использовать процедуру FA () или оператор | |.
Процедура FA() Реализация процедуры FA () еще проще, чем процедуры F (). 1 2 3 4 5 6
procedure fa( p_message in varchar2, p_args in Argv default emptyDebugArgv ) i s begin debug_it( p_message, p_args ); end fa;
Как видите, эта процедура непосредственно вызывает DEBUG_IT О , вообще ничего не делая. Вас, возможно, интересует, почему процедура F () не вызывает процедуру FA (), а процедура FA о не выполняет все необходимые действия сама. Далее это станет понятно. Пока же скажем лишь то, что это связано с определением того, кто вызвал процедуру пакета DEBUG.
Процедура DEBUG_IT() Эта процедура — координатор пакета DEBUG. Процедуры F () и FA о вызывают ее, а она в свою очередь вызывает четыре приватных процедуры, которые мы кратко описали ранее. При наличии любого пакета вроде DEBUG (пакета с несколькими точками входа, с общими функциональными возможностями) всю реальную работу надо возложить на приватную управляющую процедуру. Таким образом, если необходимо будет изменить алгоритм работы пакета, изменение будет выполняться в одном месте. Все точки входа, в данном случае — процедуры F () и FA (), останутся синхронизированными с точки зрения общих функциональных возможностей. Рассмотрим реализацию процедуры-координатора пакета DEBUG. 1 2 3 4 5 6 7 8 Э 10 11 12 13
procedure debug it( р message in varchar2, p argv in argv ) is 1 message long := null; 1 header long := null; call who_called_me boolean := true; 1 owner varchar2(255); 1 object varchar2(255); 1 lineno number; 1 dummy boolean; begin
По способу вызова процедуры DEBUG_IT () в процедурах F () и FA () понятно, что она принимает строку сообщения и параметр типа AGRV. МЫ также задали несколько локальных переменных. Прежде всего надо проверить, включена ли вообще отладка для текущего пользователя. Для этого из таблицы DEBUGTAB выбираются записи, в которых значение в
528
Приложение А
столбце USERID совпадает с именем текущего пользователя. В данном случае мы сравниваем его со значением встроенной функции USER, которая возвращает имя пользователя, выполняющего процедуру. 14 15 16 17 18
for с in ( select * from debugtab where userid = user ) loop
Затем нам нужно вызвать первую из четырех приватных процедур — WHO_CALLED_ME (). Вызов выполняется в условном операторе для повышения производительности. Если есть две записи с одним и тем же значением USERID, НО разными значениями FILENAME; нет смысла второй раз вызывать процедуру WHO_CALLED_ME ()
в цикле (поскольку она даст тот же результат). Это уменьшит нагрузку на процессор. 19 20 21 22 23
if call_who_called_me then who_called_me( l_owner, l_object, l_lineno ) ; call_who_called_me := false; end if;
Теперь мы хотим проверить, входят ли в значение переменной L_OBJECT, которое вернула процедура WHO_CALLED_ME () (список имен объектов через запятую), имена объектов, которые надо отлаживать. 24 25 26 27 28
if instr( ',' II с.modules II ',', V II l_object II ',' ) о с.modules = 'ALL1 then
0 or
Наконец, если определено, что отладочные сообщения надо выдавать, мы вызываем остальные три приватные процедуры, которые и будут выполнять необходимые действия. 29 30 31 32 33 34 35 36
l_header := build_it( с, l_owner, l_object, l_lineno ); l_message := parse_it( p_message, p_argv, length(l_header) ); l_dummy := file_it( c.dir, c.filename, l_header II 1 message ); end if; end loop; end debug_it;
Обратите внимание, что мы не проверяем возвращаемое значение функции FILE_IT (). Дело в том, что в случае сбоя при выдаче сообщения мы ничего не смо-
жем сделать. Мы не хотим останавливать работу приложения только потому, что пакет DEBUG не может выдать сообщение. Надо выявлять причину, почему сообщения не выданы (см. раздел "Поиск причин проблем в пакете DEBUG" настоящего приложения).
Создание утилиты DEBUG
529
Поиск соответствия в строках Вас может интересовать, почему строки поиска в функции INSTR () — 24 и 25 — дополнены запятыми. Это делается для того, чтобы гарантировать, что никакая подпрограмма не будет отлаживаться по ошибке. Рассмотрим случай, когда есть две процедуры: А () и АА (). Если разработчик хочет отлаживать только процедуру АА (), а отладочный вызов был выполнен из процедуры А (), то без добавления запятых отладочное сообщение будет ошибочно выдаваться. Давайте посмотрим, как работает ФУНКЦИЯ INSTRO . SQL> select instr( 'Samantha', 'man' ) position from dual; POSITION 3 -
Функция INSTR () ищет второй параметр в первом, и если находит, то возвращает индекс его первого вхождения. В рассмотренном выше примере функция INSTR о нашла строку MAN В строке SAMANTHA, начиная с позиции 3. Возвратимся к примеру с процедурами АО и АА(). Строка А входит в строку АА, но мы не хотим отлаживать процедуру А (), поэтому помещаем запятые с двух сторон строк и ищем ,А, в строке ,АА, . Поскольку соответствие не находится, отладочная информация не выдается, что и требовалось. Вот пример, иллюстрирующий это: SQL> declare 2 debug_procedure_name long := 'A'; 3 list_of_debuggable_procs long := 'AA'; 4 begin 5 if i n s t r ( list_of_debuggable_procs, 6 debug_j?rocedure_name ) 0 then 7 dbms_output.put_line( 'found i t ' ); 8 else 9 dbms_output.put_line( 'did not find it' ) ; 10 end if; 11 i f i n s t r ( ' , ' | | list_of_debuggable_procs || ',', 12 ' , ' 1 1 debug_procedure_name II ' , ' ) 0 then 13 dbms_output.put_line( 'found i t ' ) ; 14 else 15 dbms__output.put_line( 'did not find i t ' ) ; 16 end if; 17 end; 18 / found i t did not find i t
Процедура WHO_CALLED_ME() Первая из четырех процедур, которые вызываются в теле процедуры DEBUG_IT (), — это процедура WHO_CALLED_ME (). Она определяет, с какой строки кода и в какой IS
Зак. 348
530
Приложение А
процедуре было выдано отладочное сообщение. В этой процедуре мы хотим использовать стандартную функцию Oracle DBMS_UTILITY . FORMAT_CALL_STACK () для получения стека вызовов. Стек вызовов — это список процедур и функций, которые мы выполнили. Если мы изначально вызывали процедуру А (), которая вызвала процедуру в (), вызвавшую процедуру с (), то стек вызовов показывает, где именно в коде мы сейчас находимся и через какие процедуры прошли, чтобы добраться до этой точки. Стек вызовов также содержит дополнительную информацию, такую, как номера строк. Затем можно использовать функции обработки строк — SUBSTRO , INSTRO , LTRIMO и RTRIMO — для выборки интересующей нас информации. Давайте разберемся, как это можно сделать. Первая часть кода определяет процедуру и локальные переменные. Обратите внимание на три параметра в режиме оит. Поскольку эта процедура должна вернуть несколько параметров, пришлось оформить ее как процедуру с оит-параметрами, а не как функцию (которая может вернуть только одно значение). 1 2 3 4 5 6 7 8 9
procedure who_called_me( o_owner out varchar2, o_object out varchar2, o_lineno out number ) is l_call_stack long default dbms_utility.format_call_stack; l_line varchar2(4000); begin
Далее в комментариях (в строках с 10 по 19) представлен пример результата функции DBMS_UTILITY . FORMAT_CALL_STACK (). Первые три строки — заголовок. Каждая следующая строка — это строка в стеке вызовов. Информация в каждой строке включает, среди прочего, владельца, имя объекта и номер строки. Именно эти три компонента нам надо выделить и вернуть. 10 11 12 13 14 15 16 17 18
19
/* PL/SQL Call Stack object line object handle number name 86c60290 17 package body UTILITY .DEBUG 86c60290 212 package body UTILITY .DEBUG 86c60290 251 package body UTILITY .DEBUG 86aa28fO 1 procedure OPS$CLBECK .A 1 anonymous block 86a9e940 */
20
Основная задача, которую должна выполнить процедура WHO_CALLED_ME (), — пропуск первых шести строк в стеке вызовов. Первые три строки, как мы знаем, это заголовочная информация, а следующие три строки — вызовы в самом пакете DEBUG. Речь идет о следующих трех вызовах (в обратном порядке): > вызов процедуры F() или FA() (строка 16);
Создание утилиты DEBUG
531
> вызов управляющей процедуры DEBUG_IT о (строка 15); > вызов процедуры WHO_CALLED_ME () (строка 14). Если бы процедура F () вызывала процедуру FA о , а та выполняла действия процедуры DEBUG_IT о , в стеке было бы разное количество уровней в зависимости от того, какая из двух процедур отладки вызвана, и процедуру WHO_CALLED_ME () реализовать было бы значительно сложнее. Давайте рассмотрим следующую секцию кода: 21 22 23 24 25
for i in 1 .. 6 loop l_call_stack := substr( l_call_stack, instr( l_call_stack, chr(10) )+l ) ; end loop;
Итак, локальная переменная L_CALL_STACK начинается со строки, которая нас интересует. Для простоты давайте установим локальную переменную равной именно этой строке, игнорируя все, что идет после нее. 26 27 28 29
l_line := ltrim( substr( l_call_stack, 1, instr( l_call_stack, chr(10) ) - 1 ) ) ;
Если стек вызовов выглядит так, как в предыдущем примере, переменная L_LINE будет иметь следующее значение: 86aa28fO
I procedure OPS$CLBECK.A
Теперь начнем разбирать L_LINE. Сначала удалим дескриптор объекта, 86aa28f О, присвоив переменной L_LINE подстроку ее же значения, начиная с первого пробела. Это значение передается функции LTRIMO , которая удаляет все начальные пробелы. 30 31
l_line := l t r i m ( s u b s t r ( l _ l i n e , instr( l _ l i n e ,
' ' )));
Значение L L I N E теперь имеет вид: 1
procedure OPS$CLBECK.A
Теперь значение L_LINE начинается с номера строки кода, с которой была вызвана процедура DEBUG . F () или DEBUG . FA (). Мы хотим сохранить эту информацию и включить ее в отладочное сообщение, чтобы разработчик мог легко найти, где именно в коде было выдано это сообщение. Мы присваиваем это значение параметру O_LINENO, переданному в режиме оит, а затем удаляем его из L L I N E . 32 33 34
o_lineno := to_number(substr(l_line, 1, instr(l_line, ' '))); l_line := ltrim(substr(l_line, instr(l_line, ' ')));
Примечание Для удаления следующего слова из L L I N E МЫ используем тот же прием, что и для удаления дескриптора объекта на предыдущем шаге.
532
Приложение А
Затем нам нужно удалить тип объекта, который вызвал процедуру пакета DEBUG. Сложность в том, что удалению подлежит иногда одно, а иногда два слова. Если процедура пакета DEBUG вызвана из процедуры или функции в теле пакета, метода в теле типа или из анонимного блока, то надо будет также удалить слово BODY ИЛИ BLOCK из значения L_LINE С ПОМОЩЬЮ следующего кода: 35 36 37 38 39 40 41
l_line := ltrim( substr( l_line, instr( l_line, ' ' ))); if l_line like 'block.%' or l_line like 'body%' then l_line := ltrim( substr( l_line, instr( l_line, ' ' ))); end if;
Теперь в строке осталось только ИМЯ_ВЛАДЕЛЫДА.ИМЯ_ОБЪЕКТА. МЫ устанавливаем соответствующие значения двум другим параметрам, переданным в режиме оит, используя все четыре перечисленных функции обработки строк. 42 43 44 45 46 47
o_owner := ltrim( rtrim( substr( l_line, 1, instr( l_line, '.' )-l ))); o_object := ltrim( rtrim( substr( l_line, instr( l_line, '.' )+l )));
Наконец, если процедура пакета DEBUG была вызвана из анонимного блока, в стеке вызовов нет значений ИМЯ_ВЛАДЕЛЬЦА . ИМЯ_ОБЪЕКТА, так что в результате присвоений параметры O_OWNER И O_OBJECT получают значения NULL. МЫ проверяем, не имеет ли параметр O_OWNER значения NULL. ЕСЛИ ЭТО так, то нужно установить параметру O_OWNER значение, равное имени пользователя, организовавшего текущий сеанс, а параметру O_OBJECT — значение ANONYMOUS BLOCK. 48 49 50 51 52 53
if o_owner is null then o_owner := user; o_object := 'ANONYMOUS BLOCK'; end if; end who_called_me;
Процедура BUILD_IT() Как только мы определили, что вызов DEBUG должен генерировать отладочное сообщение, вызываем вторую внутреннюю процедуру пакета, BUILD_IT О , для построения и возврата заголовка отладочного сообщения. Этот заголовок содержит всю информацию о вызове процедуры пакета DEBUG, сформатированную в соответствии с профилем отладки, установленным с помощью процедуры INIT (). Она может включать временную отметку момента вызова, имя владельца и имя объекта, из которого был выполнен вызов, а также номер строки в объекте. Мы определили эту информацию в процедуре WHO_CALLED_ME () и перенаправили ее процедуре BUILD_IT() вместе с другой информацией для форматирования заголовка.
Создание утилиты DEBUG 1 2 3 4 5 6 7 8 9
533
function build_it( p_debug_row in debugtab%rowtype, p_owner in varchar2, p_object in varchar2, p_lineno number ) return varchar2 is l_header long := null; begin
В процедуре BUILD_IT о есть всего три шага. Сначала надо проверить, требуется ли включать значение SESSION_ID В информацию заголовка. Если да, мы присваиваем глобальную переменную G_SESSION_ID локальной переменной L_HEADER. Глобальную переменную мы используем по соображениям производительности. Идентификатор сеанса не меняется с момента означивания пакета DEBUG, так что нет смысла получать его при каждом вызове. Вместо этого мы можем просто установить глобальную переменную один раз при означивании пакета DEBUG, а затем просто ссылаться на нее при необходимости. Мы покажем, как это делается, чуть позже, пока же просто давайте предположим, что переменная G_SESSION_ID установлена корректно. 10 11 12 13
if p_debug_row. session_id = 'YES' then l_header := g_session_id || ' - '; end if;
Затем нам необходимо определить, требуется ли включать в сообщение дату и время. Если да, мы добавляем соответственно сформатированную временную отметку к значению L_HEADER. 14 15 16 17 18 19 20
if p_debug_row.show_date = 'YES' then l_header : = l_header || to_char( sysdate, nvl( p_debug_row.date_format, 'MMDDYYYY HH24MISS' ) ); end if;
Наконец, добавляем имя владельца, имя объекта и номер строки к значению L_HEADER и возвращаем его вызывающей процедуре, DEBUG_IT (). 21 22 23 24 25 26 27 28 29 30 31
1 header := 1 header ||
•f II
lpad( substr( p owner | 1 '. 1 II P_ object greatest( 1, length < P_ owner | 1 '. '1 1 p object ) least( p debug row.name_length , 61 ) + 1 ) >, least( p debug row.name length , 61 ) ) 1 1 lpad ( p lineno, 5 ) | | ') '; return 1 header:
534
Приложение А 32 33
end build_it;
Здесь достаточно хитро используются встроенные функции GREATEST (), LEAST () и SUBSTR () для установки требуемого размера значений имени владельца и имени объекта. Мы разрешаем разработчику управлять размером выдаваемых имен, потому что если всегда использовать максимально допустимую длину имен, в них будет слишком много пробелов. Максимальная длина идентификаторов в Oracle — 30 символов, значит, строка ИМЯ_ВЛАДЕЛЬЦА . ИМЯ_ОБЪЕКТА может быть длиной в 61 символ. Однако, поскольку обычно это не так, мы даем разработчикам возможность управлять длиной выдаваемых имен. Следует также отметить тип параметра P_DEBUG_ROW (DEBUGTAB%ROWTYPE). Поскольку мы вызываем эту процедуру в цикле FOR ПО курсору в процедуре DEBUGIT (), а курсор построен по запросу SELECT *, мы можем передавать всю строку в функцию BUILD_IT () в одной переменной. Это упрощает определение процедуры и делает ее вызов короче.
Функция PARSE_IT() Как только заголовок сообщения сформирован, может потребоваться изменение самого сообщения. В функции PARSE_IT () выполняются подстановки соответствующих значений вместо %s. Нам надо передавать функции PARSE_IT () не только строку сообщения, но и значения для подстановки в нее (в виде значения типа ARGV), И длину строки заголовка. 1 2 3 4 5 6 7 8 9 10 11
function parse_it( p_message in varchar2, p_argv in argv, p_header_length in number ) return varchar2 is l_message long := null; l_str long := p_message; l_idx number := 1; l_ptr number := 1; begin
Примечание Немного позже вы поймете, почему так важна длина заголовка.
Прежде всего надо проверить, требуется ли вообще что-то подставлять. Если в сообщении нет символов % или \, то можно просто вернуть значение P_MESSAGE без изменений. 12 13 14 15 16
if nvl( instr( p message, '%' ), 0 ) - 0 and nvl( instr( p message, •V ), о )= 0 then return p message; end if;
Создание утилиты DEBUG
535
Теперь в цикле мы ищем вхождения % и завершаем цикл, когда их больше нет. 17 18 19 20
loop l_ptr := instr( l_str, '%' ) ; exit when ljptr = 0 or l_ptr is null;
Если символ % найден, мы добавляем все символы до него к локальной переменной L_MESSAGE. 21 22 23
l_message := l_message || substr( l_str, 1, l_ptr-l ); l_str := substr( l_str, l_ptr+l );
Дальше мы проверяем символ сразу после символа %. Если это символ s, значит, мы нашли позицию, в которую должна быть сделана подстановка, и выполняем ее. 24 25 26 27 28
if substr( l_str, 1, 1 ) = 's' then l_message := l_message || p_argv(l_idx); l_idx := l_idx + 1; l _ s t r := substr( l _ s t r , 2 ) ;
Если символ, следующий сразу после %, это еще один %, мы добавляем один символ % к L_MESSAGE и продолжаем. В Oracle при наличии двух одиночных кавычек в строке первая маскирует вторую, и выдается только одна. Это же соображение действует у нас для символа процента. 29 30 31 32
elsif substr( l_str,l,l ) = '%' then l_message := l_message II '%'; l_str := substr( l_str, 2 );
Если же вслед за первоначально найденным символом % идет не s и не %, значит, это просто символ % в строке, и его надо добавить к значению L_MESSAGE ДЛЯ выдачи. 33 34 35 36 •37 38
else 1 message := 1 message | end if; end loop;
Теперь делаем второй проход по сообщению в поисках последовательностей \N И \т. Тот, кто знаком с языками С или Java, знает, что для вставки символов перевода строки или табуляции в выдаваемую строку используются последовательности \N И \т соответственно. Поскольку мы копируем стиль использования последовательностей %s вместо подставляемых параметров, принятых в языке С, управляющие последовательности для вставки символов новой строки и табуляции мы тоже добавили. Алгоритм работы здесь такой же, как и в предыдущем цикле. Сначала сбрасываем локальные переменные.
536
Приложение А 39 40 41
l_str := l_message |I l_str; l_message : = null;
Затем ищем в цикле символ \ и выходим из цикла, когда ни одного больше нет. 42 43 44 45
loop l_ptr := instr( l_str, '\' ) ; exit when l_ptr = 0 or l_ptr is null;
Если символ \ найден, добавляем все, что было до него, к значению L_MESSAGE. 46 47 48
l_message : = l_message I I substr( l_str, 1, l_ptr-l ) ; l_str := substr( l_str, l_ptr+l );
Теперь проверяем символ сразу после \. Если это п, добавляем в сообщение новую строку, и как раз при этом используется длина заголовка сообщения. Мы хотим, чтобы новая строка начиналась с позиции сразу под начальной позицией первой строки, которая идет сразу под заголовком. Поэтому новую строку надо дополнить пробелами до длины заголовка для правильного выравнивания. 49 50 51 52 53
if substr( l_str, 1, 1 ) = 'n' then l_message := l_message || chr(10) || rpad( ' ', p_header_length, ' ' ); l_str :- substr( l_str, 2 ) ;
Если же следующий символ — t, добавляем в L_MESSAGE СИМВОЛ табуляции. 54 55 56 57
elsif substr( l_str, 1, 1 ) = 't* then l_message := l_message || chr(9); l_str := substr( l_str, 2 ) ;
Если следующий символ — снова \, обрабатываем его, как раньше обрабатывали удвоенные символы процента. 58 59 60 61
elsif substr( l_str, 1, 1 ) = 'V then l_message := l_message | | ' V ; l_str := substr( l_str, 2 );
Иначе это просто символ \, и мы его выдаем. 62 63 64 65 66 67
else l_message := l_message |I '\'; end if; end loop;
Осталось только вернуть разобранную и сформатированную строку.
Создание утилиты DEBUG 537 68 69 70
r e t u r n l_message | |
l_str;
end p a r s e _ i t ;
В этой процедуре нет ничего сложного. Надо только быть внимательным при использовании функций SUBSTR () и INSTR (), чтобы не потерять символы в процессе обработки.
Функция FILEJTQ Наконец, заголовок и сообщение проанализированы, сформатированы и готовы к выдаче. Теперь можно попытаться записать полученную информацию в требуемый файл с помощью четвертой и последней приватной функции пакета DEBUG: FILE_IT (). Ее определение и локальные переменные вполне понятны. Эта функция возвращает TRUE ИЛИ FALSE, В зависимости от того, удалось ли ей успешно записать сообщение в файл. 1 2 3 4 5 6 7 8
function £ile_it( p_file in debugtab.filename%type, p_dir in debugtab.dir%type, p_message in varchar2 ) return boolean is l_handle utl_file.file_type; begin
Теперь обратите внимание, что мы определяем параметр P _ F I L E типа DEBUGTAB . FILENAME%TYPE. Это гарантирует, что тип данных параметра будет соответствовать типу столбца в таблице DEBUGTAB. Напомним: функция FILE_IT () вызывается с именем файла, хранящимся в базе данных, а получается это имя с помощью цикла FOR по курсору в процедуре DEBUG_IT о . Чтобы предотвратить несоответствие типов, мы определяем этот параметр типа DEBUGTAB.FILENAME%TYPE. ОН ПОЗВОЛИТ поддерживать для него такой же тип, как у столбца FILENAME таблицы. Так же определяется и параметр P_DIR. Для открытия файла мы используем стандартный пакет Oracle UTL_FILE. 9 10 11 12 13 14
l_handle := utl_file.fopen( location => p_dir, filename => p_file, open_mode => 'a', max_linesize => 32767 ) ;
Затем записываем сообщение в файл. 15 16
utl_file.put( l_handle, " ); utl_file.put_line( l_handle, p_message ) ;
Потом закрываем файл и возвращаем значение TRUE. 17 18
utl_file.fclose(
l_handle );
538
Приложение А 19 20
return true;
Вам может показаться, что было бы проще и эффективнее оставить файл открытым и просто выдавать в него сообщения каждый раз. Это верно. Но поскольку мы не знаем, когда будет сделан последний вызов пакета DEBUG, МЫ не можем знать, когда же закрывать файл. Так что, хотя постоянное открытие и закрытие файла снижает производительность, мы вынуждены делать именно так. Примечание Помните, что пакет DEBUG обычно используется на стадии разработки приложения, поэтому небольшое снижение производительности допустимо. Далее мы продемонстрируем, как избавиться от большинства дополнительных расходов ресурсов на выполнение вызовов DEBUG при переводе приложения в производственную среду, не меняя ни единой строки его кода.
Последний блок кода — обработчик исключительных ситуаций в функции FILE I T ( ) . 21 22 23 24 25 26 27 28 29
exception when others then if utl file.is open ( 1 handle utl file.fclose( 1 handle ) end i f ;
then
return false; end file i t ;
Мы используем пакет UTL_FILE, поэтому ДОЛЖНЫ перехватывать и обрабатывать любые исключительные ситуации, которые могут быть возбуждены. При использовании пакета UTL_FILE может возникать много различных исключительных ситуаций, и любая из них не позволит нам выдать сообщение. Мы не хотим, чтобы пакет DEBUG приводил к сбою вызвавшей его подпрограммы, поэтому перехватываем все исключительные ситуации с помощью обработчика WHEN OTHERS И закрываем файл, возвращая FALSE, поскольку отладочное сообщение не выдано. Примечание Это может сбивать с толку разработчика, ожидающего, что отладочная информация генерируется. См. описание наиболее типичных проблем, с которыми вы можете столкнуться при использовании DEBUG, В разделе "Поиск причин проблем в пакете DEBUG".
Процедура INIT() Мы закончили анализировать весь код для выдачи отладочных сообщений в файл, но еще не предоставили разработчику способа проинициализировать DEBUG. Именно для этого предназначена процедура INIT О . Она принимает, как параметры, все атрибуты, которые разработчик может установить в своем профиле. Значения этих
Создание утилиты DEBUG
539
параметров установлены по умолчанию, поэтому для настройки профиля можно просто вызвать DEBUG, INIT О . 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
procedure init( p_modules p_dir p_file p_user p_show_date p_date_format p_name_len p_show_sesid
in in in in in in in in
varchar2 varchar2 varchar2 varchar2 varchar2 varchar2 number varchar2
default default default default default default default default
'ALL', 'TEMP', user || '.dbg', user, 'YES', 'MMDDYYYY HH24MISS', , 30, 'NO' ) is
pragma autonomous_transaction; debugtab_rec debugtab%rowtype; l_message long; begin
Прежде всего надо удалить любые профили, конфликтующие с тем, который мы задаем. 15 16 17 18
delete where and and
from debugtab userid = p_user filename = p_file dir = p_dir;
Теперь мы выполняем вставку, используя конструкцию RETURNING INTO оператора INSERT для получения вставленных значений. 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
insert into debugtab( userid, modules, dir, filename, show_date, date_format, name_length, session_id ) values ( p_user, pjnodules, p_dir, p_file, p_show_date, p_date_format, p_name_len, p_show_sesid ) returning userid, modules, dir, filename, show_date, date_format, name_length, session_id into debugtab_rec.userid, debugtab_rec.modules, debugtab_rec.dir, debugtab_rec.filename, debugtab_rec.show_date, debugtab_rec.date_format, debugtab_rec.name_length, debugtab_rec.session_id;
Помните, что мы создали триггер BIU_FER_DEBUGTAB ДЛЯ таблицы DEBUGTAB, который может изменить данные по ходу вставки в таблицу. Триггер переводит в верхний регистр значения некоторых столбцов. Можно передать значение yes в столбец SESSION_ID, но триггер переведет значение в верхний регистр и сохранит его как YES. Мы хотим точно знать, как выглядит вставленная строка, поэтому возвращаем ее значения. Эти возвращенные значения сохраняются в записи DEBUGTAB_REC, которая имеет тип DEBUGTAB%ROWTYPE, так что соответствие типов гарантировано.
540
Приложение А
Затем мы хотим записать в отладочный файл текущие установки профиля отладки. Для этого построим строку сообщения, которая включает все значения, вставленные в таблицу DEBUGTAB. 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
1 m e s s a g e := chr(10) I I 'Debug p a r a m e t e r s initialized o n ' 1 1 to char( sysdate, 'dd-MON-yyyy h h 2 4 : m i : s s ' ) 1 chr (10); USER: ' 1 1 1 m e s s a g e := 1 m e s s a g e I| ' d e b u g t a b rec.userid || c h r ( 1 0 ) ; MODULES: ' 1 1 1 m e s s a g e :- l_message I| ' d e b u g t a b rec.modules II c h r ( 1 0 ) ; DIRECTORY: ' 1 1 1 m e s s a g e := 1 m e s s a g e || ' d e b u g t a b rec.dir | I chr ( 1 0 ) ; 1 m e s s a g e := l_message I I ' FILENAME: ' 1 1 d e b u g t a b rec.filename || c h r ( 1 0 ) ; 1 m e s s a g e ::= 1 m e s s a g e I I ' SHOW D A T E : ' 1 1 d e b u g t a b rec.show date || c h r ( 1 0 ) ; l_message ;:= ljnessage || ' DATE FORMAT: ' 1 1 d e b u g t a b rec.date format II chr ( 1 0 ) ; 1 m e s s a g e ::= ljnessage || ' NAME LENGTH: ' 1 I d e b u g t a b rec.name length II c h r ( 1 0 ) ; 1 m e s s a g e := ljnessage || 'SHOW SESSION I D : ' 1 1 d e b u g t a b rec.session i d || c h r ( 1 0 ) ;
Наконец, пытаемся записать сообщение, вызывая функцию FILE_IT (). На этот раз возвращаемое значение функции FILE_IT () нас интересует, поскольку, если невозможно записать в указанный файл, не имеет смысла продолжать инициализацию. Если вызов F I L E I T () был неудачным, мы откатываем вставку и возбуждаем ошибку приложения. 54 55 56 57 58 59 60 61
if not file_it( debugtab_rec.filename, l_message ) then rollback; raise_application_error( -20001, 'Can not open file "' || debugtab_rec.filename || '"' ) ; end if;
В противном случае мы фиксируем транзакцию и завершаем работу. 62 63 64
commit; end init;
Вы можете подумать, что фиксировать или откатывать транзакцию в этой процедуре не стоит, поскольку это может повлиять на открытую транзакцию. Но обратите внимание на строку 10: pragma autonomous_transaction;
Создание утилиты DEBUG
541
Это означает, что процедура INIT () работает, как отдельная транзакция. Фиксация или откат в ней не повлияют на транзакцию, из которой эта процедура вызвана.
Процедура CLEARQ Обеспечив способ создания профиля отладки, нам надо обеспечить и его удаление. Это делается с помощью очень простой процедуры CLEAR (). Она просто удаляет из таблицы DEBUGTAB любые строки, соответствующие переданным параметрам. Мы снова используем директиву PRAGMA AUTONOMOUSJTRANSACTION, чтобы гарантировать, что фиксация не затронет вызывающую транзакцию. 1 2 3 4 5 6 7 8 9 10 11
procedure clear( p_user in varchar2 default user, p_dir in varchar2 default null, p_file in varchar2 default null ) is pragma autonomous_transaction; begin delete from debugtab where userid = p_user and dir = nvl( p_dir, dir ) and filename = nvl( p_file, filename ); commit; end clear;
Примечание Если в этой процедуре вызывающий не указывает имя файла и каталог, будут удалены все профили для указанного пользователя.
Процедура STATUSQ Наконец, мы предоставляем процедуру STATUS ДЛЯ выдачи текущего профиля отладки. Аналогично процедуре CLEAR (), ей передается имя пользователя и необязательные каталог и имя файла. Поскольку процедура STATUS выдает результаты с помощью DBMS_OUTPUT . PUT_LINE, вызывать ее надо из среды SQL*Plus или из другого клиента, поддерживающего пакет DBMS_OUTPUT. 1 2 3 4
procedure status( p_user in varchar2 default user, p_dir in varchar2 default null, p_file in varchar2 default null ) is
6 7 8
l_found boolean := false; begin
Мы выдаем заголовок и результаты с помощью процедур DBMSJDUTPUT. PUT_LINE. 9 10 11
dbms_output.put_line( chr(10) ); dbms_output.put_line( 'Debug info for ' || p_user );
542
Приложение А Затем проходим в цикле FOR ПО курсору все соответствующие запросу строки. 12 13 14 15 16 17 18 19
for с in ( select * from debugtab where userid = p_user and dir = nvl( p_dir, dir ) and nvl( p_file, filename ) = filename ) loop dbms_output.put_line( ' ' || rpad( '-', length( p_user ), '-' ) ) ;
Устанавливаем локальной булевой переменной L_FOUND значение TRUE, чтобы знать, что найдена, по крайней мере, одна соответствующая строка. 20
l_found := t r u e ;
Затем используем процедуру DBMS_OUTPUT.PUT_LINE ДЛЯ выдачи результата разработчику. 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
dbms_output.put_line( 'USER: c.userid ); dbms_output.put_line( 'MODULES: с.modules ) ; dbms_output.put_line( 'DIRECTORY: сdir ); dbms_output.put_line( 'FILENAME: с filename ) ; dbms_output.put_line( 'SHOW DATE: c.show_date ); dbms_output.put_line( 'DATE FORMAT: с.date_format ); dbms_output.put_line( 'NAME LENGTH: с.name_length ) ; dbms_output.put_line( 'SHOW SESSION ID: c.session_id ) ; dbms_output.put_line( ' ' ); end loop;
' || ' II 1
11
1
11
1
11
1
11
1
11
' ||
Наконец, если ни одна строка не найдена, мы уведомляем вызывающего об этом факте с помощью следующего сообщения: 40 41 42 43 44
if not l_found then dbms_output.put_line( 'No debug setup.' ); end if; end status;
Последние штрихи Единственный код, который мы еще не прокомментировали, — это код инициализации пакета DEBUG. ЕСЛИ помните, мы использовали глобальную переменную G_SESSION_ID для хранения идентификатора сеанса. Было сказано, что мы устанав-
Создание утилиты DEBUG 543 ливаем это значение один раз, при первом вызове процедуры пакета DEBUG. Переменная эта определена следующим образом: package body debug as g_session_id varchar2(2000);
А код, в котором она инициализируется, имеет вид: begin g_session_id end debug;
:= userenv('SESSIONID');
Вот и весь код утилиты DEBUG. В действии эта утилита была продемонстрирована в главе 10.
Поиск причин проблем в пакете DEBUG Вы видели, что пакет DEBUG создан так, что он просто не срабатывает, если записать сообщение в файл не удалось. В этом разделе представлен ряд проверок, которые надо выполнить, если вы не получаете сообщений от утилиты DEBUG ИЛИ получаете сообщения об ошибках при инициализации профиля.
Ошибка при инициализации профиля: файл не существует Проверьте, имеет ли сервер Oracle право записи в соответствующий каталог. Это можно сделать, зарегистрировавшись в SQL*Plus как DBA и выполнив следующее: SQL> show parameter utl_file_dir NAME
TYPE
VALUE
utl_file_dir
string
/tmp
Проверьте, что значение то же, что и в передаваемом каталоге. Это значение в ОС Unix зависит от регистра символов, поэтому перепроверьте дважды, чтобы убедиться в точном совпадении. Альтернативный способ проверки — просмотр файла INIT.ORA И проверка установленного в нем значения. Помните, что даже если сейчас значение в файле INIT . ORA совпадает с переданным именем каталога, фактически оно может отличаться, потому что было изменено после запуска сервера. Примечание Значения в файле INIT.ORA читаются только при запуске сервера.
Ошибка при инициализации профиля: файл существует Проверьте права доступа к файлу. Владельцу процесса Oracle необходимо право на запись в этот файл.
544
Приложение А
Сообщения в файл отладки не выдаются Проверьте следующее: > убедитесь с помощью процедуры DEBUG . STATUS (), что профиль отладки для соответствующего пользователя включен; > убедитесь, что владелец процесса Oracle имеет право на запись в каталог, указанный в профиле. Понятно, что когда профиль инициализировался, каталог и файл были доступны, но с тех пор все могло измениться; > убедитесь, что владелец процесса Oracle имеет право на запись в сам файл. Опять-таки, с момента инициализации профиля все могло измениться; >• убедитесь, что задан каталог, соответствующий значению параметра инициализации UTL_FILE_DIR.
Предметный указатель Автономная транзакция 495 в триггере 319 оправданное использование 229 Анализ 50 частичный 58 Аналитическая функция 75 percentile_cont 78 Ассоциативные массивы 110, 254 Атрибут %BULK_EXCEPTIONS 201 %FOUND 146, 147 %NOTFOUND 146, 204 %ROWCOUNT 146 %ROWTYPE 169, 172, 173, 177, 205, 475 использование 170, 171, 210 %TYPE 162, 169, 206, 208, 209, 475, 480 использование 163, 164 CLIENT_DENTIFIER 425 ORA_LOGIN_USER 303 ORA_SYSEVENT 303 курсора 146 Аудит 290, 351 вставки строк 234 данных 326, 327 детальный 229, 423 операторов SELECT 226 Б База данных активная 301 виртуальная приватная 106 В Внешняя таблица 358 Выборка массивом 158
д Демонстрационные таблицы, создание 20 Дескриптор DAD 440, 441, 467
конфигурирование 441 Динамический SQL 502 причины использования 288 проблемы 298 эффективный 288 Директива PRAGMA AUTONOMOUSJRANSACTION 541 Доказуемость 18, 42 Ж Журнал Oracle Magazine 15 аудита 326 сообщений 354, 360 3 Задание 290 ошибки при выполнении 341 Запись 170 Защелка 28 Защита исходного кода 428 эшелонированная 419 на уровне строк 519 И Идентификатор строки 35 Индекс по функции 119, 276 Индекс-таблица 33 Инкапсуляция 91 Исключительная ситуация DUP_VAL_ON_INDEX 44, 46 NO_DATA_FOUND 137, 138, 365, 480 TOO_MANY_ROWS 137, 138, 480 VALUE_ERROR 480 ZERO_DIVIDE 480 обработка 60, 475 пользовательская 478 предопределенная 477 системная 477
546
Предметный указатель М
Каталог $0RACLE_BASE/admin/HMfl_6a3bi/bdump 354 $ORACLE_HOME/APACHE/MODPLSQL/OWA 442 $ORACLE_HOME/rdbms/admin 115 BACKGROUND_DUMP_DEST 25 C:\oracle\ora81\sqlplus\admin 22 USER_DUMP_DEST 25 Квота 384 Кеш библиотечный 54 словаря 54 Класс, Statement 58 Ключ, суррогатный 314 Ключик, атрибуты 450 Команда @PLUSTRCE 23 @UTLXPLAN 22 alter session 24 DESCRIBE 410 print 156 regedit 22 SET SERVEROUTPUT 442, 483 tail 498, 501 Компонент MOD_PLSQL 439 Oracle Text 474 Конструкция AUTHID 410 AUTHID CURRENT.USER 405 CONNECT BY 93 LIMIT 203, 204 MODEL 82, 84 ON DELETE CASCADE 305 ORDER BY 358 RETURNING 259 TABLE 360 WHEN 480 WHERE 186, 348 изменяемая 291 различные варианты 292 Контекст 106, 107 использование 109, 296 приложения 425 Кризис зависимости 92 Курсор, явный 92 Курсорные выражения 154 переменные 151
Медиана, алгоритм вычисления 78 Многоверсионность 329 Множественная обработка 191 Модуль MOD_PLSQL 440, 441, 442, 452, 453 report_sal_adjustment 66 Мутирующая таблица 323 Н Набор 359 SQL%BULK_EXCEPTIONS 201 зачем использовать 187 Обработка транзакций 218 Обработчик WHEN OTHERS 229, 479, 487, 538 неправильное использование 480 Объект DIRECTORY, использование 523 Объектный тип 175 Оператор 137 ALTER SESSION 24, 25 ALTER TRIGGER 310 AUDIT 326 CASE 82, 186 COMMIT 47, 311 использование 223 CREATE DIRECTORY 499 CREATE PACKAGE 90 CREATE PUBLIC SYNONYM 22 CREATE TABLE 127 CREATE TRIGGER 264, 307 DDL выполнение в транзакции 224 EXECUTE IMMEDIATE 74, 296 FORALL 200, 201 GOTO 478 GRANT 22, 23 INSERT 312 RAISE 476, 479 без имени исключительной ситуации 477 ROLLBACK 311 использование 223 SELECT 145, 268, 311 SELECT INTO 137 TABLE() 183, 186 TRUNCATE 423 UPDATE 312 использование записей 174
Предметный указатель завершающий транзакцию 223 Оптимизатор 120 Опция PREFETCH 191 Отладка избирательная 513 конвейерных функций в реальном времени 501 правильный способ 519 производственного кода 516 Ошибка ORA-01555 372, 373 ZERO_DIVIDE 476, 477 мутирующей таблицы 223, 324 П Пакет 21, 181 APPLICATION JYPES 166 DBMS_APPL!CATION_INFO 19, 126, 489, 493 DBMS_FGA 122, 311, 423 DBMS JOB 46, 47, 134, 225, 290, 301, 336, 337, 371 использование 342 dbmsjob DBMS_LDAP 474 DBMS_LOCK 491 использование 370 DBMS_METADATA 130, 357 использование 358 DBMS_OBFUSCATION JOOLKIT 432, 474 DBMS_OUTPUT 19, 21, 442, 482, 501 использование 483, 514 ограничение на размер буфера 486 проблемы 484 DBMS_RANDOM 43, 66 DBMS_ROWID 131 DBMS_SESSION 107, 427 DBMS^SQL 5 1 , 57, 288, 296 DBMS_UTIUTY 125, 126 DEBUG 518, 532 код инициализации 542 основы использования 509 приватные процедуры 508 процедура 525 BUILD_IT() 532 DEBUGJTf) 527 структура 507, 524 тип ARGV 526 функция FILEJTO 537 HTF 442, 443, 444 методы 445
547
НТР 442, 443, 444 методы 445 OWA 442, 443 OWA_COOKIE 442, 450 OWA_PATTERN 367 OWA_UTIL 442 PROACTIVE 381 RS 187 возможности 186 спецификация 182 тело 183 RUNSTATS 28, 181, 306 STANDARD 93, 95, 109, 118, 477 UTL_FILE 19, 192, 193, 368, 423, 498, 515, 523, 537, 538 использование 500 спецификация 499 UTL_HTTP 122, 466, 469 использование 473 пример использования 467 UTL^RECOMP 97 UTlTsMTP 377 для обхода ошибки мутирующей таблицы 320 для работы с файлом сообщений 355 когда не использовать 119 основные преимущества 87 переменные 90 превентивного контроля 381 раздел инициализации 91 "Типичный" пример 87 уведомления спецификация 377 Параметр ARRAYSIZE 189 BACKGROUND_DUMP_DEST 354, 357, 365 CURSOR_SHARING 280, 282 JOB_QUEUE_PROCESSES 337 LOG_ARCHIVE_DEST_ 384 MAX_DUMP_FILE_SIZE 25 SQLJRACE 24, 25, 51 использование 27 TIMED_STATISTICS 24 USER_DUMP_DEST 25 UTL_FILE_DIR 192, 423, 499, 523, 544 инициализации 383 Перегрузка подпрограмм 89 Переключение контекста 195, 231, 271 Перекомпиляция 99
548
Предметный указатель
Переменная среды DAD_NAME 447 DOCUMENTTABLE 447 HTTP_REFERER 448 HTTP_USER,AGENT 447 REMOTE_ADDR 448 REMOTEJJSER 447 REQUEST_CHARSET 447 REQUESTJANA.CHARSET 448 REQUEST_METHOD 447 REQUEST_PROTOCOL 446 SQLPATH 22 WEB_AUTHENT_PREFIX 447 использование 445 Поддержка версий 301 Подзапрос 82 Подсказка CARDINALITY 64 NO_MERGE 276 NOCOPY 215, 217 Пользователь SCOTT 130 SYS 122, 311, 426, 428 SYSTEM 396, 399, 426 Поток Streams, конфигурирование 332 Права создателя использование 399 вызывающего когда использовать 407 ограничение 409 Прагма EXCEPTIONJNIT 478, 479 SERIALLY_REUSABLE 90 Предварительная выборка 138, 189 Представление 371 ALL_OBJECTS 62, 202 ALLSOURCE 429 ALL_USERS 27 DBA_FGA_AUDIT_TRA!L 229 DBA_SEGMENTS 388 DBAJAB_COLS 176 DBA_TAB_COLUMNS 176 DBAJABLES 240 DBA_UPDATABLE_COLUMNS 76 DBA_USERS 238, 239 EMP_WITH_AUDIT 228 SELECT 28 STATS 180, 182, 183
SYS.V_$LATCH 28 SYS.V $MYSTAT 28 SYS.V_$STATNAME 28 TAB_COLUMNS 335 USER DEPENDENCIES 92, 93, 94, 95, 166, 309 USERJNDEXES 121 USER JOBS 371 USERJ3BJECTS 95, 96, 106, 266, 310 USERJAB_COLUMNS 264 USER_TRIGGER_COLS 309 USERJRIGGERS 310 V$ARCHIVE.DEST 384 V$BACKUP_SET 382 V$LATCH 28, 179 V$MYSTAT 28, 74, 179 V$MYSTATS 99, 202, 249, 261 V$PROCESS 396, 399, 428 V$RESOURCE_UMIT 391 V$SESSION 131, 390, 396, 399, 428, 490, 493 V$SESSION_EVENT 53 V$SESSION_LONGOPS 493, 494 V$SGASTAT~ 117 V$SQL 297, 491 V$SQL_SHARED_CURSOR 244, 245 V$STATNAME 28, 179 V$TIMER 179 вложенное 149 Преобразование вложенного представления 275 типов, неявное 167 Привилегия ADMINISTER DATABASE TRIGGER 307, 349 ALTER SYSTEM 403 ALTER USER 289, 353 CREATE ANY CONTEXT 107 CREATE ANY TRIGGER 307 CREATE PUBLIC SYNONYM 22 CREATE SESSION 408 CREATE TABLE 22 CREATE TRIGGER 307 EXECUTE 401, 410, 411, 442, 429, 521 для пакета 431 EXECUTE ANY PROCEDURE 411,432 SELECT 179, 239, 399, 426 Принцип наименьших привилегий 403 Производительность 40, 231
Предметный указатель Пространство имен, USERENV 425 Протокол HTTP 466 SOAP 468 Процедура 525 CLOSE 51 DBMS APPLICATION INFO.SET_SESSION_LONGOPS 495 DBMS_OUTPUT.ENABLE 484 DBMS_OUTPUT.PUT_LINE 77, 541, 542 DBMS_SESSION.SET_CONTEXT 425 DBMS_SESSION.SETJDENTIFIER 425 DEBUG.CLEAR 515 HTP.FORMCLOSE 459 HTP.FORMOPEN 459 HTP.P 443 HTP.PRN 455 LOGJT 448, 449 MY_DOC_LISTING 454 OWA.COOKIE.GET 451 OWA_UTIL.GET_CGI_ENV 448 OWA_UTIL.HTTP_HEADER_CLOSE 451 OWA_UTILSHOWPAGE 442, 444 OWAINIT 443, 446, 449 PARSE 51 PRINTJABLE 407 PRINTENV 446, 449 RAISE_APPUCATION_ERROR() 479 SAVE_IN_DB 380 SEND.EMAIL 378 UPLOAD_DOC 454 UTL_FILE.FREMOVE 134 с правами вызывающего 238 Процесс, QMNO 374 Псевдостолбец ROWNUM 66, 276, 295 Р Разбор 50 Разделяемый пул 50 Режим ARCHIVELOG 383 Рекурсия 113 Роль CONNECT 415, 521 DBA 238, 239, 415, 417, 426, 428 PUBLIC 417, 427, 442, 512, 521 RESOURCE 521 SYSDBA 428
549
Руководство Application Developers Fundamental Guide 229 Database Administrator's Guide 276 Oracle HTTP Server Administration Guide 441 Oracle9i Application Developer's Guide 303, 331 Oracle 9i Database Performance Tuning Guide and Reference 23, 27, 276 PL/SQL User's Guide and Reference 9.2 432 SQL*Plus User's Guide and Reference 23 SQL Reference 357 С Сайт http://apress.com 354, 355, 519, 521 http://asktom.oracle.com 15, 28, 178 http://asktom.oracle.com/-tkyte 178 http://www.OakTable.net 11 http://www.oracledba.co.uk 13 Metalink 128 oraperf.veritas.com 53 Свойство PL/SQL 57 Связываемая переменная 49, 50 Серия OakTable Press 11 создание 12 Сеть OakTable 11 Система RUNSTATS 28 использование 30 Событие 10053 277, 278 10520 101 latch free 54 Сообщение об ошибке ORA-00376 374 ORA-4031 116 ORA-4091 323 Спецификация 104 Среда JDeveloper 519 SQL*Plus 444
Средство оценки производительности 17 Схема SYS 442 UTILITY 521 Сценарий 69 catproc.sql .101 CONNECT.SQL 22 DEMOBLD.SQL 20
550
Предметный указатель
LOGIN.SQL 21 REPTEST.SQL 70, 72, 74, 75, 294 utldtree.sql 94 T Таблица DBA_SOURCE 273 DEBUGTAB 521 DEPT 218 EMP_SAL_LOG 67 PLAN TABLE 22 SRC 63, 65, 69, 77, 80, 188, 203 документов 453 мутирующая 318 организованная по индексу 33 с поддержкой версий 329 теневая 326 Табличное пространство, SYSTEM 393 Технология Oracle Streams 301, 328, 331 Тип данных LONG RAW 452 PLSJNTEGER 169 SYS.ANYDATA 331 SYS_REFCURSOR 151 TIMESTAMP 517 вложенной таблицы 63 объектный 177 Точечная зависимость 103 Транзакция автономная 223 перезапускаемая 84 Трассировка 24, 284 Трассировочный файл 24 контроль 25 получение имени 25 Триггер 301 DML 311 INSTEAD OF 302, 315, 317 для автоматизации аудита 283 для обеспечения защиты 417 для реализации отложенной обработки 321 зависимость 309 на ошибку сервера 347 на регистрацию 346, 425 на событие logon 107 на событие приостановки 348 на событие базы данных 349 ошибки 349
ограничение 310 операторный 302 порядок срабатывания 303 производительность 305 состояние 310 строчный 302 У Утилита DEBUG 19, 505 создание 521 OERR 477 PRINTTABLE 427 RS 194 RUNSTATS 178, 187 переделка 179 SQL*Plus 20, 44, 152, 154, 158, 175, 483 TKPROF 24, 25, 27, 52, 158, 284 использование 26 WRAP 432, 436 использование 433
Ф Файл $ORACLE_HOME/rdbms/admin/dbmsapin.sql 489 $ORACLE_HOME/rdbms/admin/dbmsotpt.sql 482 $ORACLE_HOME/rdbms/admin/stdspec.sql 477 $ORACLE_HOME/rdbms/admin/utlfile.sql 499 INIT.ORA 24 сообщений 353 как внешняя таблица 357 обработка 360 проблема при использовании 372 прокрутка 367 просмотр содержимого 375 спецификация 356 структура 355 трассировочный 374 Форум, Miracle Database Forum 12 Функция 528 BITAND 61 DBMS_UTIUTY.FORMAT CALL STACK 126, 488 DBMSJJTILITY.FORMAT CALL STACK() 508, 530 DBMSJJTIUTY.GET TIME 70, 105, 126, 167, 255, 257
Предметный указатель DECODE 77 GREATEST 534 HTF.ANCHOR 462 INSTR() 529 LEAST 534 OPEN CURSOR 51 PORT_STRING 127 SQLCODE 487 SQLERRM 487 SOUNDEX 284, 285, 287 SUBSTR 485, 534 SYS.CONTEXT 107, 109, 284, 296, 425 SYSDATE 284, 417, 449 UID 284, 286 USER 287, 528 UTL.HTTP.REQUEST 468 UTL_HTTP.REQUEST_PIECES 468 UTL_RAW.CAST_TO_VARCHAR2 455 конвейерная 63, 233, 233, 250 использование 246
ц Цикл FOR, по курсору 145 Э Эффективность, как добиться 48
Иностранные термины AUTOTRACE использование 23 настройка 22
DEPT 155 ЕМР 148 FGA 423 HTML DB 13 INTEGER 169 native compilation 41 Oracle 10g 82 новые возможности 82 Oracle Streams 18 Patrick Sack 16 PL/SQL 37 зачем использовать 37 эффективность 40 PLSJNTEGER 169 RMAN 382 ROWID 35 ROWNUM 147 SOAP 468 SOAP-конверт 469 SOAP-сервер 469 SQL 77 использование вместо PL/SQL 77 SQL-оператор 287 рекурсивный 287 SQLJRACE 24 VPD 106 Web-службы 466 XDB 466, 468 использованые механизмы 471 XML 466, 471
551
ТОРГОВЫЕ МЕСТА ТОРГОВО-ИЗДАТЕЛЬСКОГО ДОМА «ДС» КИЕВ ,Х\ >Щв*
ХАРЬКОВ
®
метро "Петровка" *Торпмо-излатоплШ(ОШша"Ди«Софт'
ст. метро Пушкинская
|^Щ
хпи
АДРЕСА МАГАЗИНОВ, ГДЕ МОЖНО КУПИТЬ КНИГИ ТИД «ДС»
ПАРТНЕРЫ ТИД «ДС» Киев т./ф. 272-12-54, 272-60-34 Книга-почтой: 03055, а\я 100 e-mail:
[email protected] e-mail:
[email protected]
Днепропетровск т.(0562)33-27-74 8-067-565-52-87 e-mail:
[email protected]
Харьков
КИЕВ
ДОНЕЦК
"Знания", ул. Крещатик, 46 т. (044)224-22-91 "Техническая книга", ул.Красноармейская,51 т. (044)227-25-86 "Сучасник", пр-r Победы, 29 т. (044)274-52-35 "Библиотечный коллектор", пр-т 40-летия Октября, 100/2 т. (044)263-20-54, 263-20-04 тел./факс (044)263-60-56
ЧП "ИнфоКом" ул.Постышева,133 т. (0622)305-22-04, 381 02-75 www.infokorn. dn.ua "Идея" ул. Артема, 84 (062)304-20-22, 381-09-32 www.idea.cOFn.ua
ДНЕПРОПЕТРОВСК
Мережа магазине "Свгг Книжик" Театральний бульвар, 7 т. (0562)33-77-85 вул. Пастера, 10 т. (056)726-43-81 "Техническая книга" пр.К.Маркса, 40 т. (0562)744-86-72 "Книги" пр-т Гагарина, 98 т. (056)776-58-04
ЛЬВОВ
Львов т./ф. (0322)39-87-08 e-mail:
[email protected]
"Техническая книга", пл. Рынок, 10 т. (0322)72-0654 "Влас", НУ "Лшеська полггехнга" корпус 4,5, т. (0322)39-8708 E-mail:
[email protected]
Москва
ХАРЬКОВ
т. (057)783-99-28 e-mail:
[email protected]
Т. (095)726-80-67 e-mail:
[email protected]
Санкт-Петербург т. (812)251-41-94 e-mail:
[email protected] Индекс 190103, а\я №66 000 "ДиаСофт ЮП"
"Вища школа" ул.Петровского.6 т. (0572)47-80-20 "Books" ул. Сумская, 51, т. (0572)14-04-71
КРИВОЙ РОГ "Букииист-Солон" пл.Ос8Обождения 1 т. (0564)92-37-32
МОСКВА "Библио-Глобус", ул.Мясницкая.6 т. (095)928-87-44 "Дом технической книги", Ленинский пр-т, 40 т. (095)137-60-38 "Московский дом книги', ул. Новый Арбат, 8 "Мир" Ленинградский пр-т, 78 т. (095)152-45-11 "Дом книги на Ладожской" ул. Ладожская,8, стр.1 г. (095)267-03-02 E-mail:
[email protected] www.dom-knigi.ru, "Молодая гвардия", ул.Большая Полянка,28 т. (095)238-11-44, 238-(О-32
САНКТ-ПЕТЕРБУРГ "Дом книги" Невский пр-т, 28 т. (812)318-64-16 "Техническая книга", ул. Пушкинская, 2, т. (812)164-65-65 "Энергия" Московский пр-т, 189 т.(812)443-01-47
МИНСК "Книга XXI" пр-т Ф.Скорины, 92, ст.м. Московская, т. (0172)64-31-05, 64-27-97
Читатель! Библиотекарь! ЭТО для тебя! ЭТО стало возможным: ВСЕ выходящие книги СНГ! ВСЕ тематики! 1. 2. 3. 4. 5. 6. 7.
Наличие удобной емкой базы данных книг. Автоматическое пополнение новинками. Оценка книги без ее физического наличия. Заказ книги наиболее удобным способом. Минимальные расходы на связь при получении информации. Просмотр и выбор без постоянного подключения к сети ИНТЕРНЕТ. Ведение личной библиотеки.
А что нужно? Получить персонифицированное ПО, написав письмо по адресу
[email protected] с просьбой и желаемым именем вашего рабочего места. Например, Kulibin. Мы подготовим именованный конфигурационный файл вида Kulibin.vrd. Его нужно установить на свой компьютер. Для примера, создайте на диске С:\ каталог DATA. В каталог C:\DATA поместите исполняемый файл MailerT.exe, все файлы с расширением *.bpl, файл данных Kulibin.vrd. Создайте ярлык MailerT.exe и в свойствах ярлыка впишите C:\DATA\Mailert.exe fh=Kulibin. He забудьте сделать каталог C:\VRDMAIL — здесь программа создает почтовый файл для посылки его
[email protected] и сюда же надо помещать пришедшие от нас файлы для приема почты в программе (кнопочки со стрелками вверх и вниз соответственно в правом нижнем уголке главного окна программы). Вы только отправляете файлы и получаете при помощи электронной почты или физически на носителе. Смотреть книги можно так: вызвать из меню «Товар» о «Приход Товара» — это форма для поиска и заказа товара (второе делать не обязательно). Первый способ. Нажать зеленый плюс справа. Добавятся «ВСЕ» имеющиеся в системе книги. Если отсортировать по обложке, щелкнем на соответствующем поле «хидера» правой кнопкой мыши, то получим самые новые книги сверху. Искать можно при помощи ввода буквосочетаний веденных через «+». Например, введем в поисковое поле (белый прямоугольничек для ввода) внизу майли+рограм — получим результат поиска две карточки книги с множеством интересных параметров (обложка, название, аннотация, содержание, статья о книге и др.): «Учимся программировать на C++ вместе с Джоном Смайли» и «Учимся программировать на С# вместе с Джоном Смайли». Сортировать можно по возрастанию и убыванию, нажимая на соответствующие поля «хидера» правой/левой кнопкой мыши. Второй способ состоит в поиске посредством классификатора: справа вверху есть закладка «Раздел». Активируем ее при помощи мыши. Увидим «зеленый плюс» и «книжечку» — жмем на кнопку с изображением книжки. Видим разделы. Двойной щелчок и входим глубже в раздел. Зайдя в нужный раздел, жмем кнопку «ОК» сверху. Теперь на «зеленый плюс» — появятся только книги из нижестоящих подразделов.
Отобранные книги можно сохранить в именованный шаблон, скопировав необходимые книги слева (из рабочей области) вправо (область шаблона). Выделять, копировать — аналогично «Нортон KoMaHflepy»(Insert, +, *, - и пр.). Удаление F8 из рабочей области — не удаляет из «базы данных», а только из рабочей области — добавить снова можно аналогично вышеописанному способом (посредством «зеленого плюса»). Программа устойчива к непрофессиональным действиям пользователя, поэтому ей и «базе данных» вы не навредите, (а только себе — убить свои нужные шаблоны можно). Именованные шаблоны делаются так: справа вверху активируете панель «Шаблон». Жмете на кнопку с книжечкой. Появляется возможность сделать новый шаблон и войти во внутрь любого имеющегося и там сделать его. После нажатия на кнопку Новый Шаблон, появиться строка с текущей датой в названии шаблона. В правой ячейке строки вы можете его переименовать, войдя в редактирование двойным щелчком, в нужное вам название, нажать стрелку «вверх». Потом двойным щелчком войти в него (правая ячейка строки), нажать «ОК», копировать в него из рабочей области. Теперь в отличие от рабочей области, шаблон готов и будет доступен всегда при входе и выходе из программы, пока вы его не модифицируете или удалите (F8). Никакого дополнительного ПО, кроме операционной системы Windows, не нужно. В операционных UNIX-системах работает в пакете Wine (Linux, FreeBSD и т.д.). Задавайте вопросы в письменном виде на
[email protected] или в устном виде по телефону 8 10 380 44 272-12-54. С удовольствием ответим.
Используйте достижения прогресса! Превратите работу в удовольствие! Президент ТИД «ДиаСофт» — Александр Видоменко
Объектно-ориентированная сетевая база данных «Mailer» Представляемая нами объектно-ориентированная сетевая база данных обладает следующими особенностями: 1. Гибкость. Рабочее место составляется, как из кирпичиков, из готовых модулей (протоколов), устанавливаются связи между рабочими местами и клиент серверные отношения между протоколами, права доступа к информации из протоколов. 2. Возможность удаленного конфигурирования и перепланировки. Наличие той или иной информации на рабочих местах, права доступа к ней, пути и направление движения информации между рабочими местами может задаваться и меняться удаленно. При необходимости конфигурирования большого количества рабочих мест можно использовать внутренний форт-подобный язык программирования. 3. Относительная устойчивость к сбоям программного обеспечения и аппаратной части компьютера. Живучесть базы данных заложена в ее структуре и позволяет даже при гибели рабочего места восстановить его по сети из других рабочих месте минимальной потерей. Теряется, как правило, только та информация, которая не была отправлена на другие рабочие места. Отметим крайне простую организацию репликации данных и концентрации информации у менеджеров различного звена. Большая часть сбоев в RAM компьютера, равно как и преднамеренные искажения, обнаруживаются автоматически и, как правило, информация восстанавливается из сети без участия человека. 4. Скорость работы. Время получения информации по 10000 позициям товара 1000 клиентам по 10 филиалам за 5 месяцев с разбивкой по месяцам (500 тыс. строк данных) составляет порядка 30 сек. на 2,6 ГГц Pentium4 512 RAM. В реальном режиме работы при выписке товара выдает движение товара по складу и поставщику с диаграммой (при этом менее чем за секунду пересчитываются около 180 тысяч накладных). 5. Малая избыточность при хранении данных. Занимаемое дисковое пространство для ~ 180000 штук накладных ~ 7 Мбайт. Рост линейный. Не требуется переупаковка и вообще какое- либо обслуживание со стороны администратора. 6. Автономность. Для работы не требуется никакого дополнительного программного обеспечения кроме операционной системы. 7. Многоплатформенность. Работает в операционных системах Linux, Windows 95, 98, 2000, ХР, 2003. Минимальная конфигурация ПК: Р100 HDD 500 MByt 64 RAM. Средняя конфигурация ПК: Celeron 333 HDD 1GB 256 RAM. 8. He требуется постоянное соединение между рабочими местами. Средний трафик в день между рабочими местами 3 Кбайт. Информацией можно обмениваться даже при помощи дискет. Блокировки рабочих мест или какой-либо информации при обмене данными не производится. 9. Простота. База данных написана на Delphi5 и состоит из примерно 50 тысяч строк. BDE, ODBC и другие средства Delphi по работе с базами данных не используются. Объем исполняемого файла порядка 1 MB. 10. Моделирование войлочных структур. ПО описывает структуры, в которых можно связать любое рабочее место с любым другим обменом информации по перечню протоколов, что гибче широко известных: звезда, снежинка. На основе представляемой базы данных реализуется бизнес пространство фирмы с электронным документооборотом топологии «звезда», «снежинка», «войлочная структура». Морфология общего бизнес пространства представлена на рис. 1. Внутренние области — это бизнес пространство отдельной фирмы.
Внутренняя область фирмы 3
Внутренняя область фирмы 5
Внутренняя область фирмы 6
Внутренняя область фирмы 4
Примечание* Внутренняя область фирмы N-совокупность узлов и связей. Фп - узел, рабочее место. Кп - внешние клиенты-партнеры. связь, по котрой "путешествует* набор протоколов, зависящий от администрирования.
Рис 1. Морфология общего бизнес пространства Среднее время обучения пользователя 8 часов. По вопросам реализации проекта и за консультацией обращаться
[email protected], 10 380 44 531-90-20.
Книги издательства "ДиаСофт" ISBN
Автор
Название
стр. формат
Использование ПК в целом 966-7393-28-3 Нортон П-, Гу£эабота на персональном компьютере. Самоучитель 5-93772-074-1 Михлин ЕМ. Эффективный самоучитель работы на ПК второе издание 5-93772-116-0