What is the best method to atomically update a row if exists, else insert a new row into an table?
e.g. say we have this example below.
CREATE TABLE Test(
id integer,
val varchar(255)
)
DELIMITER $$
DROP PROCEDURE IF EXISTS `TestUpsert` $$
CREATE PROCEDURE `TestUpsert` (in value1 varchar(255), in id1 int)
BEGIN
IF EXISTS(Select 1 from Test where id=id1) THEN
UPDATE Test set val = value1 where id = id1;
ELSE
insert into Test(id, val) VALUES (id1, value1);
END IF;
END $$
DELIMITER ;
What changes can we make to TestUpsert to make it atomic?
Use on duplicate key update:
CREATE PROCEDURE `TestUpsert` (
in in_value varchar(255),
in in_id int
)
BEGIN
insert into test(id, val)
values (in_id, in_val)
on duplicate key update val = in_val;
END $$
I should add that for this to work, test.id has to be declared as the primary key or unique.
I recommend you read about INSERT ... ON DUPLICATE KEY UPDATE.
See my lengthy answer about this statement here: https://stackoverflow.com/a/548570/20860
Related
Here is a sequence of very simple sql queries (MySQl 8.0)
drop table IF EXISTS stack_Compare;
drop table IF EXISTS lastNode;
CREATE TABLE stack_Compare (Station VARCHAR(20), Track INT);
CREATE TABLE lastNode (Last_Station_ID INT, I INT);
AlTER table lastNode ADD PRimary key (I);
INSERT INTO lastNode VALUES (1, 1);
Drop trigger IF EXISTS lastAdded;
DELIMITER $$
CREATE TRIGGER lastAdded AFTER INSERT
ON stack_Compare FOR EACH ROW
BEGIN
UPDATE lastNode SET Last_Station_ID = (SELECT Station.Station_ID FROM Station, stack_Compare WHERE New.Station = Station.Name) WHERE I = 1;
END $$
DELIMITER ;
DROP PROCEDURE IF EXISTS insertStation;
DROP PROCEDURE IF EXISTS insertMain;
DELIMITER $$
SET max_sp_recursion_depth=3;
CREATE PROCEDURE insertStation (insertstationID INT, starter VARCHAR(20), destination VARCHAR(20))
this_procedure:BEGIN
DECLARE nextNode INT DEFAULT NULL;
INSERT INTO stack_Compare (Station)
SELECT Name from Station WHERE Station_ID = insertstationID;
IF insertStationID IN (SELECT Station_ID FROM Station WHERE Name = destination)
THEN LEAVE this_procedure;
END IF;
SET nextNode = (SELECT Next_Station FROM Station WHERE Station_ID = insertstationID);
IF nextNode IS NULL
THEN LEAVE this_procedure;
END IF;
IF nextNode = -1
THEN LEAVE this_procedure;
ELSE CALL insertStation (nextNode, starter, destination);
END IF;
END
$$
CREATE PROCEDURE insertMain (starter VARCHAR(20), destination VARCHAR(20))
BEGIN
DECLARE nextNode INT DEFAULT NULL;
CALL insertStation(8, starter, destination);
loop1: LOOP
SET nextNode = (SELECT Station.Next_Station FROM Station, lastNode WHERE Station.Station_ID = lastNode.Last_Station_ID);
IF nextNode IN (SELECT Station_ID FROM Station WHERE Name = destination)
THEN LEAVE loop1;
END IF;
END LOOP loop1;
END;
$$
DELIMITER ;
CALL insertMain('G1','RG');
SELECT * FROM stack_Compare;
select * from lastNode;
and the station table is
So basically the query SELECT * FROM stackCompare should give
Station
G2
RG
But it only shows G2 in there and the error is Subquery returns more than 1 row
Which I am unable to find WHY from past few hours and a big project is stuck because of this. I guess after doing many experiments that there can be a problem with the Set nextNode line.
below are the queries to create and fill table.
CREATE TABLE Station (
Station_ID INT,
Name VARCHAR(20),
Line_ID INT,
Prev_Station INT,
Next_Station INT,
Hybrid_ID INT,
PRIMARY KEY (Station_ID),
FOREIGN KEY (Line_ID) REFERENCES Line(Line_ID),
FOREIGN KEY (Hybrid_ID) REFERENCES Hybrid(Hybrid_ID)
);
ALTER TABLE Station ADD FOREIGN KEY (Prev_Station) REFERENCES Station(Station_ID);
ALTER TABLE Station ADD FOREIGN KEY (Next_Station) REFERENCES Station(Station_ID);
INSERT INTO Station VALUES
(1,'R1',1,NULL,2,NULL),
(2,'R2',1,1,3,NULL),
(3,'RG',1,-1,-1,1),
(4,'RY',1,-1,-1,2),
(5,'R3',1,4,6,NULL),
(6,'R4',1,5,NULL,NULL),
(7,'G1',3,NULL,8,NULL),
(8,'G2',3,7,3,NULL),
(9,'Y1',2,NULL,4,NULL),
(10,'Y2',2,4,11,NULL),
(11,'Y3',2,10,NULL,NULL);
In my MySQL Database, I have a table with a composite primary key where the ID is not in auto_increment mode. Something like this :
CREATE TABLE table_a (
fk_table_b INT UNSIGNED NOT NULL,
id INT UNSIGNED,
label VARCHAR(80) NOT NULL,
PRIMARY KEY (fk_table_b, id),
FOREIGN KEY fk_table_b
REFERENCES table_b(id)
);
To increment the ID in function of the foreign key, I made a trigger like this :
DELIMITER $$
CREATE TRIGGER table_a_auto_increment
BEFORE INSERT ON table_a
FOR EACH ROW BEGIN
SET NEW.id = (
SELECT IFNULL(MAX(id), 0) + 1
FROM table_a
WHERE table_a.fk_table_b = NEW.fk_table_b
);
END $$
DELIMITER ;
But when I do SELECT LAST_INSERT_ID() I am getting 0 as the new id ... Normally you could override the LAST_INSERT_ID() by giving it a number like this :
INSERT table_a ( fk_table_b, id)
VALUES (1, LAST_INSERT_ID(5));
SELECT LAST_INSERT_ID(); -- -> it gives me 5
So I have tried to combine both to do this trigger :
DELIMITER $$
CREATE TRIGGER table_a_auto_increment
BEFORE INSERT ON table_a
FOR EACH ROW BEGIN
SET NEW.id = (
SELECT LAST_INSERT_ID(IFNULL(MAX(id), 0) + 1)
FROM table_a
WHERE table_a.fk_table_b = NEW.fk_table_b
);
END $$
DELIMITER ;
But it's still giving me 0 when I insert something in the base ... Do you know if there is a way to make it work ?
Thanks a lot.
-- EDIT 2020-08-14
Finally it seems impossible to override the LAST_INSERT_ID function inside the TRIGGER, so I changed my solution by removing the trigger and doing it inside my insert function like this :
INSERT table_a ( fk_table_b, id, label)
VALUES (1, LAST_INSERT_ID((
SELECT IFNULL(MAX(old_one.id), 0) + 1
FROM table_a AS old_one
WHERE old_one.fk_table_b = table_a.fk_table_b
)), "something");
And then, this is giving me the good result I can use in my backend :)
You may use additional service table:
CREATE TABLE service_table (id BIGINT AUTO_INCREMENT PRIMARY KEY);
and
DELIMITER $$
CREATE TRIGGER table_a_auto_increment
BEFORE INSERT ON table_a
FOR EACH ROW
BEGIN
SET NEW.id = (
SELECT IFNULL(MAX(id), 0) + 1
FROM table_a
WHERE table_a.fk_table_b = NEW.fk_table_b
);
DELETE FROM service_table WHERE id IS NOT NULL;
INSERT INTO service_table VALUES (NEW.id - 1);
INSERT INTO service_table VALUES (NULL);
SET NEW.id = LAST_INSERT_ID() - 1;
END $$
DELIMITER ;
fiddle (foreign key removed).
Maybe the code may be simplified a little - do it yourself.
Service table may be defined as Engine = MEMORY (if available).
The code is not safe for concurrent inserts.
How would I go about inserting while incrementing and using LAST_INSERT_ID? I have a table and trigger as such:
CREATE TABLE A(
id int NOT NULL AUTO_INCREMENT,
name char(15),
PRIMARY KEY(id)
);
CREATE TABLE B(
id int NOT NULL,
name char(15),
FOREIGN KEY(id) REFERENCES A(id)
);
delimiter //
CREATE TRIGGER T
AFTER INSERT ON B
FOR EACH ROW
BEGIN
IF (NEW.name LIKE 'A') THEN
INSERT INTO A VALUES(LAST_INSERT_ID() + 1, 'A');
END IF;
END//
delimiter ;
I know that INSERT INTO B VALUES(LAST_INSERT_ID() + 1, 'A'); doesn't work if I have multiple inserts into B because LAST_INSERT_ID() when doing multiple-row inserts, LAST_INSERT_ID() will return the value of the first row inserted (not the last). How would I go about incrementing the ID so that LAST_INSERT_ID() returns the most recent ID from the inserts`
If you want to insert the id from B, then use the id column:
CREATE TRIGGER T
AFTER INSERT ON B
FOR EACH ROW
BEGIN
IF (NEW.name LIKE 'A') THEN
INSERT INTO A VALUES(B.ID + 1, 'A');
END IF;
END//
This seems like a curious expression. Perhaps your real intention is simply to have an auto incremented id on both tables.
I realized that I could insert into A without id since its auto-incremented.
I have 'users' table with the following fields:-
user_id (int, auto increment PK)
encrypted_userid (varchar 50)
user_name (varchar 50)
user_location (varchar 50)
What I want to do is create a trigger so that when values are inserted into the users table into user_name and user_location, i want to populate the encrypted_userid field with an AES_ENCRYPTED value from user_id - e.g. AES_ENCRYPT(user_id,'MYAESKEY') but only for the newly INSERTed row
Is this possible in MySQL with some kind of trigger?
Thanks in advance.
So is there a solution to my problem that will not fail etc? using a trigger - all other solutions i tried from reading other sources just didn't work.
Well all solutions revolve around LAST_INSERT_ID() because it's the only multi-user safe way to obtain auto generated ID.
First possible way, if you're very fond of triggers, is to have a separate table for auto generated sequences. Your schema will look like this
CREATE TABLE users_seq (user_id INT NOT NULL PRIMARY KEY AUTO_INCREMENT);
CREATE TABLE users
(
user_id INT NOT NULL PRIMARY KEY DEFAULT 1,
encrypted_userid varchar(50),
user_name varchar(50),
user_location varchar(50),
FOREIGN KEY user_id_fk (user_id) REFERENCES users_seq (user_id)
);
And the trigger
DELIMITER //
CREATE TRIGGER useridinserttrigger
BEFORE INSERT ON users
FOR EACH ROW
BEGIN
INSERT INTO users_seq() VALUES();
SET NEW.user_id = LAST_INSERT_ID(),
NEW.encrypted_userid = AES_ENCRYPT(LAST_INSERT_ID(), 'MYAESKEY');
END//
DELIMITER ;
Second way is to leverage your existing schema but use a stored procedure
DELIMITER //
CREATE PROCEDURE insert_user(IN _name VARCHAR(50), IN _location VARCHAR(50))
BEGIN
DECLARE _id INT;
START TRANSACTION;
INSERT INTO users (user_name, user_location) VALUES(_name, _location);
SET _id = LAST_INSERT_ID();
UPDATE users
SET encrypted_userid = AES_ENCRYPT(_id, 'MYAESKEY')
WHERE user_id = _id;
COMMIT;
END//
DELIMITER ;
Sample usage:
CALL insert_user('johndoe', null);
I solved my problem using MySQL - Trigger for updating same table after insert (JCLG's entry)
CREATE TRIGGER `useridinserttrigger` BEFORE INSERT ON `users`
FOR EACH ROW BEGIN
DECLARE tmpid,tmpid2 INT(11);
SELECT user_id INTO tmpid FROM users ORDER BY user_id DESC LIMIT 1;
SET tmpid2=tmpid+1;
SET new.encrypted_userid=AES_ENCRYPT(tmpid2,'MYAESKEY');
END;
I have such example:
CREATE TABLE a(
id INT PRIMARY KEY AUTO_INCREMENT,
parent_id INT,
FOREIGN KEY (parent_id) REFERENCES a(id)
);
DELIMITER ;;
CREATE TRIGGER a_insert BEFORE INSERT ON a
FOR EACH ROW
BEGIN
SIGNAL SQLSTATE '01431' SET MESSAGE_TEXT = 'The foreign data source you are trying to reference does not exist.';
END;;
DELIMITER ;
INSERT INTO a(parent_id) VALUES (NULL);
INSERT INTO a(parent_id) VALUES (1);
INSERT INTO a(parent_id) VALUES (2);
INSERT INTO a(parent_id) VALUES (4);
INSERT INTO a(parent_id) VALUES (999);
SELECT * FROM a
This end up with 4 recods:
----------------
id parent_id
----------------
1 NULL
2 1
3 2
4 4
I found post online that MySQL does not support rollbacks in triggers. That is a problem because I want such hierarchy where no row points to it self and inserts like the one with ID=4 works just fine. How do I ensure there are no such records in database?
Well, the problem is with auto_increment, because you in BEFORE INSERT event you don't have that value assigned yet. On the other hand in AFTER INSERT event you can't do anything with it.
If you want to use auto_increment id column a possible solution is to use separate table for sequencing.
Your schema would look like
CREATE TABLE a_seq(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY
);
CREATE TABLE a(
id INT NOT NULL PRIMARY KEY DEFAULT 0,
parent_id INT,
FOREIGN KEY (parent_id) REFERENCES a(id)
);
And your trigger
DELIMITER $$
CREATE TRIGGER a_insert
BEFORE INSERT ON a
FOR EACH ROW
BEGIN
INSERT INTO a_seq VALUES(NULL);
SET NEW.id = LAST_INSERT_ID();
IF NEW.id = NEW.parent_id THEN
SET NEW.id = NULL;
END IF;
END$$
DELIMITER ;
If id=parent_id the trigger deliberately violates NOT NULL constraint assigning NULL value. Therefore this record won't be inserted.
Here is SQLFiddle demo. Uncomment last insert statement. It won't allow you to make such insert.