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