While creating threadsafe sequence in mysql, I came across mysql documentation - https://dev.mysql.com/doc/refman/5.6/en/information-functions.html#function_last-insert-id, suggesting to create sequence like this -
1. mysql> CREATE TABLE sequence (id INT NOT NULL);
2. mysql> INSERT INTO sequence VALUES (0);
3. mysql> UPDATE sequence SET id=LAST_INSERT_ID(id+1);
4. mysql> SELECT LAST_INSERT_ID();
My question is instead of step 4, why can't i query sequence table directly to get the id every time i need to fetch a new id? Like this - SELECT id FROM sequence; Is there any downside of directly querying the sequence table vs LAST_INSERT_ID as suggested in docs?
My mysql implementation looks like this -
CREATE TABLE sequence (id INT NOT NULL);
INSERT INTO sequence VALUES (0);
DELIMITER //
CREATE PROCEDURE nextVal(OUT nextval INT)
BEGIN
UPDATE sequence SET id=LAST_INSERT_ID(id+1);
SELECT id into nextval FROM sequence;
END //
DELIMITER ;
And to generate new id, i can use this
call nextVal(#output1);
select #output1;
Edit1: Updating sequence creation process, to be lock free, after talking to all folks that replied. I have also made the table more generic to accommodate multiple sequences in single table and used function instead of procedure
CREATE TABLE sequences (
name CHAR(20),
id BIGINT DEFAULT 1,
increment TINYINT,
UNIQUE KEY(name)
);
/* Call nextval('seqname'), and it returns the next value. */
/* If the named sequence does not yet exist, it is created with initial value 1 and increment 1 */
DELIMITER //
CREATE FUNCTION nextval (seqname CHAR(20))
RETURNS BIGINT
BEGIN
INSERT INTO sequences(name,id,increment) VALUES (seqname,LAST_INSERT_ID(1),1)
ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id+increment);
RETURN LAST_INSERT_ID();
END
//
/* Testing */
SELECT nextval('seq1');
SELECT nextval('seq2');
insert into sequences(name,id,increment) values ('seq3', 1000, 5);
SELECT nextval('seq3');
Your procedure won't work if another client runs it at the same time, because they're both updating the same row of the sequence table. You'd need to run both the UPDATE and SELECT in a transaction to prevent overlap.
LAST_INSERT_ID() is managed on a per-connection basis, so each client gets their own sequence, without having to lock each other out with transactions.
Related
I have a program in Laravel where after users register they need a badge number, I want that number to be generated randomly after they register in the database. I should use triggers but I struggle with syntax.
users table
id bigint(20)
name varchar(255)
surname varchar(255)
nr_legitimatie varchar(255)
I want that 'nr_legitimatie' field to be unique.
This is what I tried but with no success
Trigger
CREATE OR REPLACE TRIGGER numar_leg
AFTER INSERT ON users
FOR EACH ROW
DECLARE
legitimatie VARCHAR(191)
BEGIN
legitimatie =('
SELECT FLOOR(RAND() * 99999) AS random_num
FROM numbers_mst
WHERE "random_num" NOT IN (SELECT my_number FROM numbers_mst)
LIMIT 1' );
set `users`.`nr_legitimatie` = legitimatie;
END;
Here's an example of a MySQL BEFORE INSERT trigger that assigns a value to the nr_legitimatie column.
DELIMITER $$
DROP TRIGGER numar_leg$$
CREATE TRIGGER numar_leg
BEFORE INSERT ON users
FOR EACH ROW
BEGIN
DECLARE li_nrn BIGINT DEFAULT NULL;
DECLARE li_cnt BIGINT DEFAULT 1;
WHILE li_cnt > 0 DO
-- generate a new random number
SELECT FLOOR(RAND()*99999) AS nrn INTO li_nrn;
-- check if the new random number is already used
SELECT COUNT(1) INTO li_cnt FROM users u WHERE u.nr_legitimatie = li_nrn;
END WHILE;
SET NEW.nr_legitimatie := li_nrn;
END$$
DELIMITER ;
Note that this does not guarantee that the value assigned to the nr_legitimatie will be unique, because the code in the trigger is subject to a race condition. There is potential for two (or more) simultaneous sessions to each discover the same random number is not yet "unused", and each session will use it. (The check for an existing value precedes the assignment to the column.)
If we want to guarantee uniqueness, we should add a UNIQUE constraint (UNIQUE KEY) on the column in the users table.
We can also use a separate table to track the numbers that are used, with a UNIQUE constraint on the column, we can attempt inserts, and catch the error when an attempt to insert a duplicate is made.
If we introduce a tracking table, e.g.
CREATE TABLE nrn (nrn BIGINT PRIMARY KEY) ;
Then we can avoid the race condition, making the test for existing duplicate and reservation of the new value at the same time. Something like this:
DELIMITER $$
CREATE TRIGGER numar_leg
BEFORE INSERT ON users
FOR EACH ROW
BEGIN
DECLARE li_nrn BIGINT DEFAULT NULL;
DECLARE li_dup BIGINT DEFAULT 1;
DECLARE CONTINUE HANDLER FOR 1062 SET li_dup := 1;
WHILE li_dup > 0 DO
SELECT FLOOR(RAND()*99999) AS nrn INTO li_nrn;
SET li_dup := 0;
INSERT INTO nrn (nrn) VALUES (li_nrn);
END WHILE;
SET NEW.nr_legitimatie := li_nrn;
END$$
DELIMITER ;
The edge case here is the trigger is executed, a new random number is generated and reserved, but the insert into the users table fails for some reason, and we don't issue a ROLLBACK. If we issue a ROLLBACK, then our new random number reservation will also be rolled back (unless nrn is a MyISAM table).
MySQL has LAST_INSERT_ID(); function which gets the last insert.
But this is not safe: If i run some query then get LAST_INSERT_ID() and between the two another query is executed I can get the wrong id.
This can happen in multiple threads using the same connection, or using pconnect (persistend connection for multiple users)
Is there safe method for getting the ID that i want 100% ?
Thanks
Store procedure may help in the case:
Create table test (id int AUTO_INCREMENT primary key, name varchar(50) )
Store procedure as:
delimiter $$
DROP PROCEDURE IF EXISTS InserData$$
CREATE PROCEDURE InserData(IN _Name VARCHAR(50))
BEGIN
START TRANSACTION;
INSERT INTO test(name) VALUES (_Name);
SELECT LAST_INSERT_ID() AS InsertID;
COMMIT;
END
Call the stored procedure using
CALL InserData('TESTER')
Give it a try as we have transaction statement but it can't ensure the value in multi threaded environment.
The link Mysql thread safety of last_insert_id explain it will work based on per connection model.
Is using SELECT Max(ID) FROM table safer than using SELECT last_insert_id(), where they run as 2 separate queries?
According to your question the table must have a primary key.
So you can get last record from MAX(ID)
I need a sequential number sequence for the rows in a table and I need to ensure that it is always sequential with no gaps on insert , when deleted I can leave the row gap, but on insert I must fill the gaps with the new rows. The reason for this is a different system must line up one for one with the row records. Yet the db can be manipulated by others in both the sql end and also via an application ; I am thinking a trigger will allow me to accomplish the something changed part - but how to actually determine if I have gaps and perform the insert of this sequence number - even if I have to maintain the deleted sequences in a separate table and manage is fine - I am required to line up one for one with this other system no matter how the table gets manipulated .
Auto Increment field will not work as a row gets deleted the next insert will be the last auto Increment value. I would need an insert at .. or perhaps keep the row and add a field IsDeleted and force the table as read only or no more inserts / deletes ..but how to do that?
Perhaps when row is inserted I could set sequence number at gap if found or at end if not.
Does somebody have experience doing this kind of thing ?
I know there is a lot here. I tried to document it rather well inside the code and here and there. It uses Stored Procedures. You can naturally pull the code out and not use that method. It uses a main table that houses next available incrementors. It uses safe INNODB Intention Locks for concurrency. It has a reuse table and stored procs to support it.
It does not in anyway use the table myTable. It is shown there for your own imagination based on comments under your question. The summary of that is that you know that you will have gaps upon DELETE. You want some orderly fashion to reuse those slots, those sequence numbers. So, when you DELETE a row, use the stored procs accordingly to add that number. Naturally there is a stored proc to get the next sequence number for reuse and other things.
For the purposes of testing, your sectionType = 'devices'
And best of all it is tested!
Schema:
create table myTable
( -- your main table, the one you cherish
`id` int auto_increment primary key, -- ignore this
`seqNum` int not null, -- FOCUS ON THIS
`others` varchar(100) not null
) ENGINE=InnoDB;
create table reuseMe
( -- table for sequence numbers to reuse
`seqNum` int not null primary key, -- FOCUS ON THIS
`reused` int not null -- 0 upon entry, 1 when used up (reused)
-- the primary key enforces uniqueness
) ENGINE=InnoDB;;
CREATE TABLE `sequences` (
-- table of sequence numbers system-wide
-- this is the table that allocates the incrementors to you
`id` int NOT NULL AUTO_INCREMENT,
`sectionType` varchar(200) NOT NULL,
`nextSequence` int NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `sectionType` (`sectionType`)
) ENGINE=InnoDB;
INSERT sequences(sectionType,nextSequence) values ('devices',1); -- this is the focus
INSERT sequences(sectionType,nextSequence) values ('plutoSerialNum',1); -- not this
INSERT sequences(sectionType,nextSequence) values ('nextOtherThing',1); -- not this
-- the other ones are conceptuals for multi-use of a sequence table
Stored Proc: uspGetNextSequence
DROP PROCEDURE IF EXISTS uspGetNextSequence;
DELIMITER $$
CREATE PROCEDURE uspGetNextSequence(p_sectionType varchar(200))
BEGIN
-- a stored proc to manage next sequence numbers handed to you.
-- driven by the simple concept of a name. So we call it a section type.
-- uses SAFE INNODB Intention Locks to support concurrency
DECLARE valToUse INT;
START TRANSACTION;
SELECT nextSequence into valToUse from sequences where sectionType=p_sectionType FOR UPDATE;
IF valToUse is null THEN
SET valToUse=-1;
END IF;
UPDATE sequences set nextSequence=nextSequence+1 where sectionType=p_sectionType;
COMMIT; -- get it and release INTENTION LOCK ASAP
SELECT valToUse as yourSeqNum; -- return as a 1 column, 1 row resultset
END$$
DELIMITER ;
-- ****************************************************************************************
-- test:
call uspGetNextSequence('devices'); -- your section is 'devices'
After you call uspGetNextSequence() it is your RESPONSIBILITY to ensure that that sequence #
is either added into myTable (by confirming it), or that if it fails, you insert it into
the reuse table with a call to uspAddToReuseList(). Not all inserts succeed. Focus on this part.
Because with this code you cannot "put" it back into the sequences table because of
concurrency, other users, and the range already passed by. So, simply, if the insert fails,
put the number into reuseMe via uspAddToReuseList()
.
.
.
Stored Proc: uspAddToReuseList:
DROP PROCEDURE IF EXISTS uspAddToReuseList;
DELIMITER $$
CREATE PROCEDURE uspAddToReuseList(p_reuseNum INT)
BEGIN
-- a stored proc to insert a sequence num into the reuse list
-- marks it available for reuse (a status column called `reused`)
INSERT reuseMe(seqNum,reused) SELECT p_reuseNum,0; -- 0 means it is avail, 1 not
END$$
DELIMITER ;
-- ****************************************************************************************
-- test:
call uspAddToReuseList(701); -- 701 needs to be reused
Stored Proc: uspGetOneToReuse:
DROP PROCEDURE IF EXISTS uspGetOneToReuse;
DELIMITER $$
CREATE PROCEDURE uspGetOneToReuse()
BEGIN
-- a stored proc to get an available sequence num for reuse
-- a return of -1 means there aren't any
-- the slot will be marked as reused, the row will remain
DECLARE retNum int; -- the seq number to return, to reuse, -1 means there isn't one
START TRANSACTION;
-- it is important that 0 or 1 rows hit the following condition
-- also note that FOR UPDATE is the innodb Intention Lock
-- The lock is for concurrency (multiple users at once)
SELECT seqNum INTO retNum
FROM reuseMe WHERE reused=0 ORDER BY seqNum LIMIT 1 FOR UPDATE;
IF retNum is null THEN
SET retNum=-1;
ELSE
UPDATE reuseMe SET reused=1 WHERE seqNum=retNum; -- slot used
END IF;
COMMIT; -- release INTENTION LOCK ASAP
SELECT retNum as yoursToReuse; -- >0 or -1 means there is none
END$$
DELIMITER ;
-- ****************************************************************************************
-- test:
call uspGetOneToReuse();
Stored Proc: uspCleanReuseList:
DROP PROCEDURE IF EXISTS uspCleanReuseList;
DELIMITER $$
CREATE PROCEDURE uspCleanReuseList()
BEGIN
-- a stored proc to remove rows that have been successfully reused
DELETE FROM reuseMe where reused=1;
END$$
DELIMITER ;
-- ****************************************************************************************
-- test:
call uspCleanReuseList();
Stored Proc: uspOoopsResetToAvail:
DROP PROCEDURE IF EXISTS uspOoopsResetToAvail;
DELIMITER $$
CREATE PROCEDURE uspOoopsResetToAvail(p_reuseNum INT)
BEGIN
-- a stored proc to deal with a reuse attempt (sent back to you)
-- that you need to reset the number as still available,
-- perhaps because of a failed INSERT when trying to reuse it
UPDATE reuseMe SET reused=0 WHERE seqNum=p_reuseNum;
END$$
DELIMITER ;
-- ****************************************************************************************
-- test:
call uspOoopsResetToAvail(701);
Workflow ideas:
Let GNS mean a call to uspGetNextSequence().
Let RS mean Reuse Sequence via a call to uspGetOneToReuse()
When a new INSERTis desired, call RS:
A. If RS returns -1 then nothing is to be reused so call GNS which returns N. If you can successfully INSERT with myTable.seqNum=N with a confirm, you are done. If you cannot successfully INSERT it, then call uspAddToReuseList(N).
B. If RS returns > 0, note in your head that slot has reuseMe.reused=1, a good thing to remember. So it is assumed to be in the process of being successfully reused. Let's call that sequence number N. If you can successfully INSERT with myTable.seqNum=N with a confirm, you are done. If you cannot successfully INSERT it, then call uspOoopsResetToAvail(N).
When you deem it safe to call uspCleanReuseList() do so. Adding a DATETIME to the reuseMe table might be a good idea, denoting when a row from myTable was orignally deleting and causing the reuseMe row to get its original INSERT.
In my schema I had two columns ,I am new to MySQL how to concatenate columns using Computed Column in mysql
ordertbl schema
Autoorderid int auto-increment
Orderid varchar(45)
I want to value orderid value insert when new row inserted
i had created the trigger for but this trigger is not firing when i am inserting the row
insert into order_master(customerID,ItemID,quantity,Unitprice,Total) values ('07961A','1000',6,5,30)
use ntc_sales; DELIMITER
$$ CREATE TRIGGER order_master_BINS BEFORE INSERT ON order_master FOR EACH ROW
SET NEW.OrderID = CONCAT('ORD' ,NEW.AutoOrderID) ; end;
create table
enter code here
CREATE TABLE Demo(ID INT PRIMARY KEY,
IDwithChar AS 'ORD' + RIGHT('0000000' + CAST(ID AS VARCHAR(10)), 6) PERSISTED,valuevar nvarchar(45)
)
This is showing syntax error in mysql how to create table as above schema in mysql
You can't really do this using a trigger in MySQL.
A BEFORE trigger is run before the auto increment value is set, so NEW.AutoOrderID will always be 0.
An AFTER trigger cannot update the same table as it is triggered on.
In other words, instead of triggering on an INSERT, creating the row using a stored procedure that does the insert/update in a single transaction is probably the best way to proceed.
EDIT: You could use a separate table for the sequence, but that would not guarantee that the order number is the same value as AutoOrderID, just a unique value.
CREATE TABLE order_sequence (
seq INT PRIMARY KEY AUTO_INCREMENT
);
CREATE TRIGGER order_master_BINS BEFORE INSERT ON order_master
FOR EACH ROW
BEGIN
INSERT INTO order_sequence VALUES (0);
SET NEW.OrderID = CONCAT('ORD', LAST_INSERT_ID());
END
An SQLfiddle to test the alternate solution with.
I am trying to create a trigger (this is my first trigger, and question, so be gentle) that will insert new rows into two different tables.
* Edit *
Adding this in as I forgot to mention it until ypercube answered.
I am trying to avoid listing all of the column names, as in the real world usage the table this will be used on has a very large number of columns (not my design, too late to refactor).
* End Edit *
Here's what I have so far.
CREATE TABLE test_table (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
message VARCHAR(255)
);
CREATE TABLE test_table_copy LIKE test_table;
DELIMITER $$
CREATE TRIGGER copy_test_table_data AFTER INSERT ON test_table
FOR EACH ROW
BEGIN
INSERT INTO test_table_copy SELECT * FROM NEW;
END;$$
DELIMITER ;
Unfortunately this results in an error.
mysql> INSERT INTO test_table VALUES (1, 'This is a message');
ERROR 1146 (4502): Table 'test_db.NEW' doesn't exist
I am not quite sure what the problem is, I thought NEW referred the table data was being inserted into?
You could possibly get a list of column names in that table from information_schema views, then use them to create a prepared statement (using cursor to iterate column names) and CONCAT() function to glue together the query string. Then execute the prepared statement.
Seems very contrived even to me, and I'm not sure it would work (and if it did, how efficient it would be)