Howto bulk update a InnoDB table without deadlocks? - mysql

I have two tables, one having a many-to-many relationship (fooBarTable with columns fooId and barId) and another InnoDB table fooCounterTable with columns fooId and counter counting the occurences of fooId in fooBarTable.
When deleting all barId's from fooBarTable, I need to update the fooCounterTable accordingly.
The first thing I tried was this:
UPDATE fooCounterTable SET counter = counter - 1
WHERE fooId IN (SELECT fooId FROM fooBarTable WHERE barId = 42 ORDER BY fooId);
But I got this error:
MySQL error (1205): Lock wait timeout exceeded; try restarting transaction
Updating the table when adding barId's is working fine with this SQL statement:
INSERT INTO `fooCounterTable` (fooId, counter) VALUES (42,1), (100,1), (123,1)
ON DUPLICATE KEY UPDATE counter = counter + 1;
So I thought I'd do the same thing when decreasing the counter, even if it looks stupid to insert 0-Values, which should never happen:
INSERT INTO `fooCounterTable` (SELECT fooId, 0 FROM fooBarTable WHERE barId = 42 ORDER BY fooId)
ON DUPLICATE KEY UPDATE counter = counter - 1;'
This seems to work fine in most cases, but sometimes I get a deadlock:
MySQL error (1213): Deadlock found when trying to get lock; try restarting transaction
So I read about deadlocks and found out about SELECT ... FOR UPDATE and I tried this:
START TRANSACTION;
SELECT fooId FROM fooCounterTable
WHERE fooId IN (SELECT fooId FROM fooBarTable WHERE barId = 42 ORDER BY fooId) FOR UPDATE;
UPDATE fooCounterTable SET counter = counter - 1
WHERE fooId IN (SELECT fooId FROM fooBarTable WHERE barId = 42 ORDER BY fooId);
COMMIT;
which resulted in:
MySQL error (2014): commands out of sync
Can anyone tell me how to resolve my problem?
Update
The last error (2014) occured, because I did not use and free the SELECT statement's results before executing the UPDATE statement, which is mandatory. I fixed that and I got rid of error 2014, but I still have deadlocks (error 1205) from time to time and I don't understand, why.

Do you know what fooID you have just deleted when firing this query?
If so seems like this would work...
UPDATE fooCounterTable SET counter =
(SELECT count(*) FROM fooBarTable WHERE fooId = 42)
WHERE fooID = 42
I wonder if you really need that counter table tho. If your indexes are set up properly there shouldn't be too much of a speed penalty to a more normalized approach.

Related

how does mysql select for update works [duplicate]

This is not a full/correct MySQL query only pseudo-code:
Select *
from Notifications as n
where n.date > (CurrentDate-10 days)
limit by 1
FOR UPDATE
http://dev.mysql.com/doc/refman/5.0/en/select.html states:
If you use FOR UPDATE with a storage engine that uses page or row locks, rows examined by the query are write-locked until the end of the current transaction
Is here only the one record returned locked by MySQL or all records it has to scan to find the single record?
Why don't we just try it?
Set up the database
CREATE DATABASE so1;
USE so1;
CREATE TABLE notification (`id` BIGINT(20), `date` DATE, `text` TEXT) ENGINE=InnoDB;
INSERT INTO notification(id, `date`, `text`) values (1, '2011-05-01', 'Notification 1');
INSERT INTO notification(id, `date`, `text`) values (2, '2011-05-02', 'Notification 2');
INSERT INTO notification(id, `date`, `text`) values (3, '2011-05-03', 'Notification 3');
INSERT INTO notification(id, `date`, `text`) values (4, '2011-05-04', 'Notification 4');
INSERT INTO notification(id, `date`, `text`) values (5, '2011-05-05', 'Notification 5');
Now, start two database connections
Connection 1
BEGIN;
SELECT * FROM notification WHERE `date` >= '2011-05-03' FOR UPDATE;
Connection 2
BEGIN;
If MySQL locks all rows, the following statement would block. If it only locks the rows it returns, it shouldn't block.
SELECT * FROM notification WHERE `date` = '2011-05-02' FOR UPDATE;
And indeed it does block.
Interestingly, we also cannot add records that would be read, i.e.
INSERT INTO notification(id, `date`, `text`) values (6, '2011-05-06', 'Notification 6');
blocks as well!
I can't be sure at this point whether MySQL just goes ahead and locks the entire table when a certain percentage of rows are locked, or where it's actually really intelligent in making sure the result of the SELECT ... FOR UPDATE query can never be changed by another transaction (with an INSERT, UPDATE, or DELETE) while the lock is being held.
The thread is pretty old, just to share my two cents regarding the tests above performed by #Frans
Connection 1
BEGIN;
SELECT * FROM notification WHERE `date` >= '2011-05-03' FOR UPDATE;
Connection 2
BEGIN;
SELECT * FROM notification WHERE `date` = '2011-05-02' FOR UPDATE;
The concurrent transaction 2 will be blocked for sure, but the reason is NOT that the transaction 1 is holding the lock on the whole table. The following explains what has happened behind the scene:
First of all, the default isolation level of the InnoDB storage engine is Repeatable Read. In this case,
1- When the column used in where condition is not indexed (as the case above):
The engine is obliged to perform a full table scan to filter out the records not matching the criteria. EVERY ROW that have been scanned are locked in the first place. MySQL may release the locks on those records not matching the where clause later on. It is an optimization for the performance, however, such behavior violates the 2PL constraint.
When transaction 2 starts, as explained, it needs to acquire the X lock for each row retrieved although there exists only a single record (id = 2) matching the where clause. Eventually the transaction 2 will be waiting for the X lock of the first row (id = 1) until the transaction 1 commits or rollbacks.
2- When the column used in where condition is a primary index
Only the index entry satisfying the criteria is locked. That's why in the comments someone says that some tests are not blocked.
3 - When the column used in where condition is an index but not unique
This case is more complicated. 1) The index entry is locked. 2) One X lock is attached to the corresponding primary index. 3) Two gap locks are attached to the non-existing entries right before and after the record matching the search criteria.
I know this question is pretty old, but I've wanted to share the results of some relevant testing I've done with indexed columns which has yielded some pretty strange results.
Table structure:
CREATE TABLE `t1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`notid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
12 rows inserted with INSERT INTO t1 (notid) VALUES (1), (2),..., (12). On connection 1:
BEGIN;
SELECT * FROM t1 WHERE id=5 FOR UPDATE;
On connection 2, the following statements are blocked:
SELECT * FROM t1 WHERE id!=5 FOR UPDATE;
SELECT * FROM t1 WHERE id<5 FOR UPDATE;
SELECT * FROM t1 WHERE notid!=5 FOR UPDATE;
SELECT * FROM t1 WHERE notid<5 FOR UPDATE;
SELECT * FROM t1 WHERE id<=4 FOR UPDATE;
The strangest part is that SELECT * FROM t1 WHERE id>5 FOR UPDATE; is not blocked, nor are any of
...
SELECT * FROM t1 WHERE id=3 FOR UPDATE;
SELECT * FROM t1 WHERE id=4 FOR UPDATE;
SELECT * FROM t1 WHERE id=6 FOR UPDATE;
SELECT * FROM t1 WHERE id=7 FOR UPDATE;
...
I'd also like to point out that it seems the entire table is locked when the WHERE condition in the query from connection 1 matches a non-indexed row. For example, when connection 1 executes SELECT * FROM t1 WHERE notid=5 FOR UPDATE, all select queries with FOR UPDATE and UPDATE queries from connection 2 are blocked.
-EDIT-
This is a rather specific situation, but it was the only I could find that exhibits this behaviour:
Connection 1:
BEGIN;
SELECT *, #x:=#x+id AS counter FROM t1 CROSS JOIN (SELECT #x:=0) b HAVING counter>5 LIMIT 1 FOR UPDATE;
+----+-------+-------+---------+
| id | notid | #x:=0 | counter |
+----+-------+-------+---------+
| 3 | 3 | 0 | 9 |
+----+-------+-------+---------+
1 row in set (0.00 sec)
From connection 2:
SELECT * FROM t1 WHERE id=2 FOR UPDATE; is blocked;
SELECT * FROM t1 WHERE id=4 FOR UPDATE; is not blocked.
Following links from the documentation page you posted gives more information about locking. In this page
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.
This seems pretty clear that it is all rows that it has to scan.
From mysql official doc:
A locking read, an UPDATE, or a DELETE generally set record locks on every index record that is scanned in the processing of the SQL statement. It does not matter whether there are WHERE conditions in the statement that would exclude the row.
For the case discussed in Frans' answer, all rows are locked because there's a table scan during sql processing:
If you have no indexes suitable for your statement and MySQL must scan the entire table to process the statement, every row of the table becomes locked, which in turn blocks all inserts by other users to the table. It is important to create good indexes so that your queries do not unnecessarily scan many rows.
Check the latest doc here: https://dev.mysql.com/doc/refman/8.0/en/innodb-locks-set.html
As others have mentioned, SELECT... FOR UPDATE locks all rows encountered in the default isolation level. Try setting the isolation for the session which runs this query to READ COMMITTED, for example precede the query with: set session transaction isolation level read committed;
It locks all the rows selected by query.

Prevent MySQL to select the same row by compete threads

I have some table proxy. There proxies are stored. Application is multithreaded. Each thread gets proxy from proxy table. And I need to use proxies that frequency of each proxy usage must be accidentally equals. There is some field 'last_usage' with timestamp with microseconds.
Now to achieve this goal I do the next: block table, select one proxy with older last_usage, then update last_usage of selected proxy and unlock table.
table engine is inno_db.
Another my idea is to use the following solution:
SET #uids := null;
UPDATE footable
SET foo = 'bar'
WHERE fooid > 5
AND ( SELECT #uids := CONCAT_WS(',', fooid, #uids) );
SELECT #uids;
I think it should have the same effect. Because mysql should block the row or table when update is executing. And another threads should not be able to select this row.
May I use second solution for my goal ? Which way is better or can you suggest better way ?
The clean way would be to use two queries in a single transaction:
start transaction;
select foo_id into #foo_id
from foo_table
order by last_usage asc
limit 1
for update;
update foo_table
set last_usage = now()
where foo_id = #foo_id;
commit;
FOR UPDATE is used, to lock the selected row until the transaction is commited.
There is an INNODB lock explained here
https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html
which can be used in your case.
in your case using
SELECT * FROM footable WHERE fooid > 5
AND ( SELECT #uids := CONCAT_WS(',', fooid, #uids) ) FOR UPDATE;
and then to update and get rid of the lock with same session:
UPDATE footable
SET foo = 'bar'
WHERE fooid > 5
AND ( SELECT #uids := CONCAT_WS(',', fooid, #uids) );

select for update twice to same table by diffrenet key cause deadlock in MySQL

Table foo has id and name.
Transaction A select for update by id 1.
Transaction B select for update by id 1 then wait.
Transaction A select for update by name anything(even though not exists) cause Transaction B deadlock.
Why this happen ?
Below scripts reproduce deadlock.
create table foo (id int primary key, name varchar(100));
insert into foo values (1, 'foo1');
-- transaction A
start transaction;
select * from foo where id=1 for update;
-- transaction B
start transaction;
select * from foo where id=1 for update;
-- now waiting
-- transaction A
select * from foo where name='xxxxx' for update;
-- transaction B dead lock occer
I figured out close answer.
MySQL locks all records when select for update searched by no indexed column.
https://dev.mysql.com/doc/refman/5.7/en/innodb-locks-set.html
If you have no indexes suitable for your statement and MySQL must scan the entire table to process the statement, every row of the table becomes locked, which in turn blocks all inserts by other users to the table.
But, I don't know why this causes deadlock.
By the way, I resolved my problem that every select for update to search by primary key.

Mysql create or update lock

I have a stored procedure which simulate a create or update.
Here is my Algorithm:
SELECT Id INT rId FROM MY_TABLE WHERE UNIQUE_FIELD = XXX LIMIT 1;
IF (rId is not null) THEN
UPDATE ELSE INSERT
The problem is that i got duplicates. How can i prevent theses duplicates? I can't add an UNIQUE INDEX because some fields can be NULL.
Thank you.
EDIT: I'm using InnoDB. Does row lock can helpme ? Locking the whole table is not acceptable for performance reason.
Use a transaction along with the FOR UPDATE clause in SELECT. This will just lock that one row, and should not block transactions that use other rows of the table.
START TRANSACTION;
SELECT id as rId FROM my_table
WHERE unique_field = XXX LIMIT 1
FOR UPDATE;
If (rId IS NOT NULL) THEN
UPDATE ...
ELSE
INSERT ...
END;
COMMIT;

Multiple updates or update ... where in

which version is better (performance)?
1. update my_table set my_col = 1 where my_id = 100
update my_table set my_col = 1 where my_id = 110
update my_table set my_col = 1 where my_id = 120
2. update my_table set my_col = 1 where my_id in (100, 110, 120)
In your case, both ways will have almost equal response time as you are running only 3-4 queries.
But definitely 2nd way will be faster for higher number of queries or updates (bulk updates are faster) because this will reduce,
creating,binding connections to database
query compilation/optimization task of sql engine
but bulk updates has one downside also i.e. locking of tables as you are updating multiple records in single statement, table will be locked for that much duration. so perform bulk updates with considering acceptable locking period in mind.