通常在数据量较少的时候,我们并没有那么在意SQL语句的性能问题,只要能到达目的即可;但是当你面对浩大的数据量仍然这么做时,面临的往往是耗时良久或者数据崩溃;当然,数据库优化的方式有很多,这里我们着重介绍SQL优化。
准备工作:
既然要研究数据量较大的表,那么首先我们需要一个数据库,该数据库里要有很多表,表中要有很多内容;MySQL官方提供了一个模拟电影出租厅信息管理系统的数据库sakila,它的下载地址为:http://downloads.mysql.com/docs/sakila-db.zip 。压缩包中包含三个文件:
- sakila-schema.sql:数据库及表结构创建文本;
- sakila-data.sql:数据插入文本;
- sakila.mwb:sakila的MySQL Workbench数据模型,可以在MySQL工作台打开查看该数据模型。
打开cmd终端,连上MySQL数据库,执行两个脚本文件 :
mysql> source C:\\Users\\15330\\Desktop\\sakila-schema.sql;
mysql> source C:\\Users\\15330\\Desktop\\sakila-data.sql;
成功后数据就准备好了。
一、优化SQL语句的一般步骤
一般分为四步:
1. 了解数据库各种SQL语句执行的频率,通过频率初步得出可能存在的问题;
2. 定位到执行效率低的SQL语句上;
3. 通过多种方法分析导致低效SQL语句的原因;
4. 根据找到的原因提出合理的解决办法,达到优化的目的。
下面就根据这些步骤一一进行介绍。
1.1 通过show status命令查看各语句的频率
可以在MySQL连接后使用命令 SHOW [session | global] STATUS命令查看,也可以在操作系统上使用 mysqladmin extended-status 命令来查看。session关键字用来显示session级(当前连接)的统计结果,global关键字用来显示global级(自数据库上次启动至今)的统计结果。省略不写的话默认为session。
例子:
mysql> show status like 'Com_%';
+-----------------------------+-------+
| Variable_name | Value |
+-----------------------------+-------+
| Com_admin_commands | 0 |
| Com_assign_to_keycache | 0 |
| Com_alter_db | 0 |
| Com_alter_db_upgrade | 0 |
| Com_alter_event | 0 |
| Com_alter_function | 0 |
| Com_alter_instance | 0 |
| Com_alter_procedure | 0 |
| Com_alter_server | 0 |
| Com_alter_table | 2 |
。。。。。
Com_xxx表示xxx语句执行的次数,通常有以下几个比较关心:
- Com_select:执行SELECT操作的次数,一次查询只累加1;
- Com_insert:执行INSERT操作的次数,一次插入只累加1,批量插入也只算一次;
- Com_update:执行UPDATE操作的次数;
- Com_delete:执行DELETE操作的次数;
上面的参数对所有的存储引擎都适用,下面几个参数是单独针对InnoDB的,累加的算法也略有不同:
- Innodb_rows_read:SELECT查询返回的行数;
- Innodb_rows_inserted:INSERT操作插入的行数;
- Innodb_rows_updated:UPDATA操作更新的行数;
- Innodb_rows_deleted:DELETED操作删除的行数;
注意:上面的更新操作计数,是对执行次数的计数,因此不论是提交还是回滚都会进行累加。
通过Com_commit 和 Com_rollback可以看到事务的提交和回滚情况,对于回滚非常频繁的数据库,可能就意味着应用编写的有问题。
通过这些参数我们就可以了解到当前数据库的应用是以插入更新为主还是查询操作为主,以及各种类型的SQL大致执行的比例是多少;这样我们就可以根据这个大方向来判断可能会有问题的SQL语句。
以下几个参数可以便于用户了解数据库的基本情况:
- Connections:试图连接MySQL服务器的次数;
- Uptime:服务器工作时间;
- Slow_queries:慢查询的次数。
1.2 定位执行效率低的SQL语句
可以通过以下两种方式来定位执行效率低的SQL语句:
- 通过慢查询日志定位;通过设定long_query_time的大小来指定SQL语句执行时间超过设定值时就记录到慢查询日志中,然后通过查询日志就可以找到相应执行缓慢的SQL语句了;
- 慢查询日志只是在查询结束后才记录,所以,如果要实时查看,慢查询日志是无法起作用的;这时可以使用show processlist命令查看当前MySQL在进行的线程,包括线程的状态,是否锁表等等,可以看到实时的状态。
1.3 分析原因
通过EXPLAIN分析
可以通过 EXPLAIN 或 DESC 命令来获取语句的执行信息,通过这些信息我们可以分析出一些问题,例如看下面的例子:
mysql> explain select sum(amount) from customer a,payment b where 1=1 and a.customer_id = b.customer_id and email = '[email protected]' \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.01 sec)
下面解释一下各个参数的意义,这样通过参数的值就大概能清楚SQL语句执行过程中的问题:
- id:select查询的序列号,是一组数字,表示的是查询中执行select子句或者是操作表的顺序。id相同表示加载表的顺序是从上到下;id不同id值越大,优先级越高,越先被执行;id有相同,也有不同,同时存在。id相同的可以认为是一组,从上往下顺序执行;在所有的组中,id的值越大,优先级越高,越先执行。
- select_type:表示SELECT的类型,常见的取值有SIMPLE(简单表,即不使用表连接或子查询)、PRIMARY(主查询,即外层的查询)、UNION(UNION中的第二个或者后面的查询语句)、SUBQUERY(子查询中的第一个SELECT)等。
- table:输出结果集的表。
- type:MySQL在表中找到所需行的方式,或者叫访问类型;常见的取值有ALL、index、range、ref、eq_ref、const/system、NULL,性能从前到后依次变好。
- possible_keys:表示查询时可能使用的索引;
- key:表示实际使用的索引;
- key_len:使用到索引字段的长度,在不损失精度的情况下长度越短越好;
- ref:显示索引的哪一列被使用了;
- rows:扫描行的数量;
- filtered:过滤掉的行数;
- Extra:执行情况的说明和描述,通常会包含一些比较重要的额外信息;
下面着重介绍以下type类型,从性能最差到最好一次介绍:
1. type=ALL,全表扫描;
mysql> explain select * from film where rating > 9 \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: film
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 1000
filtered: 33.33
Extra: Using where
1 row in set, 1 warning (0.01 sec)
2. type=index,全索引扫描;
mysql> explain select title from film \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: film
partitions: NULL
type: index
possible_keys: NULL
key: idx_title
key_len: 767
ref: NULL
rows: 1000
filtered: 100.00
Extra: Using index
1 row in set, 1 warning (0.00 sec)
3. type=range,索引范围扫描;常见于< > <= >= between等操作符;
mysql> explain select * from payment where customer_id >=300 and customer_id <= 350\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: payment
partitions: NULL
type: range
possible_keys: idx_fk_customer_id
key: idx_fk_customer_id
key_len: 2
ref: NULL
rows: 1350
filtered: 100.00
Extra: Using index condition
1 row in set, 1 warning (0.00 sec)
4. type=ref,使用非唯一索引扫描或唯一索引的前缀扫描,返回匹配某个单独值的记录行;
mysql> explain select * from payment where customer_id = 350\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: payment
partitions: NULL
type: ref
possible_keys: idx_fk_customer_id
key: idx_fk_customer_id
key_len: 2
ref: const
rows: 23
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
5. type=eq_ref,类似于ref,区别在于使用的索引是唯一索引,对于索引的键值,表中只有一条记录匹配;简单的来说就是多表连接中使用primary key或unique index作为关联条件。
mysql> explain select * from film a,film_text b where a.film_id = b.film_id\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: b
partitions: NULL
type: ALL
possible_keys: PRIMARY
key: NULL
key_len: NULL
ref: NULL
rows: 1000
filtered: 100.00
Extra: NULL
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: a
partitions: NULL
type: eq_ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 2
ref: sakila.b.film_id
rows: 1
filtered: 100.00
Extra: Using where
2 rows in set, 1 warning (0.01 sec)
6. type=const/system,单表中最多有一个匹配行,查询起来非常迅速;这个匹配行中的其它列的值可以被优化器在当前查询中当做常量来处理,例如根据主键和唯一键进行的查询。
7. type=NULL,不用访问表或者索引,直接就可以得到结果;
还有很多取值还没列出,比如ref_or_null等等,后面遇到了再查会好一些。
再提一句,以前老版本中还有EXPLAIN EXTENDED 命令、EXPLAIN PARTITIONS命令,现在MySQL5.1版本之后都统一在EXPLAIN命令中了。
通过EXPLAIN命令分析例子中的SQL语句我们得知影响查询速度的原因是进行了全表扫描,但有时候这个方法也并不能分析出本质的原因,下面再介绍一种profile联合分析方法。
show profile分析方法:(注意5.6.7版本之后换成了新的命令performance schema)
profile方法可以清楚的看到SQL语句在各个阶段执行时所需要的时间。查看当前MySQL是否支持profile方法
mysql> select @@have_profiling;
+------------------+
| @@have_profiling |
+------------------+
| YES |
+------------------+
1 row in set, 1 warning (0.00 sec)
默认profiling是关闭的,可以通过set在session级别开启profiling,开启后只保存最近15次的运行结果:
mysql> select @@profiling;
+-------------+
| @@profiling |
+-------------+
| 0 |
+-------------+
1 row in set, 1 warning (0.00 sec)
mysql> set profiling=1;
Query OK, 0 rows affected, 1 warning (0.00 sec)
举个例子,同样是COUNT(*)操作,对于InnoDB类型的表,因为没有元数据缓存因此执行的较慢;但对于MyISAM类型的表,因为有表元数据的缓存,因此执行的较快。下面通过profile来具体分析一下:
mysql> select count(*) from payment;
+----------+
| count(*) |
+----------+
| 16049 |
+----------+
1 row in set (0.01 sec)
mysql> show profiles;
+----------+------------+------------------------------+
| Query_ID | Duration | Query |
+----------+------------+------------------------------+
| 1 | 0.01137100 | select count(*) from payment |
+----------+------------+------------------------------+
1 row in set, 1 warning (0.00 sec)
mysql> show profile for query 1;
+----------------------+----------+
| Status | Duration |
+----------------------+----------+
| starting | 0.000058 |
| checking permissions | 0.000006 |
| Opening tables | 0.000015 |
| init | 0.000016 |
| System lock | 0.000007 |
| optimizing | 0.011142 |
| executing | 0.000016 |
| end | 0.000002 |
| query end | 0.000009 |
| closing tables | 0.000008 |
| freeing items | 0.000047 |
| cleaning up | 0.000045 |
+----------------------+----------+
12 rows in set, 1 warning (0.00 sec)
mysql> create table payment_myisam like payment;
Query OK, 0 rows affected (0.04 sec)
mysql> alter table payment_myisam engine=myisam;
Query OK, 0 rows affected (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> insert into payment_myisam select * from payment;
Query OK, 16049 rows affected (0.06 sec)
Records: 16049 Duplicates: 0 Warnings: 0
mysql> select count(*) from payment_myisam;
+----------+
| count(*) |
+----------+
| 16049 |
+----------+
1 row in set (0.00 sec)
mysql> show profiles;
+----------+------------+--------------------------------------------------+
| Query_ID | Duration | Query |
+----------+------------+--------------------------------------------------+
| 1 | 0.01137100 | select count(*) from payment |
| 2 | 0.00022550 | show wainings |
| 3 | 0.00011500 | show warnings |
| 4 | 0.00015000 | show warnings |
| 5 | 0.00012900 | performance schema |
| 6 | 0.03744450 | create table payment_myisam like payment |
| 7 | 0.03600900 | alter table payment_myisam engine=myisam |
| 8 | 0.06122275 | insert into payment_myisam select * from payment |
| 9 | 0.00016400 | select count(*) from payment_myisam |
+----------+------------+--------------------------------------------------+
9 rows in set, 1 warning (0.00 sec)
mysql> show profile for query 9;
+----------------------+----------+
| Status | Duration |
+----------------------+----------+
| starting | 0.000047 |
| checking permissions | 0.000004 |
| Opening tables | 0.000013 |
| init | 0.000020 |
| System lock | 0.000005 |
| optimizing | 0.000006 |
| executing | 0.000006 |
| end | 0.000003 |
| query end | 0.000004 |
| closing tables | 0.000008 |
| freeing items | 0.000032 |
| cleaning up | 0.000016 |
+----------------------+----------+
12 rows in set, 1 warning (0.00 sec)
从例子中可以看到,InnoDB类型的表在optimizing过程中消耗了非常多的时间,而MyISAM类型的表基本没有消耗太多时间。
我们还可以用一个或多个关键字来查看optimizing消耗的那么多时间主要浪费在了什么资源上:
-
ALL: 显示所有的开销信息;
-
BLOCK IO : 显示块IO相关开销;
-
CONTEXT SWITCHS: 上下文切换相关开销;
-
CPU : 显示cpu 相关开销;
-
IPC: 显示发送和接收相关开销;
-
MEMORY: 显示内存相关开销;
-
PAGE FAULTS:显示页面错误相关开销信息;
-
SOURCE : 显示和Source_function ,Source_file,Source_line 相关的开销信息;
-
SWAPS:显示交换次数相关的开销信息;
比如要查看在CPU上消耗的时间:可以看到optimizing的时间主要消耗自系统cpu上;
mysql> show profile cpu for query 1;
+----------------------+----------+----------+------------+
| Status | Duration | CPU_user | CPU_system |
+----------------------+----------+----------+------------+
| starting | 0.000058 | 0.000000 | 0.000000 |
| checking permissions | 0.000006 | 0.000000 | 0.000000 |
| Opening tables | 0.000015 | 0.000000 | 0.000000 |
| init | 0.000016 | 0.000000 | 0.000000 |
| System lock | 0.000007 | 0.000000 | 0.000000 |
| optimizing | 0.011142 | 0.000000 | 0.015625 |
| executing | 0.000016 | 0.000000 | 0.000000 |
| end | 0.000002 | 0.000000 | 0.000000 |
| query end | 0.000009 | 0.000000 | 0.000000 |
| closing tables | 0.000008 | 0.000000 | 0.000000 |
| freeing items | 0.000047 | 0.000000 | 0.000000 |
| cleaning up | 0.000045 | 0.000000 | 0.000000 |
+----------------------+----------+----------+------------+
12 rows in set, 1 warning (0.00 sec)
如果对源码感兴趣,还可以使用 show profile source for query查看源码文件的相关信息。
通过trace分析:
MySQL5.6提供了对SQL的跟踪trace,通过trace文件可以进一步了解为什么优化器为什么选择A执行计划而不选择B执行计划,这样可以帮助我们更好的理解优化器的行为。
使用方式:首先打开trace,设置格式为JSON,并设置最大能够使用的内存大小,避免解析过程中内存太小而不能完整显示;然后执行想要做trace的SQL语句;最后检查INFORMATION_SCHEMA.OPTIMIZER_TRACE 就可以知道MySQL是如何执行SQL语句的。
mysql> set optimizer_trace="enabled=on",end_markers_in_json=on;
Query OK, 0 rows affected (0.00 sec)
mysql> set optimizer_trace_max_mem_size=1000000;
Query OK, 0 rows affected (0.00 sec)
mysql> select rental_id from 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.01 sec)
mysql> select * from information_schema.optimizer_trace \G
*************************** 1. row ***************************
QUERY: select rental_id from 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
TRACE: {
"steps": [
{
"join_preparation": {
"select#": 1,
"steps": [
{
"expanded_query": "/* select#1 */ select `rental`.`rental_id` AS `rental_id` from `rental` where ((1 = 1) and (`rental`.`rental_date` >= '2005-05-25 04:00:00') and (`rental`.`rental_date` <= '2005-05-25 05:00:00') and (`rental`.`inventory_id` = 4466))"
}
] /* steps */
。。。。。完整文件就不贴出来了,太长了
1.4 确定问题并采取相应办法
经过以上步骤大致就可以确定出现问题的原因,现在就是对应原因做出优化。比如前面提到过的那个因为是全表扫描导致查询结果缓慢的SQL语句,根据这一点,我们可以对它创建索引来提高查询效率:
mysql> create index idx_email on customer(email);
Query OK, 0 rows affected (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> explain select sum(amount) from customer a,payment b where 1=1 and a.customer_id = b.customer_id and email = '[email protected]' \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: a
partitions: NULL
type: ref
possible_keys: PRIMARY,idx_email
key: idx_email
key_len: 153
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)
可以看到对customer表的email建立索引过后,查询耗费的时间明显减少,并且查询的行数从583行减到1行,这说明建立索引可以十分精确的查找到所需内容,但有一点需要注意,它的key_len明显增大了。
二、两个简单实用的优化方法
优化的方法有很多,但是对于一般开发人员来说下面介绍的两个最简单实用,一般掌握这两个就很有用处了。
2.1 定期分析表和检查表
分析表的语法如下:
ANALYZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name ] ....
这条语句能够用于分析然后存储表的关键字分布,分析的结果可以使得系统得到准确的统计信息,使得SQL能够生成正确的执行计划。比如当某个执行某个计划时并不是预期的那样而又不知道原因时,执行一次分析表可能会解决问题。
例如对表payment进行分析
mysql> analyze table payment;
+----------------+---------+----------+----------+
| Table | Op | Msg_type | Msg_text |
+----------------+---------+----------+----------+
| sakila.payment | analyze | status | OK |
+----------------+---------+----------+----------+
1 row in set (0.02 sec)
检查表的语法如下:
CHECK TABLE tbl_name [, tbl_name] ...[option] ... option = {QUICK|FAST|MEDIUM|EXTENDED|CHANGED}
检查表的作用是检查一个或多个表是否有错误,check table语句对MyISAM和InnoDB表有作用;并且对MyISAM表来说,关键字统计数据会被更新,例如
mysql> check table payment_myisam;
+-----------------------+-------+----------+----------+
| Table | Op | Msg_type | Msg_text |
+-----------------------+-------+----------+----------+
| sakila.payment_myisam | check | status | OK |
+-----------------------+-------+----------+----------+
1 row in set (0.01 sec)
检查表语句也可以检查视图是否有错误,比如视图定义中的表已经不存在,举例如下:
1. 首先创建一个视图,依赖表payment_myisam
mysql> create view v_payment_myisam as select * from payment_myisam;
Query OK, 0 rows affected (0.01 sec)
2. 检查一下视图,发现并没有问题
mysql> check table v_payment_myisam;
+-------------------------+-------+----------+----------+
| Table | Op | Msg_type | Msg_text |
+-------------------------+-------+----------+----------+
| sakila.v_payment_myisam | check | status | OK |
+-------------------------+-------+----------+----------+
1 row in set (0.00 sec)
3. 删除视图依赖的表payment_myisam
mysql> drop table payment_myisam;
Query OK, 0 rows affected (0.00 sec)
4. 再检查视图发现出错了
mysql> check table v_payment_myisam \G
*************************** 1. row ***************************
Table: sakila.v_payment_myisam
Op: check
Msg_type: Error
Msg_text: Table 'sakila.payment_myisam' doesn't exist
*************************** 2. row ***************************
Table: sakila.v_payment_myisam
Op: check
Msg_type: Error
Msg_text: View 'sakila.v_payment_myisam' references invalid table(s) or column(s) or function(s) or definer/invoker of view lack rights to use them
*************************** 3. row ***************************
Table: sakila.v_payment_myisam
Op: check
Msg_type: error
Msg_text: Corrupt
3 rows in set (0.00 sec)
2.2 定期优化表
优化表的语法如下:
OPTIMIZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] ...
如果已经删除了表的一大部分,或者如果已经对含有可变长度行的表(含有varchar、blob、text列的表)进行了很多更改,那么应该使用OPTIMIZE TABLE语句来进行表优化。(前面第五节讲blob和text字符类型时有提到过)这个命令可以将表中的空间碎片进行合并,并且可以消除由于删除或者更新造成的的空间浪费,但这个命令只对MyISAM、BDB、InnoDB表起作用。
比如下面的例子优化表payment_myisam,由于这个表已经被删掉了,所以会出现如下情况:
mysql> optimize table payment_myisam;
+-----------------------+----------+----------+---------------------------------------------+
| Table | Op | Msg_type | Msg_text |
+-----------------------+----------+----------+---------------------------------------------+
| sakila.payment_myisam | optimize | Error | Table 'sakila.payment_myisam' doesn't exist |
| sakila.payment_myisam | optimize | status | Operation failed |
+-----------------------+----------+----------+---------------------------------------------+
2 rows in set (0.00 sec)
对于InnoDB引擎的表来说,通过设置innodb_file_per_table参数,将InnoDB设置为独立表空间模式,这样,每个数据库的每个表都会生成一个独立的ibd文件,用于存储表的数据和索引,这样做在一定程度上可以减轻InnoDB表空间回收的问题。另外,在删除大量数据后,InnoDB表可以通过alter table但是不修改引擎的方式来回收不用的空间:
mysql> alter table payment engine=innodb;
Query OK, 0 rows affected (0.43 sec)
Records: 0 Duplicates: 0 Warnings: 0
注意:ANALYZE、CHECK、OPTIMIZE、ALTER TABLE执行期间将对表进行锁定,因此一定注意要在数据库不繁忙的时候执行相关操作。
三、常用的SQL的优化
最常用的是通过索引来优化查询,这一个会单独拿出来说;其它的语句像插入、分组等待都有各自的优化方法,下面就一一进行介绍。
3.1 大批量插入数据
未完待续。。。