In the “MySQL Query tuning 101” video, Alexander Rubin provides an excellent example of when to use a covered index. On slide 25, he takes the query select name from City where CountryCode = 'USA' and District = 'Alaska' and population > 10000 and adds the index cov1(CountryCode, District, population, name) on table City. With Alex’s query tuning experience, making the right index decision is simple – but what about us mere mortals? If a query is more complicated, or simply uses more than one table, how do we know what to do? Maintaining another index can slow down INSERT statements, so you need to be very careful when choosing one. Examining the array “used_columns” could help out.
Let’s assume a more complicated version of the query was used in “MySQL Query tuning 101”:
|
1 |
select City.name as city, Country.name as country, group_concat(Language) <br>from City join CountryLanguage using(CountryCode) <br>join Country <br>where City.CountryCode=Country.Code and Continent = 'North America' and District='St George' <br>group by City.name, Country.Name; |
Can we use a covered index here?
A traditional text-based EXPLAIN already shows that it is a pretty good plan:
|
1 |
mysql> explain select City.name as city, Country.name as country, group_concat(Language) from City join CountryLanguage using(CountryCode) join Country where City.CountryCode=Country.Code and Continent = 'North America' and District='St George' group by City.name, Country.NameG<br>*************************** 1. row ***************************<br> id: 1<br> select_type: SIMPLE<br> table: Country<br> partitions: NULL<br> type: ALL<br>possible_keys: PRIMARY<br> key: NULL<br> key_len: NULL<br> ref: NULL<br> rows: 239<br> filtered: 14.29<br> Extra: Using where; Using temporary; Using filesort<br>*************************** 2. row ***************************<br> id: 1<br> select_type: SIMPLE<br> table: City<br> partitions: NULL<br> type: ref<br>possible_keys: CountryCode<br> key: CountryCode<br> key_len: 3<br> ref: world.Country.Code<br> rows: 18<br> filtered: 10.00<br> Extra: Using where<br>*************************** 3. row ***************************<br> id: 1<br> select_type: SIMPLE<br> table: CountryLanguage<br> partitions: NULL<br> type: ref<br>possible_keys: PRIMARY,CountryCode<br> key: CountryCode<br> key_len: 3<br> ref: world.Country.Code<br> rows: 4<br> filtered: 100.00<br> Extra: Using index<br>3 rows in set, 1 warning (0.00 sec)<br><br>Note (Code 1003): /* select#1 */ select `world`.`City`.`Name` AS `city`,`world`.`Country`.`Name` AS `country`,group_concat(`world`.`CountryLanguage`.`Language` separator ',') AS `group_concat(Language)` from `world`.`City` join `world`.`CountryLanguage` join `world`.`Country` where ((`world`.`City`.`District` = 'St George') and (`world`.`Country`.`Continent` = 'North America') and (`world`.`City`.`CountryCode` = `world`.`Country`.`Code`) and (`world`.`CountryLanguage`.`CountryCode` = `world`.`Country`.`Code`)) group by `world`.`City`.`Name`,`world`.`Country`.`Name` |
Can we make it better? Since our topic is covered indexes, let’s consider this possibility.
EXPLAIN FORMAT=JSON will tell us to which columns we should add covered index:
|
1 |
mysql> explain format=json select City.name as city, Country.name as country, group_concat(Language) from City join CountryLanguage using(CountryCode) join Country where City.CountryCode=Country.Code and Continent = 'North America' and District='St George' group by City.name, Country.NameG<br>*************************** 1. row ***************************<br>EXPLAIN: {<br> "query_block": {<br> "select_id": 1,<br> "cost_info": {<br> "query_cost": "927.92"<br> },<br> <br><I skipped output for groupping operation and other tables here><br><br> {<br> "table": {<br> "table_name": "City",<br> "access_type": "ref",<br> "possible_keys": [<br> "CountryCode"<br> ],<br> "key": "CountryCode",<br> "used_key_parts": [<br> "CountryCode"<br> ],<br> "key_length": "3",<br> "ref": [<br> "world.Country.Code"<br> ],<br> "rows_examined_per_scan": 18,<br> "rows_produced_per_join": 63,<br> "filtered": "10.00",<br> "cost_info": {<br> "read_cost": "630.74",<br> "eval_cost": "12.61",<br> "prefix_cost": "810.68",<br> "data_read_per_join": "4K"<br> },<br> "used_columns": [<br> "ID",<br> "Name",<br> "CountryCode",<br> "District"<br> ],<br> "attached_condition": "(`world`.`City`.`District` = 'St George')"<br> }<br> }, |
The answer is in the array “used_columns”. It lists the ID (primary key) and all columns which I used in the query:
|
1 |
"used_columns": [<br> "ID",<br> "Name",<br> "CountryCode",<br> "District"<br> ], |
Now we can try adding a covered index:
|
1 |
mysql> alter table City add index cov(CountryCode, District, Name);<br>Query OK, 0 rows affected (2.74 sec)<br>Records: 0 Duplicates: 0 Warnings: 0 |
EXPLAIN confirms what index access (“using_index”: true ) is used:
|
1 |
mysql> explain format=json select City.name as city, Country.name as country, group_concat(Language) from City join CountryLanguage using(CountryCode) join Country where City.CountryCode=Country.Code and Continent = 'North America' and District='St George' group by City.name, Country.NameG<br>*************************** 1. row ***************************<br>EXPLAIN: {<br> "query_block": {<br> "select_id": 1,<br> "cost_info": {<br> "query_cost": "296.28"<br> },<br> <br><I skipped output for groupping operation and other tables here><br><br> {<br> "table": {<br> "table_name": "City",<br> "access_type": "ref",<br> "possible_keys": [<br> "CountryCode",<br> "cov"<br> ],<br> "key": "cov",<br> "used_key_parts": [<br> "CountryCode",<br> "District"<br> ],<br> "key_length": "23",<br> "ref": [<br> "world.Country.Code",<br> "const"<br> ],<br> "rows_examined_per_scan": 2,<br> "rows_produced_per_join": 100,<br> "filtered": "100.00",<br> "using_index": true,<br> "cost_info": {<br> "read_cost": "34.65",<br> "eval_cost": "20.19",<br> "prefix_cost": "108.64",<br> "data_read_per_join": "7K"<br> },<br> "used_columns": [<br> "ID",<br> "Name",<br> "CountryCode",<br> "District"<br> ]<br> }<br> }, |
It also provides such metrics as:
Conclusion: if the number of columns in used_columns array is reasonably small, you can use it as a guide for creating a covered index.
Resources
RELATED POSTS