
Август 2004
Профессионалу разработчику
Стивен Ферстайн ( Steven Feuerstein)
Oracle 10g улучшает FORALL
(Oracle 10g Adds More to FORALL , by Steven Feuerstein)
Источник: журнал "Oracle Magazine", no.1, 2004,
http://www.oracle.com/technology/oramag/oracle/04-jan/o14tech_plsql.html
FORALL начинает, BULK COLLECT заполняет, и VALUES OFвыбирает.
В моей второй статье об улучшениях PL/SQL в Oracle Database 10g, я исследую новые возможности использования выражения FORALL с непоследовательно обрабатываемыми (nonconsecutive driving) индексами (Первую статью см. в выпуске за ноябрь/декабрь 2003). Хотя поначалу это может казаться слишком сильным утверждением, но это действительно ключевые улучшения FORALL, которые могут весьма упростить код и улучшить производительность программ, обновляющих большое количество строк в программе на PL/SQL.
Вначале рассмотрим возможности FORALL, затем исследуем вклад Oracle Database 10g в поддержку непоследовательно обрабатываемых индексов.
FORALL ускоряет DML
В Oracle8i Oracle добавил два новых DML-выражения для серверного PL/SQL: BULK COLLECT и FORALL. Оба выражения реализуют обработку массивов в PL/SQL; BULK COLLECT обеспечивает высокоскоростную выборку данных, FORALL существенно улучшает выполнение операций INSERT, UPDATE и DELETE. Улучшение достигается путём существенного уменьшения числа контекстных переключений между средами исполнения PL/SQL и SQL-выражений.
С помощью BULK COLLECT выбирается несколько строк в одну или несколько коллекций, а не в отдельную переменную или запись PL/SQL. Следующий пример использования BULK COLLECT выбирает все книги, в заголовке которых содержится "PL/SQL", в соответствующий массив записей за одно обращение к БД (
В скрипте автора – ошибка – записи извлекаются из books, т.е. из пустой PL/SQL-таблицы – прим. Переводчика).
DECLARE
TYPE books_aat
IS TABLE OF book%ROWTYPE
INDEX BY PLS_INTEGER;
books books_aat;
BEGIN
SELECT *
BULK COLLECT INTO book
FROM books
WHERE title LIKE '%PL/SQL%';
...
END;
Аналогично, FORALL, используя коллекции, передаёт данные из PL/SQL-коллекции в заданную таблицу. Следующий пример демонстрирует процедуру, в которую передаётся вложенная таблица с информацией о книгах, все данные которой коллекции затем вставляются в таблицу. В примере используются преимущества расширения возможностей выражения FORALL в Oracle9i, которые позволяют выполнять вставку записи напрямую в таблицу.
BULK COLLECT и FORALL весьма полезны не только для улучшения производительности, но и для упрощения кода операций SQL в PL/SQL. Использование FORALL INSERT для нескольких строк с очевидностью демонстрирует, почему PL/SQL считается лучшим языком программирования для БД Oracle.
CREATE TYPE books_nt
IS TABLE OF book%ROWTYPE;
/
CREATE OR REPLACE PROCEDURE add_books (
books_in IN books_nt)
IS
BEGIN
FORALL book_index
IN books_in.FIRST .. books_in.LAST
INSERT INTO book
VALUES books_in(book_index);
...
END;
Однако до появления Oracle Database 10g, существовало существенное ограничение по использованию коллекций с FORALL: БД считывала содержимое коллекции последовательно с первой до последней строчки в диапазоне выражения IN. Если обнаруживалось, что строка в указанном диапазоне не определена, возникала исключительная ситуация ORA-22160:
ORA-22160: element at index [N] does not exist
В простых случаях использования выражения FORALL это не вызывало никаких проблем. Но чтобы преимущества FORALL использовались максимально широко, требование, чтобы все массивы, обрабатываемые выражением FORALL, были последовательно заполнены, может увеличить сложность программ и снизить их производительность.
В Oracle Database 10g PL/SQL вводит два новых оператора для выражения FORALL, INDICES OF и VALUES OF, которые позволяют выборочно указывать, какие строки из обрабатываемого массива должны быть использованы в последующем DML-операторе.
INDICES OF очень удобен, когда массив разрежен или содержит пропуски. Синтаксис выражения следующий:
FORALL indx IN INDICES
OF sparse_collection
INSERT INTO my_table
VALUES sparse_collection (indx);
VALUES OF отвечает за обратную ситуацию: неважно разрежен массив или нет, но требуется использовать только подмножество элементов в нём. Тогда можно использовать VALUES OF для указания только на те значения, которые необходимо использовать в DML-выражении. Синтаксис выражения следующий:
FORALL indx IN VALUES OF pointer_array
INSERT INTO my_table
VALUES binding_array (indx);
В оставшейся части статьи рассмотрим, как перейти от использования цикла CURSOR FOR к использованию FORALL и как оператор VALUES OF может облегчить жизнь.
Заменяем циклы FOR на FORALL
Предположим, требуется написать процедуру, которая повышает зарплаты определённым сотрудникам (как определено в функции comp_analysis.is_eligible) и делает запись в таблицу истории для тех сотрудников, кто не был удостоен повышения. Корпорация очень большая и в ней работает очень большое количество работников.
Это не является чрезмерно сложной задачей для программиста на PL/SQL. Нет даже необходимости использовать BULK COLLECT или FORALL, как показано в
Листинге 1, чтобы решить данную задачу. Вместо этого используется цикл CURSOR FOR и отдельные выражения INSERT и UPDATE. Это простой способ; но, к сожалению, операцию надо выполнить за 10 мин., а “традиционный” подход потребует 30 минут или больше.
К счастью, предприятие переведено на Oracle9i и, что ещё более хорошо, я изучил массовую обработку записей на недавнем семинаре Oracle (эти сведения также можно получить из превосходных презентаций, доступных на Oracle Technology Network). Итак, я решил переписать программу, используя коллекции и массовую обработку записей. Результат показан в Листинге 2.
Быстрый взгляд на Листинг 1 и Листинг 2 очевидно должен показать следующее: переход к коллекциям и массовой обработке может увеличить объём и сложность кода. Но если необходимо существенное повысить производительность, то такое увеличение весьма оправдано. Теперь давайте рассмотрим более подробно, как удается сохранить логику условий внутри цикла CURSOR FOR при использовании FORALL.
Определяем типы коллекций и коллекции
В Листинге 2 в первой части декларативной секции (строки с 6 по 11) объявляется несколько различных типов коллекций, по одной для каждого поля, которые будут извлекаться из таблицы сотрудников. Помимо этого объявляется один тип-коллекция как employee%ROWTYPE, однако FORALL пока не поддерживает операций над коллекциями записей, в которых есть ссылки на отдельные поля записи. Поэтому я должен отдельно объявить коллекции для следующих полей – ID сотрудников, зарплата, дата приёма на работу.
Затем объявляются коллекции, которые необходимы для каждого из этих полей (строки с 13 по 21). Сначала коллекции, соответствующие выбираемым колонкам (строки с 13 по 15):
employee_ids employee_aat;
salaries salary_aat;
hire_dates hire_date_aat;
Теперь необходима коллекция, содержащая ID только тех сотрудников, кому назначено повышение зарплаты (строка 17):
approved_employee_ids employee_aat;
И, наконец, объявляется одна коллекция для каждого поля, используемого для записи тех, кто не получил права на повышение (строки с 19 по 21):
denied_employee_ids employee_aat;
denied_salaries salary_aat;
denied_hire_dates hire_date_aat;
Углубляемся в код
Теперь, после объявления структур данных, давайте пропустим исполняемую часть процедуры (строки с 72 по 75), чтобы разобраться, как коллекции используются для ускорения процесса.
retrieve_employee_info;
partition_by_eligibility;
add_to_history;
give_the_raise;
Для написания этой программы использовалось поэтапное улучшение (также известное как "разработка сверху-вниз"). Поэтому вместо очень длинной и труднодоступной исполняемой части, получилось четыре строки с названиями шагов процедуры. Сначала извлекается информация о сотрудниках (все сотрудники данного отдела). Затем сотрудники разделяются на тех, кто получил повышение и тех, кому было отказано. Тех, кому отказано, после разделения можно занести в таблицу истории, а для остальных зафиксировать прибавление зарплаты.
Написание кода в таком стиле делает результат гораздо более читаемым. И тогда с легкостью можно углубиться в детали необходимого программного блока.
После того, как коллекции объявлены, можно использовать BULK COLLECT для извлечения информации о сотрудниках (строки с 23 по 30). Это эффективно заменяет цикл CURSOR FOR. Теперь данные загружены в коллекции.
Алгоритм сортировки сотрудников (строки с 32 по 46) требует, чтобы я прошёл по каждой записи в коллекции и сделал отметку о необходимости повышения зарплаты. Если повышение подтверждается, ID сотрудника копируется из коллекции, заполненной запросом, в коллекцию сотрудников для повышения зарплаты. Если сотрудник не получил права на повышение, копируются поля ID сотрудника, зарплата, дата найма, т.к. эти данные необходимы для заполнения таблицы истории.
Теперь, когда исходные данные распределены по двум коллекциям, их можно использовать как источники для двух различных операций FORALL (начинаются со строк 51 и 66). Я массово вставляю данные из коллекции не получивших права на повышение в таблицу employee_history (процедура add_to_history), и массово обновляю данные о сотрудниках, получивших повышение в таблице сотрудников с помощью процедуры give_the_raise.
Теперь, в конце нашего исследования давайте более внимательно взглянем на процедуру add_to_history (строки с 48 по 61). Выражение FORALL (строка 51) содержит оператор IN, который определяет диапазон номеров строк, которые будут использоваться в массовой вставке. При рассмотрении второй версии процедуры, я буду ссылаться на коллекцию, используемую для определения данного диапазона как на “управляющую коллекцию”. Однако в этой версии add_to_history я просто скажу: используйте все строки, определённые в denied_employee_ids. Внутри предложения INSERT, используются все три коллекции для записи истории; назовём их "коллекции данных". Теперь можно увидеть, что управляющие коллекции и коллекции данных не обязаны совпадать. Это весьма важно при изучении нововведений в Oracle Database 10g.
В Листинге 2 пришлось почти в два раза увеличить количество строк кода по сравнению с
Листингом 1 из-за того, что код
Листинга 2 должен был быть выполнен за определённое отведённое для него время. До появления Oracle Database 10g в такой ситуации я был бы просто счастлив, что уложился вовремя и переключился бы на другие задачи.
Однако благодаря последней версией PL/SQL в Oracle Database 10g имеется значительно больше возможностей для улучшения производительности, читабельности и объёма кода.
Используем VALUES OF
В Oracle Database 10g теперь можно задать подмножество строк в управляющей коллекции, используемой в выражении FORALL. Подмножество можно определить двумя способами:
- Совпадение номеров строк в коллекции данных с номерами строк в управляющей коллекции. Это можно сделать с помощью оператора INDICES OF.
- Совпадение номеров строк в коллекции данных со значениями, которые можно найти в определённых строках управляющей коллекции. Это можно сделать с помощью оператора VALUES OF.
Оператор VALUES OF будет использоваться во втором и последнем варианте give_raises_in_department.
Листинг 3 содержит полный код этой версии. Пропустим части программы, оставшиеся без изменений.
Начиная с объявления коллекций, заметим, что дополнительных коллекций для хранения информации о поощрениях и наказаниях больше не объявлено. Вместо этого в
Листинге 3 (строки с 17 по 21) объявлены две вспомогательные коллекции: одна для поощренных сотрудников, другая для тех, кто не был поощрён. Тип данных в обеих коллекциях булевский; как скоро будет видно, тип коллекции вообще не играет роли. Как определены строки важно только для оператора FORALL.
|
Затраченное время для трёх реализаций процедуры give_raises_in_department с 50,000 строк в таблице сотрудников. |
Реализация
|
Затраченное время
|
|
Цикл CURSOR FOR |
00:00:38.01 |
|
Массовая обработка в Pre-Oracle Database 10g |
00:00:06.09 |
|
Массовая обработка в Oracle Database 10g |
00:00:02.06 |
|
Затраченное время для трёх реализаций процедуры give_raises_in_department со 100,000 строк в таблице сотрудников. |
Реализация
|
Затраченное время
|
|
CURSOR FOR loop |
00:00:58.01 |
|
Массовая обработка в Pre-Oracle Database 10g |
00:00:12.00 |
|
Массовая обработка в Oracle Database 10g |
00:00:05.05 |
Таблица 1: Временной тест с 50,000 и 100,000 записей
Подпрограмма retrieve_employee_info такая же, как и раньше, но способ разделения данных совсем другой (строки с 32 по 44). Вместо копирования записей с данными из одной коллекции в другую (это относительно медленная операция), просто объявляется строка (присвоением значения TRUE) вспомогательной коллекции, которая соответствует номеру строки коллекции с ID сотрудников.
Теперь можно использовать коллекции approved_list и denied_list, как управляющие, в двух различных выражениях FORALL (начинающихся со строк 49 и 65).
Для вставки в таблицу the employee_history, используется следующее выражение:
FORALL indx IN VALUES OF denied_list
Для выполнения корректировок (для учёта повышений зарплат), используется следующее выражение:
FORALL indx IN VALUES OF approved_list
В обоих DML-выражениях коллекции данных – это оригинальные коллекции, заполненные на этапе выполнения BULK COLLECT; никакое копирование не использовалось. Используя VALUES OF, БД Oracle выбирает только те строки в коллекции данных, номера которых совпадают с номерами в управляющей коллекции.
Используя преимущества VALUES OF в данной программе, удалось избежать копирования записей, заменив его простым списком с номерами строк. Для больших массивов уход от выполнения такого копирования весьма ощутим. Для тестирования преимуществ нововведений в Oracle Database 10g, я загрузил таблицу сотрудников и выполнил тест с 50,000 и затем со 100,000 строк. Я изменил до-Oracle Database 10g вариант массовой обработки, сделав больше копий содержимого коллекций для эмуляции реальных данных. Затем, используя SQL*Plus SET TIMING ON для отображения затраченного времени для
каждой версии, получил результат, показанный в
Таблице 1.
Результат очевиден: переход от отдельных DML-выражений к массовой обработке приводит к существенному сокращению затраченного времени, с 38 до 6 секунд для 50,000 строк и с 58 до 12 секунд для 100,000 строк. Кроме того, избежав копирования данных с помощью VALUES OF, затраченное время сократилось примерно наполовину.
Даже без улучшения производительности, VALUES OF и родственный оператор INDICES OF, увеличивают гибкость языка PL/SQL и делают его проще для написания интуитивно более понятного и легче поддерживаемого кода.
PL/SQL на данном этапе предстаёт перед нами зрелым и мощным языком. И как следствие этого, нововведения являются инкрементальными и эволюционными. Тем не менее, рассмотренные нововведения могут дать существенный выигрыш в производительности приложения и продуктивности труда разработчиков. VALUES OF -прекрасный образец таких нововведений.
Стивен Ферстайн (steven@stevenfeuerstein.com) крупный специалист по языку PL/SQL. Он автор девяти книг по PL/SQL (все от O'Reilly & Associates, oracle.oreilly.com), включая Oracle PL/SQL Best Practices и Oracle PL/SQL Programming. Он ведёт курсы по языку (http://www.minmaxplsql.com/) и является главным техническим консультантом в Quest Software.
REM drop table lots_of_employees;
REM drop table employee_history;
CREATE TABLE lots_of_employees (
employee_id NUMBER(10),
last_name VARCHAR2(15),
first_name VARCHAR2(15),
middle_initial VARCHAR2(1),
job_id NUMBER(3),
manager_id NUMBER(4),
hire_date DATE DEFAULT SYSDATE NOT NULL,
salary NUMBER(7,2),
commission NUMBER(7,2),
department_id NUMBER(10)
);
create unique index un_employee_id ON lots_of_employees (employee_id);
create index in_employee_department ON lots_of_employees (department_id);
CREATE TABLE employee_history (
employee_id NUMBER(4),
hire_date DATE DEFAULT SYSDATE NOT NULL,
salary NUMBER(7,2) ,
activity VARCHAR2(100)
);
CREATE OR REPLACE PROCEDURE load_employee (
first_in IN PLS_INTEGER
, last_in IN PLS_INTEGER
)
IS
-- Put lots of rows into the table
maxnum PLS_INTEGER;
BEGIN
FOR indx IN first_in .. last_in
LOOP
SELECT MAX (employee_id)
INTO maxnum
FROM lots_of_employees;
INSERT INTO lots_of_employees
(employee_id, last_name, first_name
, salary, department_id, hire_date
)
VALUES (maxnum + 1, 'Feuerstein' || indx, 'Steven'
, indx, MOD (indx, 4) * 10, SYSDATE
);
IF MOD (indx, 1000) = 0
THEN
COMMIT;
END IF;
END LOOP;
COMMIT;
END;
/
CREATE OR REPLACE PACKAGE comp_analysis
IS
FUNCTION is_eligible (id_in IN lots_of_employees.employee_id%TYPE)
RETURN BOOLEAN;
END comp_analysis;
/
CREATE OR REPLACE PACKAGE BODY comp_analysis
IS
FUNCTION is_eligible (id_in IN lots_of_employees.employee_id%TYPE)
RETURN BOOLEAN
IS
BEGIN
RETURN MOD (id_in, 2) = 0;
END;
END comp_analysis;
/
CREATE OR REPLACE PROCEDURE give_raises_in_department1 (
dept_in IN lots_of_employees.department_id%TYPE
, newsal IN lots_of_employees.salary%TYPE
)
IS
CURSOR emp_cur
IS
SELECT employee_id, salary, hire_date
FROM lots_of_employees
WHERE (department_id = dept_in OR dept_IN IS NULL);
emp_rec emp_cur%ROWTYPE;
BEGIN
OPEN emp_cur;
LOOP
FETCH emp_cur
INTO emp_rec;
EXIT WHEN emp_cur%NOTFOUND;
IF comp_analysis.is_eligible (emp_rec.employee_id)
THEN
UPDATE lots_of_employees
SET salary = newsal
WHERE employee_id = emp_rec.employee_id;
ELSE
INSERT INTO employee_history
(employee_id, salary
, hire_date, activity
)
VALUES (emp_rec.employee_id, emp_rec.salary
, emp_rec.hire_date, 'RAISE DENIED'
);
END IF;
END LOOP;
END give_raises_in_department1;
/
SHO ERR
REM
REM Pre-10g create multiple copies of collection
REM for different purposes.
REM
CREATE OR REPLACE PROCEDURE give_raises_in_department2 (
dept_in IN lots_of_employees.department_id%TYPE
, newsal IN lots_of_employees.salary%TYPE
)
IS
TYPE employee_aat IS TABLE OF lots_of_employees.employee_id%TYPE
INDEX BY PLS_INTEGER;
TYPE salary_aat IS TABLE OF lots_of_employees.salary%TYPE
INDEX BY PLS_INTEGER;
TYPE hire_date_aat IS TABLE OF lots_of_employees.hire_date%TYPE
INDEX BY PLS_INTEGER;
employee_ids employee_aat;
salaries salary_aat;
hire_dates hire_date_aat;
approved_employee_ids employee_aat;
denied_employee_ids employee_aat;
denied_salaries salary_aat;
denied_hire_dates hire_date_aat;
PROCEDURE retrieve_employee_info
IS
BEGIN
SELECT employee_id, salary, hire_date
BULK COLLECT INTO employee_ids, salaries, hire_dates
FROM lots_of_employees
WHERE (department_id = dept_in OR dept_IN IS NULL);
END;
PROCEDURE partition_by_eligibility
IS
BEGIN
FOR indx IN employee_ids.FIRST .. employee_ids.LAST
LOOP
IF comp_analysis.is_eligible (employees (indx))
THEN
approved_employee_ids (indx) := employees (indx);
ELSE
denied_employee_ids (indx) := employees (indx);
denied_salaries (indx) := salaries (indx);
denied_hire_dates (indx) := hire_dates (indx);
END IF;
END LOOP;
END;
PROCEDURE add_to_history
IS
BEGIN
FORALL indx IN denied_employee_ids.FIRST .. denied_employee_ids.LAST
INSERT INTO employee_history
(employee_id
, salary
, hire_date, activity
)
VALUES (denied_employee_ids (indx)
, denied_salaries (indx)
, denied_hire_dates (indx), 'RAISE DENIED'
);
END;
PROCEDURE give_the_raise
IS
BEGIN
FORALL indx IN approved_employee_ids.FIRST .. approved_employee_ids.LAST
UPDATE lots_of_employees
SET salary = newsal
WHERE employee_id = approved_employee_ids (indx);
END;
BEGIN
retrieve_employee_info;
partition_by_eligibility;
add_to_history;
give_the_raise;
END give_raises_in_department2;
/
SHO ERR
REM
REM 10g usage of INDICES OF
REM
CREATE OR REPLACE PROCEDURE give_raises_in_department3 (
dept_in IN lots_of_employees.department_id%TYPE
, newsal IN lots_of_employees.salary%TYPE
)
IS
TYPE employee_aat IS TABLE OF lots_of_employees.employee_id%TYPE
INDEX BY PLS_INTEGER;
TYPE salary_aat IS TABLE OF lots_of_employees.salary%TYPE
INDEX BY PLS_INTEGER;
TYPE hire_date_aat IS TABLE OF lots_of_employees.hire_date%TYPE
INDEX BY PLS_INTEGER;
employee_ids employee_aat;
salaries salary_aat;
hire_dates hire_date_aat;
TYPE guide_aat IS TABLE OF BOOLEAN
INDEX BY PLS_INTEGER;
approved_list guide_aat;
denied_list guide_aat;
PROCEDURE retrieve_employee_info
IS
BEGIN
SELECT employee_id, salary, hire_date
BULK COLLECT INTO employee_ids, salaries, hire_dates
FROM lots_of_employees
WHERE (department_id = dept_in OR dept_IN IS NULL);
END;
PROCEDURE partition_by_eligibility
IS
BEGIN
FOR indx IN employee_ids.FIRST .. employee_ids.LAST
LOOP
IF comp_analysis.is_eligible (employees(indx))
THEN
approved_list (indx) := TRUE;
ELSE
denied_list (indx) := TRUE;
END IF;
END LOOP;
END;
PROCEDURE add_to_history
IS
BEGIN
FORALL indx IN INDICES OF denied_list
INSERT INTO employee_history
(employee_id
, salary
, hire_date, activity
)
VALUES (employees (indx)
, salaries (indx)
, hire_dates (indx)
, 'RAISE DENIED'
);
END;
PROCEDURE give_the_raise
IS
BEGIN
FORALL indx IN INDICES OF approved_list
UPDATE lots_of_employees
SET salary = newsal
, hire_date = hire_dates(indx)
WHERE employee_id = employees(indx);
END;
BEGIN
retrieve_employee_info;
partition_by_eligibility;
add_to_history;
give_the_raise;
END give_raises_in_department3;
/
SHO ERR
REM
REM 10g usage of VALUES OF
REM
CREATE OR REPLACE PROCEDURE give_raises_in_department4 (
dept_in IN lots_of_employees.department_id%TYPE
, newsal IN lots_of_employees.salary%TYPE
)
IS
TYPE employee_aat IS TABLE OF lots_of_employees.employee_id%TYPE
INDEX BY PLS_INTEGER;
TYPE salary_aat IS TABLE OF lots_of_employees.salary%TYPE
INDEX BY PLS_INTEGER;
TYPE hire_date_aat IS TABLE OF lots_of_employees.hire_date%TYPE
INDEX BY PLS_INTEGER;
employee_ids employee_aat;
salaries salary_aat;
hire_dates hire_date_aat;
TYPE guide_aat IS TABLE OF PLS_INTEGER
INDEX BY PLS_INTEGER;
approved_list guide_aat;
denied_list guide_aat;
PROCEDURE retrieve_employee_info
IS
BEGIN
SELECT employee_id, salary, hire_date
BULK COLLECT INTO employee_ids, salaries, hire_dates
FROM lots_of_employees
WHERE (department_id = dept_in OR dept_IN IS NULL);
END;
PROCEDURE partition_by_eligibility
IS
BEGIN
FOR indx IN employee_ids.FIRST .. employee_ids.LAST
LOOP
IF comp_analysis.is_eligible (employees(indx))
THEN
approved_list (indx) := indx;
ELSE
denied_list (indx) := indx;
END IF;
END LOOP;
END;
PROCEDURE add_to_history
IS
BEGIN
FORALL indx IN VALUES OF denied_list
INSERT INTO employee_history
(employee_id
, salary
, hire_date, activity
)
VALUES (employees (indx)
, salaries (indx)
, hire_dates (indx)
, 'RAISE DENIED'
);
END;
PROCEDURE give_the_raise
IS
BEGIN
FORALL indx IN VALUES OF approved_list
UPDATE lots_of_employees
SET salary = newsal
, hire_date = hire_dates(indx)
WHERE employee_id = employees(indx);
END;
BEGIN
retrieve_employee_info;
partition_by_eligibility;
add_to_history;
give_the_raise;
END give_raises_in_department4;
/
SHO ERR
SET TIMING ON
BEGIN
give_raises_in_department1 (NULL, 1000);
END;
/
BEGIN
give_raises_in_department2 (NULL, 1000);
END;
/
BEGIN
give_raises_in_department3 (NULL, 1000);
END;
/
|