Some of our Percona Support customers report performance degradation after a major MySQL upgrade, and there can be many different reasons for this. These days, the most common major upgrade is from MySQL 5.7 (which recently reached EOL) to 8.0, and I am going to emphasize one important case that affects many database instances.
Queries having many IN values use Equality Range Optimization in MySQL query optimizer. Let’s say our query looks like this:
|
1 |
SELECT COUNT(*) FROM test.sbtest1 WHERE id IN (10,50,200,...,30822); |
There are ten thousand values in the compared list. Running this one under MySQL 5.7 results in the following execution statistics:
|
1 |
mysql57 > source query1.sql<br>+----------+<br>| count(*) |<br>+----------+<br>| 17433 |<br>+----------+<br>1 row in set (0.16 sec) |
Slow log entry (truncated for readability):
|
1 |
# Time: 2024-06-23T13:34:05.912909Z<br># User@Host: msandbox[msandbox] @ localhost [] Id: 6<br># Query_time: 0.161071 Lock_time: 0.021591 Rows_sent: 1 Rows_examined: 17433<br>SET timestamp=1719149645;<br>select count(*) from test.sbtest1 where id in (...); |
As well as handler stats confirm the index is used properly and optimization worked:
|
1 |
mysql57 > show status like 'ha%';<br>+----------------------------+-------+<br>| Variable_name | Value |<br>+----------------------------+-------+<br>| Handler_commit | 1 |<br>| Handler_delete | 0 |<br>| Handler_discover | 0 |<br>| Handler_external_lock | 2 |<br>| Handler_mrr_init | 0 |<br>| Handler_prepare | 0 |<br>| Handler_read_first | 0 |<br>| Handler_read_key | 17433 |<br>| Handler_read_last | 0 |<br>| Handler_read_next | 0 |<br>| Handler_read_prev | 0 |<br>| Handler_read_rnd | 0 |<br>| Handler_read_rnd_next | 0 |<br>| Handler_rollback | 0 |<br>| Handler_savepoint | 0 |<br>| Handler_savepoint_rollback | 0 |<br>| Handler_update | 0 |<br>| Handler_write | 0 |<br>+----------------------------+-------+<br>18 rows in set (0.00 sec) |
The query EXPLAIN confirms the range type and index:
|
1 |
mysql57 > source explain1.sql<br>*************************** 1. row ***************************<br> id: 1<br> select_type: SIMPLE<br> table: sbtest1<br> partitions: NULL<br> type: range<br>possible_keys: PRIMARY<br> key: PRIMARY<br> key_len: 4<br> ref: NULL<br> rows: 17433<br> filtered: 100.00<br> Extra: Using where; Using index<br>1 row in set, 1 warning (0.06 sec) |
However, in our upgraded instance, using the same table, we end up with the following results:
|
1 |
mysql80 > source query1.sql<br>+----------+<br>| count(*) |<br>+----------+<br>| 17433 |<br>+----------+<br>1 row in set, 1 warning (0.38 sec)<br><br>mysql80 > show warnings;<br>*************************** 1. row ***************************<br> Level: Warning<br> Code: 3170<br>Message: Memory capacity of 8388608 bytes for 'range_optimizer_max_mem_size' exceeded. Range optimization was not done for this query.<br>1 row in set (0.00 sec) |
So the above warning already informs us about problems, which lead to a much worse optimizer plan as below:
|
1 |
# Time: 2024-06-23T13:44:51.610282Z<br># User@Host: msandbox[msandbox] @ localhost [] Id: 18<br># Query_time: 0.385067 Lock_time: 0.000004 Rows_sent: 1 Rows_examined: 1200000<br>SET timestamp=1719150291;<br>select count(*) from test.sbtest1 where id in (...);<br><br>mysql80 > show status like 'ha%';<br>+----------------------------+---------+<br>| Variable_name | Value |<br>+----------------------------+---------+<br>| Handler_commit | 1 |<br>| Handler_delete | 0 |<br>| Handler_discover | 0 |<br>| Handler_external_lock | 2 |<br>| Handler_mrr_init | 0 |<br>| Handler_prepare | 0 |<br>| Handler_read_first | 1 |<br>| Handler_read_key | 1 |<br>| Handler_read_last | 0 |<br>| Handler_read_next | 1200000 |<br>| Handler_read_prev | 0 |<br>| Handler_read_rnd | 0 |<br>| Handler_read_rnd_next | 0 |<br>| Handler_rollback | 0 |<br>| Handler_savepoint | 0 |<br>| Handler_savepoint_rollback | 0 |<br>| Handler_update | 0 |<br>| Handler_write | 0 |<br>+----------------------------+---------+<br>18 rows in set (0.00 sec) |
Query plan surprisingly shows a secondary index on a different column, and practically all table rows scanned:
|
1 |
mysql80 > source explain1.sql<br>*************************** 1. row ***************************<br> id: 1<br> select_type: SIMPLE<br> table: sbtest1<br> partitions: NULL<br> type: index<br>possible_keys: PRIMARY<br> key: k<br> key_len: 4<br> ref: NULL<br> rows: 1183608<br> filtered: 50.00<br> Extra: Using where; Using index<br>1 row in set, 2 warnings (0.05 sec) |
Now, the range optimization has the allowed memory limit defined by the range_optimizer_max_mem_size variable, which is, however, the same for MySQL 5.7 and 8.0 (8MB)! So why doesn’t the very same query against the same table fit in the newer version?
Let’s check the related memory footprint as instrumented in Performance Schema. On 5.7 (obtained after truncating the P_S table and running the query), it is about 5.5 MB:
|
1 |
mysql57 > select * from performance_schema.memory_summary_by_thread_by_event_name where thread_id=(select THREAD_ID from performance_schema.threads where processlist_id=CONNECTION_ID()) and<br> event_name='memory/sql/test_quick_select'G<br>*************************** 1. row ***************************<br> THREAD_ID: 32<br> EVENT_NAME: memory/sql/test_quick_select<br> COUNT_ALLOC: 104<br> COUNT_FREE: 104<br> SUM_NUMBER_OF_BYTES_ALLOC: 5705856<br> SUM_NUMBER_OF_BYTES_FREE: 5705856<br> LOW_COUNT_USED: 0<br> CURRENT_COUNT_USED: 0<br> HIGH_COUNT_USED: 104<br> LOW_NUMBER_OF_BYTES_USED: 0<br>CURRENT_NUMBER_OF_BYTES_USED: 0<br> HIGH_NUMBER_OF_BYTES_USED: 5705856<br>1 row in set (0.00 sec) |
But on MySQL 8.0, it is much higher, around 11.5 MB, so higher than the allowed limit of 8 MB:
|
1 |
mysql8.0 > select * from performance_schema.memory_summary_by_thread_by_event_name where thread_id=PS_CURRENT_THREAD_ID() and event_name='memory/sql/test_quick_select'G<br>*************************** 1. row ***************************<br> THREAD_ID: 47<br> EVENT_NAME: memory/sql/test_quick_select<br> COUNT_ALLOC: 18<br> COUNT_FREE: 18<br> SUM_NUMBER_OF_BYTES_ALLOC: 12099576<br> SUM_NUMBER_OF_BYTES_FREE: 12099576<br> LOW_COUNT_USED: 0<br> CURRENT_COUNT_USED: 0<br> HIGH_COUNT_USED: 18<br> LOW_NUMBER_OF_BYTES_USED: 0<br>CURRENT_NUMBER_OF_BYTES_USED: 0<br> HIGH_NUMBER_OF_BYTES_USED: 12099576<br>1 row in set (0.00 sec) |
Increasing the variable fixes the query plan:
|
1 |
mysql80 > set range_optimizer_max_mem_size=12*1024*1024;<br>Query OK, 0 rows affected (0.00 sec)<br><br>mysql80 > source query1.sql<br>+----------+<br>| count(*) |<br>+----------+<br>| 17433 |<br>+----------+<br>1 row in set (0.10 sec) |
In my opinion, this memory requirement difference is unjustified. Therefore I reported the regression here:
https://bugs.mysql.com/bug.php?id=115327
And separately, a misleading documentation bug:
https://bugs.mysql.com/bug.php?id=115062
This regression becomes more impactful the more indexes are on the table, as MySQL 8.0 is affected by this other related bug:
https://bugs.mysql.com/bug.php?id=104000
Upgrading to MySQL 8.x may be challenging and should be well-tested before implementation to avoid some very bad surprises, like the one above! In the long term, though, it is inevitable, as 5.7 reached EOL. Before this particular regression or any other that affects you is fixed, you may consider using our post-EOL support for the time.
As an addition, let me remind you that we have a tool useful for checking if any query behaves differently on the new version. Here is a quick update that illustrates how the extra warning was spot by it:
|
1 |
$ pt-upgrade h=127.0.0.1,P=5756,u=msandbox,p=***,D=test h=127.0.0.1,P=8055,u=msandbox,p=msandbox,D=test slow57.log<br><br>#-----------------------------------------------------------------------<br># Logs<br>#-----------------------------------------------------------------------<br><br>File: slow57.log<br>Size: 141681<br><br>#-----------------------------------------------------------------------<br># Hosts<br>#-----------------------------------------------------------------------<br><br>host1:<br><br> DSN: h=127.0.0.1,P=5756<br> hostname: przemek-dbg<br> MySQL: MySQL Community Server (GPL) 5.7.44<br><br>host2:<br><br> DSN: h=127.0.0.1,P=8055<br> hostname: przemek-dbg<br> MySQL: MySQL Community Server - GPL 8.0.37<br><br>########################################################################<br># Query class F4A5056EC85D02D0<br>########################################################################<br><br>Reporting class because it has diffs, but hasn't been reported yet.<br><br>Total queries 1<br>Unique queries 1<br>Discarded queries 0<br><br>select count(*) from test.sbtest? where id in(?+)<br><br>##<br>## Warning diffs: 1<br>##<br><br>-- 1.<br><br>No warning 3170<br><br>vs.<br><br> Code: 3170<br> Level: Warning<br>Message: Memory capacity of 8388608 bytes for 'range_optimizer_max_mem_size' exceeded. Range optimization was not done for this query.<br><br>select count(*) from test.sbtest1 where id in (90, ... ,13668,15161)<br><br>#-----------------------------------------------------------------------<br># Stats<br>#-----------------------------------------------------------------------<br><br>failed_queries 0<br>not_select 0<br>queries_filtered 0<br>queries_no_diffs 0<br>queries_read 1<br>queries_with_diffs 1<br>queries_with_errors 0 |
Resources
RELATED POSTS