Quantcast
Channel: partitions – Cloud Data Architect
Viewing all articles
Browse latest Browse all 413

Migrating to MySQL 8.0 for WordPress – episode 3: query optimization

$
0
0

Feed: Planet MySQL
;
Author: Frederic Descamps
;

Now that MySQL 8.0.3 RC1 is installed and that we saw how to verify the workload, it’s time to see if we can optimize some of the queries. As explained before, rewriting queries when using a product like WordPress is complicated but maybe we can do something for the indexes ?

So, do you remember how to check the query that was consuming most resources ? Let’s find it out again:

mysql> select t1.*, QUERY_SAMPLE_TEXT from statement_analysis as t1 
                join performance_schema.events_statements_summary_by_digest as t2 
                on t2.digest=t1.digest and t2.SCHEMA_NAME = t1.db where db = 'wp_lefred' 
                limit 1G
*************************** 1. row ***************************
            query: SELECT `option_name` , `option ... options` WHERE `autoload` = ? 
               db: wp_lefred
        full_scan: *
       exec_count: 103
        err_count: 0
       warn_count: 0
    total_latency: 2.97 s
      max_latency: 600.21 ms
      avg_latency: 28.82 ms
     lock_latency: 39.46 ms
        rows_sent: 28825
    rows_sent_avg: 280
    rows_examined: 208849
rows_examined_avg: 2028
    rows_affected: 0
rows_affected_avg: 0
       tmp_tables: 0
  tmp_disk_tables: 0
      rows_sorted: 0
sort_merge_passes: 0
           digest: 52292f0ae858595a6dfe100f30207c9f
       first_seen: 2017-10-24 14:59:50.492922
        last_seen: 2017-10-24 23:45:26.192839
QUERY_SAMPLE_TEXT: SELECT option_name, option_value FROM wp_options WHERE autoload = 'yes'

Nothing special… let’s check the Query Execution Plan (QEP):

mysql> explain SELECT option_name, option_value FROM wp_options WHERE autoload = 'yes'G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: wp_options
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1551
     filtered: 10.00
        Extra: Using where
1 row in set, 1 warning (0.02 sec)

Hummm… so here we can definitely see that this is a full table scan (type: ALL) and it scans 1551 rows.

Let’s now verify the table’s structure:

mysql> show create table wp_optionsG
*************************** 1. row ***************************
       Table: wp_options
Create Table: CREATE TABLE `wp_options` (
  `option_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `option_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
  `option_value` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
  `autoload` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'yes',
  PRIMARY KEY (`option_id`),
  UNIQUE KEY `option_name` (`option_name`)
) ENGINE=InnoDB AUTO_INCREMENT=980655 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
1 row in set (0.08 sec)

OK so it’s clear that autoload is not indexed… and that the type doesn’t seems super adequate…

Let’s verify:

mysql> select distinct autoload FROM wp_options;
+----------+
| autoload |
+----------+
| yes      |
| no       |
+----------+
2 rows in set (0.00 sec)

mysql> select  autoload, count(*) FROM wp_options group by autoload;
+----------+----------+
| autoload | count(*) |
+----------+----------+
| yes      |      280 |
| no       |     1309 |
+----------+----------+
2 rows in set (0.00 sec)

Now let’s run the query and check if MySQL performs really a full table scan:

mysql> flush status;
Query OK, 0 rows affected (0.07 sec)

mysql> SELECT option_name, option_value FROM wp_options WHERE autoload = 'yes'G
...
mysql> show status like 'ha%';
+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| Handler_commit             | 1     |
| Handler_delete             | 0     |
| Handler_discover           | 0     |
| Handler_external_lock      | 2     |
| Handler_mrr_init           | 0     |
| Handler_prepare            | 0     |
| Handler_read_first         | 1     |
| Handler_read_key           | 1     |
| Handler_read_last          | 0     |
| Handler_read_next          | 0     |
| Handler_read_prev          | 0     |
| Handler_read_rnd           | 0     |
| Handler_read_rnd_next      | 1590  |
| Handler_rollback           | 0     |
| Handler_savepoint          | 0     |
| Handler_savepoint_rollback | 0     |
| Handler_update             | 0     |
| Handler_write              | 0     |
+----------------------------+-------+
18 rows in set (0.06 sec)

Handler_read_rnd_next is incremented when the server performs a full table scan and this is a
counter you don’t really want to see with a high value. So indeed in our case we perform a full table scan.

The QEP can also be more detailed when using the JSON format:

mysql> explain format=JSON SELECT option_name, option_value FROM wp_options WHERE autoload = 'yes'G
*************************** 1. row ***************************
EXPLAIN: {
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "202.85"
    },
    "table": {
      "table_name": "wp_options",
      "access_type": "ALL",
      "rows_examined_per_scan": 1546,
      "rows_produced_per_join": 154,
      "filtered": "10.00",
      "cost_info": {
        "read_cost": "187.39",
        "eval_cost": "15.46",
        "prefix_cost": "202.85",
        "data_read_per_join": "131K"
      },
      "used_columns": [
        "option_name",
        "option_value",
        "autoload"
      ],
      "attached_condition": "(`wp_lefred`.`wp_options`.`autoload` = 'yes')"
    }
  }
}
1 row in set, 1 warning (0.03 sec)

This is already enough information for this query, but we could have even more details enabling the OPTIMIZER TRACE:

mysql> SET OPTIMIZER_TRACE = "enabled=on";
Query OK, 0 rows affected (0.01 sec)

mysql> explain SELECT option_name, option_value FROM wp_options WHERE autoload = 'yes'G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: wp_options
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1546
     filtered: 10.00
        Extra: Using where
1 row in set, 1 warning (0.00 sec)


mysql> select * from INFORMATION_SCHEMA.OPTIMIZER_TRACEG
*************************** 1. row ***************************
                            QUERY: explain SELECT option_name, option_value 
                                   FROM wp_options WHERE autoload = 'yes'
                            TRACE: {
  "steps": [
    {
      "join_preparation": {
        "select#": 1,
        "steps": [
          {
            "expanded_query": "/* select#1 */ select `wp_options`.`option_name` AS `option_name`,
            `wp_options`.`option_value` AS `option_value` from `wp_options` 
             where (`wp_options`.`autoload` = 'yes')"
          }
        ]
      }
    },
    {
      "join_optimization": {
        "select#": 1,
        "steps": [
          {
            "condition_processing": {
              "condition": "WHERE",
              "original_condition": "(`wp_options`.`autoload` = 'yes')",
              "steps": [
                {
                  "transformation": "equality_propagation",
                  "resulting_condition": "(`wp_options`.`autoload` = 'yes')"
                },
                {
                  "transformation": "constant_propagation",
                  "resulting_condition": "(`wp_options`.`autoload` = 'yes')"
                },
                {
                  "transformation": "trivial_condition_removal",
                  "resulting_condition": "(`wp_options`.`autoload` = 'yes')"
                }
              ]
            }
          },
          {
            "substitute_generated_columns": {
            }
          },
          {
            "table_dependencies": [
              {
                "table": "`wp_options`",
                "row_may_be_null": false,
                "map_bit": 0,
                "depends_on_map_bits": [
                ]
              }
            ]
          },
          {
            "ref_optimizer_key_uses": [
            ]
          },
          {
            "rows_estimation": [
              {
                "table": "`wp_options`",
                "table_scan": {
                  "rows": 1546,
                  "cost": 48.25
                }
              }
            ]
          },
          {
            "considered_execution_plans": [
              {
                "plan_prefix": [
                ],
                "table": "`wp_options`",
                "best_access_path": {
                  "considered_access_paths": [
                    {
                      "rows_to_scan": 1546,
                      "filtering_effect": [
                      ],
                      "final_filtering_effect": 0.1,
                      "access_type": "scan",
                      "resulting_rows": 154.6,
                      "cost": 202.85,
                      "chosen": true
                    }
                  ]
                },
                "condition_filtering_pct": 100,
                "rows_for_plan": 154.6,
                "cost_for_plan": 202.85,
                "chosen": true
              }
            ]
          },
          {
            "attaching_conditions_to_tables": {
              "original_condition": "(`wp_options`.`autoload` = 'yes')",
              "attached_conditions_computation": [
              ],
              "attached_conditions_summary": [
                {
                  "table": "`wp_options`",
                  "attached": "(`wp_options`.`autoload` = 'yes')"
                }
              ]
            }
          },
          {
            "refine_plan": [
              {
                "table": "`wp_options`"
              }
            ]
          }
        ]
      }
    },
    {
      "join_explain": {
        "select#": 1,
        "steps": [
        ]
      }
    }
  ]
}
MISSING_BYTES_BEYOND_MAX_MEM_SIZE: 0
          INSUFFICIENT_PRIVILEGES: 0
1 row in set (0.01 sec)

For this particular easy query, this is not important but that can be useful. Particularly when you know that you can influence on the cost model.

Optimizing the query

It’s then clear that we could benefit from an index here (and maybe reduce the size of the field, but I won’t modify table structures for now).

Let’s create an index on autoload:

mysql> alter table wp_options add index autoload_idx(autoload);

And we can verify the QEP with the new index:

mysql> explain SELECT option_name, option_value FROM wp_options WHERE autoload = 'yes'G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: wp_options
   partitions: NULL
         type: ref
possible_keys: autoload_idx
          key: autoload_idx
      key_len: 82
          ref: const
         rows: 280
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

Don’t forget that with MySQL 8.0 it’s also possible to set some indexes invisible. Good candidates are the indexes returned by these queries:

mysql> select * from schema_unused_indexes where object_schema='wp_lefred';
+---------------+-------------+--------------+
| object_schema | object_name | index_name   |
+---------------+-------------+--------------+
| wp_lefred     | wp_links    | link_visible |
| wp_lefred     | wp_termmeta | term_id      |
| wp_lefred     | wp_termmeta | meta_key     |
| wp_lefred     | wp_users    | user_email   |
+---------------+-------------+--------------+

mysql> select * from schema_redundant_indexes where table_schema='wp_lefred';
Empty set (0.20 sec)

This post now concludes the migration to MySQL 8.0 for WordPress series and I hope this article will help you finding which queries need to be optimized !


Viewing all articles
Browse latest Browse all 413

Trending Articles