Bug #101391 | Performance Schema storage engine doesn't use index optimizations on group by | ||
---|---|---|---|
Submitted: | 30 Oct 2020 3:04 | Modified: | 19 Nov 2020 16:09 |
Reporter: | Agustín G | Email Updates: | |
Status: | Verified | Impact on me: | |
Category: | MySQL Server: Performance Schema | Severity: | S5 (Performance) |
Version: | 8.0, 8.0.22 | OS: | Any |
Assigned to: | Marc ALFF | CPU Architecture: | Any |
[30 Oct 2020 3:04]
Agustín G
[30 Oct 2020 6:18]
MySQL Verification Team
Hello Agustín, Thank you for the report. thanks, Umesh
[3 Nov 2020 9:41]
Marc ALFF
Thanks for the detailed report. This is actually not a bug, explanations below. --- When executing a query, the optimizer considers different execution plans, and based on costs estimates for each plan, picks the most efficient one. Having an index allows the optimizer to consider it, but is by no mean a guarantee that the index WILL be used. In particular, for a table with very few rows, the optimizer may still decide to perform a full table scan and apply a where clause, instead of using an index, even if a proper index is available. The fact that the same query, even on the same data, can be executed with different plans using a performance schema table or an innodb table is due to the different costs estimates returned by each engines, which have different characteristics. As long as the following conditions are true: - the data returned is correct - the execution time for the plan chosen by the optimizer is satisfactory then there is no "bug" in the query execution. Put another way, to have a confirmed bug: - either the data returned is incorrect, - or the execution time fails catastrophically, typically because a wrong execution plan was used, pointing to bad cost estimation. This does not seem to be the case here.
[5 Nov 2020 19:40]
Trey Raymond
ref "When executing a query, the optimizer considers different execution plans, and based on costs estimates for each plan, picks the most efficient one." I mean, yeah, most of the time it works alright, but it's no secret that it's an imperfect process with some guesswork - it doesn't always pick the most efficient one. Else we wouldn't need fixes to the optimizer in every release, yeah? So it sounds like the cost estimate figures for that engine are a bit off. Which means this is still a bug, but the fix would be to review and tweak those, vs changing the optimizer code. That sounds easier, the optimizer code is large and complex. Don't see how "the solution is easier than assumed" makes this "not a bug" though.
[19 Nov 2020 12:09]
Przemyslaw Malkowski
Hi Marc, I think we all very much welcomed indexes in P_S as a result of https://dev.mysql.com/worklog/task/?id=6616, in a hope that things will be now more efficient. Unfortunately, for some reason it appears to be not picking the indexes in all cases, as efficient as for InnoDB engine, which I guess we'd all benefit if could be improved. Maybe the query provided in the test case does not meet this requirement: "- or the execution time fails catastrophically, typically because a wrong execution plan was used, pointing to bad cost estimation." It is true that for small number of rows the optimizer may choose table scan, but I can see the issue applies also to big number of rows as well. The problem is that a similar P_S query may be fairly fast when there is no active locking situation in the instance. But for high concurrent environment with many tables, same query may become extremely slow. Here is another query showing more significant speed difference between P_S and InnoDB when we have 10k (empty) tables opened in a few transactions. Interestingly, for this one EXPLAIN shows the index is used, but estimated rows is far different as well as the execution time is ~150x (!) faster on InnoDB. The only visible change in the plan is usage of "Using index condition" for InnoDB. Test case: $ for i in {1..10000}; do mysql test -e "create table if not exists t$i (id int primary key)"; done $ echo "use test; begin;" > trx.sql $ for i in {1..10000}; do echo "select * from t$i;" >> trx.sql; done -- session 1 $ mysql -A mysql > source trx.sql -- session 2 $ mysql -A mysql > source trx.sql -- session 3 $ mysql -A mysql > source trx.sql -- session 4 $ mysql -A mysql > source trx.sql -- Above sessions are kept open -- session 5 mysql > select count(*) from performance_schema.metadata_locks\G *************************** 1. row *************************** count(*): 40809 1 row in set (0.03 sec) mysql > select thd_blocker.processlist_id -> from performance_schema.metadata_locks mdl_blocker -> join performance_schema.metadata_locks mdl_exclusive -> on mdl_blocker.object_schema=mdl_exclusive.object_schema -> and mdl_blocker.object_name=mdl_exclusive.object_name -> and mdl_blocker.owner_thread_id!=mdl_exclusive.owner_thread_id -> join performance_schema.metadata_locks mdl_blocked -> on mdl_blocked.object_schema=mdl_exclusive.object_schema -> and mdl_blocked.object_name=mdl_exclusive.object_name -> and mdl_blocked.owner_thread_id!=mdl_exclusive.owner_thread_id -> join performance_schema.threads thd_blocker -> on thd_blocker.thread_id=mdl_blocker.owner_thread_id -> where mdl_exclusive.object_type='TABLE' -> and mdl_exclusive.lock_type='EXCLUSIVE' -> and mdl_exclusive.lock_status='PENDING' -> and mdl_blocker.object_type='TABLE' -> and mdl_blocker.lock_type='SHARED_READ' -> and mdl_blocker.lock_status='GRANTED' -> and mdl_blocked.object_type='TABLE' -> and mdl_blocked.lock_type='SHARED_READ' -> and mdl_blocked.lock_status='PENDING' -> group by mdl_blocker.owner_thread_id; Empty set (1 min 47.90 sec) Same query for InnoDB engine (while all the locking threads are still there): mysql > truncate test.metadata_locks; insert into test.metadata_locks select * fro m performance_schema.metadata_locks; Query OK, 0 rows affected (0.05 sec) Query OK, 40823 rows affected (1.87 sec) Records: 40823 Duplicates: 0 Warnings: 0 mysql > select thd_blocker.processlist_id -> from test.metadata_locks mdl_blocker -> join test.metadata_locks mdl_exclusive -> on mdl_blocker.object_schema=mdl_exclusive.object_schema -> and mdl_blocker.object_name=mdl_exclusive.object_name -> and mdl_blocker.owner_thread_id!=mdl_exclusive.owner_thread_id -> join test.metadata_locks mdl_blocked -> on mdl_blocked.object_schema=mdl_exclusive.object_schema -> and mdl_blocked.object_name=mdl_exclusive.object_name -> and mdl_blocked.owner_thread_id!=mdl_exclusive.owner_thread_id -> join performance_schema.threads thd_blocker -> on thd_blocker.thread_id=mdl_blocker.owner_thread_id -> where mdl_exclusive.object_type='TABLE' -> and mdl_exclusive.lock_type='EXCLUSIVE' -> and mdl_exclusive.lock_status='PENDING' -> and mdl_blocker.object_type='TABLE' -> and mdl_blocker.lock_type='SHARED_READ' -> and mdl_blocker.lock_status='GRANTED' -> and mdl_blocked.object_type='TABLE' -> and mdl_blocked.lock_type='SHARED_READ' -> and mdl_blocked.lock_status='PENDING' -> group by mdl_blocker.owner_thread_id; Empty set (0.72 sec) EXPLAIN for P_S: mysql > explain for connection 10\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: mdl_blocker partitions: NULL type: ref possible_keys: OBJECT_TYPE,OWNER_THREAD_ID key: OBJECT_TYPE key_len: 258 ref: const rows: 10 filtered: 1.00 Extra: Using where; Using temporary *************************** 2. row *************************** id: 1 select_type: SIMPLE table: thd_blocker partitions: NULL type: eq_ref possible_keys: PRIMARY key: PRIMARY key_len: 8 ref: performance_schema.mdl_blocker.OWNER_THREAD_ID rows: 1 filtered: 100.00 Extra: NULL *************************** 3. row *************************** id: 1 select_type: SIMPLE table: mdl_exclusive partitions: NULL type: ref possible_keys: OBJECT_TYPE key: OBJECT_TYPE key_len: 776 ref: const,performance_schema.mdl_blocker.OBJECT_SCHEMA,performance_schema.mdl_blocker.OBJECT_NAME rows: 147 filtered: 0.90 Extra: Using where *************************** 4. row *************************** id: 1 select_type: SIMPLE table: mdl_blocked partitions: NULL type: ref possible_keys: OBJECT_TYPE key: OBJECT_TYPE key_len: 776 ref: const,performance_schema.mdl_blocker.OBJECT_SCHEMA,performance_schema.mdl_blocker.OBJECT_NAME rows: 147 filtered: 100.00 Extra: Using where 4 rows in set (0.00 sec) EXPLAIN for InnoDB: mysql > explain for connection 10\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: mdl_blocker partitions: NULL type: ref possible_keys: OBJECT_TYPE,OWNER_THREAD_ID key: OBJECT_TYPE key_len: 258 ref: const rows: 18995 filtered: 1.00 Extra: Using index condition; Using where; Using temporary *************************** 2. row *************************** id: 1 select_type: SIMPLE table: mdl_exclusive partitions: NULL type: ref possible_keys: OBJECT_TYPE key: OBJECT_TYPE key_len: 776 ref: const,test.mdl_blocker.OBJECT_SCHEMA,test.mdl_blocker.OBJECT_NAME rows: 3 filtered: 1.31 Extra: Using where *************************** 3. row *************************** id: 1 select_type: SIMPLE table: mdl_blocked partitions: NULL type: ref possible_keys: OBJECT_TYPE key: OBJECT_TYPE key_len: 776 ref: const,test.mdl_blocker.OBJECT_SCHEMA,test.mdl_blocker.OBJECT_NAME rows: 3 filtered: 1.31 Extra: Using where *************************** 4. row *************************** id: 1 select_type: SIMPLE table: thd_blocker partitions: NULL type: eq_ref possible_keys: PRIMARY key: PRIMARY key_len: 8 ref: test.mdl_blocker.OWNER_THREAD_ID rows: 1 filtered: 100.00 Extra: NULL 4 rows in set (0.00 sec) Tested on MySQL 8.0.22.
[19 Nov 2020 16:59]
Marc ALFF
Hi Przemyslaw, Thank you for the more detailed query and use case. Based on this, the bug is now verified then. A couple of remarks: To analyze, but I think the issue is not really whether to force the optimizer to use an index or not, but rather the execution cost of an actual index fetch in the performance schema. For a "real" hash index, one can expect a cost in O(1). For a "real" tree index, one can expect a cost in O(LOG(N)) For a performance schema index (definitively fake then), the code internally scans the whole table to filter rows. The filter is simple, but the internal complexity is then O(N) for each fetch, which makes a full index scan O(N^2) per table. Performance schema indexes are good to support simple PK or FK lookups, but fail here when used to implement a join. With table test.metadata_locks joined 3 times, and N=40K, this is now a problem. Regards, -- Marc Alff