Why EXISTS() always returns true? - mysql

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.

Related

Triggers are calculating the wrong sum, giving unexpected results

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.

MYSQL AFTER Trigger UPDATE IF

I want to UPDATE my row after it has been updated by the user.
Example if after the user selects 32, and Principal previously entered is > 1000. Then Update the 32 to 40.
CREATE TRIGGER updateusers_id AFTER UPDATE ON Table
FOR EACH ROW
BEGIN
IF(NEW.users_id = 32 AND Table.Principal > 50000) THEN
UPDATE loans SET users_id = 40;
END IF;
END
Think about it for a second and you'll realize the first problem: if what you actually need here is an AFTER update trigger... then what would a BEFORE update trigger do?
AFTER UPDATE means after the update query has already changed the row data. What you are actually looking to is BEFORE UPDATE -- you want to hijack the update in mid-stream, before the table data actually gets changed, and potentially modify the values that will actually be written to the database.
UPDATE loans SET users_id = 40;
There are two problems with this. First, you know what an UPDATE without WHERE does... right? It updates all the rows in the table. Luckily, the server didn't let you do that. But the solution is not to add a WHERE clause -- it's this:
IF(NEW.users_id = 32 AND NEW.Principal > 50000) THEN
SET NEW.users_id = 40;
END IF;
The NEW pseudotable contains the row data that the user has tried to cause the UPDATE (that fired the trigger) to write to the row. You are in a BEFORE UPDATE ... FOR EACH ROW trigger, so setting NEW values overrides anything the user tried to do... which appears to be what you are wanting to accomplish.

MySQL UPDATE and CASE

New to MySQL (coming from Oracle), and I have this UPDATE :
UPDATE the_table
SET the_col = 'the_value',
the_col2 = CASE WHEN the_col = 'the_value' THEN 'x' ELSE 'y' END
WHERE a = 1;
The idea is to set the_col2 to 'x' if the_col is set to 'the_value', otherwise set it to 'y'.
What I am seeing is the_col being updated fine, but the_col2.
I am not getting an error reported so I am assuming the syntax/usage is OK.
It is as if the "SET the_col = 'the_value'" sets the value so that when referenced in the CASE it has the new value. I think this is unlikely, but this seems to be what is happening.
What I want to do is test the column's value pre-update.
I don't think that the_col has the new value when you do the set the_col2 case...
But one way to find out - run the update twice, doing the_col, then the_col2. Compare answers (you'll have to - should - make a temporary table to be safe. Note: to create a temporary test table (since you're new to MySQL and it might be different), a) run DESCRIBE your_table; b) use the values in that to CREATE TABLE temp_table; c) create a subquery to read out all the rows in your_table and insert them into temp_table - e.g. INSERT into temp_table values (select * from your table). Good luck.

Update MySQL Row When Updating Using Another Table

I'm trying to use a trigger to update a column on my database when a row gets updated.
This is the trigger
CREATE
DEFINER=`root`#`localhost`
TRIGGER `mysql_development`.`update_translated_position`
BEFORE UPDATE ON `mysql_development`.`players_to_teams`
FOR EACH ROW
BEGIN
UPDATE players_to_teams
INNER JOIN position_translator
ON NEW.position = position_translator.RawPosition
SET NEW.translated_position = position_translator.NCAAposAbbrev1;
END$$
I need to "calculate" the translated_position from the raw position input (in case someone gives me a non-standard position).
I think this is locking the row, because I am getting a 1096, no table used error.
I need to update the players_to_teams row being updated using the external position_translator table.
Use SET directly rather than UPDATE (and therefore avoid the join altogether):
SET NEW.translated_position := (
SELECT NCAAposAbbrev1 FROM position_translator WHERE RawPosition = NEW.position
);

Mysql trigger issue (i think it's firing)

I have an issue with a trigger on a mysql database. I have a table such as follows:
id int not null auto_increment (PK)
parent_id int not null,
rank int not null
What I'm trying to do is use a trigger to update the rank to the next highest +10 when they have the same parent_id, but this doesn't seem to be working.
DELIMITER $$
DROP TRIGGER IF EXISTS after_insert $$
create trigger after_insert
after insert on mytable
FOR EACH row
BEGIN
IF EXISTS (SELECT rank FROM mytable WHERE parent_id = new.parent_id AND id != new.id ORDER BY rank DESC LIMIT 1) THEN
UPDATE mytable SET rank = 10
WHERE id = new.id;
ELSE
UPDATE mytable SET rank = 20
WHERE id = new.id;
END IF;
END
$$
I've tried setting the new rank to a variable and calling the update statement using that, and again it didn't work. I even created another table to log what values were being selected and that worked perfectly so I can't quite understand what's going on. Is it a case of, although the trigger is "AFTER INSERT" the insert hasn't actually happened so it can't update the row it's just inserted? Another reason I ask this is, I've even tried updating the rank to different values e.g 1 and 2 depending on which statement it goes to, but it always ends up being 0.
I think you're on the right track with this thought:
Is it a case of, although the trigger is "AFTER INSERT" the insert hasn't actually happened so it can't update the row it's just inserted?
From the FAQ:
B.5.9: Can triggers access tables?
A trigger can access both old and new data in its own table. A trigger can also affect other tables, but it is not permitted to modify a table that is already being used (for reading or writing) by the statement that invoked the function or trigger.
The documentation isn't clear that what you're doing won't work. OTOH, the documentation isn't clear that what you're trying to do will work either.
I think you'd be better off using a BEFORE INSERT trigger and setting NEW.rank in there. Then, the new row would have the right rank value when it is actually inserted into the table rather than patching it after. Also, you'd be able to simplify your existence check to just this:
EXISTS(SELECT rank FROM mytable WHERE parent_id = new.parent_id)
as NEW.id wouldn't have a useful value and the new row wouldn't be in the table anyway; the ORDER BY and LIMIT are also unnecessary as you're just checking if something exists so I took them out.
A BEFORE INSERT trigger seems to match your intent better anyway and that will give you correct data as soon as it is inserted into your table.
If you want the rank to be set +10 more than highest "brother's" rank, you could use:
DELIMITER $$
DROP TRIGGER IF EXISTS whatever $$
create trigger whatever
BEFORE INSERT ON mytable
FOR EACH row
BEGIN
SET NEW.rank = 10 + COALESCE(
( SELECT max(rank)
FROM mytable
WHERE parent_id = NEW.parent_id
), 0 ) ;
END
$$