Bug #104421 | Bad plan for WHERE customer_id = @cid ORDER BY id LIMIT 10 with compound PK. | ||
---|---|---|---|
Submitted: | 26 Jul 2021 19:58 | Modified: | 29 Jul 2021 7:03 |
Reporter: | Jean-François Gagné | Email Updates: | |
Status: | Verified | Impact on me: | |
Category: | MySQL Server: Optimizer | Severity: | S2 (Serious) |
Version: | 8.0.26, 5.7.35 | OS: | Any |
Assigned to: | CPU Architecture: | Any |
[26 Jul 2021 19:58]
Jean-François Gagné
[26 Jul 2021 20:00]
Jean-François Gagné
Reproduction for 5.7.35.
Attachment: 5.7.35.txt (text/plain), 7.94 KiB.
[28 Jul 2021 10:27]
MySQL Verification Team
Hello Jean-François, Thank you for the report and feedback. I tried to reproduce the issue at my end but not seeing any issues. I'll be joining the activity log shortly for your reference(only diff being I have modified the *alter* to include PK to match your table definition ). Please let me know if I'm missing anything. Thank you. Sincerely, Umesh
[28 Jul 2021 10:27]
MySQL Verification Team
MySQL Server 8.0.26 test results
Attachment: 104421_8.0.26.log (application/octet-stream, text), 7.32 KiB.
[28 Jul 2021 20:44]
Jean-François Gagné
Thanks for looking into that Umesh. If I am modifying your perl script to this: for(my $i=1; $i <= 25*1000; $i++) { my $txt = 'x' x 10; my $customer_id = 1 + int(rand(4294967295-1)); my $object_id = 1 + int(rand(4294967295-1)); my $type_id = 1 + int(rand(255 - 1)); my $j=$i*2-1; print "$j,$customer_id,$object_id,$type_id,$txt\n"; $j++; print "$j,123456789,$object_id,$type_id,$txt\n"; } And if I load this way: { echo "BEGIN;"; perl bug104421_jfg.pl | while read l; do echo "INSERT INTO p VALUES(${l/xxxxxxxxxx/\'xxxxxxxxxx\'});"; done | sed -e '1000~1000s/^/COMMIT;BEGIN;/'; } | ./use test_jfg After the alter, I am getting the bad plan: explain SELECT id, object_id, details FROM p WHERE customer_id = 123456789 ORDER BY id LIMIT 10\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: p partitions: NULL type: index possible_keys: PRIMARY,customer_id key: id key_len: 8 ref: NULL rows: 20 filtered: 50.00 Extra: Using where 1 row in set, 1 warning (0.00 sec) In this case, the bad plan is fast because the rows with customer_id = 123456789 are evenly distributed in the table: mysql [localhost:8026] {msandbox} (test_jfg) > pager cat > /dev/null; flush local status; SELECT id, object_id, details FROM p WHERE customer_id = 123456789 ORDER BY id LIMIT 10\G pager; show status like "Handler_read%"; PAGER set to 'cat > /dev/null' Query OK, 0 rows affected (0.00 sec) 10 rows in set (0.00 sec) Default pager wasn't set, using stdout. +-----------------------+-------+ | Variable_name | Value | +-----------------------+-------+ | Handler_read_first | 1 | | Handler_read_key | 1 | | Handler_read_last | 0 | | Handler_read_next | 19 | | Handler_read_prev | 0 | | Handler_read_rnd | 0 | | Handler_read_rnd_next | 0 | +-----------------------+-------+ 7 rows in set (0.01 sec) Things change if I mess-up with the row distribution using this script to generate data: for(my $i=1; $i <= 25*1000; $i++) { my $txt = 'x' x 10; my $customer_id = 1 + int(rand(4294967295-1)); my $object_id = 1 + int(rand(4294967295-1)); my $type_id = 1 + int(rand(255 - 1)); print "$i,$customer_id,$object_id,$type_id,$txt\n"; } for(my $i=1; $i <= 25*1000; $i++) { my $txt = 'x' x 10; my $customer_id = 1 + int(rand(4294967295-1)); my $object_id = 1 + int(rand(4294967295-1)); my $type_id = 1 + int(rand(255 - 1)); my $j=25*1000+$i*2-1; print "$j,$customer_id,$object_id,$type_id,$txt\n"; $j++; print "$j,123456789,$object_id,$type_id,$txt\n"; } We are still having the same bad plan: mysql [localhost:8026] {msandbox} (test_jfg) > explain SELECT id, object_id, details FROM p WHERE customer_id = 123456789 ORDER BY id LIMIT 10\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: p partitions: NULL type: index possible_keys: PRIMARY,customer_id key: id key_len: 8 ref: NULL rows: 20 filtered: 50.00 Extra: Using where 1 row in set, 1 warning (0.00 sec) But the query execution is bad (runtime is ok, but row examined is bad, and runtime would be much worse if the table was bigger and not fitting in RAM): mysql [localhost:8026] {msandbox} (test_jfg) > pager cat > /dev/null; flush local status; SELECT id, object_id, details FROM p WHERE customer_id = 123456789 ORDER BY id LIMIT 10\G pager; show status like "Handler_read%"; PAGER set to 'cat > /dev/null' Query OK, 0 rows affected (0.00 sec) 10 rows in set (0.06 sec) Default pager wasn't set, using stdout. +-----------------------+-------+ | Variable_name | Value | +-----------------------+-------+ | Handler_read_first | 1 | | Handler_read_key | 1 | | Handler_read_last | 0 | | Handler_read_next | 25019 | | Handler_read_prev | 0 | | Handler_read_rnd | 0 | | Handler_read_rnd_next | 0 | +-----------------------+-------+ 7 rows in set (0.00 sec) And this is why I like the plan below is better, but I do not understand why we need to hint this (as written in the Description, my guess is that rows is bad in below): mysql [localhost:8026] {msandbox} (test_jfg) > explain SELECT id, object_id, details FROM p use index (customer_id) WHERE customer_id = 123456789 ORDER BY id L IMIT 10\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: p partitions: NULL type: ref possible_keys: customer_id key: customer_id key_len: 4 ref: const rows: 37439 filtered: 100.00 Extra: NULL 1 row in set, 1 warning (0.00 sec) And the good plan above leads to a good execution time and rows examined: mysql [localhost:8026] {msandbox} (test_jfg) > pager cat > /dev/null; flush local status; SELECT id, object_id, details FROM p use index (customer_id) WHERE customer_id = 123456789 ORDER BY id LIMIT 10\G pager; show status like "Handler_read%"; PAGER set to 'cat > /dev/null' Query OK, 0 rows affected (0.00 sec) 10 rows in set (0.00 sec) Default pager wasn't set, using stdout. +-----------------------+-------+ | Variable_name | Value | +-----------------------+-------+ | Handler_read_first | 0 | | Handler_read_key | 1 | | Handler_read_last | 0 | | Handler_read_next | 9 | | Handler_read_prev | 0 | | Handler_read_rnd | 0 | | Handler_read_rnd_next | 0 | +-----------------------+-------+ 7 rows in set (0.00 sec) I hope this helps. Thanks again Umesh for the time spent on this, and best regards, Jean-François Gagné
[28 Jul 2021 21:46]
Jean-François Gagné
One more thing, maybe this bug similar to Bug#97001, also described in [1] and [2]. I do not think it is a duplicate though as Bug#97001 is the optimizer having 2 bad choices and choosing the worse (avoiding a sort), and this bug is the optimizer clearly choosing a bad plan when a good one exists. [1]: https://www.percona.com/community-blog/2019/07/29/mysql-optimizer-naughty-aberrations-on-q... [2]: https://blog.jcole.us/2019/09/30/reconsidering-access-paths-for-index-ordering-a-dangerous...
[29 Jul 2021 4:01]
MySQL Verification Team
Thank you, Jean-François. I'll give it a try today to confirm at my end. Sincerely, Umesh
[29 Jul 2021 7:03]
MySQL Verification Team
Thank you, Jean-François. -- 8.0.26 cat bug104421_jfg.pl for(my $i=1; $i <= 25*1000; $i++) { my $txt = 'x' x 10; my $customer_id = 1 + int(rand(4294967295-1)); my $object_id = 1 + int(rand(4294967295-1)); my $type_id = 1 + int(rand(255 - 1)); my $j=$i*2-1; print "$j,$customer_id,$object_id,$type_id,$txt\n"; $j++; print "$j,123456789,$object_id,$type_id,$txt\n"; } { echo "BEGIN;"; perl bug104421_jfg.pl | while read l; do echo "INSERT INTO p VALUES(${l/xxxxxxxxxx/\'xxxxxxxxxx\'});"; done | sed -e '1000~1000s/^/COMMIT;BEGIN;/'; } | bin/mysql -uroot -S /tmp/mysql_ushastry.sock --local-infile test_jfg mysql> explain SELECT id, object_id, details FROM p WHERE customer_id = 123456789 ORDER BY id LIMIT 10\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: p partitions: NULL type: index possible_keys: PRIMARY,customer_id key: id key_len: 8 ref: NULL rows: 20 filtered: 50.00 Extra: Using where 1 row in set, 1 warning (0.00 sec) - mysql> show status like "Handler_read%"; +-----------------------+-------+ | Variable_name | Value | +-----------------------+-------+ | Handler_read_first | 1 | | Handler_read_key | 1 | | Handler_read_last | 0 | | Handler_read_next | 19 | | Handler_read_prev | 0 | | Handler_read_rnd | 0 | | Handler_read_rnd_next | 0 | +-----------------------+-------+ 7 rows in set (0.00 sec) mysql> . regards, Umesh