These are the statements
INSERT INTO toolate (name,type,date)
SELECT name, type ,date
FROM homework
WHERE date < CURRENT_DATE()
and
DELETE FROM homework WHERE date < CURRENT_DATE()
I need to combine these two so that my event will work in a proper order.
Firstly the INSERT statement then the DELETE one.
That way I can still see homework that's past date while having a clean homework table and it needs to happen automatically thus why I'm using events. Of course I will welcome a different solution.
You can't combine these two in a single query. However, an alternative would be to use STORED PROCEDURE and execute these two inside a transaction with EXIT HANDLER e.g.:
BEGIN
START TRANSACTION;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
EXIT PROCEDURE;
END;
INSERT INTO toolate (name,type,date)
SELECT name, type ,date
FROM homework
WHERE date < CURRENT_DATE()
DELETE FROM homework WHERE date < CURRENT_DATE()
COMMIT;
END
This will make sure both of these queries are executed sequencially, and if DELETE query fails, INSERT will be rolled back.
Here's MtSQL's documentation for stored procedures.
Related
In Mysql I have two concurrent processes that need to read some rows and update a flag based on a condition.
I have to write a stored procedure with transaction but the problem is that sometimes the two processes updates the same rows.
I have a table Status and I want read 15 rows where the flag Reserved is true, then update those rows setting the flag Reserved to False.
The updated rows must be returned to the client.
My stored procedure is:
CREATE DEFINER=`user`#`%` PROCEDURE `get_reserved`()
BEGIN
DECLARE tmpProfilePageId bigint;
DECLARE finished INTEGER DEFAULT 0;
DECLARE curProfilePage CURSOR FOR
SELECT ProfilePageId
FROM Status
WHERE Reserved is false and ((timestampdiff(HOUR, UpdatedTime, NOW()) >= 23) or UpdatedTime is NULL)
ORDER BY UpdatedTime ASC
LIMIT 15;
DECLARE CONTINUE HANDLER
FOR NOT FOUND SET finished = 1;
DECLARE EXIT HANDLER FOR SQLEXCEPTION ROLLBACK;
DECLARE EXIT HANDLER FOR SQLWARNING ROLLBACK;
START TRANSACTION;
DROP TEMPORARY TABLE IF EXISTS TmpAdsProfile;
CREATE TEMPORARY TABLE TmpAdsProfile(Id INT PRIMARY KEY AUTO_INCREMENT, ProfilePageId BIGINT);
OPEN curProfilePage;
getProfilePage: LOOP
FETCH curProfilePage INTO tmpProfilePageId;
IF finished = 1 THEN LEAVE getProfilePage;
END IF;
UPDATE StatusSET Reserved = true WHERE ProfilePageId = tmpProfilePageId;
INSERT INTO TmpAdsProfile (ProfilePageId) VALUES (tmpProfilePageId);
END LOOP getProfilePage;
CLOSE curProfilePage;
SELECT ProfilePageId FROM TmpAdsProfile;
COMMIT;
END
Anyway, if I execute two concurrent processes that call this stored procedure, sometimes they update the same rows.
How can I execute the stored procedure in an atomic way?
Simplify this a bit and use FOR UPDATE. That will lock the rows you want to change until you commit the transaction. You can get rid of the cursor entirely. Something like this, not debugged!
START TRANSACTION;
CREATE OR REPLACE TEMPORARY TABLE TmpAdsProfile AS
SELECT ProfilePageId
FROM Status
WHERE Reserved IS false
AND ((timestampdiff(HOUR, UpdatedTime, NOW()) >= 23) OR UpdatedTime IS NULL)
ORDER BY UpdatedTime ASC
LIMIT 15
FOR UPDATE;
UPDATE Status SET Reserved = true
WHERE ProfilePageId IN (SELECT ProfilePageId FROM TmpAdsProfile);
COMMIT;
SELECT ProfilePageId FROM TmpAdsProfile;
That temporary table will only ever have fifteen rows in it. So indexes and PKs and all that are not necessary. Therefore you can use CREATE ... AS SELECT ... to create and populate the table in one go.
And, consider recasting your UpdatedTime filter so it can use an index.
AND (UpdatedTime <= NOW() - INTERVAL 23 HOUR OR UpdatedTime IS NULL)
The appropriate index for the SELECT query is
CREATE INDEX status_update ON Status (Reserved, UpdatedTime, ProfilePageId);
The faster your SELECT operation can be, the less time your transaction will take, so the better your overall performance will be.
I am trying to make a MySQL stored procedure that processes a book purchase and inserts records into other tables about the purchase. However, these insertions can only happen if three conditions are met: the customer is in the system, the book is in the system, and there is enough quantity.
I want to check for each condition individually, and if it passes the first condition, it moves to the next, but if it doesn't, I want it to end the procedure and return a value, and so on for each condition. If it passes all three conditions, the insertions can happen. Here's how I coded it:
DELIMITER //
CREATE PROCEDURE process_purchase(
IN book_key INT,
IN customer_key INT,
IN quantity INT
)
BEGIN
DECLARE book_inventory_key_var INT;
DECLARE purchase_key_var INT;
SELECT book_inventory_key
INTO book_inventory_key_var
FROM book_inventory
WHERE book_key = book_key.book_inventory_key;
SELECT purchase_key
INTO purchase_key_var
FROM purchases
WHERE customer_key = customer_key.purchases;
IF customer_key != customer_key.customers THEN
SELECT '-1';
ELSEIF book_key != book_key.books THEN
SELECT '-2';
ELSEIF quantity < quantity_on_stock(book_key) THEN
SELECT '-3';
ELSE
INSERT INTO purchases VALUES (customer_key, CURDATE());
INSERT INTO purchase_items (book_inventory_key, purchase_key, quantity) VALUES (book_inventory_key_var, purchase_key_var, quantity);
SELECT '1';
END IF;
END//
DELIMITER ;
I compare the customer and book keys to their values in the other tables, and the quantity to the quantity_on_stock stored function I previously made. I use a chain of IF-ELSEIF to go through each condition one by one, and if all of them are passed, the insertions occur. If not, it won't go to the next condition, and will return the SELECT message, correct? The procedure runs without errors, but I am unsure if this is the correct method, or if there's a better way of going about this.
Checking sequentially is subject to race conditions. Breaking this paradigm is key to moving from a procedural to SQL based method. Use the database features of to obtain consistency rather than procedural code.
purchase_items should have foreign key constraints to book_key and customer_key tables. If an insert generates a FK exception then one of these apply depending on the error. DECLARE HANDLER will help catch these errors.
For the quantity:
INSERT INTO purchase_items (book_inventory_key, purchase_key, quantity)
SELECT book_key, purchase_key, quantity
FROM books WHERE book_key = book.id AND book.available >= quantity
If there are no ROW_COUNT for this, then there wasn't sufficient quantity.
You will also need to reduce the amount of books available in the same SQL transaction.
If you don't have to do this in a STORED PROCEDURE, don't. A lot of the constructs here are easier in application code. If this is an assignment, where store procedures are require to get extra marks, get through it, and never write a stored procedure again.
I have two tables acceptances and turnovers. Let's just assume that these tables both have the columns id, and date only.
I need to be able to insert same dates across the 2 tables only up to 4 times.
For example: 5 users are trying to create a booking schedule.
3 of the 5 users create an acceptance schedule.
2 of the 5 users create a turnover schedule.
1 of these inserts should be rejected even in race conditions.
I have the following stored procedure:
BEGIN
DECLARE acceptanceDateEntryCount int;
DECLARE turnoverDateEntryCount int;
SELECT COUNT(date) FROM acceptances WHERE DATE(date) = DATE(insertDate) INTO acceptanceDateEntryCount;
SELECT COUNT(date) FROM turnovers WHERE DATE(date) = DATE(insertDate) INTO turnoverDateEntryCount;
IF((acceptanceDateEntryCount + turnoverDateEntryCount) < 4) THEN
INSERT INTO acceptances (date) VALUES (insertDate);
ELSE
SIGNAL SQLSTATE
'45030'
SET
MESSAGE_TEXT = 'Cannot create acceptance schedule. Max limit of 4 same date entries are found in acceptances table and turnovers table',
MYSQL_ERRNO = '45030';
END IF;
END
NOTE: This stored procedure is for creating an acceptance booking only. But there is another stored procedure which is just the same, but inserts into the turnovers table.
Is this stored procedure enough to make sure that even in race conditions where a lot of users try to create a booking schedule, if entries of the same date exists, the insert will be rejected if it has the same date.
Or should I just scrap this and use pessimistic locking to make sure that inserts on these tables do not run concurrently?
Tack FOR UPDATE on the end of each SELECT statement.
Also include
START TRANSACTION;
and
COMMIT;
I have set-up a simple event that runs every hour and adds a record like this:
ON SCHEDULE EVERY 1 HOUR STARTS '2015-01-01 00:00:00'
DO
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE a INT;
DECLARE cursor_1 CURSOR FOR SELECT item_id FROM item WHERE NOW()>expiration_date AND has_expired = 0;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN cursor_1;
read_loop: LOOP
FETCH cursor_1 INTO a;
IF done THEN
LEAVE read_loop;
END IF;
UPDATE item SET has_expired=1 WHERE quote_id=a;
INSERT INTO item_log (item_id, message) VALUES (a, 'Item is now expired');
END LOOP;
END
This thing runs 24 times a day and it works as expected, however, there is another idea to create events dynamically and attach them with a given record, e.g.
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 3 WEEK
DO
BEGIN
UPDATE item SET has_expired=1 WHERE item_id=232;
INSERT INTO item_log (item_id, message) VALUES (232, 'Item is now expired');
END
Of course the above would have different values of interval and ids, but that would mean that there are possibly 1000s or tens of thousands of events.
Now, would that be a problem? Limitations and performance wise?
I can imagine that if there are no records, or just few created a month then first approach will be constantly running for nothing. However if there will be few items added an hour, then it will mean that DB could reach thousands of one-time events. Would that not cause problems of its own?
Would you like to run that 100% faster? Get rid of it. Instead, have SELECTs include the clause AND (expiration_date < NOW())
OK, so you asked about the code. Here are some comments:
The UPDATE and INSERT need to be in a transaction.
The SELECT needs FOR UPDATE and should be in the transaction, too. But this is less important because, unless you ever change expiration_date, it never matters.
Cursors suck, performance-wise. Select 100 rows to purge, then run one UPDATE and one INSERT.
Scanning the table for this flag will be a slow "table scan" unless you have an index starting with expiration_date.
I have a situation in which I don't want inserts to take place (the transaction should rollback) if a certain condition is met. I could write this logic in the application code, but say for some reason, it has to be written in MySQL itself (say clients written in different languages will be inserting into this MySQL InnoDB table) [that's a separate discussion].
Table definition:
CREATE TABLE table1(x int NOT NULL);
The trigger looks something like this:
CREATE TRIGGER t1 BEFORE INSERT ON table1
FOR EACH ROW
IF (condition) THEN
NEW.x = NULL;
END IF;
END;
I am guessing it could also be written as(untested):
CREATE TRIGGER t1 BEFORE INSERT ON table1
FOR EACH ROW
IF (condition) THEN
ROLLBACK;
END IF;
END;
But, this doesn't work:
CREATE TRIGGER t1 BEFORE INSERT ON table1 ROLLBACK;
You are guaranteed that:
Your DB will always be MySQL
Table type will always be InnoDB
That NOT NULL column will always stay the way it is
Question: Do you see anything objectionable in the 1st method?
From the trigger documentation:
The trigger cannot use statements that explicitly or implicitly begin or end a transaction such as START TRANSACTION, COMMIT, or ROLLBACK.
Your second option couldn't be created. However:
Failure of a trigger causes the statement to fail, so trigger failure also causes rollback.
So Eric's suggestion to use a query that is guaranteed to result in an error is the next option. However, MySQL doesn't have the ability to raise custom errors -- you'll have false positives to deal with. Encapsulating inside a stored procedure won't be any better, due to the lack of custom error handling...
If we knew more detail about what your condition is, it's possible it could be dealt with via a constraint.
Update
I've confirmed that though MySQL has CHECK constraint syntax, it's not enforced by any engine. If you lock down access to a table, you could handle limitation logic in a stored procedure. The following trigger won't work, because it is referencing the table being inserted to:
CREATE TRIGGER t1 BEFORE INSERT ON table1
FOR EACH ROW
DECLARE num INT;
SET num = (SELECT COUNT(t.col)
FROM your_table t
WHERE t.col = NEW.col);
IF (num > 100) THEN
SET NEW.col = 1/0;
END IF;
END;
..results in MySQL error 1235.
Have you tried raising an error to force a rollback? For example:
CREATE TRIGGER t1 BEFORE INSERT ON table1
FOR EACH ROW
IF (condition) THEN
SELECT 1/0 FROM table1 LIMIT 1
END IF;
END;