SQL - Count number of rows in a table before updating it - sql

Im doing a college project on Oracle SQL and I keep getting the error "table (...) is mutating, trigger/function may not see it" when I try to update a table. My participateTeams table has 3 columns (eventName varchar(), teamId number, teamPlace number).
The Objective here is that when I update the teamPlace of a given team in an event, the place MUST be lower or equal than the number of participant teams in that same event.
My trigger is as follows:
Before update on participateTeams
for each row
Declare participants number;
Begin
select count(*) into participants from participateteams where :New.eventName = participateteams.eventName;
if(:NEW.teamplace>participants) then
RAISE_APPLICATION_ERROR(-20250,'O lugar da equipa é inválido');
end if;
End;
/
From what I've researched it's because I'm trying to read the same table that called the trigger.
I have also have tried exporting the select part of the code into a function, but the issue persists.
The participants in the event are the teams theirselves and not the individuals that form those same teams. Example: If team A, team B and team C participate in an event E, the count for that same event E should be 3.
Any tips? Thank you.

The mutating table error is just a pain. And what you are trying to do is rather tricky. Here is one approach:
Add a column to teams with the count of participants.
Maintain this column with insert/update, and delete triggers on participateteams.
Write a user-defined function to fetch the count for a given team.
Add a check constraint in participateteams using the user-defined function.

The logically correct trigger for what you want to achieve is this :-
CREATE OR REPLACE TRIGGER TRIG_CHK
AFTER INSERT OR UPDATE OF teamPlace ON participateteams
FOR EACH ROW
DECLARE
participants NUMBER;
BEGIN
select count(*) into participants from participateteams where :New.eventName =
participateteams.eventName;
if(:NEW.teamplace>participants) then
RAISE_APPLICATION_ERROR(-20250,'O lugar da equipa é inválido');
end if;
End;
update participateteams set teamPlace = 2 where eventName = 'B';
But when it gives Mutating table error when trying to update table participateteams,to solve this tricky situation , what I did is :-
step1 :Declare the table columns needed in a package header
CREATE OR REPLACE PACKAGE PKG_TEAMS AS
v_eventName participateteams.eventName%type;
v_teamPlace participateteams.teamPlace%type;
end;
step2 : Initialize the variables in a row level trigger
CREATE OR REPLACE TRIGGER TRG_row
AFTER INSERT OR UPDATE OF teamPlace
ON participateteams
FOR EACH ROW
BEGIN
PKG_TEAMS.v_eventName := :NEW.eventName;
PKG_TEAMS.v_teamPlace := :NEW.teamPlace;
END;
step3 :Now ,rather than using :NEW pseudo columns, using globally initialized variables
CREATE OR REPLACE TRIGGER TRG_stmt
AFTER INSERT OR UPDATE OF teamPlace ON participateteams
DECLARE
v_participants NUMBER;
BEGIN
SELECT COUNT(*) INTO v_participants FROM participateteams
WHERE eventName = PKG_TEAMS.v_eventName;
if(PKG_TEAMS.v_teamPlace>v_participants) then
RAISE_APPLICATION_ERROR(-20250,'CANNOT UPDATE,AGAINST RULES');
end if;
End;

Related

Updating the record of same table when new record is inserted or updated in oracle

I am new to learning Oracle. I have a task in which I need to update value of any previous record if new record contains its reference.
Table structure is as below :
Review_Table
(review_id number pk,
review_name varchar2,
previous_review number null,
followup_review number null
)
Here previous_review and followup_review columns are objects of same table i.e Review_table.
Now consider we have two records in Review_table A and B, A does not have any previous or followup review. When user creates/updates the record B and he selects record A as previous record, then we want to automatically update (via trigger) the value of A record's followup review with B's Review ID.
I have tried writing following trigger
create or replace trigger "REVIEW_T1"
AFTER insert or update on "REVIEW_TABLE"
for each row
begin
update REVIEW_TABLE
set review_follow_up_review = :new.REVIEW_ID
where REVIEW_ID = :new.REVIEW_PREVIOUS_REVIEW;
end;
But I am getting error as : REVIEW_TABLE is mutating, trigger/function may not see it ORA-06512
I have tried searching everything but was unable to find any solution for it
TL;DR: No trigger, no mutating. Do not use trigger to change another row in the same table.
I absolutely agree with #StevenFeuerstein's comment:
I also suggest not using a trigger at all. Instead, create a package that contains two procedures, one to insert into table, one to update. And within these procedures, implement the above logic. Then make sure that the only way developers and apps can modify the table is through this package (don't grant privs on the table, only execute on the package).
Take a look at the following example.
Prepare the schema:
create table reviews (
id number primary key,
name varchar2 (32),
previous number,
followup number
);
create or replace procedure createNextReview (name varchar2, lastId number := null) is
lastReview reviews%rowtype;
nextReview reviews%rowtype;
function getLastReview (lastId number) return reviews%rowtype is
begin
for ret in (
select * from reviews where id = lastId
for update
) loop return ret; end loop;
raise_application_error (-20000, 'last review does not exist');
end;
procedure insertReview (nextReview reviews%rowtype) is
begin
insert into reviews values nextReview;
exception when others then
raise_application_error (-20000, 'cannot insert next review');
end;
procedure setFollowUp (nextId number, lastId number) is
begin
update reviews set
followup = nextId
where id = lastId
;
exception when others then
raise_application_error (-20000, 'cannot update last review');
end;
begin
if lastId is not null then
lastReview := getLastReview (lastId);
end if;
nextReview.id := coalesce (lastReview.id, 0)+1;
nextReview.name := name;
nextReview.previous := lastId;
insertReview (nextReview);
if lastReview.Id is not null then
setFollowUp (nextReview.id, lastReview.Id);
end if;
exception when others then
dbms_output.put_line (
'createNextReview: '||sqlerrm||chr(10)||dbms_utility.format_error_backtrace ()
);
end;
/
Execute:
exec createNextReview ('first review')
exec createNextReview ('next review', 1)
See the outcome of work done:
select * from reviews;
ID NAME PREVIOUS FOLLOWUP
---------- ---------------- ---------- ----------
1 first review 2
2 next review 1
First you need to read about triggers, mutating table error and compound triggers: http://docs.oracle.com/cd/E11882_01/appdev.112/e25519/triggers.htm#LNPLS2005
Your trigger is AFTER UPDATE OR INSERT. Means if you run UPDATE OR INSERT statements on this table, the trigger will fire. But you are trying to update the same table again inside your trigger, which is compl. wrong.
I think you can fix this by rewriting this as a before trigger, rather than an after trigger.

ORACLE SQL trigger and SUM()

I have a table COMMANDE and a table REGROUPE. I have that function:
CREATE OR REPLACE PROCEDURE multiplicateur(a NUMBER, taxes NUMBER, c OUT NUMBER)
IS
BEGIN
c := a * taxes ;
END multiplicateur;
/
I'm trying to make a trigger using that founction to update a total price with taxes from a command that can contain more than one item. I tried this but it doesn't want to work:
create or replace TRIGGER MAJ_PRIX_COMMANDE
AFTER INSERT OR UPDATE OR DELETE ON REGROUPE
FOR EACH ROW
DECLARE
resultat NUMBER;
BEGIN
UPDATE COMMANDE
SET COMMANDE.prixTotal = multiplicateur((CAST((SELECT SUM(prixRegroupe)FROM REGROUPE WHERE REGROUPE.numCommande = :NEW.numCommande)AS NUMBER)),1.15,resultat)
WHERE COMMANDE.numCommande = :NEW.numCommande;
END;
Can someone help me?
How about this? I sent this from my iPhone; this code hasn't been tested.
create or replace TRIGGER MAJ_PRIX_COMMANDE
AFTER INSERT OR UPDATE OR DELETE ON REGROUPE
FOR EACH ROW
DECLARE
resultat NUMBER;
l_groupe NUMBER; --will store sum based on numCommande
BEGIN
--retrieve sum of prixRegroupe
SELECT SUM(r.prixRegroupe)
INTO l_groupe
FROM regroupe r
WHERE r.numCommande = :NEW.numCommande;
--call procedure and pass the sum of priRegroupe
multiplicateur(l_groupe, 1.15, resultat);
--use precedures out argument to update commande
UPDATE COMMANDE c
SET c.prixTotal = resultat
WHERE c.numCommande = :NEW.numCommande;
END;
This code won't work. It will result in a mutating trigger error. Within a trigger you cannot query or modify the same table that the trigger is based on until after the trigger has completed execution.
ORA-04091: table name is mutating, trigger/function may not see it
Your problem right now is that you're querying the regroupe table while the trigger is trying to insert/update the same table. This cannot be accomplished.
You'll need to find a way to get the sum of the prixRegroup column some other way. It's late if I think of anything I'll respond tomorrow.

Limit data input based on count of instances in table

Oracle 12c.
I currently have a table to hold patient visits containing a physician id, patient id, and data/time of visit.
I would like to create a constraint that, upon data entry, checks whether a specific physician has 5 appointments in that given day. If the physician does have 5 appointments, no additional appointment can be added.
Is there any way to do this other than using a stored procedure for entry?
If I were to use a stored procedure (as opposed to a trigger due issues declaring a variable) I receive the following error: Error(4,8): PLS-00103: Encountered the symbol "UPDATE" when expecting one of the following: := . ( # % ; not null range default character
I am unsure if this is because I can't use a BEFORE UPDATE on a procedure. Any thoughts?
CREATE OR REPLACE PROCEDURE doc_apt_limit_5
IS
v_visit_count
BEFORE UPDATE OR INSERT ON aa_patient_visit
FOR EACH ROW
BEGIN
SELECT (COUNT(*)) INTO v_visit_count
FROM aa_patient_visit
WHERE physid = :NEW.physid
GROUP BY physid, visittime;
IF v_visit_count > 4 THEN
RAISE_APPLICATION_ERROR(-20001, 'physician is fully booked on this date');
END IF;
END;
Go with trigger. Probably the best solution in this scenario.
CREATE OR REPLACE TRIGGER doc_apt_limit_5 BEFORE
UPDATE OR
INSERT ON aa_patient_visit FOR EACH ROW
DECLARE v_visit_count PLS_INTEGER;
BEGIN
SELECT COUNT(*)
INTO v_visit_count
FROM aa_patient_visit
WHERE physid = :NEW.physid
GROUP BY physid,
visittime;
IF v_visit_count > 4 THEN
RAISE_APPLICATION_ERROR(-20001, 'physician is fully booked on this date');
END IF;
END;

Oracle SQL Creating Trigger to Increment a Sequence Number For Each Row

I'm trying to create a trigger but I have learned I can not design it as in my first attempt, which I'm showing below. This will cause a 'mutating table' error due to selecting from the table as it is being modified. It actually didn't cause this error when inserting only one record at a time, but when I insert multiple records at once it does.
The purpose of the trigger is to count the number of records in the table where the customer is equal to the customer about to be inserted, and to set the new order_num value as count+1. I also have a public key value set by the trigger which draws from a sequence. This part works ok once I remove the order_num part of the trigger and the associated SELECT. How can I achieve what I am trying to do here? Thanks in advance.
CREATE OR REPLACE TRIGGER t_trg
BEFORE INSERT ON t
FOR EACH ROW
DECLARE
rec_count NUMBER(2,0);
BEGIN
SELECT COUNT(*) INTO rec_count
FROM t
WHERE customer_id = :NEW.customer_id;
:NEW.order_num:= rec_count+1;
:NEW.order_pk_id:= table_seq.NEXTVAL;
END;
Two triggers and temp table approach can provide solution to you seek, preventing mutating table error. However performance will most likely suffer.
create global temporary table cust_temp(customer_id number, cust_cnt number);
create or replace trigger t_trig1
before insert on t
declare
begin
insert into cust_temp select customer_id, count(*) from t group by customer_id;
end;
/
CREATE OR REPLACE TRIGGER t_trg2
BEFORE INSERT ON t
FOR EACH ROW
DECLARE
rec_count number;
BEGIN
BEGIN
SELECT cust_cnt INTO rec_count
FROM cust_temp
WHERE customer_id = :NEW.customer_id;
EXCEPTION when no_data_found then rec_count := 0;
END;
:NEW.order_num:= rec_count+1;
:NEW.order_pk_id:= table_seq.NEXTVAL;
update cust_temp set cust_cnt = rec_count + 1
where customer_id = :NEW.customer_id;
END;
/

Oracle SQL, avoiding errror "table is mutating" (trigger)

i need trigger which will check if updated worker can be moved to other team
CREATE TABLE WORKERS
(
ID_WORKER NUMBER(4,0), --FK
ID_TEAM NUMBER(2,0) --FK
);
My trigger looks like:
CREATE OR REPLACE TRIGGER TEAM_LIMIT
BEFORE INSERT OR UPDATE OF ID_TEAM ON WORKERS
FOR EACH ROW
DECLARE
V_num NUMBER;
BEGIN
SELECT Count(*) INTO V_num FROM Worker WHERE ID_TEAM=:new.ID_TEAM;
IF V_num >= 5 THEN
RAISE_APPLICATION_ERROR(-20025,' Error nr ... bleble');
END IF;
END;
this generate error : "table %s.%s is mutating, trigger/function may not see it" when row is updated. How to write this statements properly to not generate this kind of error?
You can use a compound trigger, it looks like this (not tested):
CREATE OR REPLACE TRIGGER TEAM_LIMIT
FOR INSERT OR UPDATE OF ID_TEAM ON WORKERS
COMPOUND TRIGGER
V_num NUMBER;
TYPE Row_TableType IS TABLE OF WORKERS.ID_TEAM%TYPE;
AffectedTeams Row_TableType;
BEFORE STATEMENT IS
BEGIN
AffectedTeams := Row_TableType(); -- init the table variable
END BEFORE STATEMENT;
-------------------
BEFORE EACH ROW IS
BEGIN
AffectedTeams.EXTEND;
AffectedTeams(AffectedTeams.LAST) := :NEW.ID_TEAM;
END BEFORE EACH ROW;
--------------------
AFTER STATEMENT IS
BEGIN
FOR i IN AffectedTeams.FIRST..AffectedTeams.LAST LOOP
SELECT Count(*) INTO V_num FROM Worker WHERE ID_TEAM=AffectedTeams(i);
IF V_num >= 5 THEN
RAISE_APPLICATION_ERROR(-20025,' Error nr ... bleble');
END IF;
END LOOP;
END AFTER STATEMENT;
END TEAM_LIMIT;
/
You can use a statement trigger. Statement triggers are only fired once for each statement executed rather than once for each row affected. Statement triggers are useful because they don't have the restriction that they can't query the table on which the trigger is declared. They don't have access to the :OLD and :NEW row values but with a little thought you can still accomplish what you're trying to do:
CREATE OR REPLACE TRIGGER TEAM_LIMIT
BEFORE INSERT OR UPDATE OF ID_TEAM ON WORKERS
-- Note: no FOR EACH ROW - therefore, this is a statement trigger
DECLARE
nMax_team_count NUMBER;
BEGIN
SELECT MAX(TEAM_COUNT)
INTO nMax_team_count
FROM (SELECT ID_TEAM, COUNT(*) AS TEAM_COUNT
FROM WORKERS
GROUP BY ID_TEAM));
IF nMax_team_count >= 5 THEN
RAISE_APPLICATION_ERROR(-20025,' Error nr ... bleble');
END IF;
END TEAM_LIMIT;
Instead of looking at the team of the particular work which has been updated we find the counts of workers on each team, then extract the largest count, and if it's more than four we raise the appropriate exception.
Share and enjoy.