Are mysql triggers atomic? Does this mysql trigger have concurrency issues? - mysql

I have created the following trigger to populate a field with a unique integer value.
I am using this in an InnoDB table and there is a UNIQUE key constraint on the field.
Is it possible that two concurrent inserts produce the same value and due to the unique constraint one of them fails, or are triggers "atomic"?
Are there any other issues with this code that I may not have thought of?
Is there a better whay to get the behaviour I want? Maby some soft of isomorphism on the auto increment value?
CREATE TRIGGER `generate_customer_id` BEFORE INSERT ON `users`
FOR EACH ROW BEGIN
DECLARE i INT;
DECLARE duplicate INT DEFAULT 1;
DECLARE tries INT DEFAULT 0;
WHILE duplicate > 0 DO
SET tries = tries + 1;
IF tries > 100 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'no customer id found after 100 tries', MYSQL_ERRNO = 1001;
END IF;
SET i = ROUND((RAND() * (999999999-100000000))+100000000);
SET duplicate = (SELECT COUNT(*) FROM users WHERE customer_id = i);
END WHILE;
SET NEW.customer_id = i;
END

A possible alternative to the random number generator is to create a customer_ids table with an auto_increment primary key - create a new row in the table to create a unique customer_id for the users table

Related

How to add unique random values to the column in mysql

I want to add 1000 unique random values to the column AgentID to the table agents.
I have used Upsert to ignore the duplicate values. What I have done so far:
DROP PROCEDURE IF EXISTS Generate_random;
DELIMITER $$
CREATE PROCEDURE Generate_random()
BEGIN
Drop table if exists ai_training.`Agents`;
CREATE TABLE ai_training.`Agents`(AgentID int PRIMARY KEY);
SET #first = 1;
SET #last = 1000;
WHILE(#first <= #last) Do
INSERT INTO ai_training.`Agents` VALUES(
FLOOR(RAND()*(2900000-2800000+1)+2800000)
)
ON DUPLICATE KEY UPDATE AgentID = FLOOR(RAND()*(2900000-2800000+1)+2800000);
SET #first = #first + 1;
END WHILE;
END$$
DELIMITER ;
CALL Generate_random();
Select * from ai_training.`Agents`;
The problem is I need 1000 unique agentid's and this code is generating 1000 - the repeated ones. So, if it finds 6 repeated ids it returns 994 rows and not 1000.
Is there any way I can achieve this?
Can't change the random ID generation part.
You could use information function ROW_COUNT() to check whether a row was actually inserted or not, and increment the counter accordingly.
The documentation states:
With ON DUPLICATE KEY UPDATE, the affected-rows value per row is 1 if the row is inserted as a new row, 2 if an existing row is updated, and 0 if an existing row is set to its current values.
So:
WHILE(#first <= #last) DO
INSERT INTO ai_training.`Agents`
VALUES(FLOOR(RAND()*(2900000-2800000+1)+2800000))
ON DUPLICATE KEY UPDATE AgentID = AgentID;
IF ROW_COUNT() = 1 THEN
SET #first = #first + 1;
END IF;
END WHILE;
Note that I changed the action on duplicate key to a dummy update rather than re-assigning a new random value; this makes things a little less expensive when a collision happens.
CREATE PROCEDURE Generate_random()
BEGIN
Drop table if exists ai_training.`Agents`;
CREATE TABLE ai_training.`Agents`(AgentID int PRIMARY KEY);
SET #last = 1000;
WHILE (SELECT COUNT(*) FROM ai_training.`Agents`) < #last DO
INSERT IGNORE INTO ai_training.`Agents`
VALUES
(FLOOR(RAND()*(2900000-2800000+1)+2800000));
END WHILE;
END

Unable to diagnose the problem with MySQL stored procedure

I have defined the following stored procedure to add/update a table called ImportedProduct.
If the primary key, ImportedProductId is provided and is greater than zero, then update the exiting record otherwise insert a new one:
DELIMITER //
CREATE PROCEDURE AddOrUpdateImportedProduct (
IN ImportedProductId BIGINT,
IN UniqueThirdPartyCode VARCHAR(64),
IN BranchId BIGINT
)
BEGIN
IF ImportedProductId <= 0 THEN
INSERT INTO ImportedProduct(UniqueThirdPartyCode, BranchId)
VALUES(UniqueThirdPartyCode, BranchId);
ELSE
UPDATE
ImportedProduct
SET
UniqueThirdPartyCode = UniqueThirdPartyCode,
BranchId = BranchId
WHERE
ImportedProductId = ImportedProductId;
END IF;
END //
DELIMITER ;
Now I run the following code to update an existing row:
CALL AddOrUpdateImportedProduct (1, 'y-105', 24);
I can see that the record with with ImportedProductId = 1 exists in the table, but I am getting the following error:
You are using safe update mode and you tried to update a table without
a WHERE that uses a KEY column To disable safe mode
I am pretty sure ImportedProductId = ImportedProductId holds always.. Perhaps rename your variable or add an alias to the updated table.

MySQL AUTO INCREMENT with Prefix (YYMM) that reset every month

I saw another post explaining the use of sequence table to create prefixed AUTO INCREMENT ID.
Link to referenced post
SQL Fiddle
http://sqlfiddle.com/#!2/0ed88/1
I need the generated auto increment format to be: YYMM + AUTO INCREMENT VALUE that reset every month. For example now is January, 2015. The generated id should be: 15011, 15012, 15013, etc. Next month February 2015, the generated id should be: 15021, 15022, 15023, etc. I can use the above method to generate the prefix, however how do I reset the AUTO INCREMENT value each month? Database is InnoDB. Any help will be greatly appreciated :)
MODIFIED CODE
CREATE TABLE table1_seq
(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY
)|
CREATE TABLE Table1
(
id VARCHAR(7) NOT NULL PRIMARY KEY DEFAULT '0', name VARCHAR(30)
)|
CREATE TRIGGER tg_table1_insert
BEFORE INSERT ON table1
FOR EACH ROW
BEGIN
INSERT INTO table1_seq VALUES (NULL);
SET NEW.id = CONCAT(DATE_FORMAT(NOW(),'%y%m'), LAST_INSERT_ID());
END |
INSERT INTO Table1 (name) VALUES ('Jhon'), ('Mark')|
If you have a field DATE or DATETIME in your table then this solution maybe can help you.
Let say your table name my_table and has PK id (INT) and also a field bila (DATE). I just need to get last (biggest) id in current month from my_table then add by 1.
SET #mos= (SELECT MAX(id) FROM my_table WHERE MONTH(`bila`) = MONTH(NOW()) AND YEAR(`bila`) = YEAR(NOW()));
SET #mos = IF(
#mos IS NULL,
CONCAT(YEAR(NOW()),MONTH(NOW()),'001'),
#mos + 1
);
Then u can use #mos in your query next
To reset the AUTO_INCREMENT value, drop and recreate the table1_seq table. Given your example, a TRUNCATE statement would be sufficient (given that there aren't any foreign keys, and we assume the table is using either the MyISAM or InnoDB engine).
TRUNCATE TABLE table1_seq ;
(MySQL behavior for this statement is to create a new, empty table, with the AUTO_INCREMENT set back to the value when the table was created.)
This effectively achieves the same result as a DROP table followed by CREATE table.
That answers the question you asked. As a side note, generating a key value this way usually turns out to be a bad idea.
i've relied on the last answer above, i've used the orderDate field from my table and compared it with the current date during insertion , to decide finally if i want to reset or increment the purchaseOrderRef field:
The aim is to insert custom auto increment order id (field name: "purchaseOrderRef" ) with the format DD-MM-XXX in table [Products_SumOrders]. such that it resets the XXX to 0 automatically every month:
USE [Mydatabase] -- here you need to use your own database
GO
/****** Object: Trigger [dbo].[customAutoIncrement] Script Date: 10/1/2016 10:07:41 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TRIGGER [dbo].[customAutoIncrement]
ON [dbo].[Products_SumOrders]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
declare #maxOrderID int;
declare #maxOrderRef nvarchar(max);
declare #relativeID varchar(max);
declare #orderId int;
select #orderId =orderid from inserted;
print(#orderId);--allow me to display the inserted order id
SET #maxOrderID = (SELECT Max(orderid) FROM [Products_SumOrders] WHERE MONTH([OrderDate]) = (MONTH(GETDATE())) AND YEAR([OrderDate]) = YEAR(GETDATE()) and orderid < #orderId );
print(#maxOrderID);--last added order
--custom month format (always MM):
declare #mon int;
declare #stringMon nvarchar(10);
set #mon= MONTH(GETDATE());
IF #mon <10
set #stringMon ='0' + CONVERT(VARCHAR(1),#mon) ;
ELSE
set #stringMon = CONVERT(VARCHAR(2),#mon) ;
IF #maxOrderID is null --no orders has been added in this month:
begin
set #maxOrderRef = Substring(CONVERT(VARCHAR(4),YEAR(GETDATE())),3,4)+ '-'+ CONVERT(VARCHAR(2),#stringMon)+'-001';
end
ELSE
--custom order id format (always XXX):
begin
set #relativeID =(SELECT [purchaseOrderRef] FROM [Products_SumOrders] WHERE orderid=#maxOrderID);
set #relativeID = Substring(#relativeID,LEN(#relativeID)-(Charindex('-', REVERSE(#relativeID))-2),LEN(#relativeID));
print(CONVERT(int,#relativeID));
IF CONVERT(int,#relativeID) < 9
set #relativeID ='00' + CONVERT(VARCHAR(2),#relativeID+1) ;
ELSE
begin
if CONVERT(int,#relativeID) < 99
set #relativeID ='0' + CONVERT(VARCHAR(3),#relativeID+1) ;
else
set #relativeID = CONVERT(VARCHAR(3),#relativeID+1) ;
end
set #maxOrderRef = Substring(CONVERT(VARCHAR(4),YEAR(GETDATE())),3,4)+ '-'+ CONVERT(VARCHAR(2),#stringMon)+'-'+ CONVERT(VARCHAR(3),#relativeID);
end
print(#maxOrderRef);
UPDATE Products_SumOrders
SET purchaseOrderRef = #maxOrderRef
FROM inserted INNER JOIN [Products_SumOrders] On inserted.orderid = [Products_SumOrders].orderid
END
GO

how to optimise MySql function to work for concurrent users

I am new to MySQL. Please can you advice on how can i modify below function to ensure it does not throw locking errors when called by multiple users at the same time.
CREATE FUNCTION `get_val`(`P_TABLE` VARCHAR(50)) RETURNS int(11)
DETERMINISTIC
BEGIN
DECLARE pk_value INT DEFAULT 0;
DECLARE pk_found INT DEFAULT 0;
SELECT 1 INTO pk_found FROM pk_keys WHERE TABLE_NAME = P_TABLE;
IF pk_found = 1
THEN
UPDATE pk_keys SET TABLE_VALUE = (TABLE_VALUE + 1 ) WHERE TABLE_NAME = P_TABLE;
ELSE
INSERT INTO pk_keys VALUES ( P_TABLE, 1 );
END IF;
SELECT TABLE_VALUE INTO pk_value FROM pk_keys WHERE TABLE_NAME = P_TABLE;
RETURN pk_value;
END
thanks
CREATE FUNCTION `get_val`(`P_TABLE` VARCHAR(50)) RETURNS int(11)
DETERMINISTIC
MODIFIES SQL DATA
BEGIN
DECLARE pk_value INT DEFAULT 0;
IF EXISTS (SELECT 1 FROM pk_keys WHERE TABLE_NAME = P_TABLE)
THEN
SELECT TABLE_VALUE + 1 INTO pk_value FROM pk_keys WHERE TABLE_NAME = P_TABLE FOR UPDATE;
UPDATE pk_keys SET TABLE_VALUE = pk_value WHERE TABLE_NAME = P_TABLE;
ELSE
SET pk_value = 1;
INSERT INTO pk_keys VALUES ( P_TABLE, pk_value );
END IF;
RETURN pk_value;
END
Have a look at SELECT ... FOR UPDATE and SELECT ... LOCK IN SHARE MODE Locking Reads
Let us look at another example: We have an integer counter field in a
table child_codes that we use to assign a unique identifier to each
child added to table child. It is not a good idea to use either
consistent read or a shared mode read to read the present value of the
counter because two users of the database may then see the same value
for the counter, and a duplicate-key error occurs if two users attempt
to add children with the same identifier to the table.
Here, LOCK IN SHARE MODE is not a good solution because if two users
read the counter at the same time, at least one of them ends up in
deadlock when it attempts to update the counter.
To implement reading and incrementing the counter, first perform a
locking read of the counter using FOR UPDATE, and then increment the
counter. For example:
SELECT counter_field FROM child_codes FOR UPDATE;
UPDATE child_codes SET counter_field = counter_field + 1;
A SELECT ... FOR UPDATE reads the latest available data, setting
exclusive locks on each row it reads. Thus, it sets the same locks a
searched SQL UPDATE would set on the rows.
Also I replaced your if condition. EXISTS stops as soon as a row is found.

Update Trigger : Supertype/subtypes tables

I need some help about triggers. I’m currently developing a platform and a database in order to manage exams at my university. Here is my problem:
I have 1 supertype table, which contains all the persons registered on the platform. I have to be able to make the distinction for each person between the functions “Candidate” and “Examiner”. So I have my 2 subtype tables, one for all the candidates and one for all the examiners. To achieve that, I’m using insert triggers.
In addition, a person can be both Candidate and Examiner, but not at the same time. So after updating the supertype table, I also need a trigger to be able to delete a specific row from one of the two-subtype table and insert the user information on the other.
Here is a simplified design of these 3 tables:
My INSERT trigger :
ALTER TRIGGER [dbo].[role_insert]
ON [dbo].[alemp_persons]
FOR INSERT
AS
DECLARE #random_number int
SELECT #random_number = CAST(CAST(rand() as binary(2)) as int)
BEGIN
INSERT INTO dbo.alemp_candidates
(
id_person, random_number
)
SELECT id_person, # random_number
FROM INSERTED
WHERE function='Candidate'
INSERT INTO dbo.alemp_examiners
(
id_person
)
SELECT id_person
FROM INSERTED
Where function='Examiner'
END
GO
My UPDATE trigger :
ALTER TRIGGER [dbo].[role_update] ON [dbo].[alemp_persons]
AFTER UPDATE
AS
DECLARE #id_person int
DECLARE #newFunction int SELECT #newFunction=function FROM inserted
DECLARE #random_number int SELECT # random_number = CAST(CAST(rand() as binary(2)) as int)
IF #newFunction = 'Candidate'
BEGIN
DELETE
FROM dbo.alemp_examiners
WHERE id_person=#id_person
END
BEGIN
SET IDENTITY_INSERT dbo.alemp_candidates ON;
INSERT INTO dbo.alemp_candidates
(
id_person, random_number
)
SELECT #id_person, random_number
SET IDENTITY_INSERT dbo.alemp_candidates OFF;
END
IF #newFunction = 'Examiner'
BEGIN
DELETE
FROM dbo.alemp_candidates
WHERE id_person=#id_person
END
BEGIN
SET IDENTITY_INSERT dbo.alemp_examiners ON;
INSERT INTO dbo.alemp_examiners
(
id_person
)
SELECT #id_person
SET IDENTITY_INSERT dbo.alemp_examiners Off;
END
GO
As I said above, my INSERT trigger works as I want. However when I want to update the function of one person, I got an error :
Explicit value must be specified for identity column either when IDENTITY_INSERT is set
to ON or when a replication user is inserting into a NOT FOR REPLICATION identity column.
Some simple notes:
1) You should follow Mitch Wheat's advice and rewrite these triggers because inserted and deleted tables could have more than one row. For example, your trigger will have a bad behavior when is executed the next statement UPDATE [dbo].[alemp_persons] SET function = CASE WHEN id_person = 1 THEN 'Candidate' ELSE 'Examiner' END WHERE id_person IN (1,2) if the first person's function is 'Examiner' and the second person's function is 'Candidate'.
2) [dbo].[alemp_persons].function's data type should be [tiny]int or char(1) and not varchar(something greater than 1) (Where function='Examiner').
3) [dbo].[alemp_persons].function column should disallow Nulls.
4) [dbo].[alemp_persons].function column should has a CHECK constraint:
ALTER TABLE [dbo].[alemp_persons]
ADD CONSTRAINT CK_alemp_persons_function_Verify CHECK ( function IN ('Candidate', 'Examiner') );
5) It would be nice to add
a function column to [dbo].[alemp_candidates] and [dbo].[alemp_examiners] tables,
two check constraints on [dbo].[alemp_candidates] (function = 'Candidate') and [dbo].[alemp_examiners] (function = 'Examiner'),
an UNIQUE index on [dbo].[alemp_persons](id_person, function),
two FKs between [dbo].[alemp_candidates/examiners](id_person, function) and [dbo].[alemp_persons](id_person, function).
This way, you can be sure that [dbo].[alemp_candidates] table has only candidates and [dbo].[alemp_examiners] has only examiners and every person can be only candidate or examiner at one time.
6) You should disallow IDENTITY property for id_person columns in [dbo].[alemp_candidates] and [dbo].[alemp_examiners] table (SET IDENTITY_INSERT dbo.alemp_candidates ...).
8) And this statement IF #newFunction = 'Candidate' should raise an error because #newFunction data type is 'INT'.
9) And the AFTER UPDATE trigger on [dbo].[alemp_persons] table will move data between candidates and examiners tables (not tested):
ALTER TRIGGER [dbo].[role_update]
ON [dbo].[alemp_persons]
FOR UPDATE
AS
BEGIN
DECLARE #selected_rows TABLE (
id_person INT PRIMARY KEY, -- or BIGINT, look at alemp_person.id_person data type
new_function VARCHAR(50) NOT NULL -- look at alemp_person.function column data type
);
INSERT #selected_rows (id_person, new_function)
SELECT new.id_person, new.function
FROM inserted as new INNER JOIN deleted as old ON new.id_person = old.id_person
WHERE new.function <> old.function;
MERGER dbo.alemp_candidates AS dest
USING #selected_rows AS src ON dest.id_person = src.id_person
WHEN MATCHED THEN
DELETE
WHEN NOT MATCHED BY TARGET AND src.new_function = 'Candidate' THEN
INSERT (id_person, random_number)
VALUES (src.id_person, CONVERT(BINARY(2), CHECKSUM(NEWID()));
MERGER dbo.alemp_examiners AS dest
USING #selected_rows AS src ON dest.id_person = src.id_person
WHEN MATCHED THEN
DELETE
WHEN NOT MATCHED BY TARGET AND src.new_function = 'Examiner' THEN
INSERT (id_person)
VALUES (src.id_person);
END