Bug #117034 During testing of our production system, we discovered a potential issue with MySQL's Index Merge locking strategy. When
Submitted: 23 Dec 2024 3:31 Modified: 5 Jan 8:03
Reporter: wang max Email Updates:
Status: Open Impact on me:
None 
Category:MySQL Server Severity:S3 (Non-critical)
Version:5.7.26 OS:Any
Assigned to: CPU Architecture:Any

[23 Dec 2024 3:31] wang max
Description:
Index Merge execution may lead to deadlocks in InnoDB when executing concurrent UPDATE statements. The current behavior suggests that MySQL might be acquiring primary key locks before completing all secondary index scans, which deviates from the expected Index Merge execution flow.

Expected behavior:
1. Index Merge should first scan and acquire locks on all involved secondary indexes
2. Collect and sort rowids in the sort buffer 
3. Only then proceed to acquire primary key locks for the final table access

Current behavior:
- Based on deadlock information, MySQL appears to be acquiring primary key locks before completing secondary index scans
- This leads to deadlocks in scenarios where they could be avoided
- Example deadlock scenario:
 - Transaction 1: Has locks on secondary index idx_list_id_child_type
 - Transaction 2: Has different locks on the same secondary index
 - Both transactions then attempt to acquire primary key locks, causing deadlock

Test case:
1. Table has composite indexes (list_id, child_type) and (child_id, child_type)
2. Two concurrent UPDATEs filtering on list_id=21832, different child_ids, and child_type=1
3. Both updates attempt to use Index Merge access path
4. Deadlock occurs between secondary and primary key lock acquisitions

This appears to be an optimization opportunity in the Index Merge execution strategy. A more ordered approach to lock acquisition could prevent these deadlocks while maintaining transaction isolation.

The issue affects high-concurrency workloads where multiple transactions perform updates using Index Merge access paths on overlapping sets of rows.

How to repeat:
IDK,
1.1. Create table structure:
```sql
CREATE TABLE `tg_list_rel` (
 `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
 `list_id` bigint(11) NOT NULL COMMENT '清单id',
 `period_id` int(11) DEFAULT '0' COMMENT '清单周期',
 `child_id` bigint(11) NOT NULL COMMENT '目标清单id或者目标id',
 `child_type` tinyint(1) DEFAULT '0' COMMENT '子对象类型、1:目标、2:清单',
 `goal_group_id` bigint(11) NOT NULL DEFAULT '0' COMMENT '目标分组ID',
 `goal_group_sort` int(11) NOT NULL DEFAULT '9999' COMMENT '分组排序',
 `approve` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否校验通过,1:通过 2:不通过',
 `weight` int(11) DEFAULT '0' COMMENT '个人清单中目标类型的区分,0:默认值,1:我负责的,2:我贡献的',
 `supporter_flag` int(11) DEFAULT '0' COMMENT '个人清单所属人作为支持方的标识',
 `sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序权重',
 `valid` tinyint(4) DEFAULT '1' COMMENT '删除标识 1:有效 0:已删除或无效',
 `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
 `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
 `create_uid` int(11) DEFAULT NULL COMMENT '创建者uid',
 `create_name` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建者姓名',
 `create_mis` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建者mis',
 `create_empid` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建者工号id',
 `update_uid` int(11) DEFAULT NULL COMMENT '最后更新者uid',
 `update_name` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '最后更新者姓名',
 `update_mis` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '最后更新者mis',
 `update_empid` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '最后更新者工号id',
 PRIMARY KEY (`id`),
 KEY `idx_list_id_child_type` (`list_id`,`child_type`),
 KEY `idx_child` (`child_id`,`child_type`),
 KEY `idx_list_id_period` (`list_id`,`period_id`)
) ENGINE=InnoDB AUTO_INCREMENT=418022 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='TG清单关系表';
2.SQL:
-- Transaction 1
UPDATE tg_list_rel
SET valid = 0,
    update_time = '2024-12-21 10:24:57.157',
    update_uid = *,
    update_name = '*',
    update_mis = '*',
    update_empid = '*'
WHERE list_id = 21832 
AND child_id in (81742)
AND child_type = 1
AND valid = 1;

-- Transaction 2 (concurrent)
UPDATE tg_list_rel
SET valid = 0,
    update_time = '2024-12-21 10:24:57.157',
    update_uid = *,
    update_name = '*',
    update_mis = '*',
    update_empid = '*'
WHERE list_id = 21832 
AND child_id in (87569)
AND child_type = 1
AND valid = 1;

3.Deadlock Information
LATEST DETECTED DEADLOCK
------------------------
2024-12-21 10:24:57 0x7eee7eefd700
*** (1) TRANSACTION:
TRANSACTION 24601978667, ACTIVE 0 sec starting index read
mysql tables in use 3, locked 3
LOCK WAIT 7 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 2
MySQL thread id 58252803, OS thread handle 139547230336768, query id 13415112428 10.27.49.189 kms_teamgoals_gtyz updating
/*id:bb3c1a0c*//*ip=10.27.49.189*/update tg_list_rel
SET `valid` = 0,
update_time = '2024-12-21 10:24:57.157',
update_uid = *,
update_name = '*',
update_mis = '*',
update_empid = '*' 
WHERE ( list_id = 21832
and child_id in
(
81742
)
and child_type = 1
and `valid` = 1 )
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 10155 page no 10382 n bits 760 index idx_list_id_child_type of table `teamgoals`.`tg_list_rel` trx id 24601978667 lock_mode X waiting
*** (2) TRANSACTION:
TRANSACTION 24601978660, ACTIVE 0 sec fetching rows
mysql tables in use 3, locked 3
27 lock struct(s), heap size 3520, 93 row lock(s), undo log entries 2
MySQL thread id 58177712, OS thread handle 139562796963584, query id 13415112400 10.27.49.189 kms_teamgoals_gtyz updating
/*id:bb3c1a0c*//*ip=10.27.49.189*/update tg_list_rel
SET `valid` = 0,
update_time = '2024-12-21 10:24:57.157',
update_uid = *,
update_name = '*',
update_mis = '*',
update_empid = '*' 
WHERE ( list_id = 21832
and child_id in
(
87569
)
and child_type = 1
and `valid` = 1 )
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 10155 page no 10382 n bits 760 index idx_list_id_child_type of table `teamgoals`.`tg_list_rel` trx id 24601978660 lock_mode X
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 10155 page no 14562 n bits 176 index PRIMARY of table `teamgoals`.`tg_list_rel` trx id 24601978660 lock_mode X locks rec but not gap waiting
*** WE ROLL BACK TRANSACTION (1)
[23 Dec 2024 13:47] wang max
May I ask whether it is related to the following code?
Code:
if ((result= read_record.read_record(&read_record)) == -1)
  {
    result= HA_ERR_END_OF_FILE;
    end_read_record(&read_record);
    free_io_cache(head);
    /* All rows from Unique have been retrieved, do a clustered PK scan */
    if (pk_quick_select)
    {
      doing_pk_scan= TRUE;
      if ((result= pk_quick_select->init()) ||
          (result= pk_quick_select->reset()))
        DBUG_RETURN(result);
      DBUG_RETURN(pk_quick_select->get_next());
    }
  }

  DBUG_RETURN(result);
}
[23 Dec 2024 14:56] wang max
I'm sorry, my previous code was incorrect. This issue has been bothering me for a long time, so let me restate my problem: I've encountered a deadlock in the production environment. The table structure, deadlock information, and deadlock SQL have already been shared above. I don't understand why the primary key index and the secondary index are waiting for each other. I've seen many discussions on forums suggesting that it's due to index merging. My understanding is that index merging involves intersecting the IDs obtained from two indexes before going back to the table, so this problem shouldn't occur. Could it be that there are some extreme scenarios where the secondary indexes go back to the table separately?
[5 Jan 8:03] wang max
Can someone answer me, please.