Real-life scenario
We have two different applications, one for admin and another one for regular users and customers. Inside the admin panel, one of our queries was consuming too much time to execute, leading to timeouts and high CPU usage. The first thing we did was get the whole query and run an EXPLAIN command on it, and it returned to us something like this:
{
"query_block": {
"select_id": 1,
"const_condition": "1",
"filesort": {
"sort_key": "maskedTableName.`id` desc",
"temporary_table": {
"table": {
"table_name": "maskedTable",
"access_type": "const",
"possible_keys": ["PRIMARY"],
"key": "PRIMARY",
"key_length": "162",
"used_key_parts": ["id"],
"ref": ["const"],
"rows": 1,
"filtered": 100
},
... // A LOT OF MORE INFORMATION THAT WE CAN IGNORE
"table": {
"table_name": "maskedTableName2",
"access_type": "const",
"possible_keys": [
"masked_field_key_foreign",
"masked_field_key_perf"
],
"rows": 1,
"filtered": 100
},
"table": {
"table_name": "maskedTableName3",
"access_type": "ref",
"possible_keys": ["masked_field_key_id_foreign"],
"key": "masked_field_key_id_foreign",
"key_length": "162",
"used_key_parts": ["masked_field_id"],
"ref": ["const"],
"rows": 1,
"filtered": 100
},
"block-nl-join": {
"table": {
"table_name": "maskedTableName11",
"access_type": "ALL",
"rows": 13850,
"filtered": 100
},
"buffer_type": "flat",
"buffer_size": "256Kb",
"join_type": "BNL",
"attached_condition": "trigcond(trigcond(0) and trigcond(0) and trigcond(0) and trigcond(0) and trigcond(0) and trigcond(0) and trigcond(0) and trigcond(0)) and trigcond(0)"
},
"subqueries": [
{
"query_block": {
"select_id": 4,
"table": {
"table_name": "maskedTableName12",
"access_type": "ref",
"possible_keys": [
"maskedTableName12_id_forein",
"maskedTableName12_idx"
],
"key": "maskedTableName12_id_forein",
"key_length": "163",
"used_key_parts": ["masked_field_id"],
"ref": ["const"],
"rows": 1,
"filtered": 100,
"index_condition": "maskedTableName12_id_forein.field_id = 'dbde90fecec23126'",
"attached_condition": "maskedTableName12_id_forein.`event` = 'event.canceled'"
}
}
},
{
"query_block": {
"select_id": 3,
"table": {
"table_name": "maskedTable55",
"access_type": "ALL",
"rows": 193,
"filtered": 100,
"attached_condition": "'312890132' = tablemasked.provider_id and tablemasked.deleted_at is null"
}
}
}
]
}
}
}
}
Our first action with this return was looking at all access types returned and finding those that were not CONST and those that were ALL. The reason for that is that there are a few different access types, and they indicate how the data is being joined and queried. Const means that a constant time is used to query the information, which is a good thing. ALL means that the DBMS is doing a full table scan, which is, most of the time, what causes slowness in these queries.
The access types are system, const, eq_ref, ref, fulltext, ref_or_null, index_merge, unique_subquery, index_subquery, range, index, and ALL.
If you want details about each of them, I highly recommend you to take a look at it:
https://dev.mysql.com/doc/refman/8.4/en/explain-output.html. For this article, we will keep it simple and restricted to CONST and ALL.
Well, if we look at one of the first items returned by the EXPLAIN, the maskedTable is not the problem. Take a look at its return:
"table": {
"table_name": "maskedTable",
"access_type": "const",
"possible_keys": ["PRIMARY"],
"key": "PRIMARY",
"key_length": "162",
"used_key_parts": ["id"],
"ref": ["const"],
"rows": 1,
"filtered": 100
},
The access type is CONST; it is being handled by a PRIMARY KEY that is indexed, and a single row is returned. The problem does not reside there. Now, moving to maskedTableName2 and maskedTableName3, they have the same characteristics, so they are not good candidates for us to worry about. But when we move to the next one, we see this:
"block-nl-join": {
"table": {
"table_name": "maskedTableName11",
"access_type": "ALL",
"rows": 13850,
"filtered": 100
},
"buffer_type": "flat",
"buffer_size": "256Kb",
"join_type": "BNL",
"attached_condition": "trigcond(trigcond(0) and trigcond(0) and trigcond(0) and trigcond(0) and trigcond(0) and trigcond(0) and trigcond(0) and trigcond(0)) and trigcond(0)"
},
In the access type, we have “ALL,” which means that for joining this table, we are doing a full table scan (reading the full book to get the information). When we check the number of rows, we see 13850, and it informs us that a Nested Loop Join is happening. Nested Loops are something we need to pay attention to; it is the equivalent of having a for loop inside another for loop (and we know that we need to avoid this kind of operation). It makes this a great candidate for us to work on. The last one we can make changes on is this one:
"query_block": {
"select_id": 3,
"table": {
"table_name": "maskedTable55",
"access_type": "ALL",
"rows": 193,
"filtered": 100,
"attached_condition": "'312890132' = tablemasked.provider_id and tablemasked.deleted_at is null"
}
}
But if we check the number of rows, this is not as bad as the first one. So, our priority should be to handle the maskedTableName11 first. And that's what we did back in the day. In this case, we saw that the problem was residing in the way we were calling our ORM; it was doing a LEFT JOIN on a false condition, and it was returning the wrong data all the time and slowing down the query. After fixing it, the query was faster. We also add proper indexation, as shown above. Then, for maskedTableName55, the problem was just indexation. We added the index, and the query sped up again. In this real scenario, we went from 40s to 2.5s in production.
This is just a simple example of how you could investigate and speed up queries in a real-life scenario. Of course, this is not how you will approach every single situation, and indexation is not the only thing that will speed up and save performance on your project. This is just the first part of a series of articles I intend to write about this subject, but I hope it helps you to at least start taking a look at it.
Additional methods that can improve database performance
- Partitioning
- Normalization/Denormalization
- Caching
- Index Rebuilding
- Replication
- Sharding
Thanks for reading this article. The next parts will be focused on different topics to help you improve your database performance.
This article was originally written by Luã Enrique Zangrande and published on
Medium.