Triggers are calculating the wrong sum, giving unexpected results - mysql

I have a weird issue with my Trigger. There are 2 tables: Table A and Table B.
Whenever a row is inserted to Table A sum of a column in this table is inserted into Table B
It was working fine at first, but recently I noticed when >1 rows are inserted at the exact time for a user, the trigger returns sum in a weird way.
CREATE TRIGGER `update_something` AFTER INSERT ON `Table_A`
FOR EACH ROW BEGIN
DECLARE sum BIGINT(20);
SELECT IFNULL(SUM(number), 0) INTO sum FROM Table_A WHERE `user` = NEW.user;
UPDATE Table_B SET sum_number = sum WHERE id = NEW.id;
END
Example:
Table A
User X has a sum of 15 currently, then (with almost no delay in between):
Number 5 is inserted for him
Number 7 is inserted for him
Table B
On this table where we hold the sum, sum for this user was 15
Trigger updates this table in this way:
20
22 <--- Wrong, this should be 27
As you can see there isn't any number 2 inserted, it adds 7-5 = 2 for some reason.
How is that possible and why does it subtract 5 from 7 and add 2 to the sum instead of normally adding 7?
Edit 1:
Warning: This won't work, check the accepted answer instead
One of answers suggested select for update method.
Will this SELECT ... FOR UPDATE affect the performance negatively in a huge way?
CREATE TRIGGER `update_something` AFTER INSERT ON `Table_A`
FOR EACH ROW BEGIN
DECLARE sum BIGINT(20);
SELECT IFNULL(SUM(number), 0) INTO sum FROM Table_A WHERE `user` = NEW.user FOR UPDATE;
UPDATE Table_B SET sum_number = sum WHERE id = NEW.id;
END
Basically we only add FOR UPDATE to the end of SELECT line like this and it will perform Row Lock in InnoDB to fix the issue?
SELECT IFNULL(SUM(number), 0) INTO sum FROM Table_A WHERE user = NEW.user FOR UPDATE;
Edit 2 (Temporary Fix):
In case some one needs a very quick temporary fix for this before doing the actual & logical suggested fix: What I did was to put a random usleep(rand(1,500000)) before INSERT query in PHP to reduce the chance of simultaneous inserts.

The reason for this behaviour is that the inserted data is only committed to the database when the trigger finishes executing. So when both insert operations (5 and 7) execute the trigger in parallel, they read the data as it is in their transaction, i.e. the committed data with the changes made in their own transaction, but not the changes made in any other ongoing transaction.
The committed data in table A sums up to 20 for both transactions, and to that is added the record that is inserted in their own transaction. For the one this is 5, for the other it is 7, but as these records were not yet committed, the other transaction does not see this value.
That is why the sum is 20+5 for the one, and 20+7 for the other. The transactions then both update table B, one after the other (because table B will be locked during an update and until the end of the transaction), and the one that is latest "wins".
To solve this, don't read the sum from table A, but keep a running sum in Table B:
CREATE TRIGGER `update_something` AFTER INSERT ON `Table_A`
FOR EACH ROW BEGIN
UPDATE Table_B SET sum_number = sum_number + NEW.number WHERE id = NEW.id;
END;
/
I suppose you already have triggers for delete and update on Table_B, as otherwise you'd have another source of inconsistencies.
So these need to be (re)written too:
CREATE TRIGGER `delete_something` AFTER DELETE ON `Table_A`
FOR EACH ROW BEGIN
UPDATE Table_B SET sum_number = sum_number - OLD.number WHERE id = OLD.id;
END;
/
CREATE TRIGGER `update_something` AFTER UPDATE ON `Table_A`
FOR EACH ROW BEGIN
UPDATE Table_B SET sum_number = sum_number - OLD.number WHERE id = OLD.id;
UPDATE Table_B SET sum_number = sum_number + NEW.number WHERE id = NEW.id;
END;
/
This way you prevent to lock potentially many rows in your triggers.
Then, after you have done the above, you can fix the issues from the past, and do a one-shot update:
update Table_B
join (select id, user, ifnull(sum(number),0) sum_number
from Table_A
group by id, user) A
on Table_B.id = A.id
and Table_B.sum_number <> A.sum_number
set Table_B.sum_number = A.sum_number;

You get this because of the race condition in the trigger. Both triggeres are fired at the same time, thus SELECT returns the same value for both of them - 15. Then first trigger updates tha value adding 5 and resulting in 20, and then the second update is run with 15 + 7 = 22.
What you should do is use SELECT ... FOR UPDATE instead. This way if first trigger issues the select, then the second one will have to wait until first one finishes.
EDIT:
Your question made me think, and maybe using FOR UPDATE is not the best solution. According to documentation:
For index records the search encounters, SELECT ... FOR UPDATE locks the rows and any associated index entries, the same as if you issued an UPDATE statement for those rows.
And because you are selecting the sum of entries from Table A it will lock those entries, but will still allow inserting new ones, so the problem would not be solved.
It would be better to operate only on data from Table B inside the trigger, as suggested by trincot.

Related

How to execute the Data in chunks or Batch wise in loop using MYSQL

I have table which have 20 million records . I have recently added another column to that table.
I need to update the data into that column.
I'm using MYSQL community edition, when I execute the direct update like this :
Update Employee SET Emp_mail = 'xyz_123#gmail.com'
System Getting hanged and Need to close the execution abruptly.
But when I update the statement with filter condition it is executing fine.
Update Employee SET Emp_mail = 'xyz_123#gmail.com' where ID <= 10000;
Update Employee SET Emp_mail = 'xyz_123#gmail.com' where ID >= 10000 AND ID <= 10000 ;
--------
--------
no of Times
Now I'm looking for looping script where I can execute in chunk wise.
For example in SQL it is like this but I'm not sure of MYSQL:
BEGIN
I int = 0 ;
cnt = 0 ;
while 1 > cnt
SET i = i + 1;
Update Employee SET Emp_mail = 'xyz_123#gmail.com' where ID >= cnt AND ID <= I
END
Note : this is a random script syntax wise there may be some errors . Please ignore it.
I'm looking for Looping in MYSQL
In a row based database system as MySQL, if you need to update each and every row, you should really explore a different approach:
ALTER TABLE original_table RENAME TO original_table_dropme;
CREATE TABLE original_table LIKE original_table_dropme;
ALTER TABLE original_table ADD emp_mail VARCHAR(128);
INSERT INTO original_table SELECT *,'xyz_123#gmail.com'
FROM original_table_dropme;
Then, maybe keep the original table for a while - especially to transfer any constraints, primary keys and grants from the old table to the new - and finally drop the %_dropme table.
Each update, in a row based database, of a previously empty column to a value, will make each row longer than it originally was, and require a reorganisation, internally. If you do that with millions of rows, the effort needed will increase exponentially.

Problem creating a new trigger with a subquery

I'm trying to make a trigger that will update a column whenever there is an insert on another table. In my case, whenever I insert a new like in the table student_likes_post, I want the table forum_post to update its likes column accordingly. This is my query:
use mydb;
DELIMITER //
CREATE TRIGGER update_likes
after INSERT
ON student_likes_post FOR EACH ROW
BEGIN
UPDATE forum_post
SET forum_post.likes = (
select count(*)
FROM student_likes_post
WHERE student_likes_post.post_id = forum_post.id
);
END;
DELIMITER;
However, when I run it, it just keeps running forever, nothing is happening. The subquery is working though individually. I tried other triggers on the same table student_likes_post that have the same issue. Any idea how I can get this to work? Do you think is a problem with the table itself or with the code?
You probably want to update only the row in forum_post for the post that the student liked, not all the forum_posts, right?
Your trigger is currently updating all the rows in forum_post, and running the subquery once for each row in forum_post.
Here's another way to write the trigger, that updates only the single respective forum_post row:
CREATE TRIGGER update_likes
after INSERT
ON student_likes_post FOR EACH ROW
BEGIN
DECLARE like_count INT;
SELECT COUNT(*) INTO like_count
FROM student_likes_post WHERE post_id = NEW.post_id;
UPDATE forum_post
SET likes = like_count
WHERE id = NEW.post_id;
END

Update counter field when a new record inserted on another table

I have two tables and I want to update a counter-record field Table A when a new record inserted or deleted on table B and the code is the same.
Table A Fields
ID-CODE-COUNTER-etc..
Table B Fields
ID-CODE-etc..
When a new record inserted/deleted on Table B I want to count-it on Table A COUNTER FIELD if they have the same CODE field.
I suppose this can be done with trigger but I need some help on this.
We could probably be achieved using MySQL triggers.
We should consider the option of not storing a counter on table_a, and instead just getting a count from table_b.
There's are also some concerns we need to be aware of in implementing triggers. There's a potential performance impact, and for triggers performing DML, there's a potential for introducing lock contention. Triggers aren't free, and they are not a "magic bullet".
We need an AFTER UPDATE trigger (to handle cases where a row is updated to change the value of the code column), as well as AFTER INSERT and AFTER DELETE triggers.
We are going to want a suitable index available on table_b, e.g.
ON table_a (code)
Assuming that table_a counter column is NOT NULL, and that counter is initialized to the value we expect.
As a first cut:
-- after delete
DELIMITER $$
CREATE TRIGGER table_b_ad
AFTER DELETE ON table_b
FOR EACH ROW
BEGIN
-- decrement counter on row of table_a with matching OLD code
UPDATE table_a a
SET a.counter = a.counter - 1
WHERE a.code <=> OLD.code
;
END$$
DELIMITER ;
-- after insert
DELIMITER $$
CREATE TRIGGER table_b_ai
AFTER INSERT ON table_b
FOR EACH ROW
BEGIN
-- increment counter on row of table_a with matching NEW code
UPDATE table_a a
SET a.counter = a.counter + 1
WHERE a.code <=> NEW.code
;
END$$
DELIMITER ;
-- after update
DELIMITER $$
CREATE TRIGGER table_b_au
AFTER UPDATE ON table_b
FOR EACH ROW
BEGIN
IF NOT ( NEW.code <=> OLD.code ) THEN
-- decrement counter on row of table_a that matches OLD code
UPDATE table_a a
SET a.counter = a.counter - 1
WHERE a.code <=> OLD.code
;
-- increment counter on row of table_a that matches NEW code
UPDATE table_a a
SET a.counter = a.counter + 1
WHERE a.code <=> NEW.code
;
END IF;
END$$
DELIMITER ;
Note:
Within a MySQL trigger,
NEW.code refers to the "new" value assigned to column code by an INSERT or UPDATE statement.
OLD.code refers to the "old" value that was in the column code before a UPDATE or DELETE statement.
<=> (spaceship operator) is NULL-safe comparison operator, returns either TRUE or FALSE. Does not return NULL like the regular comparison operator does when comparing a NULL value.
x <=> y is short hand equivalent to writing ( x = y OR ( x IS NULL AND y IS NULL ) )
The purpose for doing the comparison in the UPDATE trigger is that if the value of code didn't change, there's no need to go through the rigmarole of subtracting 1 from the count for that code value, and then immediately adding 1 back to that same count. It's more efficient just to avoid running the unnecessary UPDATE statements.

Why EXISTS() always returns true?

Here is my query (used in a TRIGGER):
update user_details
set views_profile = views_profile + 1
where user_id = new.user_id and not exists (
SELECT 1
FROM views_profile vp
WHERE vp.user_id = new.user_id and vp.viewer_id = new.viewer_id
)
The TRIGGER:
As you can see, my query is an UPDATE statement and the problem is, it never happens. According to some tests, the problem is related to EXISTS. When I remove it, that UPDATE happens.
Anyway, why EXISTS is true all the time? Even when there isn't any row in views_profile table?
You are using a TRIGGER with AFTER INSERT so the new line is available on the UPDATE (and can be found on the EXISTS). You can change the time to BEFORE.

Simple Update trigger + simple row insert

Total novice at triggers... all the docs don't bother with beginner stuff.
I just want to update the row(s) that has/have been updated. My trigger below updates the entire table. The trigger below just tests two columns for changes.
How do I restrict this update trigger to the update only the updated rows and not the entire table?
ALTER TRIGGER [dbo].[geog_update] ON [dbo].[Site]
FOR UPDATE
AS
SET NOCOUNT ON
IF (UPDATE(Latitude) OR UPDATE(Longitude))
BEGIN
UPDATE Site
SET geog = geography::Point([Latitude], [Longitude], 4326)
WHERE Latitude is not null and Longitude is not null
END
Can I use the same trigger for inserted rows by just using FOR UPDATE, INSERT? Probably not since the IF checks for UPDATE() on a column, unless INSERT implies UPDATE.
Ok first, you never under any circumstances design a trigger to update only one row in SQL Server. You design it to update the rows that were inserted, deleted or updated. Triggers operate on batches and you cannot assume that you will never change more than one record in code that hits the database.
You do that by joining to one of two pseudotables that are available only in triggers, inserted or deleted. Inserted contains the new records or the values after an update, delted contains the values for the records deleted or the values before an update occurred.
You could try something like:
ALTER TRIGGER [dbo].[geog_update] ON [dbo].[Site]
FOR UPDATE
AS
SET NOCOUNT ON
UPDATE S
SET geog = geography::Point(i.[Latitude], i.[Longitude], 4326)
FROM Site s
JOIN Inserted I on s.id = i.id
WHERE Latitude is not null and Longitude is not null
Here is the final UPDATE trigger. I made an INSERT trigger that is almost identical.
CREATE TRIGGER [dbo].[geog_update] ON [dbo].[Site]
FOR UPDATE
AS
SET NOCOUNT ON
IF (UPDATE(Latitude) OR UPDATE(Longitude))
BEGIN
UPDATE t1
SET t1.geog = geography::Point(t1.Latitude, t1.Longitude, 4326)
FROM Site AS t1 INNER JOIN Inserted AS t2 ON t1.Site_ID = t2.Site_ID
WHERE (t1.Latitude IS NOT NULL)
AND (t1.Longitude IS NOT NULL)
AND (t1.Record_Archive_Date IS NULL)
END