SQL 表正在变异...错误
Posted
技术标签:
【中文标题】SQL 表正在变异...错误【英文标题】:SQL Table is mutating... Error 【发布时间】:2013-12-12 23:15:09 【问题描述】:我正在尝试编写一个触发器,该触发器将禁止医院的任何房间提供超过 3 项服务。表 RoomServices 有一个房间号和它拥有的服务。因此,确定这一点的唯一方法是按房间号对房间进行分组并计算服务。我已经尝试了代码:
CREATE TRIGGER RoomServiceLimit
BEFORE INSERT OR UPDATE ON RoomServices
FOR EACH ROW
DECLARE
numService NUMBER;
CURSOR C1 IS SELECT count(*) AS RoomCount FROM RoomServices WHERE roomNumber = :new.roomNumber;
BEGIN
IF(inserting) THEN
SELECT count(*) into numService FROM RoomServices WHERE roomNumber = :new.roomNumber;
if(numService > 2) THEN
RAISE_APPLICATION_ERROR(-20001,'Room ' || :new.roomNumber || ' will have more than 3 services.');
END IF;
END IF;
IF(updating) THEN
FOR rec IN C1 LOOP
IF(rec.RoomCount > 2) THEN
RAISE_APPLICATION_ERROR(-20001,'Room ' || :new.roomNumber || ' will have more than 3 services.');
END IF;
END LOOP;
END IF;
END;
/
我尝试使用插入和更新分别运行每个方法,插入总是有效,更新总是会给我变异表错误。我不知道如何解决这个问题,所以任何建议都将不胜感激。
谢谢!
【问题讨论】:
Believe me XXX in a URL,一些成人过滤 ISP 过滤此 URL 的结果。 这是Oracle,不是mysql触发器,错误信息也来自Oracle,请编辑问题并将MySql
标签替换为Oracle
。
您有什么理由使用 CURSOR 作为 UPDATE 部分而不是 INSERT 部分?此外,在 UPDATE 中使每个房间的服务计数从 2 变为 3 的唯一方法是更新 roomNumber....这里没有解决方案只是 cmets。
尝试使用语句级触发器而不是行级触发器。删除 FOR EACH ROW
并再次尝试运行您的 DML。
【参考方案1】:
没有可靠的方法可以使用触发器来强制执行这种约束。一种可能的方法是使用物化视图,该视图在提交时自动刷新,并具有强制执行业务规则的检查约束:
create table roomservices (
pk number not null primary key,
roomnumber number);
create materialized view mv_roomservices
refresh on commit as
select
pk,
roomnumber,
count(*) over (partition by roomnumber) as cnt
from roomservices;
alter table mv_roomservices add constraint
chk_max_2_services_per_room check (cnt <= 2);
现在,每当您为一个房间添加两个以上的服务并尝试提交事务时,您都会收到 ORA-12008 异常(物化视图刷新路径错误)。
【讨论】:
这不是唯一的方法。还有垫子。提交时查看非常慢,我不乐意在每次检查时都使用我的表格的完整副本 第二行失败:插入房间服务值(12,3);插入客房服务值(12,4);插入房间服务值(12,5); 如果您尝试在同一事务中插入多于两行 - 是的。您必须回滚并重试。 方式挺有意思的。但这是否意味着我也不能进行大规模操作?喜欢更新所有行,还是从查询数据集中插入?【参考方案2】:我假设 RoomServices:
a) 是一个没有进行密集修改的小表 b) 永远不会存在提供超过 3 项服务的房间注意:您说“超过 3 个服务”,但您的代码说“超过 2 个服务”。所以我会使用“超过 2 个服务”。
那么,使用语句触发器呢?
CREATE OR REPLACE TRIGGER RoomServiceLimit
AFTER INSERT OR UPDATE ON RoomServices
DECLARE
badRoomsCount NUMBER;
badRoomsList VARCHAR2(32767); -- adjust the varchar2 size according to your requirements
BEGIN
SELECT COUNT(*), LISTAGG(roomNumber, ', ') WITHIN GROUP (ORDER BY 1)
INTO badRoomsCount, badRoomsList
FROM (SELECT roomNumber FROM RoomServices GROUP BY roomNumber HAVING COUNT(*) > 2);
IF (badRoomsCount > 0) THEN
RAISE_APPLICATION_ERROR(-20001,'Room/s '||badRoomsList||' will have more than 2 services.');
END IF;
END;
/
如果 RoomServices 很小但有太多更改(插入或更新),那么您可以考虑在 RoomNumber 上创建一个索引。
如果我的假设是错误的,请尝试以下操作:
CREATE GLOBAL TEMPORARY TABLE RoomServicesAux as SELECT roomNumber FROM RoomServices WHERE 1=0;
/
CREATE OR REPLACE TRIGGER PreRoomServiceLimit
BEFORE INSERT OR UPDATE ON RoomServices
BEGIN
DELETE FROM RoomServicesAux;
END;
/
CREATE OR REPLACE TRIGGER RowRoomServiceLimit
BEFORE INSERT OR UPDATE OF roomNumber ON RoomServices FOR EACH ROW
BEGIN
INSERT INTO RoomServicesAux VALUES (:NEW.roomNumber);
END;
/
CREATE OR REPLACE TRIGGER RoomServiceLimit
AFTER INSERT OR UPDATE ON RoomServices
DECLARE
badRoomsCount NUMBER;
badRoomsList VARCHAR2(32767); -- adjust the varchar2 size according to your requirements
BEGIN
SELECT COUNT(*), LISTAGG(roomNumber, ', ') WITHIN GROUP (ORDER BY 1)
INTO badRoomsCount, badRoomsList
FROM (
SELECT roomNumber
FROM RoomServices
WHERE roomNumber in (SELECT roomNumber FROM RoomServicesAux)
GROUP BY roomNumber
HAVING COUNT(*) > 2
);
DELETE FROM RoomServicesAux;
IF (badRoomsCount > 0) THEN
RAISE_APPLICATION_ERROR(-20001,'Room/s '||badRoomsList||' will have more than 2 services.');
END IF;
END;
/
或者,如果您有 Oracle 11g 或更高版本,那么您可以使用复合触发器:
CREATE OR REPLACE TYPE RoomsListType IS TABLE OF INTEGER; -- change to the type of RoomServices.rowNumber
/
CREATE OR REPLACE TRIGGER RoomServiceLimit
FOR INSERT OR UPDATE OF roomNumber ON RoomServices
COMPOUND TRIGGER
RoomsList RoomsListType := RoomsListType();
badRoomsCount NUMBER;
badRoomsList VARCHAR2(32767); -- adjust the varchar2 size according to your requirements
AFTER EACH ROW IS
BEGIN
RoomsList.EXTEND;
RoomsList(RoomsList.COUNT) := :NEW.roomNumber;
END AFTER EACH ROW;
AFTER STATEMENT IS
BEGIN
SELECT COUNT(*), LISTAGG(roomNumber, ', ') WITHIN GROUP (ORDER BY 1)
INTO badRoomsCount, badRoomsList
FROM (
SELECT roomNumber
FROM RoomServices
WHERE roomNumber in (SELECT * FROM table(RoomsList))
GROUP BY roomNumber
HAVING COUNT(*) > 2
);
IF (badRoomsCount > 0) THEN
RAISE_APPLICATION_ERROR(-20001,'Room/s '||badRoomsList||' will have more than 2 services.');
END IF;
END AFTER STATEMENT;
END;
/
【讨论】:
有什么区别?您仍在尝试访问同一个表中可能已更新的行 - 这将再次触发突变 问题是尝试使用行触发器访问表。 statemet 触发器可以访问其表。 RDBMS 试图确保 4 个基本原则,称为 ACID 原则(原子性、一致性、隔离性和持久性)。在语句级别原子性意味着您将插入/删除/更新视为单个更改。如果您执行更改许多行原子性的单个语句,请确保您可以看到第一次更改之前和最后一次更改之后的表,但没有人必须看到第一次更改和最后一次更改之间的表。变异表问题通常(但并非唯一)与访问其基表的行触发器和多行插入/更新/删除语句相关。【参考方案3】:如果没有一些变通方法,您似乎无法解决此问题。如果您找不到更好的东西,请查看:
我猜你有桌子房间,否则创建一个:
alter table Room add (
servicesCount integer default 0 not null check (servicesCount <= 3)
);
然后用当前值更新这个数字(不确定语句是否有效,这里不是重点)
update Room r
set servicesCount = (select count(*)
from RoomServices s
where r.roomNumber = s.roomNumber);
然后在你的触发器中
create trigger RoomServiceLimit
before insert or update on RoomServices
for each row
begin
update Room
set servicesCount = servicesCount + 1
where roomNumber = :new.roomNumber;
end;
看起来很丑,但是,正如我所说,我不确定你能找到更好的触发器。
编辑 完整的工作示例
drop table Room;
drop table RoomServices;
create table Room (
roomNumber integer primary key,
servicesCount integer default 0 not null check (servicesCount <= 3)
);
create table RoomServices (
roomNumber integer,
service varchar2(100),
comments varchar2(4000)
);
create trigger RoomServiceLimit
before insert or update or delete on RoomServices
for each row
begin
if inserting then
update Room
set servicesCount = servicesCount + 1
where roomNumber = :new.roomNumber;
elsif updating and :old.roomNumber != :new.roomNumber then
update Room
set servicesCount = servicesCount + 1
where roomNumber = :new.roomNumber;
update Room
set servicesCount = servicesCount - 1
where roomNumber = :old.roomNumber;
elsif deleting then
update Room
set servicesCount = servicesCount - 1
where roomNumber = :old.roomNumber;
end if;
end;
/
insert into Room(roomNumber) values (1);
insert into Room(roomNumber) values (2);
insert into RoomServices(roomNumber,service,comments) values (1,'cleaning','first');
insert into RoomServices(roomNumber,service,comments) values (1,'drying','second');
insert into RoomServices(roomNumber,service,comments) values (1,'watering','third');
insert into RoomServices(roomNumber,service,comments) values (1,'something','third'); -- error
select * from room;
insert into RoomServices(roomNumber,service,comments) values (2,'something','2: first');
update RoomServices
set comments = null
where roomNumber = 2;
select * from room;
update RoomServices -- error
set roomNumber = 1
where roomNumber = 2;
select * from room;
delete from RoomServices where roomNumber = 1;
select * from room;
【讨论】:
对不起,这完全坏了。您在每次更新时递增计数器,甚至无需检查更新中更改了哪些字段。因此,当我在 roomservices 中有一个评论列并两次更改该评论时,此触发器将引发错误。这也无法处理任何删除(必须减少服务计数)。 是的,但我并没有假装完整的解决方案。你在这里说的所有东西都是无用的,不包含任何无法解决的问题。一个主题的主要问题是解决变异表问题。我展示了这个想法。 很公平。但即使您发布了完整的触发器,它也不会可靠地工作,因为您无法使用触发器解决变异表问题。这在 Oracle 中根本不可能。请参阅asktom.oracle.com/pls/asktom/… 和有关多用户并发的相关线程。 真的吗?我无法解决它并且它不起作用?查看更新版本并运行脚本。不可能?没有什么是不可能的! 当然,多用户并发是一个问题,但这是整个问题的问题,因为作者无论如何都试图用触发器来解决它。以上是关于SQL 表正在变异...错误的主要内容,如果未能解决你的问题,请参考以下文章
Oracle - 在没有触发器的情况下更新表时出现“表正在变异,触发器/函数可能看不到它”错误