优化SQL的一般步骤
一、分析执行计划
1、通过 EXPLAIN 分析低效 SQL 的执行计划
mysql> explain select sum(amount) from customer a, payment b where 1=1 and a.customer_id = b.customer_id and email = 'JANE.BENNETT@sakilacustomer.org' \G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: a
partitions: NULL
type: ALL
possible_keys: PRIMARY
key: NULL
key_len: NULL
ref: NULL
rows: 599
filtered: 10.00
Extra: Using where
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: b
partitions: NULL
type: ref
possible_keys: idx_fk_customer_id
key: idx_fk_customer_id
key_len: 2
ref: sakila.a.customer_id
rows: 26
filtered: 100.00
Extra: NULL
2 rows in set, 1 warning (0.00 sec)
输出项说明
# select_type :表示 SELECT 的类型,常见的取值有
SIMPLE(简单表,即不使用表连接或子查询)、
PRIMARY(主查询,即外层的查询)、
UNION(UNION 中的第二个或者后面的查询语句)、
SUBQUERY(子查询中的第一个 SELECT )
# table :输出结果集的表
# type : 表示 MySQL 在表中找到所需行的方式,或者叫访问类型,常见的类型如下,从左至右,性能由最差到最好
ALL < index < range < ref < eq_ref < const,system < NULL
1) ALL :全表扫描, MySQL 遍历全表来找到匹配的行。
2) index:索引全扫描, MySQL 遍历整个索引来查询匹配的行。
3) range:索引范围扫描,常见于 < 、 <= 、 > 、 >= 、 between 等操作符。
4) ref:使用非唯一索引扫描或唯一索引的前缀扫描,返回匹配某个单独值得记录行。
5) eq_ref:类似 ref ,区别在于使用的索引是唯一索引,也就是在多表连接中使用 primary key 或者 unique index 作为关联条件。
6) const,system:单表中最多有一个匹配行,查询起来非常迅速,所以这个匹配行中的其他列的值可以被优化器在当前查询中当作常量来处理。
7) NULL:MySQL 不用访问表或者索引,直接就能够得到结果。
8) ref_of_null:与 ref 类似,区别在于条件中包含对 NULL 的查询。
9) index_merge:索引合并优化。
10)unique_subquery:in 的后面是一个查询主键字段的子查询。
11)index_subquery:与 unique_subquery 类似,区别在于 in 的后面是查询非唯一索引字段的子查询
# possible_keys :表示查询是可能使用的索引。
# key :表示实际使用的索引。
# key_len :使用到索引字段的长度。
# rows :扫描行的数量。
# Extra :执行情况的说明和描述,包含不适合在其他列中显示但是对执行计划非常重要的额外信息。
1) Using where:表示优化器除了利用索引来加快访问之外,还需要根据索引回表查询数据。
2) Using index:表示直接访问索引就足够获取到所需要的数据,不需要通过索引回表,也就是平时说的覆盖索引扫描。
3) Using index condition:表示 MySQL 使用了 ICP(Index Condition Pushdown)来进一步优化查询。Pushdown 表示操作下放,某些情况下的条件过滤操作下放到存储引擎。
4) Using filesort:不是通过索引直接返回排序结果的排序都叫 Filesort 排序
# filtered :表示存储引擎返回的数据在server层过滤后,剩下多少满足查询的记录数量的比例
2、分区表执行计划
使用 explain partitions 的输出结果中有一列 partitions ,其中显示 SQL 所需要访问的分区名字 p2
mysql> explain partitions select * from customer_part where customer_id = 130 \G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: customer_part
partitions: p2
type: const
possible_keys: PRIMARY
key: PRIMARY
key_len: 2
ref: const
rows: 1
filtered: 100.00
Extra: NULL
1 row in set, 2 warnings (0.00 sec)
二、查看执行过程中线程的每个状态和消耗时间
方法 1:通过 show profile 分析
1) 分析 SQL 执行过程中线程的每个状态和消耗时间
注意:在 MySQL 5.7 中,profile 已经不建议使用,而使用 performance schema 中的一系列性能视图来代替。
#1) 查看是否支持 profile
mysql> select @@have_profiling;
+------------------+
| @@have_profiling |
+------------------+
| YES |
+------------------+
1 row in set, 1 warning (0.00 sec)
#2) 默认 profiling 是关闭的,可以通过 set 语句在 Session 级别开启 profiling
mysql> select @@profiling;
+-------------+
| @@profiling |
+-------------+
| 0 |
+-------------+
1 row in set, 1 warning (0.01 sec)
mysql> set profiling=1;
Query OK, 0 rows affected, 1 warning (0.00 sec)
#3) 首先,在一个 InnoDB 引擎的付款表 payment 上,执行一个 count(*) 查询。
mysql> select count(*) from payment;
+----------+
| count(*) |
+----------+
| 16049 |
+----------+
1 row in set (0.32 sec)
#4) 执行完毕后,通过 show profiles 语句,看到当前 SQL 的 Query_ID 为 2。
mysql> show profiles;
+----------+------------+------------------------------+
| Query_ID | Duration | Query |
+----------+------------+------------------------------+
| 1 | 0.00026275 | select @@profiling |
| 2 | 0.32845575 | select count(*) from payment |
+----------+------------+------------------------------+
2 rows in set, 1 warning (0.00 sec)
#5) 通过 show profile for query 语句能够看到执行过程中线程的每个状态和消耗时间。
mysql> show profile for query 2;
+----------------------+----------+
| Status | Duration |
+----------------------+----------+
| starting | 0.000174 |
| checking permissions | 0.000016 |
| Opening tables | 0.000024 |
| init | 0.000019 |
| System lock | 0.000012 |
| optimizing | 0.000007 |
| statistics | 0.000017 |
| preparing | 0.000015 |
| executing | 0.000004 |
| Sending data | 0.328066 |
| end | 0.000020 |
| query end | 0.000016 |
| closing tables | 0.000012 |
| freeing items | 0.000029 |
| cleaning up | 0.000026 |
+----------------------+----------+
15 rows in set, 1 warning (0.00 sec)
Sending data 状态表示 MySQL 线程开始访问数据行并把结果返回给客户端,而不仅仅是返回结果给客户端,由于 Sending data 状态下,MySQL 线程往往需要做大量的磁盘读取操作,所以经常是整个查询中耗时最长的状态。
2) 获取到最耗时的线程状态后,MySQL 支持进一步选择 all、cpu、block io、context、switch、page faults 等明细类型来查看 MySQL 在使用什么资源上耗费过高的时间
mysql> show profile cpu for query 2;
+----------------------+----------+----------+------------+
| Status | Duration | CPU_user | CPU_system |
+----------------------+----------+----------+------------+
| starting | 0.000174 | 0.000037 | 0.000133 |
| checking permissions | 0.000016 | 0.000003 | 0.000010 |
| Opening tables | 0.000024 | 0.000005 | 0.000019 |
| init | 0.000019 | 0.000004 | 0.000014 |
| System lock | 0.000012 | 0.000003 | 0.000009 |
| optimizing | 0.000007 | 0.000001 | 0.000006 |
| statistics | 0.000017 | 0.000004 | 0.000014 |
| preparing | 0.000015 | 0.000003 | 0.000011 |
| executing | 0.000004 | 0.000001 | 0.000003 |
| Sending data | 0.328066 | 0.134053 | 0.446006 |
| end | 0.000020 | 0.000002 | 0.000009 |
| query end | 0.000016 | 0.000004 | 0.000012 |
| closing tables | 0.000012 | 0.000002 | 0.000010 |
| freeing items | 0.000029 | 0.000007 | 0.000022 |
| cleaning up | 0.000026 | 0.000006 | 0.000020 |
+----------------------+----------+----------+------------+
15 rows in set, 1 warning (0.00 sec)
方法 2 :查询 information_schema.profiling 表
可以更清晰的看到 SQL 执行过程中线程的每个状态和消耗时间和当前线程占总消耗时间的百分比。
mysql> select state,sum(duration) as total_r,round(100 * sum(duration)/(select sum(duration) from information_schema.profiling where query_id = 2),2) as pct_r,count(*) as calls,sum(duration)/count(*) as "R/Call" from information_schema.profiling where query_id = 2 group by state order by total_r desc;
+----------------------+----------+-------+-------+--------------+
| state | total_r | pct_r | calls | R/Call |
+----------------------+----------+-------+-------+--------------+
| Sending data | 0.328066 | 99.88 | 1 | 0.3280660000 |
| starting | 0.000174 | 0.05 | 1 | 0.0001740000 |
| freeing items | 0.000029 | 0.01 | 1 | 0.0000290000 |
| cleaning up | 0.000026 | 0.01 | 1 | 0.0000260000 |
| Opening tables | 0.000024 | 0.01 | 1 | 0.0000240000 |
| end | 0.000020 | 0.01 | 1 | 0.0000200000 |
| init | 0.000019 | 0.01 | 1 | 0.0000190000 |
| statistics | 0.000017 | 0.01 | 1 | 0.0000170000 |
| checking permissions | 0.000016 | 0.00 | 1 | 0.0000160000 |
| query end | 0.000016 | 0.00 | 1 | 0.0000160000 |
| preparing | 0.000015 | 0.00 | 1 | 0.0000150000 |
| System lock | 0.000012 | 0.00 | 1 | 0.0000120000 |
| closing tables | 0.000012 | 0.00 | 1 | 0.0000120000 |
| optimizing | 0.000007 | 0.00 | 1 | 0.0000070000 |
| executing | 0.000004 | 0.00 | 1 | 0.0000040000 |
+----------------------+----------+-------+-------+--------------+
15 rows in set, 2 warnings (0.00 sec)
三、分析优化器如何选择执行计划
使用 trace 分析优化器如何选择执行计划
#1) 打开 trace ,设置格式为 JSON 。
mysql> set optimizer_trace="enabled=on",end_markers_in_json=on;
Query OK, 0 rows affected (0.00 sec)
#2) 设置 trace 最大能够使用的内存大小,避免解析过程中因为默认内存过小而不能够完整显示。
mysql> set optimizer_trace_max_mem_size=1000000;
Query OK, 0 rows affected (0.00 sec)
#3) 接下来执行想做 trace 的 SQL 语句。
mysql> select rental_id from sakila.rental where 1=1 and rental_date >= '2005-05-25 04:00:00' and rental_date <= '2005-05-25 05:00:00' and inventory_id=4466;
+-----------+
| rental_id |
+-----------+
| 39 |
+-----------+
1 row in set (0.34 sec)
#4) 然后,检查 information_schema.OPTIMIZER_TRACE 就可以知道 MySQL 是如何执行 SQL 语句的。
mysql> select * from information_schema.OPTIMIZER_TRACE \G;
四、确定问题并采取相应的优化措施
#1) 从执行计划可以确认对客户表 customer 的全表扫描导致不理想。
mysql> explain select sum(amount) from sakila.customer a,sakila.payment b where 1=1 and a.customer_id=b.customer_id and email='JANE.BENNETT@sakilacustomer.org' \G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: a
partitions: NULL
type: ALL
possible_keys: PRIMARY
key: NULL
key_len: NULL
ref: NULL
rows: 599
filtered: 10.00
Extra: Using where
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: b
partitions: NULL
type: ref
possible_keys: idx_fk_customer_id
key: idx_fk_customer_id
key_len: 2
ref: sakila.a.customer_id
rows: 26
filtered: 100.00
Extra: NULL
2 rows in set, 1 warning (0.00 sec)
#2) 对 customer 的 email 字段创建索引
mysql> create index idx_email on sakila.customer(email);
Query OK, 0 rows affected (0.06 sec)
Records: 0 Duplicates: 0 Warnings: 0
#创建索引后对客户表 customer 需要扫描的行数明显减少。
mysql> explain select sum(amount) from sakila.customer a,sakila.payment b where 1=1 and a.customer_id=b.customer_id and email='JANE.BENNETT@sakilacustomer.org' \G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: a
partitions: NULL
type: ref
possible_keys: PRIMARY,idx_email
key: idx_email
key_len: 203
ref: const
rows: 1
filtered: 100.00
Extra: Using index
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: b
partitions: NULL
type: ref
possible_keys: idx_fk_customer_id
key: idx_fk_customer_id
key_len: 2
ref: sakila.a.customer_id
rows: 26
filtered: 100.00
Extra: NULL
2 rows in set, 1 warning (0.00 sec)