Bug #111636 Query not using the index even with FORCE INDEX
Submitted: 30 Jun 2023 22:34 Modified: 3 Jul 2023 12:41
Reporter: Roberto de Bem Email Updates:
Status: Not a Bug Impact on me:
None 
Category:MySQL Server: Optimizer Severity:S5 (Performance)
Version:8.0.33,5.7.41 OS:Any
Assigned to: CPU Architecture:Any

[30 Jun 2023 22:34] Roberto de Bem
Description:
The force index hint is ignored in update commands that don't fit into the range_optimizer_max_mem_size size. For select commands, it works independently of the size of the range_optimizer_max_mem_size.

How to repeat:
Create the table:
CREATE TABLE `test_table` (
  `col1` varchar(128) NOT NULL DEFAULT '',
  `col2` varchar(128) NOT NULL DEFAULT '',
  `col3` varchar(99) NOT NULL DEFAULT '',
  `col4` varchar(150) NOT NULL DEFAULT '',
  `col5` int(10) unsigned NOT NULL DEFAULT '0',
  `col6` int(10) unsigned NOT NULL DEFAULT '0',
  `col7` tinyint(3) unsigned NOT NULL DEFAULT '1',
  `col8` datetime DEFAULT NULL,
  `col9` tinyint(3) unsigned NOT NULL DEFAULT '0',
  `col10` datetime NOT NULL,
  `col11` tinyint(3) unsigned NOT NULL DEFAULT '0',
  `col12` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `col13` timestamp NOT NULL,
  `col14` varchar(32) NOT NULL DEFAULT 'empty',
  `col15` varchar(99) NOT NULL DEFAULT '_',
  PRIMARY KEY (`col1`,`col14`,`col2`,`col3`,`col15`),
  KEY `temp_del_time_idx` (`col10`),
  KEY `idx_col2_col3` (`col2`,`col3`),
  KEY `idx_col12` (`col12`),
  KEY `col14_index` (`col14`),
  KEY `pk_substitute` (`col14`,`col2`,`col3`,`col15`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8;

Set a very low range_optimizer_max_mem_size:
MySQL > set session range_optimizer_max_mem_size=100;
Query OK, 0 rows affected (0.00 sec)

Run the select:
explain SELECT * FROM test_table FORCE KEY(pk_substitute) WHERE test_table.col14 = '22222222AAAAAAAAAAAAAAAAAA' AND (test_table.col2, test_table.col3, test_table.col15) IN  (   ( "AAAA","1111111111","aaaa" ), ( "BBBB","2222222222","bbbb" ), ( "CCCC","3333333333","cccc" ), ( "AAAA","4444444444","dddd" ), ( "BBBB","5555555555","eeee" ), ( "CCCC","6666666666","ffff" ), ( "AAAA","7777777777","gggg" ), ( "BBBB","8888888888","hhhh" ), ( "CCCC","9999999999","iiii" ), ( "AAAA","11111111110","jjjj" ), ( "BBBB","12222222221","kkkk" ), ( "CCCC","13333333332","llll" ), ( "AAAA","14444444443","mmmm" ), ( "BBBB","15555555554","nnnn" ), ( "CCCC","16666666665","oooo" ), ( "AAAA","17777777776","ppp" ), ( "BBBB","18888888887","qqqq" ));
+----+-------------+------------+------------+------+---------------+---------------+---------+-------+------+----------+-------------+
| id | select_type | table      | partitions | type | possible_keys | key           | key_len | ref   | rows | filtered | Extra       |
+----+-------------+------------+------------+------+---------------+---------------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | test_table | NULL       | ref  | pk_substitute | pk_substitute | 98      | const |    1 |   100.00 | Using where |
+----+-------------+------------+------------+------+---------------+---------------+---------+-------+------+----------+-------------+

It's using the pk_substitute.

Run the update:
explain UPDATE test_table FORCE KEY(pk_substitute) SET test_table.col11 = 1, test_table.col9 = 1  WHERE test_table.col14 = '22222222AAAAAAAAAAAAAAAAAA' AND (test_table.col2, test_table.col3, test_table.col15) IN  (   ( "AAAA","1111111111","aaaa" ), ( "BBBB","2222222222","bbbb" ), ( "CCCC","3333333333","cccc" ), ( "AAAA","4444444444","dddd" ), ( "BBBB","5555555555","eeee" ), ( "CCCC","6666666666","ffff" ), ( "AAAA","7777777777","gggg" ), ( "BBBB","8888888888","hhhh" ), ( "CCCC","9999999999","iiii" ), ( "AAAA","11111111110","jjjj" ), ( "BBBB","12222222221","kkkk" ), ( "CCCC","13333333332","llll" ), ( "AAAA","14444444443","mmmm" ), ( "BBBB","15555555554","nnnn" ), ( "CCCC","16666666665","oooo" ), ( "AAAA","17777777776","ppp" ), ( "BBBB","18888888887","qqqq" ));
+----+-------------+------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table      | partitions | type  | possible_keys | key     | key_len | ref  | rows | filtered | Extra       |
+----+-------------+------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
|  1 | UPDATE      | test_table | NULL       | index | NULL          | PRIMARY | 1468    | NULL |    1 |   100.00 | Using where |
+----+-------------+------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+

Increase the size of range_optimizer_max_mem_size:
MySQL > set session range_optimizer_max_mem_size=40000;
Query OK, 0 rows affected (0.00 sec)

Then it starts to use the pk_substitute as well:
explain UPDATE test_table FORCE KEY(pk_substitute) SET test_table.col11 = 1, test_table.col9 = 1  WHERE test_table.col14 = '22222222AAAAAAAAAAAAAAAAAA' AND (test_table.col2, test_table.col3, test_table.col15) IN  (   ( "AAAA","1111111111","aaaa" ), ( "BBBB","2222222222","bbbb" ), ( "CCCC","3333333333","cccc" ), ( "AAAA","4444444444","dddd" ), ( "BBBB","5555555555","eeee" ), ( "CCCC","6666666666","ffff" ), ( "AAAA","7777777777","gggg" ), ( "BBBB","8888888888","hhhh" ), ( "CCCC","9999999999","iiii" ), ( "AAAA","11111111110","jjjj" ), ( "BBBB","12222222221","kkkk" ), ( "CCCC","13333333332","llll" ), ( "AAAA","14444444443","mmmm" ), ( "BBBB","15555555554","nnnn" ),
( "CCCC","16666666665","oooo" ), ( "AAAA","17777777776","ppp" ), ( "BBBB","18888888887","qqqq" ));
+----+-------------+------------+------------+-------+---------------+---------------+---------+-------------------------+------+----------+-------------+
| id | select_type | table      | partitions | type  | possible_keys | key           | key_len | ref                     | rows | filtered | Extra       |
+----+-------------+------------+------------+-------+---------------+---------------+---------+-------------------------+------+----------+-------------+
|  1 | UPDATE      | test_table | NULL       | range | pk_substitute | pk_substitute | 1082    | const,const,const,const |   17 |   100.00 | Using where |
+----+-------------+------------+------------+-------+---------------+---------------+---------+-------------------------+------+----------+-------------

Suggested fix:
The optimizer should be able to use a more efficient plan than a costly full-table scan. It should use the Force index independently of the size of the range_optimizer_max_mem_size as the select commands.
[3 Jul 2023 7:42] Przemyslaw Malkowski
Likely related: https://bugs.mysql.com/bug.php?id=109107
[3 Jul 2023 12:41] MySQL Verification Team
Hi Mr. de Bem,

Thank you for your performance bug report.

However, it is not a bug.

The problem is that the UPDATE is changing the values of the columns in the `pk_substutute` key. SELECT does not change anything, so it has not problem of selecting the best available index.

If your UPDATE would change values of the columns that are not used in the range search , it should be using the same key.

However, if changed nodes of those columns could fit in the buffer of the sufficient size, as set in range_optimizer_max_mem_size, then it can use the best index too.

This is expected behaviour.