MySQL: update a record, on duplicate key leave NEWER one, and delete OLDER one? See inside - mysql

There is a table:
CREATE TABLE `mytable` (
`user_id` INT(10) UNSIGNED NOT NULL,
`thing_id` VARCHAR(100) NOT NULL DEFAULT '',
`lock_date` DATETIME NOT NULL,
`lock_id` VARCHAR(36) NOT NULL,
PRIMARY KEY (`user_id`,`thing_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
and some values there:
INSERT INTO mytable(user_id,thing_id,lock_date,lock_id)
VALUES
(51082,'299ac9ff-2b2b-102d-8ff6-f64c971398c3','2012-03-16 00:39:12','ec7b2008-6ede-11e1-aac2-5924aae99221'),
(108325,'299ac9ff-2b2b-102d-8ff6-f64c971398c3','2013-02-05 19:30:03','7c6de986-6edd-11e1-aac2-5924aae99221'),
(108325,'d90b354d-4b5f-11e0-9959-47117d41cf4b','2012-03-16 00:47:41','1c243032-6ee0-11e1-aac2-5924aae99221');
I want to delegate all records of user_id = 108325 to user_id = 51082, and if both users have an equal thing_id field, leave the newer one only (lock_date1 > lock_date2), so that I have following result:
51082,'299ac9ff-2b2b-102d-8ff6-f64c971398c3','2013-02-05 19:30:03','7c6de986-6edd-11e1-aac2-5924aae99221'
108325,'d90b354d-4b5f-11e0-9959-47117d41cf4b','2012-03-16 00:47:41','1c243032-6ee0-11e1-aac2-5924aae99221'
Note that 51082 now has a newer record: lock_date = '2013-02-05 19:30:03' instead of '2012-03-16 00:39:12'.
So, how can I update a row, and on duplicate key leave the newer one (by some particular field)?
Thanks!

INSERT INTO
mytable(user_id,thing_id,lock_date,lock_id)
VALUES
(51082,'299ac9ff-2b2b-102d-8ff6-f64c971398c3','2012-03-16 00:39:12','ec7b2008-6ede-11e1-aac2-5924aae99221'),
(108325,'299ac9ff-2b2b-102d-8ff6-f64c971398c3','2013-02-05 19:30:03','7c6de986-6edd-11e1-aac2-5924aae99221'),
(108325,'d90b354d-4b5f-11e0-9959-47117d41cf4b','2012-03-16 00:47:41','1c243032-6ee0-11e1-aac2-5924aae99221')
ON DUPLICATE KEY UPDATE SET
user_id = VALUES(user_id),
lock_date = VALUES(lock_date),
lock_id = VALUES(lock_id)

Related

Creating a MySQL trigger to set a boolean value to true if a collection of booleans are true

I am creating an inventory management app in node.js that uses MySQL as a database. I have a weak entity “rental_item” that holds the items in a particualr rental. The issue is that the rental may not come back all at once so I need a way of marking the “rental_returned” boolean in the rental table true only when all of the “item_returned” entires are true.
Here is my table structure:
CREATE TABLE `rental` (
`rental_id` int NOT NULL AUTO_INCREMENT,
`renter_id` int NOT NULL,
`date_in` date NOT NULL,
`date_out` date NOT NULL,
`sig_path` varchar(50) NOT NULL,
`doc_path` varchar(50) NOT NULL,
`col_name` varchar(50) NOT NULL,
`col_path` varchar(50) NOT NULL,
`cost` decimal(15,2) NOT NULL,
`rental_returned` tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`rental_id`),
UNIQUE KEY `doc_path` (`doc_path`),
UNIQUE KEY `col_path` (`col_path`),
UNIQUE KEY `sig_path` (`sig_path`),
KEY `renter_key` (`renter_id`),
CONSTRAINT `renter_key` FOREIGN KEY (`renter_id`) REFERENCES `renter` (`renter_id`)
)
CREATE TABLE `rental_item` (
`rental_id` int NOT NULL,
`i_ID` varchar(20) NOT NULL,
`item_returned` tinyint(1) NOT NULL DEFAULT '0',
KEY `rental_key` (`rental_id`),
KEY `rental_item_key` (`i_ID`),
CONSTRAINT `rental_item_key` FOREIGN KEY (`i_ID`) REFERENCES `item` (`i_ID`),
CONSTRAINT `rental_key` FOREIGN KEY (`rental_id`) REFERENCES `rental` (`rental_id`) ON DELETE CASCADE
)
I am currently doing this through the mysql2 node.js module and just checking for all the values of a given rental_id. I then found out about triggers and thought this way could be better. I fiddled round with things like this Trigger with table join, but couldn’t wrap my head around how to get the rental_id of the entry that was updated from rental_item, then check that all entires in rental_item with that id have item_returned = 1, and finally update the rental table to show that all the items/the complete rental has been returned.
I understand that this sould be an update after trigger on rental_item but dont know how to handle the conditionals or loops needed.
Use NEW.rental_id to get the ID of the row that was updated.
CREATE TRIGGER rental_returned AFTER UPDATE ON rental_item
FOR EACH ROW
UPDATE rental
SET rental_returned = (
NOT EXISTS (
SELECT *
FROM rental_item
WHERE rental_id = NEW.rental_id
AND item_returned = 0))
WHERE rental_id = NEW.rental_id

multicolumn unique key mysql insert

I have a mysql database, and a table structure like this:
CREATE TABLE `user_session_log` (
`stat_id` int(8) NOT NULL AUTO_INCREMENT,
`metric` tinyint(1) NOT NULL DEFAULT '0',
`platform` tinyint(1) NOT NULL DEFAULT '0',
`page_id` varchar(128) DEFAULT '_empty_',
`target_date` date DEFAULT NULL,
`country` varchar(2) DEFAULT NULL COMMENT 'ISO 3166 country code (2 symbols)',
`amount` int(100) NOT NULL DEFAULT '0.000000' COMMENT 'counter or amount',
`unique_key` varchar(180) DEFAULT NULL COMMENT 'Optional unique identifier',
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`stat_id`),
UNIQUE KEY `unique_key` (`unique_key`) USING BTREE,
KEY `target_date` (`target_date`)
) ENGINE=InnoDB AUTO_INCREMENT=21657473 DEFAULT CHARSET=utf8
What I'm trying to achieve is to log the active sessions / unique users based on date, and page_id, and country. Currently I'm able to achieve this by generating multiple insert statements with unique_key, buy adding a page_id and date in the unique key but I want something a little bit different.
The logic should be: insert new row of unique_key (semi-unique user id), where country = this, date = this, page_id = this. If there is already a row with such information (same page_id, unique_key, and date + country) - update the amount = (amount) + 1; (session).
So I could do lookups like :
SELECT sum(amount) WHERE page_id = "something" AND target_date = "2018-12-21"
This would give me a number of sessions. OR:
SELECT COUNT(*) WHERE page_id = "something" AND target_date = "2018-12-21"
This would give me a number of active users on that pagee_id on that day
OR:
SELECT COUNT(*) WHERE target_date = "2018-12-21"
Which would give me a result of total users on that day.
I know about unique index, but would it give me a result I'm looking for?
Edit, a sample insert:
INSERT INTO `user_session_log` (`platform`,`page_id`,`target_date`,`country`,`amount`,`unique_key`,`created`,`modified`) VALUES ('1','page_id_54','2018-10-08','US',1,'ea3d0ce0406a838d9fd31df2e2ec8085',NOW(),NOW()) ON DUPLICATE KEY UPDATE `amount` = (amount) +1, `modified` = NOW();
and the table should know if theres a duplicate based on if theres a same unique_key + date + country + platform + page_id, otherwise just insert a new row.
Right now I'm doing this differently by having different metrics and a unique_key generated already containing the date + page_id and then hashed. that way it's unique by means i can filter the different unique users on a day basis, but I can't filter the amount of sessions that unique user has had, or how long he uses the software and similar.
Firstly create unique index on all the columns that needs to be unique as follows:
ALTER TABLE user_session_log ADD UNIQUE INDEX idx_multi_column ON user_session_log (unique_key, date, country, platform, page_id);
then you can use INSERT ... ON DUPLICATE KEY UPDATE query to insert/update.

Update if exists, otherwise insert (without unique keys)

I have a table that is created like this:
'CREATE TABLE `boss_kills` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`characterid` int(11) DEFAULT NULL,
`mobid` int(11) DEFAULT NULL,
`amount` int(11) NOT NULL DEFAULT ''0'',
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=25 DEFAULT CHARSET=latin1'
Goal: I'm trying to create a system where a player kills a boss in the game and it records the boss's ID and the player's ID in the table. I want to be able to write one query where it updates if the player is already logged with the specific boss and inserts if he or she isn't.
Research: I did a lot of research online and people suggest the INSERT INTO ... ON DUPLICATE KEY UPDATE, but that only works if you either know your primary key or have a unique key, none of which I have or know.
Tries: I've tried
IF EXISTS (...) THEN UPDATE (...) ELSE INSERT(...)
and
UPDATE (...) IF ##ROWCOUNT = 0 INSERT INTO (...)
but they don't work. I get syntax errors.
If needed I can provide the errors thrown by the above tries. My current code for trying to update is this (but it throws a SQL syntax error):
Attempt 1:
UPDATE boss_kills
SET amount = amount + 1
WHERE characterid = ? AND mobid = ?
IF ##ROWCOUNT = 0
INSERT INTO boss_kills (characterid, mobid, amount) VALUES (?, ?, 1)
Attempt 2:
IF NOT EXISTS (SELECT id FROM boss_kills WHERE characterid = ? AND mobid = ?)
THEN
INSERT INTO boss_kills VALUES (DEFAULT, ?, ?, 1)
ELSE
UPDATE boss_kills SET amount = amount + 1 WHERE characterid = ? AND mobid = ?
It seems like (characterid, mobid) could make a unique index constraint for your table, thus allowing you to use INSERT ... ON DUPLICATE KEY UPDATE.
Use below script to create your table
CREATE TABLE boss_kills (
id int(11) NOT NULL AUTO_INCREMENT,
characterid int(11) DEFAULT NULL,
mobid int(11) DEFAULT NULL,
amount int(11) NOT NULL DEFAULT 0,
PRIMARY KEY ( id ),
UNIQUE ( characterid, mobid )
) ENGINE=MyISAM AUTO_INCREMENT=25 DEFAULT CHARSET=latin1;
Note that I've removed backticks around your table and column names - they are not necessary.
Inserting a row
INSERT INTO boss_kills (characterid, mobid, amount)
VALUES (?, ?, 1)
ON DUPLICATE KEY UPDATE amount = amount + 1;

Ignore cascade on foreign key update?

To preface, I'm not very experienced with database design. I have a table of hashes and ids. When a group of new hashes are added, each row in the group gets the same id. If any hash within the new group already exists in the database, all hashes in the new group and existing group(s) get a new, shared id (effectively merging ids when hashes are repeated):
INSERT INTO hashes
(id, hash)
VALUES
($new_id, ...), ($new_id, ...)
ON DUPLICATE KEY UPDATE
repeat_count = repeat_count + 1;
INSERT INTO hashes_lookup SELECT DISTINCT id FROM hashes WHERE hash IN (...);
UPDATE hashes JOIN hashes_lookup USING (id) SET id = '$new_id';
TRUNCATE TABLE hashes_lookup;
Other tables reference these ids, so that if an id changes, foreign key constraints take care of updating the ids across tables. The issue here, however, is that I can't enforce uniqueness across any of the child tables. If I do, my queries fail with:
Foreign key constraint for table '...', record '...' would lead to a duplicate entry in table '...'
This error makes sense, given the following test case where id and value are a composite unique key:
id | value
---+-------
a | 1
b | 2
c | 1
Then a gets changed to c:
id | value
---+-------
c | 1
b | 2
c | 1
But c,1 already exists.
It would be ideal if there was an ON UPDATE IGNORE CASCADE option, so that if a duplicate row exists, any duplicating inserts are ignored. However, I'm pretty sure the real issue here is my database design, so I am open to any and all suggestions. My current solution is to not enforce uniqueness across child tables, which leads to a lot of redundant rows.
Edit:
CREATE TABLE `hashes` (
`hash` char(64) NOT NULL,
`id` varchar(128) NOT NULL,
`repeat_count` int(11) NOT NULL DEFAULT '0',
`insert_timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `hash` (`hash`) USING BTREE,
KEY `id` (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=latin1
CREATE TABLE `emails` (
`id` varchar(128) NOT NULL,
`group_id` char(5) NOT NULL,
`email` varchar(500) NOT NULL,
KEY `index` (`id`) USING BTREE,
UNIQUE KEY `id` (`id`,`group_id`,`email`(255)) USING BTREE,
CONSTRAINT `emails_ibfk_1` FOREIGN KEY (`id`) REFERENCES `hashes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=latin1
I think will be good to create table hash_group to store id of hash group:
CREATE TABLE `hash_group` (
`id` BIGINT AUTO_INCREMENT NOT NULL,
`group_name` varchar(128) NOT NULL,
`insert_timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `group_name` (`group_name`) USING BTREE,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
And change structure of existing tables:
CREATE TABLE `hashes` (
`hash` char(64) NOT NULL,
`hash_group_id` BIGINT NOT NULL,
`repeat_count` int(11) NOT NULL DEFAULT '0',
`insert_timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `hash` (`hash`) USING BTREE,
KEY `hashes_hash_group_id_index` (`hash_group_id`) USING BTREE,
CONSTRAINT `hashes_hash_group_id_fk` FOREIGN KEY (`hash_group_id`) REFERENCES `hash_group` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `emails` (
`hash_group_id` BIGINT NOT NULL,
`group_id` char(5) NOT NULL,
`email` varchar(500) NOT NULL,
KEY `emails_hash_group_id_index` (`hash_group_id`) USING BTREE,
UNIQUE KEY `emails_unique` (`hash_group_id`,`group_id`,`email`(255)) USING BTREE,
CONSTRAINT `emails_ibfk_1` FOREIGN KEY (`hash_group_id`) REFERENCES `hash_group` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Also create trigger to update hash group if you need to do it:
DELIMITER $$
CREATE TRIGGER `update_hash_group_name` AFTER UPDATE ON `hashes`
FOR EACH ROW
BEGIN
UPDATE `hash_group`
SET `group_name` = md5(now()) -- replace to you hash formula
WHERE id = NEW.hash_group_id;
END;$$
DELIMITER ;
And create function for getting actual group id:
DROP FUNCTION IF EXISTS get_hash_group;
DELIMITER $$
CREATE FUNCTION get_hash_group(id INT) RETURNS INT
BEGIN
IF (id IS NULL) THEN
INSERT INTO `hash_group` (`group_name`)
VALUES (md5(now())); -- replace to you hash
RETURN LAST_INSERT_ID();
END IF;
RETURN id;
END;$$
DELIMITER ;
Scenario:
Initial fill:
INSERT INTO `hash_group` (id, group_name) VALUES
(1, 'test1'),
(2, 'test2'),
(3, 'test3');
INSERT INTO `hashes` (hash, hash_group_id) VALUES
('hash11', 1),
('hash12', 1),
('hash13', 1),
('hash2', 2),
('hash3', 3);
INSERT INTO `emails` (hash_group_id, group_id, email)
VALUES
(1, 'g1', 'example1#'),
(2, 'g1', 'example2#'),
(3, 'g1', 'example2#');
Updating of hash_group scenario:
START TRANSACTION;
-- Get #min_group_id - minimum group id (we will leave this id and delete other)
SELECT MIN(hash_group_id) INTO #min_group_id
FROM hashes
WHERE hash IN ('hash11', 'hash12', 'hash2', 'hash15');
-- Replace other group ids in email table to #min_group_id
UPDATE `emails`
SET `hash_group_id` = #min_group_id
WHERE `hash_group_id` IN (
SELECT hash_group_id
FROM hashes
WHERE #min_group_id IS NOT NULL
AND hash IN ('hash11', 'hash12', 'hash2', 'hash15')
-- Update only if we are gluy several hash_groups
AND `hash_group_id` > #min_group_id
);
-- Delete other hash_groups and leave only group with #min_group_id
DELETE FROM `hash_group` WHERE `id` IN (
SELECT hash_group_id
FROM hashes
WHERE #min_group_id IS NOT NULL
AND hash IN ('hash11', 'hash12', 'hash2', 'hash15')
-- Delete only if we are gluy several hash_groups
AND `hash_group_id` > #min_group_id
);
-- #group_id = existing hash_group.id or create new if #min_group_id is null (all inserted hashes are new)
SELECT get_hash_group(#min_group_id) INTO #group_id;
-- Now we can insert new hashes.
INSERT INTO `hashes` (hash, hash_group_id) VALUES
('hash11', #group_id),
('hash12', #group_id),
('hash2', #group_id),
('hash15', #group_id)
ON DUPLICATE KEY
UPDATE repeat_count = repeat_count + 1;
COMMIT;
I maybe wrong but I think you mis-named the id field in hashes.
I think you should rename the id field in hashes to something like group_id, then have a AUTO_INCREMENT field called id that should also be PRIMARY in hashes that the id in emails refers to this field instead. When you want to update and relate all the hashes together, you update the group_id field instead of id, and id remains unique across the table.
This way you can avoid the cascade problem, also you will always know the original hash that the email was referring to.
Sure, if you want to fetch all the hashes related to an email (old and the new) you must exectue and extra query, but I think it solves all your problems.
Edit:
you can use a trigger to do this
The trigger goes like this
DELIMITER $$
CREATE TRIGGER `update_hash_id` AFTER UPDATE ON `hashes`
FOR EACH ROW
BEGIN
UPDATE `emails` SET `id` = NEW.id WHERE `id` = OLD.id;
END;$$
DELIMITER ;
and you must remove the foreign key relation too.
The solution, which we have arrived in chat chat:
/* Tables */
CREATE TABLE `emails` (
`group_id` bigint(20) NOT NULL,
`email` varchar(500) NOT NULL,
UNIQUE KEY `group_id` (`group_id`,`email`) USING BTREE,
CONSTRAINT `emails_ibfk_1` FOREIGN KEY (`group_id`) REFERENCES `entities` (`group_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=latin1
CREATE TABLE `hashes` (
`group_id` bigint(20) NOT NULL,
`hash` varchar(128) NOT NULL,
`repeat_count` int(11) NOT NULL DEFAULT '0',
UNIQUE KEY `hash` (`hash`),
KEY `group_id` (`group_id`),
CONSTRAINT `hashes_ibfk_1` FOREIGN KEY (`group_id`) REFERENCES `entities` (`group_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=latin1
CREATE TABLE `entities` (
`group_id` bigint(20) NOT NULL,
`entity_id` bigint(20) NOT NULL,
PRIMARY KEY (`group_id`),
KEY `entity_id` (`entity_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
CREATE TABLE `entity_lookup` (
`entity_id` bigint(20) NOT NULL,
PRIMARY KEY (`entity_id`) USING HASH
) ENGINE=MyISAM DEFAULT CHARSET=latin1
/* Inserting */
START TRANSACTION;
/* Determine next group ID */
SET #next_group_id = (SELECT MAX(group_id) + 1 FROM entities);
/* Determine next entity ID */
SET #next_entity_id = (SELECT MAX(entity_id) + 1 FROM entities);
/* Merge any entity ids */
INSERT IGNORE INTO entity_lookup SELECT entity_id FROM entities JOIN hashes USING(group_id) WHERE HASH IN(...);
UPDATE entities JOIN entity_lookup USING(entity_id) SET entity_id = #next_entity_id;
TRUNCATE TABLE entity_lookup;
/* Add the new group ID to entity_id */
INSERT INTO entities(group_id, entity_id) VALUES(#next_group_id, #next_entity_id);
/* Add new values into hashes */
INSERT INTO hashes (group_id, HASH) VALUES
(#next_group_id, ...)
ON DUPLICATE KEY UPDATE
repeat_count = repeat_count + 1;
/* Add other new values */
INSERT IGNORE INTO emails (group_id, email) VALUES
(#next_group_id, "email1");
COMMIT;
Adding an extra integer column to each of the child tables would avoid this problem altogether by using it as a primary key. The key never changes because it isn't a reference to anything else.
Using composite keys as primary keys is generally something that you want to avoid. And considering that this key combination is not always unique, I would definitely say you need a dedicated primary key in all of your child tables with this problem.
You can even auto increment it so you aren't manually assigning it every time. For example..
Create Table exampleTable
(
trueID int NOT NULL AUTO_INCREMENT,
col1 int NOT NULL,
col2 varChar(50)
PRIMARY KEY(trueID)
)
Then, when two of the rows in a child table are set with identical values (for whatever reason), the primary key stays unique, preventing any conflicts in the Database that could arise.

ON DUPLICATE KEY UPDATE refuses to update

Having some troubles with ON DUPLICATE KEY UPDATE in MySQL. Below is the query im trying to run.
INSERT INTO `Overall` ( `rsn` , `starting_xp` , `starting_lvl` ) VALUES ( 'iWader' , '195843626' , '2281' ) ON DUPLICATE KEY UPDATE `current_xp` = '195843626' AND `current_lvl` = '2281'
It inserts fine, but when there is a duplicate it doesnt update, and doesnt throw any errors.
Running the query through PMA returns no error and doesnt update
Removing the ON DUPLICATE KEY UPDATE section returns a duplicate key error
This is the structure of my table
CREATE TABLE IF NOT EXISTS `overall` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`rsn` varchar(12) NOT NULL,
`starting_xp` int(10) unsigned NOT NULL,
`starting_lvl` int(10) unsigned NOT NULL,
`current_xp` int(10) unsigned NOT NULL,
`current_lvl` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `rsn` (`rsn`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=1;
After ON DUPLICATE KEY UPDATE you should not need to use and with the fields, use , instead.
ON DUPLICATE KEY UPDATE `current_xp` = '195843626', `current_lvl` = '2281'
Try this:
INSERT INTO `Overall` ( `rsn` , `starting_xp` , `starting_lvl` ) VALUES ( 'iWader' , '195843626' , '2281' ) ON DUPLICATE KEY UPDATE `current_xp` = '195843626', `current_lvl` = '2281';
The AND in your UPDATE clause is wrong. The AND is used in boolean expressions like "is foo true AND bar true?"
Here you want to update column current_xp, current_lvl.