How to delete duplicate rows if they are odd else keep one - mysql

Its a card like game.
my table is
Number
Symbol
Player
1
C
F
1
S
F
1
D
F
1
H
F
2
S
F
2
C
F
2
D
F
3
H
F
2
H
S
3
S
S
I am trying to remove all the 1 cards from Player F because he have an even number of those cards and he have collect all the cards.
And i want to remove only 2 rows of the 2 cards from Player F because he have an odd number of those and the last 2 its on the S player
I am trying to create a procedure for this i only manage to keep one row of the player with this
PROCEDURE `deleteDupl`()
BEGIN
DELETE c1 FROM cards c1, cards c2 WHERE c1.Symbol > c2.Symbol AND c1.Number = c2.Number AND c1.Player = c2.Player;
END
--edit
the point of the game is to pick cards from your opponent and once you have 2 same cards(the number of card not the symbol) you drop them (no matter what symbol just random drop 2 of the same numbers)
but in the start of the game you might get more than 2 of the same cards like the F player have all the aces so he have to drop them all
or like the F player have three times the 2 card he must drop two cards (no matter what symbol) until he pick the card with number 2 from his opponent

You have not said which MySQL version you are running. This stored procedure example works on MySQL 5.6. It runs a simple GROUP BY query to get all Number, Player groups with more than 1 card. It then loops over the cursor and runs a delete for each row returned.
CREATE PROCEDURE `sp_DeletePairs`()
BEGIN
DECLARE done BOOLEAN DEFAULT FALSE;
DECLARE _number TINYINT UNSIGNED;
DECLARE _player CHAR(20);
DECLARE _count TINYINT UNSIGNED;
DECLARE `cur` CURSOR FOR
SELECT `Number`, `Player`, COUNT(*) AS `num`
FROM `cards`
GROUP BY `Number`, `Player`
HAVING `num` > 1;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN cur;
read_loop: LOOP
FETCH cur INTO _number, _player, _count;
IF done THEN
LEAVE read_loop;
END IF;
CASE
WHEN _count IN (2, 3) THEN
DELETE FROM `cards` WHERE `Number` = _number AND `Player` = _player LIMIT 2;
WHEN _count = 4 THEN
DELETE FROM `cards` WHERE `Number` = _number AND `Player` = _player LIMIT 4;
END CASE;
END LOOP;
CLOSE cur;
END
Obviously, you can wrap the following DELETE query examples in stored procedures if you so desire.
If you are on MySQL 8.0 or later you can do it using window functions -
WITH `stats` AS (
SELECT `Number`, `Symbol`, `Player`,
ROW_NUMBER() OVER (PARTITION BY `Number`, `Player` ORDER BY `Player`, `Number`, `Symbol`) AS `seq`,
COUNT(*) OVER (PARTITION BY `Number`, `Player`) AS `count_numbers`
FROM cards
)
DELETE `c`
FROM `cards` `c`
INNER JOIN `stats` `s`
ON `c`.`Number` = `s`.`Number`
AND `c`.`Symbol` = `s`.`Symbol`
AND `c`.`Player` = `s`.`Player`
WHERE `s`.`count_numbers` = 4
OR (`s`.`count_numbers` IN (2, 3) AND `s`.`seq` IN (1, 2));
Within the CTE, the ROW_NUMBER() is giving us a cumulative count within the Number, Player PARTITION. The COUNT(*) is giving us the total within the Number, Player PARTITION. We can then join between stats (the CTE) and cards on all three of the original columns. Finally, we use the WHERE clause to decide which cards to remove.
A similar approach can be taken in MySQL < 8.0, using variables for the sequence and a join to another derived table to get the count per group -
DELETE `c`
FROM `cards` `c`
INNER JOIN (
SELECT
`c`.`Number`,
`c`.`Symbol`,
`c`.`Player`,
IF(#prev_number = `c`.`Number` AND #prev_player = `c`.`Player`, #row := #row + 1, #row := 1) AS `seq`,
`counts`.`count_numbers`,
#prev_number := `c`.`Number`,
#prev_player := `c`.`Player`
FROM `cards` `c`
JOIN (SELECT #row := 0, #prev_number := 0, #prev_player:=0) t
INNER JOIN ( SELECT `Player`, `Number`, COUNT(*) AS `count_numbers` FROM `cards` GROUP BY `Player`, `Number`) AS `counts`
ON `c`.`Player` = `counts`.`Player`
AND `c`.`Number` = `counts`.`Number`
ORDER BY `c`.`Player`, `c`.`Number`
) `s`
ON `c`.`Number` = `s`.`Number`
AND `c`.`Symbol` = `s`.`Symbol`
AND `c`.`Player` = `s`.`Player`
WHERE `s`.`count_numbers` = 4
OR (`s`.`count_numbers` IN (2, 3) AND `s`.`seq` IN (1, 2));
I am definitely not suggesting using this last example, at least not in a production environment. I just included as it might be interesting to someone.

Related

Mysql On trigger each row

i need help for the below code. My problem is that i want only that code executes once per statement (after i search i checked that expression don't exists anymore only once per row).
So i tried to add:
IF NOT EXISTS
(Select count(*) FROM replay_replays_access WHERE id_game = new.id_game GROUP BY id_game HAVING count(*) <5)
THEN
But didn't work either what can i do, its duplicating sometime triplicating the information?
TRIGGER replay
AFTER UPDATE
ON table_replays FOR EACH ROW
begin
IF EXISTS
(SELECT
replay_games.room_name
FROM replay_games
WHERE replay_games.room_name = 'Tournament Room' and replay_games.id = new.id_game)
THEN
IF NOT EXISTS
(Select
count(*)
FROM replay_replays_access
WHERE id_game = new.id_game
GROUP BY id_game
HAVING count(*) <5)
THEN
INSERT INTO replay_replays_access(id_game, id_player, replay_name, do_not_hide)
SELECT
new.id_game,
replay_users.id ,
CONCAT(
(SELECT game_types
FROM replay_games
WHERE id=new.id_game),
': ',
(SELECT
descr
FROM replay_games
WHERE id=new.id_game)) ,
0
FROM replay_users
WHERE
(replay_users.admin > 0 OR
replay_users.privlevel = 'TOURNAMENT MEMBER')
AND NOT replay_users.name = (
SELECT
replay_games.creator_name
FROM replay_games
WHERE replay_games.id = new.id_game);
END IF;
END IF;
END

Problem with increment variable and stored procedure to check duplicates

I have problem with my procedure. I have table oferty_in which contain fields (id, status, ..., id_om). I want procedure which check if exist rows with the same id_om.
If exist, delete rows where status = 'N' (N - new).
My procedure almost works, but i have problem with iterate in loop. Every time I run my procedure ,procedure delete a half of rows. I don't know where is problem...
DELIMITER //
CREATE PROCEDURE check_duplicates_oferty_in()
BEGIN
DECLARE n INT DEFAULT 0;
DECLARE i INT DEFAULT 0;
DECLARE v_id_del BIGINT;
SELECT count(*) INTO n FROM oferty_in where status_oferty = 'N';
SET i=0;
WHILE i<n DO
IF EXISTS (SELECT id_om FROM oferty_in group by id_om having count(*) >= 2 LIMIT i,1) THEN
SELECT id_om INTO v_id_del FROM oferty_in group by id_om having count(*) >= 2 LIMIT i,1;
DELETE from oferty_in where id_om = v_id_del and status_oferty = 'N';
END IF;
SET i=i+1;
END WHILE;
END
//
I try also:
IF EXISTS (SELECT id_om FROM oferty_in group by id_om having count(*) >= 2 LIMIT i,1) THEN
SELECT id_om INTO v_id_del FROM oferty_in group by id_om having count(*) >= 2 LIMIT i,1;
DELETE from oferty_in where id_om = v_id_del and status_oferty = 'N';
SET i=i+1;
ELSE
SET i=i+1;
END IF;
But it's the same.
Every time half of rows. I use counter 'i' and while loop to iterate row by row on rows in oferty_in when status = 'N'. Anyone have a idea what I did wrong? Thanks for help and time.
You seem to want to delete rows with status = 'N' when id_om is duplicated.
I want procedure which check if exist rows with the same id_om. If exist, delete rows where status = 'N' (N - new).
Non-working code doesn't generally help explain logic, so this is what I am going by.
You definitely do not need a looping construct for this, nor a cursor:
delete o
from oferty_in o join
(select o2.id_om
from oferty_in o2
group by o2.id_om
having count(*) > 1 and sum(status = 'N') > 0
) o2
on o.id_om = o2.id_om
where o.status = 'N';

Select Status That Last for More Than 1 Second

I have got a problem looks simple, but I could not find the solution.
So, I have got a table with two cols like this:
Time Status
00:00:00.111 Off
00:00:00.222 On
00:00:00.345 On
00:00:01.555 On
00:00:01.666 Off
00:00:02.222 On
00:00:02.422 On
00:00:02.622 Off
00:00:05.888 Off
00:00:05.999 Off
I want to select all statuses of On which lasted for more than 1 second,
in this example, I want the sequence:
00:00:00.222 On
00:00:00.345 On
00:00:01.555 On
Could you guys give me any clue? Many thanks!
A simple GROUP BY and SUM can not do this on your current dataset, so my idea is to add a helper column:
CREATE TABLE someTable(
`time` DATETIME,
status CHAR(3),
helperCol INT
);
The helperCol is an INT and will be set as follows:
CREATE PROCEDURE setHelperCol()
BEGIN
DECLARE finished,v_helperCol INT;
DECLARE status CHAR(3);
DECLARE ts DATETIME;
DECLARE CURSOR st FOR SELECT `time`,status,helperCol FROM someTable WHERE helperCol IS NOT NULL; -- Handy for re-use: No need to go over all data, so you can save the helperCol as permanent value.
DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished = 1;
SELECT #maxVal:=MAX(helperCol) FROM helperCol;
SET finished=0;
SET helperCol=#maxVal;
IF(!helperCol>0) SET helperCol=1;
OPEN st;
FETCH ts,status,v_helperCol FROM st;
WHILE(finished=0) DO
IF(status='Off') v_helperCol=v_helperCol+1;
UPDATE someTable SET helperCol=v_helperCol WHERE `time`=ts; -- Assuming `time` is unique;
FETCH ts,status,v_helperCol FROM st;
END WHILE;
CLOSE st;
END;
Execute the procedure and the result is:
Time Status helperCol
00:00:00.111 Off 2
00:00:00.222 On 2
00:00:00.345 On 2
00:00:01.555 On 2
00:00:01.666 Off 3
00:00:02.222 On 3
00:00:02.422 On 3
00:00:02.622 Off 4
This can now be grouped and processed:
SELECT MAX(`time`)-MIN(`time`) AS diffTime
FROM someTable
WHERE status='ON'
GROUP BY helperCol
HAVING MAX(`time`)-MIN(`time`)>1;
The result of that is (you need to search for the correct datetime functions to apply in the MAX-MIN part):
1.333
Alternative:
You can also process the MAX-MIN in the stored procedure, but that would not be efficiently repeatable as the helperColumn solution is.
SELECT a.time start
, MIN(c.time) end
, TIMEDIFF(MIN(c.time),a.time) duration
FROM
( SELECT x.*, COUNT(*) rank FROM my_table x JOIN my_table y ON y.time <= x.time GROUP BY time ) a
LEFT
JOIN
( SELECT x.*, COUNT(*) rank FROM my_table x JOIN my_table y ON y.time <= x.time GROUP BY time ) b
ON b.status = a.status
AND b.rank = a.rank - 1
JOIN
( SELECT x.*, COUNT(*) rank FROM my_table x JOIN my_table y ON y.time <= x.time GROUP BY time ) c
ON c.rank >= a.rank
LEFT
JOIN
( SELECT x.*, COUNT(*) rank FROM my_table x JOIN my_table y ON y.time <= x.time GROUP BY time ) d
ON d.status = c.status
AND d.rank = c.rank + 1
WHERE b.rank IS NULL
AND d.rank IS NULL
AND a.status = 1
GROUP
BY a.time
HAVING duration >= 1;
Another, faster, method might be along these lines - unfortunately I don't think the data types and functions in my version of MySQL support fractions of a second, so this is probably a little bit wrong (there may also be a logical error)...
SELECT time
, status
, cumulative
FROM
( SELECT *
, CASE WHEN #prev = status THEN #i:=#i+duration ELSE #i:=0 END cumulative
, #prev:=status
FROM
( SELECT x.*
, TIME_TO_SEC(MIN(y.time))-TIME_TO_SEC(x.time) duration
FROM my_table x
JOIN my_table y
ON y.time > x.time
GROUP
BY x.time
) n
ORDER
BY time
) a
WHERE cumulative >= 1
AND status = 1;

MySQL - Insert multiple rows based on column value

I have the query working, just wondering if there is a better way to do this without cursors/loops/php side. I've been a DBA for 5+ years and just came across the := statement. Very cool.
Table (tblPeople) with the person ID and the number of tickets they bought.
PersonId NumTickets
1 3
2 1
3 1
I then want to assign individual tickets to each person in a new table (tblTickets), depending on how many tickets they bought. The TicketId is a key, auto increment column.
TicketId PersonId
100 1
101 1
102 1
103 2
104 3
Here is the code. It loops through the whole tblPeople over and over again incrementing a new calculated column called rowID. Then I filter out the rows based on the number of tickets they bought in the WHERE clause. The problem I see is the subquery is huge, the more people I have, the bigger the subquery gets. Just not sure if there is a better way to write this.
INSERT INTO tblTickets (PersonId)
SELECT PersonId
FROM (
SELECT s.PersonId, s.NumTickets,
#rowID := IF(#lastPersonId = s.PersonId and #lastNumTickets = s.NumTickets, #rowID + 1, 0) AS rowID,
#lastPersonId := s.PersonId,
#lastNumTickets := s.NumTickets
FROM tblPeople m,
(SELECT #rowID := 0, #lastPersonId := 0, #lastNumTickets := 0) t
INNER JOIN tblPeople s
) tbl
WHERE rowID < NumTickets
I'd add a utility table Numbers which contains all the numbers from 1 up to the maximal number of tickets a person may buy. Then you can do something like this:
INSERT INTO tblTickets (PersonId)
SELECT s.PersonId
FROM tblPeople s, Numbers n
WHERE n.number <= s.NumTickets
Following Stored procedure will serve your purpose...
DELIMITER $$
USE <your database name> $$
DROP PROCEDURE IF EXISTS `update_ticket_value2`$$
CREATE PROCEDURE `update_ticket_value2`()
BEGIN
DECLARE index_value INT;
DECLARE loop_variable INT;
SET #KeyValue = 100;
SET #LastPersonID = 0;
SET #TicketNum = 0;
SET #PersonIDToHandle = 0;
SELECT #PersonIDToHandle = PersonID, #TicketNum = NumTickets
FROM tblPeople
WHERE PersonId > #LastPersonID
ORDER BY PersonId
LIMIT 0,1;
WHILE #PersonIDToHandle IS NOT NULL
DO
SET loop_variable = 0;
WHILE(loop_variable < #TicketNum) DO
INSERT INTO tblTickets(TicketId, PersonId) VALUES(#KeyValue + loop_variable, #PersonIDToHandle);
SET loop_variable = loop_variable + 1;
END WHILE;
SET #LastPersonID = #PersonIDToHandle;
SET #PersonIDToHandle = NULL;
SET #KeyValue = #KeyValue + #TicketNum;
SELECT #PersonIDToHandle := PersonID, #TicketNum := NumTickets
FROM tblPeople
WHERE PersonId > #LastPersonID
ORDER BY PersonId
LIMIT 0,1;
END WHILE;
END$$
DELIMITER ;
Call the procedure as:
CALL update_ticket_value2();
Hope it helps...

MySQL update column with unrelated table data

I have two tables like this in mysql
a.cardnumber (unique)
a.position (numerical 3 digits or null)
a.serial
b.serial (unique)
b.lastused
I want to update any rows in "a" where position is above 600 AND "a.serial" is blank with any serial from "b.serial" where "b.lastused" is either null or more than 30 days ago. When the serial is copied into "a.serial" I want to update "b.lastused" with today's date so I know that the relevant "b.serial" has been used today.
There is no relation to the two tables apart from the serial and any serial from b can be used with any cardnumber in a.
I've tried this using my limited knowledge of mysql but I keep getting an error from my mysql desktop program to say I have an error in my query :(
Any help much appreciated!
I'm assuming here that you want to use a separate b.serial for each row to be updated in a. (This isn't specifically stated, but it seems to me to be most likely; please feel free to correct my assumption if it is wrong.)
I setup a small example. It wasn't clear what the datatypes for each of the columns, so I used INT where I wasn't sure. I used DATE datatype (rather than DATETIME) for lastused.
CREATE TABLE a (`cardnumber` VARCHAR(10) NOT NULL PRIMARY KEY, `position` INT, `serial` INT);
CREATE TABLE b (`serial` INT NOT NULL PRIMARY KEY, lastused DATE);
INSERT INTO a VALUES ('x0000',555,NULL),('x0001',700,123),('a1111',601,NULL),('a2222',602,NULL);
INSERT INTO b VALUES (100,'2012-07-15'),(101,NULL),(102,'2010-01-01'),(103,NULL),(104,NULL);
SELECT * FROM a;
SELECT * FROM b;
Based on the conditions you give, the rows with cardnumbers 'a1111' and 'a2222' should get updated, the other two rows should not (position <= 600, serial already assigned).
Before we run an UPDATE, we want to first run a SELECT that returns the rows to be updated, along with the values that will be assigned. Once we get that, we can convert that to a multi-table UPDATE statement.
SELECT a.cardnumber AS `a.cardnumber`
, a.position AS `a.position`
, a.serial AS `a.serial`
, b.serial AS `b.serial`
, b.lastused AS `b.lastused`
FROM (
SELECT #i := #i + 1 AS i
, aa.*
FROM a aa
JOIN (SELECT #i := 0) ii
WHERE aa.position > 600 /* assuming `position` is numeric datatype */
AND aa.serial IS NULL /* assuming 'blank' represented by NULL */
ORDER BY aa.cardnumber
) ia
JOIN (
SELECT #j := #j + 1 AS j
, bb.serial
, bb.lastused
FROM b bb
JOIN (SELECT #j := 0) jj
WHERE bb.lastused IS NULL
OR bb.lastused < DATE_ADD(NOW(),INTERVAL -30 DAY)
ORDER BY bb.serial
) jb
ON ia.i = jb.j
JOIN a ON a.cardnumber = ia.cardnumber
JOIN b ON b.serial = jb.serial
To convert that to an UPDATE, replace the SELECT ... FROM with UPDATE, and add a SET clause to assign new values to the tables.
UPDATE (
SELECT #i := #i + 1 AS i
, aa.*
FROM a aa
JOIN (SELECT #i := 0) ii
WHERE aa.position > 600
AND aa.serial IS NULL
ORDER BY aa.cardnumber
) ia
JOIN (
SELECT #j := #j + 1 AS j
, bb.serial
, bb.lastused
FROM b bb
JOIN (SELECT #j := 0) jj
WHERE bb.lastused IS NULL
OR bb.lastused < DATE_ADD(NOW(),INTERVAL -30 DAY)
ORDER BY bb.serial
) jb
ON ia.i = jb.j
JOIN a ON a.cardnumber = ia.cardnumber
JOIN b ON b.serial = jb.serial
SET a.serial = b.serial
, b.lastused = DATE(NOW())
-- 4 row(s) affected
You can run the queries for the inline views seperately (ia, jb) to verify that these are getting the rows you want to update.
The join from ia to a, and from jb to b, should be on the primary keys unique key.
The purpose of the ia and jb inline views is to get sequential numbers assigned to those rows so we can match them to each other.
The joins to a and b are to get back to the row in the original table, which is what we want to update.
(Obviously, some adjustments need to be made if serial is not an INT, or lastused is a DATETIME rather than a DATE.)
But this is an example of how I would go about doing the UPDATE you want to do (as best I understood it.)
NOTE: This approach works with MySQL versions that support subqueries. For MySQL 4.0, you would need to run this in steps, storing the results from the "ia" and "jb" inline views (subqueries) into actual tables. Then reference those tables in the query in place of the inline views. The ii and jj subqueries can be removed, and replaced with separate SELECT #i := 0, #j := 0 statement prior to the execution of the queries that reference these variables.
let me know if this works
Update table_a
set serial =
(
select b.serial from table_b b
where b.lastused = NULL
OR b.lastused < (current date - 30) limit 1
)
where cardnumber in
(
select a.cardnumber
from table_a a
where a.position > 600
and a.serial = NULL
)
update table_b b
set b.lastused = current date
where b.lastused = NULL
OR b.lastused < (current date - 30)