Уважаемые подписчики! Этот выпуск посвящен
дискуссии, которую провел Том Кайт с подписчиками группы comp.databases.oracle.misc
в 1999 году. Думаю, обсуждение это стоит того, чтобы его воспроизвести и
прокомментировать. Я выбросил подробности переписки, оставив только вопросы,
комментарии и ответы.
Вопрос:
Мне всегда хотелось, чтобы кто-нибудь объяснил мне, в чем суть понятия
мутирующих таблиц. Я поискал информацию о мутирующих таблицах в
нескольких руководствах, но само понятие для меня остается весьма смутным. Нельзя
ли привести пример? Как избегать такой ситуации?
Ответ Тома Кайта:
Я воспроизведу старое сообщение на эту тему (к сожалению, в службе deja news
его, повидимому, нет). Далее следует мой ответ на аналогичный вопрос. Я приведу как вопрос, так и ответ - имеет смысл сначала перечитать вопрос, чтобы понять, о чем идет
речь. Я объясняю, почему возникает ошибка мутирующей таблицы (для вашей же пользы)
и как ее избежать...
Исходный вопрос, заданный в ноябре 1997 года
У меня есть строчный триггер before insert, прекрасно работающий при
выполнении оператора INSERT INTO table VALUES(x, x, x), но если
выполнить INSERT INTO table (SELECT x, x, x FROM other_table), он
выдает сообщение об ошибке "ORA-04091: table is mutating, trigger/function may
not see it."
Что вызывает такое различие?
Подробнее:
У меня есть 2 таблицы:
CREATE TABLE member
(group_id NUMBER NOT NULL,
unit_id NUMBER NOT NULL,
carr NUMBER NOT NULL,
CONSTRAINT pk_member PRIMARY KEY (group_id, unit_id) );
CREATE TABLE carr_change
(group_id NUMBER NOT NULL,
old_carr NUMBER NOT NULL,
new_carr NUMBER NOT NULL);
Мне необходим триггер, чтобы при вставке строк в member,
проверялся предыдущий элемент (unit) группы. Если у него другое
значение атрибута carr, чем у вставляемого, мне необходимо вставить
строку в таблицу carr_change для пакетной обработки ночью. Таким образом,
если имеется запись (1, 1, 7) в таблице member, и вставляется строка
(1, 2, 6), необходимо записать строку (1, 7, 6) в таблицу
carr_change.
Вот какой триггер я написал:
CREATE OR REPLACE TRIGGER before_member_trig
BEFORE INSERT ON member
FOR EACH ROW
DECLARE
l_prev_carr member.carr%TYPE;
BEGIN
SELECT carr
INTO l_prev_carr
FROM member
WHERE group_id = :new.group_id
AND unit_id = (:new.unit_id - 1) );
IF :new.carr != l_prev_carr
THEN
INSERT INTO carr_change (group_id, old_carr, new_carr)
VALUES (:new.group_id, l_prev_carr, :new.carr);
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
NULL; -- this is fine, first unit for a group
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(SQLERRM);
END;
/
Этот триггер хорошо работает для вставок с конструкцией VALUES, но
не срабатывает при вставке с подзапросом. Ошбика мутирующей таблицы,
ORA -04091. Нет ли идей, почему он работает иначе и как это исправить?
Ответ:
Это сделано для защиты вас от ваших собственных действий.
Поскольку таблица при многострочной вставке, фактически, мутирует (изменяется), можно получить разные результаты при выполнении
точно такого же оператора insert, в зависимости от порядка строк
в подзапросе (например: оператор insert при стоимостной оптимизации может
дать другой результат, чем при запроса оптимизации на основе правил).
Например, путь таблица была пуста. М ы выполняем вставку значений
(1, 1, 11 );
(1, 2, 12 );
(1, 3, 13 );
Если подзапрос вернет значения в таком порядке, триггер вызовет вставку двух
строк в таблицу carr_change.
Если же строки из подзапроса вернутся (помните, выполнется тот же запрос,
просто изменяется порядок вставки строк - им вы не можете управлять) в таком порядке:
(1, 3, 13 );
(1, 2, 12 );
(1, 1, 11 );
ни одна строка вставлена не будет, в отличие от предыдущего варианта (причем
мы не можем это проконтролировать).
Поэтому можно вставить одну строку и использовать триггер
BEFORE INSERT/FOR EACH ROW, читающий таблицу (при этом не будет
двусмысленности, результат вставки еще невидим, поэтому невозможны и конфликты вроде
показанного выше, когда при разных выполнениях одного и того же оператора
получаются разные результаты). При вставке одной строки таблица еще не начала
"мутировать", когда срабатывает триггер before insert/for
each row (но она будет уже мутирующей для триггера after insert/for each
row, даже при вставке одной строки).
Ну, и как же это обойти? Использовать триггер after. Например, я переписал
ваш триггер следующим образом:
create or replace package member_pkg
as
type rArray is table of rowid index by binary_integer;
rids rArray;
cnt number;
end;
/
create or replace trigger member_bi
before insert on member
begin
member_pkg.cnt := 0;
end;
/
create or replace trigger member_aifer
after insert on member
for each row
begin
member_pkg.cnt := member_pkg.cnt+1;
member_pkg.rids( member_pkg.cnt ) := :new.rowid;
end;
/
CREATE OR REPLACE TRIGGER member_ai
after INSERT ON member
DECLARE
l_prev_carr member.carr%TYPE;
l_rec member%rowtype;
begin
for i in 1 .. member_pkg.cnt loop
BEGIN
select * into l_rec from member where rowid = member_pkg.rids(i);
SELECT carr
INTO l_prev_carr
FROM member
WHERE group_id = l_rec.group_id
AND unit_id = (l_rec.unit_id - 1 );
IF l_rec.carr != l_prev_carr THEN
INSERT INTO carr_change (group_id, old_carr, new_carr)
VALUES (l_rec.group_id, l_prev_carr, l_rec.carr);
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
dbms_output.put_line('no data');
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(SQLERRM);
END;
end loop;
end;
/
Так, что действия
insert into member
select 1, 1, 11 from dual union all
select 1, 2, 12 from dual union all
select 1, 3, 13 from dual;
select * from carr_change;
rollback;
insert into member
select 1, 3, 13 from dual union all
select 1, 2, 12 from dual union all
select 1, 1, 11 from dual ;
select * from carr_change;
теперь дают согласованный (тот же) ответ, даже если строки
вставляются в "обратном" порядке (если бы мы не считали таблицу
"мутирующей", два представленных выше оператора insert дали бы
разные результаты при использовании однострочного триггера - один
вставил бы 2 строки в таблицу carr_change, а второй - 0 строк.)
Поскольку в триггере необходим только доступ к новым (:new) значениям,
в строчном триггере можно просто сохранить идентификаторы строк. Если бы
необходимы были и старые значения, пришлось бы сохранять их в отдельных массивах.
Комменатрий В.К.
Общая идея обхода ограничения мутирующих таблиц, надеюсь, ясна? Необходимо в строчном
триггере где-то запоминать или помечать измененные строки, а затем в операторномafter-триггере выполнять те операции, которые приводят в строчном
триггере к ошибке мутирующей
таблицы. Операторный триггер имеет дело с уже выполненными изменениями, и может либо
сделать на их основе все, что угодно, либо откатить транзакцию, если получены
некорректные данные.
Весь вопрос в том, где сохранять затронутые строки и необходимые значения.
Почему-то слушатели моих курсов упорно отказываются использовать
продемонстрированное Томом решение на базе PL/SQL-таблиц. И все норовят
использовать таблицы базы данных, путь даже временные...
В следующем выпуске
Я ухожу в отпуск. Рассылка возобновит выход в конце августа. Вас ждет много интересного. Следите за новостями на
сайте OpenXS Initiative и шлите
мне свои вопросы и пожелания по дальнейшей
работе рассылки.