SQL Table is mutating... Error - oracle

I'm attempting to write a trigger that will disallow any room in a hospital to have more than 3 services. The table RoomServices has a room number and a service that it has. So the only way to determine this is to group the rooms by room number and count the services. I have tried the code:
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;
/
I've tried running each method separately with insert and update, and inserting always works and updating will always give me the mutating table error. I don't know how else to go about solving this problem, so any advice would be greatly appreciated.
Thanks!

There is no reliable way to enforce this kind of constraint using triggers. One possible approach is to use a materialized view that automatically refreshes on commit and has a check constraint enforcing your business rule:
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);
Now, whenever you add more than two services for a room and try to commit your transaction, you will get a ORA-12008 exception (error in materialized view refresh path).

I assume that RoomServices:
a) is a small table that is not intensively modified
b) there will never exist a room with more than 3 services
Note: you say "more than 3 services" but your code says "more than 2 services". So I will use "more than 2 services".
Then, what about using a statement trigger?
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;
/
If RoomServices is small but have too many changes (inserts or updates) then you may consider create an index on RoomNumber.
If my assumptions are false try something like:
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;
/
Or if you have Oracle 11g or greater then you can use a compound trigger:
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;
/

Seems you cannot solve this issue without some workarounds. If there is nothing better you can find, check this out:
I guess you have table Room, otherwise create one:
alter table Room add (
servicesCount integer default 0 not null check (servicesCount <= 3)
);
Then update this number with current values (not sure if the statement is valid, it is not the key point here)
update Room r
set servicesCount = (select count(*)
from RoomServices s
where r.roomNumber = s.roomNumber);
then in your trigger
create trigger RoomServiceLimit
before insert or update on RoomServices
for each row
begin
update Room
set servicesCount = servicesCount + 1
where roomNumber = :new.roomNumber;
end;
Looks quite ugly, but, as I've told, I am not sure you can find anything better with trigger.
EDIT
The complete working example
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;

Related

how can i put a query in decode function?

create table ss( no number, filepath varchar2(300) )
I want to have 5 or less duplicate values of 'no' in this table
when select count(no) from ss where no=#{no} <5, insert into ss values({no},{filepath})
so duplicate values of 'no' can't be over 5.
how can i do this?
You could create a similar trigger to implement this logic:
CREATE OR REPLACE TRIGGER set_no_ss_tbl_trg
BEFORE INSERT ON ss_tbl
FOR EACH ROW
BEGIN
DECLARE
l_cnt_no NUMBER;
BEGIN
SELECT COUNT(1)
INTO l_exceeding
FROM g_piece
WHERE refdoss = :new.no;
IF l_cnt_no > 5 THEN
SELECT MIN(no)
INTO :new.no
FROM (SELECT COUNT(1), no
FROM ss_tbl
GROUP BY no
HAVING COUNT(1) + 1 <= 5);
END IF;
END;
END;

trigger is not updating table

I created a trigger that updates a table I created whenever I update a column in a different table. So far my trigger compiles but when I updated the column the trigger doesn't seem to fire or do anything.
Can anyone help me?
CREATE TABLE bb_sales_sum (
idProduct number(2) NOT NULL,
total number(6,2),
quantity number);
CREATE OR REPLACE TRIGGER BB_SALESUM_TRG
AFTER UPDATE OF orderplaced on bb_basket
FOR EACH ROW
WHEN (NEW.orderplaced = 1)
DECLARE
lv_count Number;
BEGIN
if :new.orderplaced = 1 then
for item in
(select idproduct, (quantity * price) AS total, quantity
from bb_basketitem
where idbasket = :old.idbasket)
loop
select count(*)
into lv_count
from bb_sales_sum where idProduct = item.idproduct;
if lv_count = NULL then
INSERT INTO bb_sales_sum
VALUES (item.idproduct, item.total, item.quantity);
else
update bb_sales_sum
set quantity = item.quantity where
idProduct = item.idproduct;
end if;
end loop;
end if;
END;
/
update bb_basket
set orderplaced = 1
where idbasket = 14;
select * from bb_sales_sum;
You may use a similar MERGE statement using the values from bb_basketitem instead of a for loop.
CREATE OR REPLACE TRIGGER BB_SALESUM_TRG
AFTER UPDATE OF orderplaced on bb_basket
FOR EACH ROW
WHEN (NEW.orderplaced = 1)
BEGIN
MERGE INTO bb_sales_sum t USING
( select idproduct, (quantity * price) AS total, quantity
from bb_basketitem item
where idbasket = :old.idbasket
) s
ON (s.idproduct = t.idproduct )
WHEN MATCHED THEN UPDATE
SET quantity = s.quantity
WHEN NOT MATCHED THEN
INSERT (
idproduct,quantity,total)
VALUES
( s.idproduct,s.quantity,s.total );
END;
/
DEMO

How to remove ORA-01562: Failed to extend rollback segment error while updating 100 million rows

A table is having 100 million records and I need to update a column by adding 10% into the salary of each employee. when I execute update statement I am getting this error:
ORA-01562: Failed to extend rollback segment
How can I update this column for the best performance result?
update employee
set salary = salary + (salary*10/100)
OR
declare
i number(10);
limit number(10) := 100000;
begin
for i in 1 .. limit loop
update employee
set salary = salary + (salary*10/100)
where rownum = i;
limit := limit + 100000;
end loop;
end;
Looks like you are using Oracle version 8i or prior, as the rollback segments have been replaced with undo segments from Oracle 9i onwards.
To solve the problem, I would suggest you to check the trace file to see which rollback segment is creating the problem, then create a bigger rollback segment depending upon the update transaction size.
Try this:
DECLARE
CURSOR CUR
IS
SELECT ROWID, A.*
FROM YOUR_SALARY_TABLE A;
TYPE CUR_TYPE IS TABLE OF CUR%ROWTYPE
INDEX BY PLS_INTEGER;
L_CUR CUR_TYPE;
LIM NUMBER := 100000; -- Update chunk size
BEGIN
OPEN CUR;
LOOP
FETCH CUR BULK COLLECT INTO L_CUR LIMIT LIM;
FOR INDX IN 1 .. L_CUR.COUNT
LOOP
UPDATE YOUR_SALARY_TABLE S
SET S.SALARY_COLUMN = S.SALARY_COLUMN * 2 -- Multiplying here
WHERE ROWID = L_CUR (INDX).ROWID;
END LOOP;
COMMIT;
EXIT WHEN L_CUR.COUNT < LIM;
END LOOP;
CLOSE CUR;
END;
You can try this approach: link
Information about parallel: link
We can use FORALL also to achieve what is required. Hope this below snippet helps.
DROP TABLE test_so1
/
CREATE TABLE TEST_SO1
( COL1 NUMBER, COL2 VARCHAR2(100)
)
/
--Insert values
INSERT INTO TEST_SO1
SELECT LEVEL,'AVRAJIT'||LEVEL FROM DUAL CONNECT BY LEVEL < 10000
/
--FORALL UPDATE
DECLARE
type TEST_REC
IS
RECORD
(
COL1 NUMBER,
COL2 VARCHAR2(100),
col3 VARCHAR2(100));
type TEST_TAB
IS
TABLE OF TEST_REC;
LV_TAB TEST_TAB;
CURSOR LV_CUR
IS
SELECT col1,col2,rowid FROM TEST_SO1;
BEGIN
OPEN LV_CUR;
LOOP
FETCH LV_CUR BULK COLLECT INTO LV_TAB LIMIT 1000;
EXIT
WHEN LV_TAB.COUNT=0;
FORALL I IN LV_TAB.FIRST..LV_TAB.LAST
UPDATE TEST_SO1 SET COL2 = 'shubhojit' WHERE ROWID = lv_tab(i).col3;
COMMIT;
END LOOP;
END;
/

Create table if it does not exist, and enter one row after creating

I need to create a table if it does not exist, and when it is created add a single row to it.
I'm new to oracle and PL/SQL so I basically need an equivalent of the following T-SQL:
IF OBJECT_ID('my_table', 'U') IS NULL
BEGIN
CREATE TABLE my_table(id numeric(38,0), date datetime)
INSERT INTO my_table
VALUES (NULL, 0)
END
if you want to check table creation
DECLARE count NUMBER;
BEGIN
count := 0;
SELECT COUNT(1) INTO count from user_tables WHERE table_name= 'MY_TABLE';
IF COL_COUNT = 0 THEN
EXECUTE IMMEDIATE 'create table ....';
END IF;
END;
/
A checking for DML .please note you have to sepcify your pk columns and values.
DECLARE count NUMBER;
BEGIN
count := 0;
SELECT COUNT(1) INTO count from MY_TABLE WHERE id= 0 and name='Something';
IF COL_COUNT = 0 THEN
EXECUTE IMMEDIATE 'insert into MY_TABLE (id,name) values(0,''something'') ';
END IF;
END;
/
also note I recomand to specify columns when you insert into a table
Another approach is to use exception logic. I changed field names and types according to Oracle rules
declare
eAlreadyExists exception;
pragma exception_init(eAlreadyExists, -00955);
begin
execute immediate 'CREATE TABLE my_table(id number, dateof date)';
execute immediate 'INSERT INTO my_table VALUES (NULL, sysdate)';
exception when eAlreadyExists then
null;
end;
but may be it is not a good idea to create tables dynamically
In my opinion, you should not be creating objects on the fly. You should think about your design before implementing it.
Anyway, if you really want to do it this way, then you need to do it programmatically in PL/SQL (ab)using EXECUTE IMMEDIATE.
However, I would prefer the CTAS i.e. create table as select if you want to create a table ta once with a single row. For example,
SQL> CREATE TABLE t AS SELECT 1 id, SYSDATE dt FROM DUAL;
Table created.
SQL> SELECT * FROM t;
ID DT
---------- ---------
1 29-MAY-15
SQL>
The table is created permanently.
If you are looking for a temporary table, which you could use to store session specific data , then look at creating Global temporary table.
From documentation,
Use the CREATE GLOBAL TEMPORARY TABLE statement to create a temporary
table. The ON COMMIT clause indicates if the data in the table is
transaction-specific (the default) or session-specific
You can use NOT EXISTS with select statement:
IF NOT EXISTS(SELECT 1 FROM my_table) THEN
CREATE TABLE my_table(id NUMBER, date date);
COMMIT;
INSERT INTO my_table(id, date) values (NULL, O);
COMMIT;
END IF;
UPDATE
According to the comment, I cannot use Exist directly in PL/SQL. So this is another way to do it:
begin
select case
when exists(select 1
from my_table)
then 1
else 0
end into l_exists
from dual;
if (l_exists = 1)
then
-- anything
else
EXECUTE IMMEDIATE 'CREATE TABLE my_table(id NUMBER, date date)';
EXECUTE IMMEDIATE 'INSERT INTO my_table(id, date) values (NULL, O)';
end if;
end;

How to write a Procedure with multiple operations like select and update

Table
Id
Count
I want to write a procedure to find 'Count' in the table with 'Id' as key.After getting 'count' i have to increment it and update back in the table for that 'Id'.How can I write this with procedure without using cursors.
I want a simple procedure like below, BUT ITS NOT EXECUTING.IT SAYS PROCEDURE SUCCESSFUL WITH COMPILATION ERRORS.Help me out.
create or replace PROCEDURE newpro( inId IN NUMBER, outcount OUT NUMBER) is
select COUNT into outcount from Table1 WHERE ID= inId ;
BEGIN
outcount := outcount +1;
update Table1 set COUNT = outcount WHERE ID = inId ;
END;
UPDATE tableName
SET "Count" = "Count" + 1
WHERE ID = valueHere
SEE SQLFiddle Demo
try this one
create or replace Procedure Newpro
(
Inid in number,
Outcount out number
) is
begin
select count + 1
into Outcount
from Table1
where Id = Inid;
update Table1
set count = Outcount
where Id = Inid;
end;

Resources