В ходе этого урока мы с вами рассмотрим некоторые темы, выходящие за рамки элементарных средств SQL К концу урока вы будете иметь в своем арсенале понимание основ использования курсоров, сохраненных процедур, триггеров динамического SQL прямого вызова и вложенного SQL, а также SQL, сгенерированного средствами SQL
Основными на этом уроке будут следующие темы
• Что такое курсор
• Использование сохраненных процедур
• Триггеры
• Основы динамического SQL
• Использование SQL для генерирования SQL
• Прямой вызов SQL и вложенный SQL
• Интерфейс уровня вызовов
Средства, выходящие за рамки элементарных
Возможности SQL, обсуждаемые здесь, выходят за рамки базовых операций, которые мы с вами рассматривали до сих пор и к которым можно отнести операции выполнения запросов, создания структур базы данных и манипуляции данными Возможности, которые мы рассмотрим в ходе этого занятия, доступны во многих реализациях баз данных, использующих расширения стандартного SQL, обсуждавшегося до сих пор
Не все обсуждаемые здесь вопросы определяются стандартом ANSI SQL поэтому обязательно проверьте по документации синтаксис и правила использования предлагаемых ниже команд Для сравнения мы здесь приводим синтаксис команд нескольких ведущих производителей баз данных
Для многих людей слово "курсор" ассоциируется с мигающим прямоугольником на экране монитора, предназначенным для указания того места где в файле или приложении находится пользователь Здесь же под курсором понимается нечто иное Курсор SQL — это область памяти базы данных, где был сохранен последний из операторов SQL Если текущим оператором SQL оказывается запрос, в памяти сохраняется также и строка запроса Эта строка называется текущим значением курсора или текущей строкой Соответствующая область памяти именуется и оказывается доступной программам
Курсор обычно используется для извлечения порций данных из базы данных При этом программа получает возможность анализировать текущие строки курсора Курсоры обычно используются в рамках SQL, встроенного в программы процедурного типа Некоторые курсоры создаются автоматически сервером базы данных, другие же объявляются программистом Использование курсоров в базах данных разных производителей может иметь свои особенности
Рассмотрим для сравнения операторы объявления курсора для двух наиболее распространенных систем Microsoft SQL Server и Oracle
Синтаксис оператора объявления курсора в Microsoft SQL Server следующий
DECLARE ШЧ_КУРСОРА CURSOR
FOR OnEPATOP_SELECT
[ FOR {READ ONLY UPDATE [ СПИСОК__СТОЛБЦОВ ]}]
Синтаксис оператора объявления курсора в Oracle следующий
DECLARE CURSOR ИМЯ_ КУРСОРА
IS {ОПЕРАТОР_SELECT}
Создаваемый следующим оператором курсор будет содержать все записи таблицы EMPLOYEE_TBL
DECLARE CURSOR EMP_CURSOR IS
SELECT * FROM EMPLOYEE_TBL
{ ДРУГИЕ ОПЕРАТОРЫ ПРОГРАММЫ }
После определения курсора для доступа к нему стандартом ANSI предусмотрены следующие операторы
OPEN Открывает определенный ранее курсор
FETCH Извлекает данные курсора, помещая их в переменную программы
CLOSE Закрывает курсор после завершения работы с ним
При открытии курсора выполняется указанный в определении этого курсора оператор SELECT, и результат выполнения оператора сохраняется в специальной области памяти
Синтаксис оператора для открытия курсора в dBase следующий
OPEN ИМЯ_КУРСОРА
Синтаксис оператора для открытия курсора в Oracle следующий.
OPEN ИМЯ_КУРСОРА [ ПАРАМЕТР1 [, ПАРАМЕТР2 ]]
Например, следующий оператор открывает курсор
EMP_CURSOR: OPEN EMP_CURSOR
После того как курсор будет открыт, его содержимое (т. е. результат выполнения его запроса) можно прочитать с помощью оператора FETCH.
Синтаксис оператора FETCH в Microsoft SQL Server следующий.
FETCH ИМЯ_КУРСОРА [ INTO СПИСОК_ПОЛУЧАТЕЛЬ ]
Синтаксис оператора в Oracle следующий.
FETCH CURSOR ИМЯ_КУРСОРА {INTO : ГЛАВНАЯ_ПЕРЕМЕННАЯ }
[[ INDICATOR ] : ПЕРЕМЕННАЯ_ИНДИКАТОР ]]
[, : ГЛАВНАЯ_ПЕРЕМЕННАЯ
[[ INDICATOR ] : ПЕРЕМЕННАЯ_ИНДИКАТОР ]]
I USING DESCRIPTOR ДЕСКРИПТОР ]
Синтаксис оператора в dBase следующий.
FETCH ИМЯ_КУРСОРА INTO ПЕРЕМЕННЫЕ_ПАМЯТИ
Например, чтобы извлечь содержимое курсора EMP_CURSOR и поместить в переменную с именем EMP_RECORD, можно использовать оператор FETCH следующего вида.
FETCH EMP_CURSOR INTO EMP_RECORD
Очевидно, если имеется возможность открыть курсор, должна быть предусмотрена и возможность закрыть его. Закрыть курсор несложно, только следует помнить, что после закрытия курсор становится недоступным для программ пользователей.
Закрытие курсора не обязательно освобождает выделенную для него память. В некоторых базах данных освободить занятую курсором память можно только явно с помощью команды DEALLOCATE. В результате выполнения этой команды освобождается занятая курсором память и назначенное курсору имя, которое теперь можно будет использовать снова. В других базах данных память освобождается автоматически при закрытии курсора. В таком случае ранее занятая курсором память оказывается доступной для других операций по мере необходимости.
В Microsoft SQL Server синтаксис операторов закрытия курсора и освобождения занимаемой курсором памяти будет следующим.
CLOSE ИМЯ_КУРСОРА
DEALLOCATE CURSOR ИМЯ_КУРСОРА
При закрытии курсора в Oracle занимаемые им ресурсы освобождаются автоматически без использования оператора DEALLOCATE. Синтаксис оператора закрытия курсора в Oracle следующий.
CLOSE ИМЯ_КУРСОРА
Для освобождения ресурсов в dBase объект должен быть закрыт и открыт вновь. Тогда ресурсы окажутся свободными, а имена — вновь доступными для использования. Синтаксис оператора закрытия курсора в dBase следующий.
CLOSE ИМЯ_КУРСОРА
Как вы могли видеть из предыдущих примеров вариации операторов в разных базах данных могут быть значительными, особенно если эти операторы используют возможности расширений SQL, о которых пойдет речь в ходе урока 24, "Расширения стандартного SQL". По поводу правильного использования курсоров в вашем конкретном случае обратитесь к документации, сопровождающей базу данных.
Сохраненные процедуры и функции
Сохраненная процедура — это набор операторов SQL, обычно называемый функцией или подпрограммой, созданный программистом для удобства использования в программах (сохраненную процедуру использовать проще, чем каждый раз записывать весь набор входящих в нее операторов SQL). Кроме того, сохраненные процедуры можно вкладывать одну в другую, т. е. одни сохраненные процедуры могут вызывать другие, последние, в свою очередь, тоже могут вызывать сохраненные процедуры и т. д.
Возможность сохранять процедуры — основа процедурного программирования. Команды SQL (CREATE TABLE, INSERT, UPDATE, SELECT'и т.д.) дают вам возможность сообщить базе данных, что делать, но не как делать. Посредством составления процедур вы получаете возможность сообщить ядру базы данных, каким образом следует обрабатывать данные.
Сохраненная процедура — это набор из одного или нескольких операторов SQL или функций, сохраненный в базе данных в откомпилированном виде, готовом для выполнения пользователем базы данных. Сохраненная функция является сохраненной процедурой, предполагающей в результате своего выполнения возврат некоторого значения.
Функции вызываются процедурами. При вызове функции ей, как процедуре, могут передаваться параметры, затем функция вычисляет некоторое значение и возвращает его вызывающей процедуре для дальнейшего использования.
При сохранении процедуры в базе данных сохраняются также и все входящие в эту процедуру подпрограммы и функции (использующие SQL). Все эти сохраняемые процедуры предварительно анализируются и сохраняются в виде, готовом для немедленного использования по команде, инициируемой пользователем.
В Microsoft SQL Server процедуры создаются с помощью оператора следующего вида.
CREATE PROCEDURE ИМЯ_ПРОЦЕДУРЫ
[ [ (] @ИМЯ_ПАРАМЕТРА
ТИП_ДАННЫХ [(ДЛИНА) | (ТОЧНОСТЬ] [, МАСШТАБ ])
[ = DEFAULT ][ OUTPUT ]]
[, @ ИМЯ_ПАРАМЕТРА
ТИП_ДАННЫХ [(ДЛИНА) | (ТОЧНОСТЬ] [, МАСШТАБ ])
[ = DEFAULT ][ OUTPUT ]] [)]]
[ WITH RECOMPILE ]
AS ОПЕРАТОРЫ_SQL
В Oracle синтаксис оператора следующий.
CREATE [ OR REPLACE ] PROCEDURE ИМЯ__ПРОЦЕДУРЫ
[ (АРГУМЕНТ [{IN | OUT | IN OUT) ] ТИП,
АРГУМЕНТ [{IN 1 OUT | IN OUT} ] ТИП] ] {IS I AS)
ТЕЛО_ПРОЦЕДУРЫ
Вот пример оператора, создающего достаточно простую процедуру.
CREATE PROCEDURE NEW_PRODUCT
(PROD_ID IN VARCHAR2, PROD_DESC IN VARCHAR2, COST IN NUMBER)
AS
BEGIN
INSERT INTO PRODUCTS_TBL
VALUES (PRODJT.D, PROD_DESC, COST);
COMMIT;
END;
Процедура создана.
Эту процедуру можно использовать для добавления новых строк в таблицу PRODUCTS_TBL.
В Microsoft SQL Server сохраненные процедуры используются следующего образом.
EXECUTE [ @ВОЗВРАЩАЕМОЕ_СОСТОЯНИЕ = ] ИМЯ ПРОЦЕДУРЫ
[[@ИМЯ__ПАРАМЕТРА = ] ЗНАЧЕНИЕ |
[@ИМЯ_ПАРАМЕТРА = } @ПЕРЕМЕННАЯ [ OUTPUT ]]
[WITH RECOMPILE]
В Oracle синтаксис следующий.
EXECUTE [ @ВОЗВРАЩАЕМОЕ_СОСТОЯНИЕ = ] ИМЯ__ПРОЦЕДУРЫ
[[@ИМЯ_ПАРАМЕТРА = ] ЗНАЧЕНИЕ | [@ШЯ ПАРАМЕТРА = ] @ПЕРЕМЕННАЯ [
OUTPUT ]]
[WITH RECOMPILE]
Давайте выполним только что созданную процедуру.
EXECUTE NEW_PRODUCT ('9999','INDIAN CORN',1.99);
PL/SQL процедура успешно выполнена.
Вы обнаружите, что синтаксис команд, используемых при работе с процедурами в базах данных разных производителей, сильно варьирует Базовые команды SQL остаются одинаковыми, но вот программные конструкции (такие как переменные, условные выражения, курсоры, циклы) могут отличаться существенно
Преимущества использования процедур
Использовать сохраненные ранее процедуры удобнее, чем отдельные операторы SQL по целому ряду причин. Некоторые из этих причин перечислены ниже.
• Операторы сохраненной процедуры уже сохранены в базе данных.
• Операторы сохраненной процедуры уже проверены и находятся в готовом для использования виде
• Возможность сохранения процедур позволяет использовать модульное программирование
• Сохраненные процедуры могут вызывать другие процедуры и функции.
• Сохраненные процедуры могут вызываться другими программами.
• При использовании сохраненных процедур результат ответ от базы данных обычно получается быстрее
• Использовать процедуры очень просто.
Триггер — это откомпилированная процедура, используемая для выполнения действий, инициируемых происходящими в базе данных событиями Триггер представляет собой сохраненную в базе данных процедуру, которая выполняется тогда, когда в отношении таблицы выполняются определенные действия (операторы языка манипуляции данными — DML). Триггер может выполняться до или после операторов INSERT, DELETE или UPDATE. Триггеры можно использовать, например, для проверки целостности данных перед выполнением INSERT, DELETE или UPDATE. С помощью триггеров можно отменять транзакции, а также модифицировать данные одних таблиц и читать данные других даже из других баз данных.
Чаще всего триггеры использовать очень удобно, однако, их использование приводит к значительному увеличению числа операций ввода-вывода. Триггеры не следует использовать тогда, когда сохраненная процедура или программа может добиться тех же результатов с меньшими накладными расходами.
Триггер можно создать с помощью оператора CREATE TRIGGER. Стандарт ANSI предлагает для этого оператора следующий синтаксис.
CREATE TRIGGER TRIGGER NAME
[[BEFORE | AFTER] TRIGGER EVENT ON TABLE NAME]
[REFERENCING VALUES ALIAS LIST ]
[TRIGGERED ACTION TRIGGER EVENT ::=
INSERT UPDATE | DELETE {OF TRIGGER COLUMN LIST]
TRIGGER COLUMN LIST ::= COLUMN NAME [, COLUMN NAME]
VALUES ALIAS LIST ::=
VALUES ALIAS LIST ::=
OLD [ROW] [AS] OLD VALUES CORRELATION NAME |
NEW {ROW} {AS} NEW VALUES CORRELATION NAME |
OLD TABLE {AS} OLD VALUES TABLE ALIAS |
NEW TABLE {AS} NEW VALUES TABLE ALIAS
OLD VALUES TABLE ALIAS ::= IDENTIFIER
NEW VALUES TABLE ALIAS ::= IDENTIFIER
TRIGGERED ACTION ::=
[FOR EACH [ROW | STATEMENT] [WHEN SEARCH CONDITION]]
TRIGGERED SQL STATEMENT
TRIGGERED SQL STATEMENT ::=
SQL STATEMENT | BEGIN ATOMIC [SQL STATEMENT;]
END
В Microsoft SQL Server синтаксис оператора для создания триггера выглядит следующим образом.
CREATE TRIGGER ИМЯ_ТРИГГЕРА
ON ИМЯ_ТАБЛИЦЫ
FOR { INSERT | UPDATE | DELETE [, ..]}
AS
OnEPATOPH_SQL
[ RETURN ]
В Oracle базовый синтаксис оператора следующий.
CREATE [ OR REPLACE ] TRIGGER ИМЯ_ТРИГГЕРА
[ BEFORE | AFTER]
[ DELETE I INSERT | UPDATE]
ON [ ПОЛЬЗОВАТЕЛЬ.ИМЯ_ТАБЛИЦЫ ]
[ FOR EACH ROW ]
[ WHEN УСЛОВИЕ ]
[ БЛОК PL/SQL ]
Вот пример создания триггера.
CREATE TRIGGER EMP_PAY_TRIG
AFTER UPDATE ON EMPLOYEE_PAY_TBL
FOR EACH ROW
BEGIN
INSERT INTO EMPLOYEE_PAY_HISTORY
(EMP_ID, PREV_PAY_RATE, PAY_RATE, DATE_LAST_RAISE,
TRANSACTION_TYPE)
VALUES
(:NEW.EMP_ID, :OLD.PAY_RATE, :NEW.PAY_RATE,
:NEW.DATE_LAST_RAISE, 'PAY CHANGE');
END;
/
Триггер создан.
В этом примере создается триггер с именем EMP_PAY_TRIG. Этот триггер вставляет строку в таблицу EMPLOYEE_PAY_HISTORY, отражая изменения таблицы EMPLOYEE_PAY_TBL каждый раз, когда данные последней обновляются.
Тело триггера изменить нельзя Для этого триггер придется либо заменить другим, либо воссоздать В некоторых реализациях SQL триггер можно заменить (если триггер с данным именем в системе уже существует) с помощью того же оператора CREATE TRIGGER.
Триггер можно удалить с помощью оператора DROP TRIGGER. Синтаксис этого оператора следующий.
DROP TRIGGER ИМЯ_ТРИГТЕPA
Динамический SQL дает возможность программисту или конечному пользователю создавать необходимые операторы SQL прямо во время выполнения программы. Создаваемые таким образом операторы передаются базе данных и возвращают данные в переменные программы, существующие во время выполнения последней.
Понять динамический SQL проще всего в сравнении со статическим. Статический SQL — это SQL, обсуждавшийся в книге до сих пор. Статический оператор SQL создается в предположении, что меняться он не будет. Хотя статические операторы SQL могут сохраняться в готовом для выполнения виде, например, в процедурах базы данных, они не обеспечивают той гибкости, которая достигается с помощью динамического SQL.
Проблема статического SQL состоит в том, что даже тогда, когда пользователю предлагается очень много видов запросов на выбор, всегда остается вероятность того, что в каких-то случаях пользователя ни один из этих "законсервированных запросов" не устроит. Динамический SQL используется в качестве средства создания подходящих для конкретной ситуации запросов непосредственно пользователем во время его работы с базой данных. После того как оператор будет приведен в нужный пользователю вид, он будет передан базе данных для проверки синтаксиса, необходимых привилегий, компиляции и, наконец, выполнения сервером базы данных. Динамический SQL создается с помощью интерфейса уровня вызовов, обсуждению которого посвящается следующий раздел.
Хотя динамический SQL обеспечивает конечным пользователям большую гибкость в построении запросов, он не может сравниться по производительности с сохраненными процедурами, операторы которых оказываются уже подготовленными оптимизатором SQL к немедленному выполнению.
Интерфейс уровня вызовов (call level interface) используется для внедрения программного кода SQL в главную программу, создаваемую, например, средствами ANSI С Создателям приложений понятие интерфейса уровня вызовов должно быть хорошо знакомым. Это один из методов, с помощью которого программист получает возможность внедрить SQL в программный код некоторых процедурных языков программирования. При использовании интерфейса уровня вызовов текст оператора SQL передается некоторой переменной программы с соблюдением правил соответствующего языка программирования. После этого получившая оператор SQL программа может его выполнить, обработав переменную, которой был передан текст этого оператора.
Типичной командой языка программирования, позволяющей вызвать оператор SQL из программы, является команда EXEC SQL.
Вот некоторые из языков программирования, поддерживающих интерфейс уровня вызовов.
• COBOL
• ANSI С
• Pascal
• Fortran
• Ada
По поводу использования опций интерфейса уровня вызовов обратитесь к документации по соответствующему языку программирования
Использование SQL для генерации SQL
Использование SQL для генерации операторов SQL в некоторых случаях значительно экономит время. Предположим, что в вашей базе данных 100 пользователей. Предположим также, что вы создаете новую роль ENABLE (пользовательский объект с соответствующими привилегиями), которую необходимо приписать всем 100 пользователям. Вместо того, чтобы вручную создавать 100 операторов GRANT, можно использовать следующий оператор SQL, который сгенерирует необходимые операторы за вас
SELECT 'GRANT ENABLE TO '|| USERNAME || ';'
FROM SYS.DBA_USERS;
В этом примере используется представление системного каталога Oracle, содержащее информацию о пользователях.
Обратите внимание на то, что здесь фраза GRANT ENABLE TO заключена в кавычки. Использование кавычек заставляет воспринимать все заключенное между ними как буквальное значение. Буквальные значения можно выбирать из таблиц точно так же, как и столбцы. USERNAME является столбцом таблицы SYS. DBA_USERS из системного каталога. Двойная вертикальная черта ( | | ) используется для конкатенации столбцов. Использование двойной вертикальной черты с последующим '; ' добавляет к концу пользовательского имени точку с запятой, означающую завершение оператора.
В результате сгенерированные операторы SQL будут выглядеть примерно так:
GRANT ENABLE TO RRPLEW;
GRANT ENABLE TO RKSTEP;
Результат необходимо сохранить в файле, который можно будет передать базе данных. База данных, в свою очередь, выполнит все операторы SQL из этого файла по очереди, избавляя вас от необходимости долгого печатания команд и тем самым сэкономив вам немало времени.
В следующий раз, когда вам придется при написании операторов SQL повторять однообразные операторы SQL несколько раз подряд, остановитесь и позвольте SQL сделать эту работу за вас.
Прямой вызов SQL и вложенный SQL
Прямое использование SQL означает выполнение операторов SQL с любого интерактивного терминала. Результаты при этом возвращаются обратно на терминал, инициировавший выполнение операторов. В данной книге до сих пор, в основном, рассматривалось прямое использование SQL. Прямое использование SQL называют также интерактивным вызовом или прямым вызовом (direct invocation)
Вюженныи SQL (embedded SQL) представляет собой программный код SQL, используемый в рамках другой программы, созданной, например, средствами Pascal, Fortran, COBOL или С Программный код SQL оказывается фактически встроенным в несущий его язык программирования посредством интерфейса уровня вызовов, как было указано выше. Встроенные в несущий язык программирования операторы SQL обычно предваряются командой ЕХЕС SQL и, как правило, завершаются точкой с запятой. В других же случаях такими завершающими командами могут быть END-EXEC или закрывающая скобка.
Вот пример вложения оператора SQL в вызывающую программу, созданную средствами ANSI С
{операторы вызывающей программы}
ЕХЕС SQL {оператор SQL};
{другие операторы вызывающей программы}
В ходе этого урока обсуждались некоторые средства SQL, выходящие за рамки элементарных. Хотя мы и не вдавались в детали, вы должны были получить общее представление о том, как используются обсуждавшиеся здесь возможности. Сначала были рассмотрены курсоры, с помощью которых можно сохранять результаты запросов в памяти. Чтобы объявившая курсор программа могла его использовать, эта программа должна открыть его. После этого содержимое курсора можно передать соответствующей переменной, чтобы программа смогла в нужный момент эти данные использовать. Данные курсора сохраняются в памяти до тех пор, пока курсор не будет закрыт, а занятая им память освобождена.
Затем были рассмотрены сохраняемые процедуры и триггеры. Сохраняемые процедуры состоят, в основном, из групп операторов SQL, хранящихся вместе в базе данных. Такие операторы вместе с другими необходимыми командами компилируются базой данных и сохраняются в виде, готовом для немедленного выполнения в любое время по требованию пользователя базы данных. Триггер тоже представляет собой сохраненную процедуру, но его выполнение инициируется автоматически в ответ на определенные события, происходящие в базе данных. Использование сохраненных процедур оказывается, как правило, более выгодным с точки зрения производительности системы, чем непосредственное использование отдельных операторов SQL.
Кроме того, были рассмотрены динамический SQL, генерирование операторов SQL средствами SQL и различия между прямым вызовом SQL и использованием вложенного SQL. Динамический SQL, в отличие от статического, позволяет динамическое создание программного кода SQL непосредственно пользователем в ходе выполнения программы. Использование SQL для генерации операторов SQL может существенно экономить время, поскольку дает возможность с помощью конкатенации и выбора подходящих буквальных значений автоматизировать трудоемкие операции по созданию больших последовательностей однообразных операторов SQL. Наконец, обсуждались различия между прямым вызовом SQL и вложенным SQL. Главное из этих различий заключается в том, что прямой вызов операторов SQL осуществляется непосредственно пользователем с любого терминала, а вложенный SQL можно фактически считать частью программы, с помощью которой обрабатываются данные.
Рассмотренные в ходе этого урока средства будут использованы для иллюстрации возможностей использования SQL в рамках реальной базы данных типичного предприятия, о котором пойдет речь на следующем уроке, "Использование SQL в локальных и глобальных сетях".
Может ли сохраненная процедура вызывать другую сохраненную процедуру?
Да. Вызываемая процедура называется также вложенной процедурой.
Как заставить курсор выполняться?
Используйте для этого оператор OPEN CURSOR. В итоге результат выполнения курсора будет сохранен в специальной области памяти.
Задания практических занятий разделены на тесты и упражнения. Тесты предназначены для проверки общего уровня понимания рассмотренного материала. Упражнения дают возможность применить на практике идеи, обсуждавшиеся в ходе текущего урока, в комбинации с идеями из предыдущих уроков. Мы рекомендуем ответить на тестовые вопросы и выполнить упражнения прежде, чем продолжать дальнейшее чтение книги. Ответы можно проверить по Приложению Б, "Ответы".
1. Можно ли изменить триггер?
2. Можно ли использовать имя курсора вновь после того, как курсор будет закрыт?
3. Какая команда используется для извлечения данных курсора после его открытия?
4. Когда выполняются триггеры — до или после выполнения команд INSERT, UPDATE И
DELETE?
1. Используя таблицы системного каталога вашей базы данных, запишите программный код SQL, с помощью которого автоматически будут созданы следующие операторы SQL. Условные имена объектов замените реальными.
a. GRANT SELECT ON ИМЯ_ТАБЛИЦЫ ТО ИМЯ_ПОЛЬЗОВАТЕЛЯ;
б. GRANT, CONNECT, RESOURCE TO ИМЯ_ПОЛЬЗОВАТЕЛЯ; В. SELECT COUNT (*) FROM ИМЯ__ТАБЛИЦЫ;
2. Запишите оператор, создающий процедуру, позволяющую удалять строки таблицы PRODUCTS_TBL. В качестве образца используйте оператор, предложенный в тексте этого урока для процедуры, добавляющей строку с новым продуктом в указанную таблицу.
3. Запишите оператор, использующий созданную в предыдущем упражнении процедуру для удаления из таблицы строки с информацией о продукте с кодом (PROD_ID), равным 9999.