2

MySQL provides an automatic mechanism to increment record IDs. This is OK for many purposes, but I need to be able to use sequences as offered by ORACLE. Obviously, there is no point in creating a table for that purpose.

The solution SHOULD be simple:

1) Create a table to hosts all the needed sequences,

2) Create a function that increases the value of a specific sequence and returns the new value,

3) Create a function that returns the current value of a sequence.

In theory, it looks simple... BUT...

When increasing the value of a sequence (much the same as nextval in Oracle), you need to prevent other sessions to perform this operation (or even fetch the current value) till the updated is completed.

Two theoretical options:

a - Use an UPDATE statement that would return the new value in a single shot, or

b - Lock the table between the UPDATE and SELECT.

Unfortunately, it would appear that MySQL does not allow to lock tables within functions / procedures, and while trying to make the whole thing in a single statement (like UPDATE... RETURNING...) you must use @-type variables which survive the completion of the function/procedure.

Does anyone have an idea/working solution for this?

Thanks.

FDavidov
  • 3,505
  • 6
  • 23
  • 59
  • Give an example of what is in your mind as a sequence to make that clear. And explain why you simply can't do this yourself with a stored proc. – Drew Jun 28 '16 at 13:41

1 Answers1

7

The following is a simple example with a FOR UPDATE intention lock. A row-level lock with the INNODB engine. The sample shows four rows for next available sequences that will not suffer from the well-known INNODB Gap Anomaly (the case where gaps occur after failed usage of an AUTO_INCREMENT).

Schema:

-- drop table if exists sequences;
create table sequences
(   id int auto_increment primary key,
    sectionType varchar(200) not null,
    nextSequence int not null,
    unique key(sectionType)
) ENGINE=InnoDB;

-- truncate table sequences;
insert sequences (sectionType,nextSequence) values
('Chassis',1),('Engine Block',1),('Brakes',1),('Carburetor',1);

Sample code:

START TRANSACTION; -- Line1
SELECT nextSequence into @mine_to_use from sequences where sectionType='Carburetor' FOR UPDATE; -- Line2 
select @mine_to_use; -- Line3
UPDATE sequences set nextSequence=nextSequence+1 where sectionType='Carburetor'; -- Line4
COMMIT; -- Line5

Ideally you do not have a Line3 or bloaty code at all which would delay other clients on a Lock Wait. Meaning, get your next sequence to use, perform the update (the incrementing part), and COMMIT, ASAP.

The above in a stored procedure:

DROP PROCEDURE if exists getNextSequence;
DELIMITER $$
CREATE PROCEDURE getNextSequence(p_sectionType varchar(200),OUT p_YoursToUse int)
BEGIN
    -- for flexibility, return the sequence number as both an OUT parameter and a single row resultset
    START TRANSACTION;
    SELECT nextSequence into @mine_to_use from sequences where sectionType=p_sectionType FOR UPDATE;
    UPDATE sequences set nextSequence=nextSequence+1 where sectionType=p_sectionType;
    COMMIT; -- get it and release INTENTION LOCK ASAP
    set p_YoursToUse=@mine_to_use; -- set the OUT parameter
    select @mine_to_use as yourSeqNum; -- also return as a 1 column, 1 row resultset
END$$
DELIMITER ;

Test:

set @myNum:= -1;
call getNextSequence('Carburetor',@myNum);
+------------+
| yourSeqNum |
+------------+
|          4 |
+------------+
select @myNum; -- 4

Modify the stored procedure accordingly for you needs, such as having only 1 of the 2 mechanisms for retrieving the sequence number (either the OUT parameter or the result set). In other words, it is easy to ditch the OUT parameter concept.

If you do not adhere to ASAP release of the LOCK (which obviously is not needed after the update), and proceed to perform time consuming code, prior to the release, then the following can occur after a timeout period for other clients awaiting a sequence number:

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

Hopefully this is never an issue.

show variables where variable_name='innodb_lock_wait_timeout';

MySQL Manual Page for innodb_lock_wait_timeout.

On my system at the moment it has a value of 50 (seconds). A wait of more than a second or two is probably unbearable in most situations.

Also of interest during TRANSACTIONS is that section of the output from the following command:

SHOW ENGINE INNODB STATUS;
Drew
  • 24,851
  • 10
  • 43
  • 78
  • Hello @Drew!!! Thanks for the answer (wasn't aware of the SELECT FOR UPDATE option). There is one thing I don't get: Why do you need the session variable `@mine_to_use` if you could set it as a FUNCTION that returns the new sequence value? I mean, affecting a global variable would be an undesirable (and potentially harmful) side-effect. Wouldn't you agree? – FDavidov Jun 29 '16 at 05:47
  • I program in stored procedures for things like this. I normally don't think of a function but for small calculations. This is for a transaction and I steer for the procs. It is not a global variable. It is merely a User Variable as opposed to a Local Variable (which is a DECLARE). – Drew Jun 29 '16 at 05:54
  • Here is the Manual Page on [User Variables](http://dev.mysql.com/doc/refman/5.7/en/user-variables.html) ... they are safe. A quote: `User-defined variables are session-specific. A user variable defined by one client cannot be seen or used by other clients.` – Drew Jun 29 '16 at 06:00
  • Ah. I see. I would normally agree with you (i.e. inserts/updates/selects belong to PROCEDURES), except that in this case it would be a **generic set of services** (i.e. create sequence, drop sequence, get sequence current value, get sequence next value, etc.) and therefor (in my taste at least) it merits being a set of functions. As I said, a matter of taste. Many thanks for your recommendation. up-voted of course. – FDavidov Jun 29 '16 at 06:02
  • Hierarchy? What do you mean? – FDavidov Jun 29 '16 at 06:04
  • I have so many comments going right now, my mind is on a self-join with someone. Sorry, no hierarchy my bad. – Drew Jun 29 '16 at 06:04
  • If you need that to go into a Local Variable with a Declare, you could do that to. So often only user variables (@'s) can be used with a Prepare and Execute, that people tend to steer toward them because a Declare is not mandated, and Locals don't work with Prepares (not that that is your issue). – Drew Jun 29 '16 at 06:07
  • Ah!!! OK, got it. In my case, I **ALWAYS** declare variables even if not needed. This is a rule I learned long ago when Software Engineering was something people care for :-). Again, many thanks sir!! – FDavidov Jun 29 '16 at 06:11
  • For instance the following won't work with a Local (DECLARE) [here](http://dev.mysql.com/doc/refman/5.7/en/sql-syntax-prepared-statements.html) ... and the manual says nothing about it explicitly and many people here waste a ton of time until they ask for help. And because I am often in tricky cross joins that require user variables (@'s), I tend to mix and match and again they are required like [here](http://stackoverflow.com/a/38000571) – Drew Jun 29 '16 at 06:14
  • Yes, I already BURNED myself with that when trying to use dynamic SQL within functions/procedures (don't remember which one). – FDavidov Jun 29 '16 at 06:20