PostgreSQL Trigger with duplicate values - sql

I'm very new in SQL and PostgreSQl, and I need a desperate answer for the following question:
I have the next two tables with the following information:
MATCH_STATISTICS
MATCH
With these two tables I need make a TRIGGER wich adds the player from MATCH_STATISTICS to MATCH with the MAX value from 'mvp_score'.
In other words, I want to add the most valuable player from MATCH_STATISTICS to MATCH but with one condition:
* If there are two players with the same mvp_score, the most valuable player stored will be that one with the MAX minutes played.
I am capable to do the first TRIGGER with the max value but when I have two duplicate values the code adds the last modified.
Anyone knows how to do that? The TRIGGER should act after INSERT, UPDATE or DELETE
Thanks so much and sorry for my bad english...

Finally I found a way to make a TRIGGER wich solves my problem, the code is...
CREATE OR REPLACE FUNCTION mvp_player_equal()
RETURNS trigger AS $$
DECLARE
max_points INTEGER;
BEGIN
SELECT MAX(mvp_score)
INTO max_points
FROM MATCH_STATISTICS;
IF (TG_OP = 'INSERT') THEN
IF max_points = NEW.mvp_score THEN
UPDATE match SET mvp_player = (SELECT player FROM MATCH_STATISTICS WHERE minutes_played =
(SELECT MAX(minutes_played) FROM MATCH_STATISTICS WHERE mvp_score = NEW.mvp_score)
AND mvp_score = NEW.mvp_score) WHERE home_team = NEW.home_team AND visitor_team = NEW.visitor_team;
END IF;
RETURN NULL;
ELSIF (TG_OP = 'UPDATE') THEN
IF max_points = NEW.mvp_score THEN
UPDATE match SET mvp_player = (SELECT player FROM MATCH_STATISTICS WHERE minutes_played =
(SELECT MAX(minutes_played) FROM MATCH_STATISTICS WHERE mvp_score = NEW.mvp_score)
AND mvp_score = NEW.mvp_score) WHERE home_team = NEW.home_team AND visitor_team = NEW.visitor_team;
END IF;
RETURN NULL;
END IF;
END;
$$LANGUAGE plpgsql;
CREATE TRIGGER mvp_player_equal AFTER INSERT OR DELETE OR UPDATE OF
mvp_score ON MATCH_STATISTICS
FOR EACH ROW
EXECUTE PROCEDURE mvp_player_equal();
But as in the previous comments is talked, that's not the better way to fix this issue. There are better ways like Gordon Linoff and a_horse_with_no_name said.

Related

I must not add row with 2 same values in columns

Can I enter a rule when creating a table so that I, as an author, can't add a review to a product I've already reviewed?
I've been thinking about triggers, but I don't know exactly how to set it. In the workbench I can check it via this code:
declare
pocet number := 0;
begin
SELECT COUNT(a."id_recenze")
INTO pocet
FROM "recenze" a
INNER JOIN (SELECT "id_komponenty", "id_autora"
FROM "recenze"
GROUP BY "id_komponenty", "id_autora"
HAVING COUNT(*) > 1) b
ON a."id_komponenty" = b."id_komponenty" AND a."id_autora" = b."id_autora";
if pocet > 2 then
DBMS_OUTPUT.put_line('Nesmite vytvaret recenzi na komponentu, u ktere jste uz recenzoval');
else
DBMS_OUTPUT.put_line('Vysledek je v poradku');
end if;
end;
But I don't want to be able to create these records.
Can someone help me, how i can do it?
I use APEX by Oracle.
EDIT: (24.4. 10:35)
In a nutshell, i don't want records, where id_autora & id_komponenty are more times. For example i don't want this:
id_recenze(PK) id_autora id_komponenty
1 2 3
2 2 3
After your explanation I see you could still use a unique index but you want to create it on id_komponenty and id_autora. That would throw an error if you tried to add a duplicate.
But I see from your code that you are trying to update with the most recent values if it's duplicated. In that case I would abandon the idea of the index and the trigger, and I would replace the INSERT statement (not shown) with Oracle's MERGE statement. This allows a simultaneous logic for insert and update, plus you get to define the criteria when you do either. It would look something like:
MERGE INTO recenze r
USING (Select <newid_komponenty> AS newk
,<newid_autora AS newa> from Dual) n
ON (r.id_komponenty=n.newk And r.id_autora=n.newa)
WHEN MATCHED THEN UPDATE SET {your update logic here}
WHEN NOT MATCHED THEN INSERT {your insert logic here}
Personally, I try to stay away from triggers when there are other solutions available. By altering your Insert statement to this Merge you get the same effect with one less DB object to keep track of and maintain.
I get it.
CREATE TRIGGER "nesmiPridat" BEFORE INSERT ON "recenze"
FOR EACH ROW BEGIN
DECLARE pocet INT(2);
DECLARE smazat INT(2);
SET pocet := (SELECT COUNT("id_recenze") FROM "recenze" WHERE (NEW."id_autora" = "id_autora") AND (NEW."id_komponenty" = "id_komponenty"));
SET smazat :=(SELECT "id_recenze" FROM "recenze" WHERE (NEW."id_autora" = "id_autora") AND (NEW."id_komponenty" = "id_komponenty"));
IF (pocet > 0) THEN
DELETE FROM "recenze" WHERE smazat."id_recenze" = NEW."id_recenze";
END IF;
END;

Trigger function updates all rows instead of one (PostgreSQL)

Using PostgreSQL.
What I have is:
3 tables
1.Customer2c with columns: CustomerID,PersonID,Number_Of_Items`.
2.SalesOrderHeader2c with columns: SalesOrderID,CustomerID.
SalesOrderDetail2c with columns: SalesOrderDetailID,SalesOrderID,OrderQty
I want to create a trigger function, that will trigger whenever someone uses
INSERT INTO 'SalesOrderDetail2c' table
and that is going to get the OrderQty that was inserted and update the correspondent Number_Of_Items field with it.
My trigger is working, but the problem is that whenever I insert a new value to the SalesOrderDetail2c, the function gets the OrderQty value and updates all the rows of Number_Of_Items with it, instead of updating just the correspondent one.
Any help appreciated. What I have so far is this (It may be copletely wrong, dont judge please!):
CREATE OR REPLACE FUNCTION FunctionTrigger2c() RETURNS TRIGGER AS
$BODY$
BEGIN
UPDATE Customer2c
SET Number_Of_Items =
(SELECT OrderQty
FROM SalesOrderDetail2c
INNER JOIN SalesOrderHeader2c ON (SalesOrderDetail2c.SalesOrderID = SalesOrderHeader2c.SalesOrderID)
INNER JOIN Customer2c ON (SalesOrderHeader2c.CustomerID = Customer2c.CustomerID)
ORDER BY SalesOrderDetailID DESC LIMIT 1
)
FROM SalesOrderHeader2c
WHERE SalesOrderHeader2c.CustomerID = Customer2c.CustomerID
;
RETURN NEW;
END;
$BODY$
language plpgsql;
CREATE TRIGGER Trigger2c
AFTER INSERT ON SalesOrderDetail2c
FOR EACH ROW
EXECUTE PROCEDURE FunctionTrigger2c();
I had to use .new as #Nicarus mentioned above! Thanks again by the way.
This is the new code and now it changes only the correspondent value.
CREATE OR REPLACE FUNCTION FunctionTrigger2c() RETURNS TRIGGER AS
$BODY$
BEGIN
UPDATE Customer2c
SET Number_Of_Items =
(SELECT new.OrderQty
FROM SalesOrderDetail2c
order by salesorderdetailid desc limit 1
)
FROM SalesOrderheader2c
WHERE (SalesOrderheader2c.salesorderID = new.salesorderID) and (salesorderheader2c.customerid = customer2c.customerid)
;
RETURN NEW;
END;
$BODY$
language plpgsql;
CREATE TRIGGER Trigger2c
AFTER INSERT ON SalesOrderDetail2c
FOR EACH ROW
EXECUTE PROCEDURE FunctionTrigger2c();
I am pointing out why your original trigger did not work. The reason it updated every row is because of your UPDATE statement. After you inserted something, it triggered the update.
When the update ran it got these things:
What table I am gonna update
What value am I gonna set on what column of the given table
What rows I am gonna implement this update on (FROM + WHERE)
The problem lies in the last part, basically calling FROM in an UPDATE statement is like calling a SELECT statement, it tried to process every row from the FROM clause tables with the given WHERE value.
PostgreSQL documentation about UPDATE statement from_list parameter states that "A
list of table expressions, allowing columns from other tables to
appear in the WHERE condition and the update expressions. This is
similar to the list of tables that can be specified in the FROM Clause
of a SELECT statement."
(https://www.postgresql.org/docs/9.1/sql-update.html)
Basically, your update statement went through every row in SalesOrderHeader2c which conveniently matched with every row of Customer2c (with SalesOrderHeader2c.CustomerID = Customer2c.CustomerID), updating the value of every row in Customer2c with the firstly found Number_Of_Items value from the UPDATE SET statement. This is why you got the same value for every row in Customer2c.
The reason the new trigger works is because the update statement is done with the correctly selected rows.
Here is my, quite general trigger that is used do count/sum values in dependent tables. You should take care of all order changes, ie not only INSERT but also DELETE and UPDATE. You also should read #Nicarus comment about using NEW (and OLD) in triggers.
I have altered it to match your schema, but didn't test it...
CREATE OR REPLACE FUNCTION FunctionTrigger2c()
RETURNS trigger AS
$BODY$
DECLARE
BEGIN
CASE TG_OP
WHEN 'INSERT' THEN
UPDATE
Customer2c
SET
Number_Of_Items = Number_Of_Items + NEW.OrderQty
FROM
SalesOrderHeader2c
WHERE
Customer2c.CustomerID = SalesOrderHeader2c.CustomerID AND
SalesOrderHeader2c.SalesOrderID = NEW.SalesOrderID AND
SalesOrderDetailID = NEW.SalesOrderDetailID;
WHEN 'UPDATE' THEN
UPDATE
Customer2c
SET
Number_Of_Items = Number_Of_Items - OLD.OrderQty
FROM
SalesOrderHeader2c
WHERE
Customer2c.CustomerID = SalesOrderHeader2c.CustomerID AND
SalesOrderHeader2c.SalesOrderID = OLD.SalesOrderID AND
SalesOrderDetailID = OLD.SalesOrderDetailID;
UPDATE
Customer2c
SET
Number_Of_Items = Number_Of_Items + NEW.OrderQty
FROM
SalesOrderHeader2c
WHERE
Customer2c.CustomerID = SalesOrderHeader2c.CustomerID AND
SalesOrderHeader2c.SalesOrderID = NEW.SalesOrderID AND
SalesOrderDetailID = NEW.SalesOrderDetailID;
WHEN 'DELETE' THEN
UPDATE
Customer2c
SET
Number_Of_Items = Number_Of_Items - OLD.OrderQty
FROM
SalesOrderHeader2c
WHERE
Customer2c.CustomerID = SalesOrderHeader2c.CustomerID AND
SalesOrderHeader2c.SalesOrderID = OLD.SalesOrderID AND
SalesOrderDetailID = OLD.SalesOrderDetailID;
END CASE;
RETURN NULL;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
UPDATE also work well if you change the SalesOrderID :)
Notice, that this trigger returns NULL and it should be set as AFTER INSERT OR UPDATE OR DELETE:
CREATE TRIGGER Trigger2c
AFTER INSERT OR UPDATE OR DELETE
ON SalesOrderDetail2c
FOR EACH ROW
EXECUTE PROCEDURE FunctionTrigger2c();

For loop update better alternative

In Oracle 11g, I am using the following in a procedure.. can someone please provide a better solution to achieve the same results.
FOR REC IN
(SELECT E.EMP FROM EMPLOYEE E
JOIN
COMPANY C ON E.EMP=C.EMP
WHERE C.FLAG='Y')
LOOP
UPDATE EMPLOYEE SET FLAG='Y' WHERE EMP=REC.EMP;
END LOOP;
Is there a more efficient/better way to do this? I feel as if this method will run one update statement for each record found (Please correct me if I am wrong).
Here's the is actual code in full:
create or replace
PROCEDURE ACTION_MSC AS
BEGIN
-- ALL MIGRATED CONTACTS, CANDIDATES, COMPANIES, JOBS
-- ALL MIGRATED CANDIDATES, CONTACTS
FOR REC IN (SELECT DISTINCT AC.PEOPLE_HEX
FROM ACTION AC JOIN PEOPLE P ON AC.PEOPLE_HEX=P.PEOPLE_HEX
WHERE P.TO_MIGRATE='Y')
LOOP
UPDATE ACTION SET TO_MIGRATE='Y' WHERE PEOPLE_HEX=REC.PEOPLE_HEX;
END LOOP;
-- ALL MIGRATED COMPANIES
FOR REC IN (SELECT DISTINCT AC.COMPANY_HEX
FROM ACTION AC JOIN COMPANY CM ON AC.COMPANY_HEX=CM.COMPANY_HEX
WHERE CM.TO_MIGRATE='Y')
LOOP
UPDATE ACTION SET TO_MIGRATE='Y' WHERE COMPANY_HEX=REC.COMPANY_HEX;
END LOOP;
-- ALL MIGRATED JOBS
FOR REC IN (SELECT DISTINCT AC.JOB_HEX
FROM ACTION AC JOIN "JOB" J ON AC.JOB_HEX=J.JOB_HEX
WHERE J.TO_MIGRATE='Y')
LOOP
UPDATE ACTION SET TO_MIGRATE='Y' WHERE JOB_HEX=REC.JOB_HEX;
END LOOP;
COMMIT;
END ACTION_MSC;
You're right, it will do one update for each record found. Looks like you could just do:
UPDATE EMPLOYEE SET FLAG = 'Y'
WHERE EMP IN (SELECT EMP FROM COMPANY WHERE FLAG = 'Y')
AND FLAG != 'Y';
A single update will generally be faster and more efficient than multiple individual row updates in a loop; see this answer for another example. Apart from anything else, you're reducing the number of context switches between PL/SQL and SQL, which add up if you have a lot of rows. You could always benchmark this with your own data, of course.
I've added a check of the current flag state so you don't do a pointless update with no chamges.
It's fairly easy to compare the approaches to see that a single update is faster than one in a loop; with some contrived data:
create table people (id number, people_hex varchar2(16), to_migrate varchar2(1));
insert into people (id, people_hex, to_migrate)
select level, to_char(level - 1, 'xx'), 'Y'
from dual
connect by level <= 100;
create table action (id number, people_hex varchar2(16), to_migrate varchar2(1));
insert into action (id, people_hex, to_migrate)
select level, to_char(mod(level, 200), 'xx'), 'N'
from dual
connect by level <= 500000;
All of these will update half the rows in the action table. Updating in a loop:
begin
for rec in (select distinct ac.people_hex
from action ac join people p on ac.people_hex=p.people_hex
where p.to_migrate='Y')
loop
update action set to_migrate='Y' where people_hex=rec.people_hex;
end loop;
end;
/
Elapsed: 00:00:10.87
Single update (after rollback; I've left this in a block to mimic your procedure):
begin
update action set to_migrate = 'Y'
where people_hex in (select people_hex from people where to_migrate = 'Y');
end;
/
Elapsed: 00:00:07.14
Merge (after rollback):
begin
merge into action a
using (select people_hex, to_migrate from people where to_migrate = 'Y') p
on (a.people_hex = p.people_hex)
when matched then update set a.to_migrate = p.to_migrate;
end;
/
Elapsed: 00:00:07.00
There's some variation from repeated runs, particularly that update and merge are usually pretty close but sometimes swap which is faster in my environment; but both are always significantly faster than updating in a loop. You can repeat this in your own environment and with your own data spread and volumes, and you should if performance is that critical; but a single update is going to be faster than the loop. Whether you use update or merge isn't likely to make much difference.

UPDATE table with PL/SQL trigger?

I have this trigger, and I am getting an arror message when I run it; "bad bind variable".
I can't seem to see where the problem lies. Any help would be appreciated.
create or replace TRIGGER trg_placed AFTER UPDATE
OF STATUS_ID ON STATUS
FOR EACH ROW
BEGIN
IF :new.STATUS_ID = 7
THEN
UPDATE STUDENT
SET PLACED_Y_N = 'Y'
WHERE RECORD_NUMBER = :NEW.record_number;
END IF;
END;
try like this:
create or replace TRIGGER trg_placed AFTER UPDATE
OF STATUS_ID ON STATUS
REFERENCING OLD AS o NEW AS n
FOR EACH ROW
BEGIN
IF n.STATUS_ID = 7
THEN
UPDATE STUDENT
SET PLACED_Y_N = 'Y'
WHERE RECORD_NUMBER = n.record_number;
END IF;
END;
If you use lowercase for Oracle objects, you'll have to surround object names with quotes (") and match the case exactly to get it to work.
like this
create or replace TRIGGER trg_placed AFTER UPDATE
OF STATUS_ID ON STATUS
FOR EACH ROW
BEGIN
IF :new.STATUS_ID = 7
THEN
UPDATE STUDENT
SET PLACED_Y_N = 'Y'
WHERE RECORD_NUMBER = :NEW."record_number"; --quotes (") required for column name.
END IF;
END;

How to write a constraint concerning a max number of rows in postgresql?

I think this is a pretty common problem.
I've got a table user(id INT ...) and a table photo(id BIGINT, owner INT). owner is a reference on user(id).
I'd like to add a constraint to the table photo that would prevent more than let's say 10 photos to enter the database for each users.
What's the best way of writing this?
Thx!
Quassnoi is right; a trigger would be the best way to achieve this.
Here's the code:
CREATE OR REPLACE FUNCTION enforce_photo_count() RETURNS trigger AS $$
DECLARE
max_photo_count INTEGER := 10;
photo_count INTEGER := 0;
must_check BOOLEAN := false;
BEGIN
IF TG_OP = 'INSERT' THEN
must_check := true;
END IF;
IF TG_OP = 'UPDATE' THEN
IF (NEW.owner != OLD.owner) THEN
must_check := true;
END IF;
END IF;
IF must_check THEN
-- prevent concurrent inserts from multiple transactions
LOCK TABLE photos IN EXCLUSIVE MODE;
SELECT INTO photo_count COUNT(*)
FROM photos
WHERE owner = NEW.owner;
IF photo_count >= max_photo_count THEN
RAISE EXCEPTION 'Cannot insert more than % photos for each user.', max_photo_count;
END IF;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER enforce_photo_count
BEFORE INSERT OR UPDATE ON photos
FOR EACH ROW EXECUTE PROCEDURE enforce_photo_count();
I included table locking in order to avoid situations where two concurrent tansactions would count photos for a user, see that the current count is 1 below the limit, and then both insert, which would cause you to go 1 over the limit. If that's not a concern for you it would be best to remove the locking as it can become a bottleneck with many inserts/updates.
You cannot write such a constraint in a table declaration.
There are some workarounds:
Create a trigger that would check the number of photos for each user
Create a photo_order column that would keep the order of photos, make (user_id, photo_order) UNIQUE, and add CHECK(photo_order BETWEEN 1 AND 10)
A better alternative would be to check the number of rows when you do the insert:
insert into photos(id,owner)
select 1,2 from dual
where (select count(*) from photos where id=1) < 10
One another approach would be to add column "photo_count" to users table, update it with triggers to make it reflect reality, and add check on it to enforce maximum number of photos.
Side benefit from this is that at any given moment we know (without counting) how many photos user has.
On other hand - the approach Quassnoi suggested is also pretty cool, as it gives you ability to reorder the photos in case user would want it.
I answered similar question here:
Cap on number of rows matching a condition in Postgres
We can save the number of user photos in the user table or a table like user_statistics and use triggers to perform atomic increment and decrement that locks one row (user row) and is safe against concurrent requests:
CREATE TABLE public.user_statistics
(
user_id integer NOT NULL,
photo_count smallint NOT NULL DEFAULT 0,
CONSTRAINT user_statistics_pkey PRIMARY KEY (user_id),
CONSTRAINT user_statistics_user_id_fkey FOREIGN KEY (user_id)
REFERENCES public.user (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE CASCADE
)
CREATE FUNCTION public.increment_user_photo_count()
RETURNS trigger
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
updated integer;
BEGIN
UPDATE
user_statistics
SET
photo_count = photo_count + 1
WHERE
user_statistics.user_id = NEW.user_id AND user_statistics.photo_count < 10;
GET DIAGNOSTICS updated = ROW_COUNT;
IF updated = 0 THEN
RAISE EXCEPTION 'a user can only have 10 photos';
END IF;
RETURN NEW;
END;
$BODY$;
CREATE TRIGGER photo_increment_user_photo_count
BEFORE INSERT
ON public.photo
FOR EACH ROW
EXECUTE PROCEDURE public.increment_user_photo_count();
CREATE FUNCTION public.decrement_user_photo_count()
RETURNS trigger
LANGUAGE 'plpgsql'
AS $BODY$
BEGIN
UPDATE
user_statistics
SET
photo_count = photo_count - 1
WHERE
user_statistics.user_id = OLD.user_id;
RETURN NULL;
-- result is ignored since this is an AFTER trigger
END;
$BODY$;
CREATE TRIGGER photo_decrement_user_photo_count
AFTER DELETE
ON public.photo
FOR EACH ROW
EXECUTE PROCEDURE public.decrement_user_photo_count();
Instead of triggers we can update the photo_count like above in a transaction at application side and throw exception (rollback) for the increment if no rows affected by the update.