MySQL Trigger to update all entries with a many-many relationship - mysql

I am trying to make a MySQL trigger that will update all my users scores if a levels reward is updated. I have got as far as working out the difference between the old and new reward but am stuck on how to update every users score who has completed a level of that. Below is a simplified table structure.
Users
+---------+-------+
| user_id | score |
+---------+-------+
Users_levels
+---------+----------+
| user_id | level_id |
+---------+----------+
Levels
+----------+---------+
| level_id | tier_id |
+----------+---------+
Tier_reward
+---------+----------+
| tier_id | reward |
+---------+----------+
This is how far I have got so far:
CREATE TRIGGER update_level_reward AFTER UPDATE ON tier_reward FOR EACH ROW
BEGIN
DECLARE REWARD INT;
SET REWARD = OLD.reward - NEW.reward;
-- UPDATE users SET score = score - REWARD WHERE user_id = ;
END$$
I have tried something along the lines of:
UPDATE users
INNER JOIN users_levels
ON users.user_id = users_levels.user_id
INNER JOIN levels
ON users_levels.level_id = levels.level_id
SET score = score - REWARD
WHERE levels.tier = OLD.tier;
However this only reduces the score once even if a user has completed more than one level from that tier.
Example
Users
+---------+-------+
| user_id | score |
+---------+-------+
| 1 | 400 |
| 2 | 700 |
+---------+-------+
Users_levels
+---------+----------+
| user_id | level_id |
+---------+----------+
| 1 | 1 |
| 1 | 2 |
| 2 | 1 |
| 2 | 3 |
+---------+----------+
Levels
+----------+---------+
| level_id | tier_id |
+----------+---------+
| 1 | 1 |
| 2 | 1 |
| 3 | 2 |
+----------+---------+
Tier_reward
+---------+----------+
| tier_id | reward |
+---------+----------+
| 1 | 200 |
| 2 | 500 |
+---------+----------+
Now if the reward for tier_id 1 is reduced to 100. User 1 should now have a score of 200 as they have completed two levels of that tier. User 2 should loose only 100 points as they have only completed one level of that tier.
Users
+---------+-------+
| user_id | score |
+---------+-------+
| 1 | 200 |
| 2 | 600 |
+---------+-------+
Tier_reward
+---------+----------+
| tier_id | reward |
+---------+----------+
| 1 | 100 |
| 2 | 500 |
+---------+----------+

Sorry about the delay !!
Here is what you can do, you need to first get the count per tier per user. Then use cursor to set these values and finally update the users table.
delimiter //
create trigger update_level_reward after update on tier_reward
for each row
begin
DECLARE done INT DEFAULT FALSE;
DECLARE tier_count int;
DECLARE user_id_fetch int;
DECLARE reward INT;
DECLARE reward_recal int ;
DECLARE cur CURSOR FOR
select
ul.user_id, count(*) as total
from users_levels ul
join levels l on l.level_id = ul.level_id
where l.tier_id = new.tier_id
group by ul.user_id;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
SET reward = OLD.reward - NEW.reward;
OPEN cur;
update_loop: LOOP
FETCH cur INTO user_id_fetch,tier_count;
IF done THEN
LEAVE update_loop;
END IF;
set reward_recal = tier_count*reward ;
update users
SET score = score - reward_recal
where user_id = user_id_fetch ;
END LOOP;
CLOSE cur;
end ; //
delimiter ;
Here what I did in mysql
create table users (user_id int,score int);
insert into users values (1,400),(2,700);
create table users_levels(user_id int,level_id int);
insert into users_levels values
(1,1),(1,2),(2,1),(2,3);
create table levels (level_id int,tier_id int);
insert into levels values
(1,1),(2,1),(3,2);
create table tier_reward(tier_id int,reward int);
insert into tier_reward values
(1,200),(2,500);
mysql> update tier_reward set reward = 100 where tier_id = 1 ;
Query OK, 1 row affected (0.15 sec)
mysql> select * from users ;
+---------+-------+
| user_id | score |
+---------+-------+
| 1 | 200 |
| 2 | 600 |
+---------+-------+
2 rows in set (0.00 sec)

Related

MYSQL / MariaDB Trigger - After update

I have 2 similar tables. TableB is containing part of Part A record.
tableA -
+---------+------------+-------+
| id | username | team |
+---------+------------+-------+
| 1 | Peter | W |
+---------+------------+-------+
| 2 | Sam | W |
+---------+------------+-------+
| 3 | Mary | W |
+---------+------------+-------+
tableB -
+---------+------------+-------+
| id | username | team |
+---------+------------+-------+
| 3 | Mary | W |
+---------+------------+-------+
Now I want to update some record in tableB:
UPDATE tableB
SET team = 'K' WHERE username = 'Mary';
How can I use trigger to sync team records in Table A for Mary only?
I am drafting the Trigger like below:
DELIMITER $$
CREATE TRIGGER after_update_tableB
AFTER UPDATE
ON tableB FOR EACH ROW
BEGIN
UPDATE tableA
SET tableA.team = new.team
WHERE tableA.id= new.id;
END $$
DELIMITER ;
I think new.id cannot locate to '3'? Because it hasn't changed?

Query to get subjects of interest for all User Y where Y shares >=3 interests with a User X

These are two tables from a part of supposed Twitter like database where users can follow other users. The User.name field is unique.
mysql> select uID, name from User;
+-----+-------------------+
| uID | name |
+-----+-------------------+
| 1 | Alice |
| 2 | Bob |
| 5 | Iron Maiden |
| 4 | Judas Priest |
| 6 | Lesser Known Band |
| 3 | Metallica |
+-----+-------------------+
6 rows in set (0.00 sec)
mysql> select * from Follower;
+-----------+------------+
| subjectID | observerID |
+-----------+------------+
| 3 | 1 |
| 4 | 1 |
| 5 | 1 |
| 6 | 1 |
| 3 | 2 |
| 4 | 2 |
| 5 | 2 |
+-----------+------------+
7 rows in set (0.00 sec)
mysql> call newFollowSuggestionsForName('Bob');
+-------------------+
| name |
+-------------------+
| Lesser Known Band |
+-------------------+
1 row in set (0.00 sec)
I want to make an operation that will suggest for a user X a list of users they may be interested in following. I thought one heuristic could be to show X for all y who user y follows where X and y follow at least 3 of the same Users. Below is the SQL I came up with to do this. My question is if it could be done more efficiently or nicer in some other ways.
DELIMITER //
CREATE PROCEDURE newFollowSuggestionsForName(IN in_name CHAR(60))
BEGIN
DECLARE xuid INT;
SET xuid = (select uID from User where name=in_name);
select name
from User, (select subjectID
from follower
where observerID in (
select observerID
from Follower
where observerID<>xuid and subjectID in (select subjectID from Follower where observerID=xuid)
group by observerID
having count(*)>=3
)
) as T
where uID = T.subjectID and not exists (select * from Follower where subjectID=T.subjectID and observerID=xuid);
END //
DELIMITER ;
Consider the following refactored SQL code (untested without data) for use in stored procedure.
select u.`name`
from `User` u
inner join
(select subf.observerID, subf.subjectID
from follower subf
where subf.observerID <> xuid
) f
on u.UID = f.subjectID
inner join
(select f1.observerID
from follower f1
inner join follower f2
on f1.subjectID = f2.subjectID
and f1.observerID <> xuid
and f2.observerID = xuid
group by f1.observerID
having count(*) >= 3
) o
on f.observerID = o.observerID
I think the basic query starts as getting all "observers" who share three "subjects" with a given observer:
select f.observerid
from followers f join
followers f2
on f.subjectid = f2.subjectid and
f2.observerid = 2
group by f.observerid
having count(*) = 3;
The rest of the query is just joining in the names to fit into your paradigm of using names for references rather than ids.

mysql insert trigger only if not duplicate

I am little bit confused with the usage of trigger event to get expected result
Here main table is fee. The structure is as follow
Fee
id | rn | fid | amount | f_month | year
====================================================
1 | 1 | 1 | 150000 | 1 | 1
2 | 1 | 2 | 50000 | 1 | 1
3 | 2 | 1 | 550500 | 2 | 1
4 | 2 | 2 | 200 | 2 | 1
5 | 3 | 1 | 550500 | 2 | 1
And the simply insert trigger has been used.
DROP TRIGGER IF EXISTS `insertinv`;
CREATE DEFINER=`root`#`localhost` TRIGGER `insertinv`
AFTER INSERT ON `fee` FOR EACH ROW INSERT INTO invoice VALUES(null, NEW.rn, NEW.year, '')
The output what I am getting
Invoice
inv | rn | y_d | status
==============================
1 | 1 | 1 | 0
2 | 1 | 1 | 0
3 | 2 | 1 | 0
4 | 2 | 1 | 0
5 | 3 | 1 | 0
But I want to apply condition
if fee.rn AND fee.f_month AND fee.year is same then stop to insert. I mean ignore the fee.fid.
and achieve following result. The expected one
Invoice
inv | rn | y_d | status
==============================
1 | 1 | 1 | 0
2 | 2 | 1 | 0
3 | 3 | 1 | 0
In trigger table inv is primary key and auto increment
Check if a inv exists for a matching year and rn.
If it does not exist, then use the insert statement.
Do the following:
DELIMITER $$
DROP TRIGGER IF EXISTS `insertinv` $$
CREATE DEFINER=`root`#`localhost` TRIGGER `insertinv`
AFTER INSERT ON `fee`
FOR EACH ROW
BEGIN
/* Declare a variable to store invoice id for matching year and rn */
DECLARE inv_exists INT(11) DEFAULT 0;
/* Fetch the invoice id if exists */
SELECT inv INTO inv_exists
FROM invoice
WHERE rn = NEW.rn AND
y_d = NEW.year;
/* if no invoice exists then insert into the table */
IF NOT(inv_exists > 0) THEN
/* Insert statement */
INSERT INTO invoice VALUES(null, NEW.rn, NEW.year, '') ;
END IF;
END $$
DELIMITER ;

Limit data from another table attribule

I have table Job and table Company. Each job item will be for a company. I would like to write a query that will list the jobs.
Company has a quantity column. When jobs are listed from Job, the number of jobs listed for a given company is the number in this Quantity column.
Data example
For example: Company A has quantity = 2. The query data will return the top 2 jobs.
How can I do this in a sql query?
Table Job
| id | Title | Company ID |
|:---|------:|:------------:|
| 1 | Job 1 | 1
| 2 | Job 2 | 1
| 3 | Job 3 | 1
| 4 | Job 4 | 2
| 5 | Job 5 | 2
Table Company
| id | Title | Quantity|
|:---|----------:|:-------:|
| 1 | Company 1| 2
| 2 | Company 2| 2
And the result of query Select * From Job => With condition limit quantity of company.
| id | Title | Company ID |
|:---|------:|:----------:|
| 1 | Job 1 | 1
| 2 | Job 2 | 1
| 4 | Job 4 | 2
| 5 | Job 5 | 2
Create procedure and you can use variable with LIMIT
DELIMITER $$
CREATE PROCEDURE `GetJobs`()
NO SQL
BEGIN
DECLARE cid INT;
DECLARE jobsLimit INT;
DECLARE n INT DEFAULT 0;
DECLARE i INT DEFAULT 0;
CREATE TEMPORARY TABLE JobListTemp (
id INT,
Title VARCHAR(255),
CompanyID INT
) ENGINE=MEMORY;
SELECT COUNT(*) FROM Company INTO n;
SET i=0;
WHILE i<n DO
SET cId = (SELECT id FROM Company LIMIT i,1);
SET jobsLimit = (SELECT quantity FROM Company WHERE id = cId LIMIT 1);
INSERT INTO JobListTemp SELECT * FROM Job WHERE CompanyID = cId LIMIT jobsLimit;
SET i = i + 1;
END WHILE;
SELECT * FROM JobListTemp;
DROP TABLE JobListTemp;
END$$
DELIMITER ;
I think you can use the inner sqlquery.
For example:
select innerTab.id,innerTab.Title,innerTab.companyid
from (select * from job order by id limit 2) innerTab
inner join Company on innerTab.companyid=company.companyid
The colname is not same with you.
So you can change it to make sure the sql can run.

Find and Delete Duplicate rows in MySQL

I'm having trouble finding duplicates in a database table with the following setup:
==========================================================================
| stock_id | product_id | store_id | stock_qty | updated_at |
==========================================================================
| 9990 | 51 | 1 | 13 | 2014-10-25 16:30:01 |
| 9991 | 90 | 2 | 5 | 2014-10-25 16:30:01 |
| 9992 | 161 | 1 | 3 | 2014-10-25 16:30:01 |
| 9993 | 254 | 1 | 18 | 2014-10-25 16:30:01 |
| 9994 | 284 | 2 | 12 | 2014-10-25 16:30:01 |
| 9995 | 51 | 1 | 11 | 2014-10-25 17:30:02 |
| 9996 | 90 | 2 | 5 | 2014-10-25 17:30:02 |
| 9997 | 161 | 1 | 3 | 2014-10-25 17:30:02 |
| 9998 | 254 | 1 | 16 | 2014-10-25 17:30:02 |
| 9999 | 284 | 2 | 12 | 2014-10-25 17:30:02 |
==========================================================================
Stock updates are imported into this table every hour, I'm trying to find duplicate stock entries (any rows which have a matching product id and store id) so I can delete the oldest. The query below is my attempt, by comparing product ids and store ids on a join like this I can find one set of duplicates:
SELECT s.`stock_id`, s.`product_id`, s.`store_id`, s.`stock_qty`, s.`updated_at`
FROM `stock` s
INNER JOIN `stock` j ON s.`product_id`=j.`product_id` AND s.`store_id`=j.`store_id`
GROUP BY `stock_id`
HAVING COUNT(*) > 1
ORDER BY s.updated_at DESC, s.product_id ASC, s.store_id ASC, s.stock_id ASC;
While this query will work, it doesn't find ALL duplicates, only 1 set, which means if an import goes awry and isn't noticed until the morning, there's a possibility that we'll be left with tons of duplicate stock entries. My MySQL skills are sadly lacking and I'm at a complete loss about how to find and delete all duplicates in a fast, reliable manner.
Any help or ideas are welcome. Thanks
You can use this query:
DELETE st FROM stock st, stock st2
WHERE st.stock_id < st2.stock_id AND st.product_id = st2.product_id AND
st.store_id = st2.store_id;
This query will delete older record having same product_id and store_id and will keep latest record.
A self join on store_id, product_id and 'is older' in combination with DISTINCT should give you all rows where also a newer version exists:
> SHOW CREATE TABLE stock;
CREATE TABLE `stock` (
`stock_id` int(11) NOT NULL,
`product_id` int(11) DEFAULT NULL,
`store_id` int(11) DEFAULT NULL,
`stock_qty` int(11) DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`stock_id`)
> select * from stock;
+----------+------------+----------+-----------+---------------------+
| stock_id | product_id | store_id | stock_qty | updated_at |
+----------+------------+----------+-----------+---------------------+
| 1 | 1 | 1 | 1 | 2001-01-01 12:00:00 |
| 2 | 2 | 2 | 1 | 2001-01-01 12:00:00 |
| 3 | 2 | 2 | 1 | 2002-01-01 12:00:00 |
+----------+------------+----------+-----------+---------------------+
> SELECT DISTINCT s1.stock_id, s1.store_id, s1.product_id, s1.updated_at
FROM stock s1 JOIN stock s2
ON s1.store_id = s2.store_id
AND s1.product_id = s2.product_id
AND s1.updated_at < s2.updated_at;
+----------+----------+------------+---------------------+
| stock_id | store_id | product_id | updated_at |
+----------+----------+------------+---------------------+
| 2 | 2 | 2 | 2001-01-01 12:00:00 |
+----------+----------+------------+---------------------+
> DELETE stock FROM stock
JOIN stock s2 ON stock.store_id = s2.store_id
AND stock.product_id = s2.product_id
AND stock.updated_at < s2.updated_at;
Query OK, 1 row affected (0.02 sec)
> select * from stock;
+----------+------------+----------+-----------+---------------------+
| stock_id | product_id | store_id | stock_qty | updated_at |
+----------+------------+----------+-----------+---------------------+
| 1 | 1 | 1 | 1 | 2001-01-01 12:00:00 |
| 3 | 2 | 2 | 1 | 2002-01-01 12:00:00 |
+----------+------------+----------+-----------+---------------------+
Or you can use a stored Procedure:
DELIMITER //
DROP PROCEDURE IF EXISTS removeDuplicates;
CREATE PROCEDURE removeDuplicates(
stockID INT
)
BEGIN
DECLARE stockToKeep INT;
DECLARE storeID INT;
DECLARE productID INT;
-- gets the store and product value
SELECT DISTINCT store_id, product_id
FROM stock
WHERE stock_id = stockID
LIMIT 1
INTO
storeID, productID;
SELECT stock_id
FROM stock
WHERE product_id = productID AND store_id = storeID
ORDER BY updated_at DESC
LIMIT 1
INTO
stockToKeep;
DELETE FROM stock
WHERE product_id = productID AND store_id = storeID
AND stock_id != stockToKeep;
END //
DELIMITER ;
And afterwards call it for every pair of the product id and store id via a cursor procedure:
DELIMITER //
CREATE PROCEDURE updateTable() BEGIN
DECLARE done BOOLEAN DEFAULT FALSE;
DECLARE stockID INT UNSIGNED;
DECLARE cur CURSOR FOR SELECT DISTINCT stock_id FROM stock;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done := TRUE;
OPEN cur;
testLoop: LOOP
FETCH cur INTO stockID;
IF done THEN
LEAVE testLoop;
END IF;
CALL removeDuplicates(stockID);
END LOOP testLoop;
CLOSE cur;
END//
DELIMITER ;
And then just call the second procedure
CALL updateTable();