文章目录
1. 引入
数据库服务器的性能好坏,除了本身硬件方面和网络传输的影响外,使用怎样的SQL语句来操作数据库同样会有很大的差异。一条编写合理、优美的SQL语句不仅可以得到正确的结果,而且还能极大的提升服务器响应的速度。因此,如果SQL语句成为了整个业务系统的性能瓶颈,那么优化SQL语句就变得十分重要了。
2. 优化SQL
2.1 获取统计信息
既然想要去优化SQL语句,那么首先就需要知道哪些SQL语句最有可能会影响整体的性能。Mysql提供了很多的命令帮助用户查看和性能相关的许多信息,通过show [session][global] status \G
命令可以查看服务器状态信息,其中:
- session:当前连接
- global:子数据库上次启动至今
例如,使用show status like 'Com______';
可以查看当前session中所有统计参数的值:
mysql> show status like "Com____________";
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| Com_alter_event | 0 |
| Com_alter_table | 0 |
| Com_create_role | 0 |
| Com_create_user | 0 |
| Com_create_view | 0 |
| Com_dealloc_sql | 0 |
| Com_drop_server | 0 |
| Com_empty_query | 0 |
| Com_execute_sql | 0 |
| Com_grant_roles | 0 |
| Com_lock_tables | 0 |
| Com_prepare_sql | 0 |
| Com_rename_user | 0 |
| Com_show_events | 0 |
| Com_show_errors | 0 |
| Com_show_fields | 0 |
| Com_show_grants | 0 |
| Com_show_status | 3 |
| Com_show_tables | 0 |
| Com_slave_start | 0 |
| Com_xa_rollback | 0 |
+-----------------+-------+
21 rows in set (0.00 sec)
其中Com_xxx
表示每个xxx语句执行的次数。通常关心的部分有:
- Com_select:执行select操作的次数
- Com_insert:执行insert操作的次数
- Com_update:执行update操作的次数
- Com_delete:执行delete操作的次数
也可以通过show status like 'Innodb_rows_%';
查看InnoDB中统计参数的值:
mysql> show status like 'Innodb_rows_%';
+----------------------+-------+
| Variable_name | Value |
+----------------------+-------+
| Innodb_rows_deleted | 1 |
| Innodb_rows_inserted | 8 |
| Innodb_rows_read | 41 |
| Innodb_rows_updated | 6 |
+----------------------+-------+
4 rows in set (0.00 sec)
其中:
- Innodb_rows_read:select查询返回的行数
- Innodb_rows_inserted:insert操作插入的行数
- Innodb_rows_updated:update操作更新的行数
- Innodb_rows_deleted:delete操作删除的行数
此外,其他相关的参数值还有:
- Connections:视图连接Mysql服务器的次数
- Uptime:服务器工作时间
- Slow_queries:慢查询次数
2.2 定位SQL
上面通过命令可以查看所有引擎或者具体某个引擎中统计参数的值,但是仍然无法定位到低效的SQL语句。Mysql中提供了如下两种方式来定位执行效率较低的SQL语句:
-
慢查询日志 : 通过慢查询日志定位那些执行效率较低的 SQL 语句,用
--log-slow-queries[=file_name]
选项启动时,mysqld 写一个包含所有执行时间超过long_query_time
秒的 SQL 语句的日志文件 -
show processlist : 慢查询日志在查询结束以后才纪录,所以在应用反映执行效率出现问题的时候查询慢查询日志并不能定位问题,可以使用
show processlist
命令查看当前MySQL在进行的线程,包括线程的状态、是否
锁表等,可以实时地查看 SQL 的执行情况,同时对一些锁表操作进行优化其中:
- id:系统分配的connection_id,用于连接数据库
- user:当前用户
- host:显示当前语句对应的ip地址和端口号
- db:当前所连接的数据库
- command:当前连接执行的命令,一般取值为sleep、query、connect等
- time:状态持续的时间
- state:当前连接的SQL语句的状态
- info:显示这个SQL语句
2.3 分析执行计划
通过前一部分可以查询到效率低的 SQL 语句之后,可以通过 EXPLAIN
或者 DESC
命令获取 MySQL如何执行 SELECT 语句的信息,包括在 SELECT 语句执行过程中表如何连接和连接的顺序。例如:
其中各字段所代表的信息如下:
字段 | 含义 |
---|---|
id | select查询的序列号,表示查询中执行select自居或者操作表的顺序 |
select_type | select的类型 |
table | 输出结果集的表 |
type | 表的连接类型 |
possible_keys | 查询时可能使用的索引,一个或多个 |
key | 查询实际使用的索引,NULL表示未用到索引 |
key_len | 索引字段的最大可能长度 |
rows | 扫描行的数量 |
extra | 执行情况的说明和描述 |
2.3.1 id
其中id字段满足如下的几个特性:
- id 相同表示加载表的顺序是从上到下
- id 不同id值越大,优先级越高,越先被执行
- id 有相同,也有不同,同时存在。id相同的可以认为是一组,从上往下顺序执行;在所有的组中,id的值越大,优先级越高,越先执行
2.3.2 select_type
其中select_type字段常见的取值有:
- SIMPLE:简单表,即不适用表连接或者子查询
- PRIMARY:主查询,外层查询
- UNION:UNION中的第二个或者后面的查询语句
- SUBQUERY:子查询中第一个select
- DERIVED:在FROM 列表中包含的子查询,被标记为 DERIVED(衍生) MYSQL会递归执行这些子查询,把结果放在临时表中
- UNION RESULT:从UNION表中获取结果的select
2.3.3 type
type 显示的是访问类型,可取值如下所示:
type | 说明 |
---|---|
NULL | MySQL不访问任何表,索引,直接返回结果 |
system | 表只有一行记录 |
const | 表示通过索引一次就找到了,const 用于比较primary key 或者 unique 索引。因为只匹配一行数据,所以很快。const于将"主键" 或 “唯一” 索引的所有部分与常量值进行比较 |
eq_ref | 类似ref,区别在于使用的是唯一索引,使用主键的关联查询,关联查询出的记录只有一条,常见于主键或唯一索引扫描 |
ref | 非唯一性索引扫描,返回匹配某个单独值的所有行。本质上也是一种索引访问,返回所有匹配某个单独值的所有行(多个) |
range | 只检索给定返回的行,使用一个索引来选择行。where 之后出现 between , < , > , in 等操作 |
index | index 与 all的区别为 index 类型只是遍历了索引树, 通常比all快, all是遍历数据文件 |
all | 将遍历全表以找到匹配的行 |
例如:
结果只从好到坏的顺序是:
system > const > eq_ref > ref > range > index > ALL
通常,针对于SQL语句的优化需要保证查询至少达到range或是ref级别。
2.3.4 extra
extra常见的输出有:
- using filesort:说明mysql会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取, 称为文件排序, 效率低
- using temporary:使用了临时表保存中间结果,Mysql在对查询结果排序时使用临时表。常见于
order by
和group by
; 效率低 - using index:表示相应的select操作使用了覆盖索引, 避免访问表的数据行, 效率不错
2.4 分析SQL
Mysql提供了show profiles
和show profile
语句帮助用户了解SQL优化中主要花费时间的地方。例如,通过 have_profiling
参数,能够看到当前MySQL是否支持profile:
mysql> select @have_profiling;
+----------------------------------+
| @have_profiling |
+----------------------------------+
| 0x |
+----------------------------------+
1 row in set (0.00 sec)
可以看到此时mysql不支持profile。如果想要在当前session中使用,需要使用set profiling=1
来开启支持。例如:
mysql> explain select * from account where id = 1 \G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: account
partitions: NULL
type: const
possible_keys: PRIMARY
key: PRIMARY
key_len: 4
ref: const
rows: 1
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
ERROR:
No query specified
mysql> show profiles;
+----------+------------+--------------------------------------------+
| Query_ID | Duration | Query |
+----------+------------+--------------------------------------------+
| 1 | 0.00035975 | select @have_profiling |
| 2 | 0.00031600 | set @profiling=1 |
| 3 | 0.00028400 | select @have_profiling |
| 4 | 0.00079150 | explain select * from account where id = 1 |
| 5 | 0.00065800 | explain select * from account where id = 1 |
+----------+------------+--------------------------------------------+
5 rows in set, 1 warning (0.00 sec)
通过show profile for query xx
可以查看具体query的profile信息,如下所示:
mysql> show profile for query 5;
+--------------------------------+----------+
| Status | Duration |
+--------------------------------+----------+
| starting | 0.000111 |
| Executing hook on transaction | 0.000014 |
| starting | 0.000013 |
| checking permissions | 0.000013 |
| Opening tables | 0.000072 |
| init | 0.000011 |
| System lock | 0.000017 |
| optimizing | 0.000018 |
| statistics | 0.000076 |
| preparing | 0.000038 |
| explaining | 0.000072 |
| end | 0.000009 |
| query end | 0.000020 |
| waiting for handler commit | 0.000015 |
| closing tables | 0.000013 |
| freeing items | 0.000121 |
| cleaning up | 0.000027 |
+--------------------------------+----------+
17 rows in set, 1 warning (0.00 sec)
在获取到最消耗时间的线程状态后,MySQL支持进一步选择all、cpu、block io 、context switch、page faults等明细类型类查看MySQL在使用什么资源上耗费了过高的时间。例如,查看第5个query对于CPU的消耗情况:
mysql> show profile cpu for query 5;
+--------------------------------+----------+----------+------------+
| Status | Duration | CPU_user | CPU_system |
+--------------------------------+----------+----------+------------+
| starting | 0.000111 | 0.000000 | 0.000000 |
| Executing hook on transaction | 0.000014 | 0.000000 | 0.000000 |
| starting | 0.000013 | 0.000000 | 0.000000 |
| checking permissions | 0.000013 | 0.000000 | 0.000000 |
| Opening tables | 0.000072 | 0.000000 | 0.000000 |
| init | 0.000011 | 0.000000 | 0.000000 |
| System lock | 0.000017 | 0.000000 | 0.000000 |
| optimizing | 0.000018 | 0.000000 | 0.000000 |
| statistics | 0.000076 | 0.000000 | 0.000000 |
| preparing | 0.000038 | 0.000000 | 0.000000 |
| explaining | 0.000072 | 0.000000 | 0.000000 |
| end | 0.000009 | 0.000000 | 0.000000 |
| query end | 0.000020 | 0.000000 | 0.000000 |
| waiting for handler commit | 0.000015 | 0.000000 | 0.000000 |
| closing tables | 0.000013 | 0.000000 | 0.000000 |
| freeing items | 0.000121 | 0.000000 | 0.000000 |
| cleaning up | 0.000027 | 0.000000 | 0.000000 |
+--------------------------------+----------+----------+------------+
17 rows in set, 1 warning (0.00 sec)
其中:
- Status:SQL语句的执行状态
- Duration:SQL执行过程中每一步的耗时
- CPU_user:当前用户占有的CPU
- CPU_system:系统占有的CPU
此外,还可以使用trace跟踪SQL语句的执行,了解Mysql底层优化器的选择。首先打开trace , 设置格式为 JSON,并设置trace最大能够使用的内存大小,避免解析过程中因为默认内存过小而不能够完整展示。具体命令如下所示:
SET optimizer_trace="enabled=on",end_markers_in_json=on;
set optimizer_trace_max_mem_size=1000000;
然后随便执行一条SQL语句,然后检查information_schema.optimizer_trace来了解Mysql的执行过程。
mysql> select * from account where id = 1;
+----+----------+-------+
| id | name | money |
+----+----------+-------+
| 1 | Forlogen | 1000 |
+----+----------+-------+
1 row in set (0.00 sec)
mysql> select * from information_schema.optimizer_trace\G;
*************************** 1. row ***************************
QUERY: select * from account where id = 1
TRACE: {
"steps": [
{
"join_preparation": {
"select#": 1,
"steps": [
{
"expanded_query": "/* select#1 */ select `account`.`id` AS `id`,`account`.`name` AS `name`,`account`.`money` AS `money` from `account` where (`account`.`id` = 1)"
}
] /* steps */
} /* join_preparation */
},
{
"join_optimization": {
"select#": 1,
"steps": [
{
"condition_processing": {
"condition": "WHERE",
"original_condition": "(`account`.`id` = 1)",
"steps": [
{
"transformation": "equality_propagation",
"resulting_condition": "multiple equal(1, `account`.`id`)"
},
{
"transformation": "constant_propagation",
"resulting_condition": "multiple equal(1, `account`.`id`)"
},
{
"transformation": "trivial_condition_removal",
"resulting_condition": "multiple equal(1, `account`.`id`)"
}
] /* steps */
} /* condition_processing */
},
{
"substitute_generated_columns": {
} /* substitute_generated_columns */
},
{
"table_dependencies": [
{
"table": "`account`",
"row_may_be_null": false,
"map_bit": 0,
"depends_on_map_bits": [
] /* depends_on_map_bits */
}
] /* table_dependencies */
},
{
"ref_optimizer_key_uses": [
{
"table": "`account`",
"field": "id",
"equals": "1",
"null_rejecting": false
}
] /* ref_optimizer_key_uses */
},
{
"rows_estimation": [
{
"table": "`account`",
"rows": 1,
"cost": 1,
"table_type": "const",
"empty": false
}
] /* rows_estimation */
},
{
"condition_on_constant_tables": "true",
"condition_value": true
},
{
"attaching_conditions_to_tables": {
"original_condition": "true",
"attached_conditions_computation": [
] /* attached_conditions_computation */,
"attached_conditions_summary": [
] /* attached_conditions_summary */
} /* attaching_conditions_to_tables */
},
{
"refine_plan": [
] /* refine_plan */
}
] /* steps */
} /* join_optimization */
},
{
"join_execution": {
"select#": 1,
"steps": [
] /* steps */
} /* join_execution */
}
] /* steps */
}
MISSING_BYTES_BEYOND_MAX_MEM_SIZE: 0
INSUFFICIENT_PRIVILEGES: 0
1 row in set (0.01 sec)
3. 合理使用索引
索引可以帮助用户极大的提升查询的效率,但是索引如果使用不当反而会起到相反的作用。为了使用索引来提升查询的效率,通常可以遵从如下的几条法则:
-
尽量进行全值匹配,即对索引中所有列都指定具体值
假设student表中针对于id、name、grade、birth建立了索引,那么使用索引时尽量对这三个字段都指定值。
select * from student where id=11 and name='Forlogen' and grade=1 and birth='2000-02-02';
-
最左前缀法则:指的是查询从索引的最左前列开始,并且不跳过索引中的列
依然使用上面的student表为例说明,索引中的字段排序为id->name->birth,如果对每个字段都指定值,那么索引是生效的。如果sql语句为如下的形式:
select * from student where name='Forlogen' and grade=1 and birth='2000-02-02';
此时索引失效。因为指定的字段跳过了id违背了最左前缀法则。如果sql语句如下所示:
select * from student where id=11 and birth='2000-02-02';
此时只有最左列的id索引生效。
-
范围查询右边的列不能使用索引
如果sql语句如下所示:
select * from student where id=11 and grade>1 and name='Forlogen' and birth='2000-02-02';
那么由于grade字段使用了范围查询,那么name和birth就不会走索引。
-
不要在索引列上进行运算操作,否则会使得索引失效
假设sql语句如下:
select * from student where len(name)=3;
由于在name字段做了运算,索引将失效。
-
字符串不加单引号会导致索引失效
当sql语句为
select * from student where name=Forlogen
,那么name字段的索引将失效。 -
尽量使用覆盖索引,避免使用
select *
如果想要查询name、grade字段,那么在sql语句中最好直接指定,而不是使用*查询所有。
-
用or分割开的条件, 如果or前的条件中的列有索引,而后面的列中没有索引,那么涉及的索引都不会被用到
假设student表中只对id和name建立了索引,此时的sql语句如下所示:
select * from student where id=1 or grade=1;
由于grade没有建立索引,即时id有索引,但整个查询语句都不会走索引。
-
以%开头的Like模糊查询,索引失效
当sql语句为
select * from student where name like '%gen';
,那么索引失效。 -
如果MySQL评估使用索引比全表更慢,则不使用索引
-
in 走索引, not in 索引失效
-
is NULL、is NOT NULL有时索引会失效
-
尽量使用复合索引,而少使用单列索引
使用
create index index_id_name on student(id, name);
优于
create index index_id on student(id); create index index_name on student(name);
4. SQL语句的优化
4.1 优化insert
当进行数据的insert操作的时候,可以考虑采用以下几种优化方案:
-
如果需要同时对一张表插入很多行数据时,应该尽量使用多个值表的insert语句,这种方式将大大的缩减客户端与数据库之间的连接、关闭等消耗。使得效率比分开执行的单个insert语句块
一次插入多条记录,而不是多次逐条插入。
-
在事务中进行数据插入
start transaction; insert .... commit;
-
数据有序插入:可以按照某一个有序字段按序插入
4.2 优化order by
Mysql中存在两种数据排序的方式:
- filesort:通过对返回数据进行排序,即所有不通过索引直接返回排序结果的排序都属于该类
- using index:通过有序索引顺序扫描直接返回有序数据
尽量减少额外的排序,通过索引直接返回有序数据。
- where 条件和Order by 使用相同的索引
- Order By 的顺序和索引顺序相同
- Order by 的字段都是升序,或者都是降序
否则肯定需要额外的操作,这样就会出现FileSort。
特别是针对于filesort来说,通过创建合适的索引,能够减少 Filesort 的出现,但是在某些情况下,条件限制不能让Filesort消失,那就需要加快 Filesort的排序操作。对于Filesort , MySQL 有两种排序算法:
- 两次扫描算法 :MySQL4.1 之前,使用该方式排序。首先根据条件取出排序字段和行指针信息,然后在排序区sort buffer 中排序,如果sort buffer不够,则在临时表 temporary table 中存储排序结果。完成排序之后,再根据行指针回表读取记录,该操作可能会导致大量随机I/O操作
- 一次扫描算法:一次性取出满足条件的所有字段,然后在排序区 sort buffer 中排序后直接输出结果集。排序时内存开销较大,但是排序效率比两次扫描算法要高。
MySQL 通过比较系统变量
max_length_for_sort_data
的大小和Query语句取出的字段总大小, 来判定是否那种排序算法,如果max_length_for_sort_data
更大,那么使用第二种优化之后的算法;否则使用第一种。可以适当提高sort_buffer_size
和max_length_for_sort_data
系统变量,来增大排序区的大小,提高排序的效率。
4.3 优化group by
由于GROUP BY 实际上也同样会进行排序操作,而且与ORDER BY 相比,GROUP BY 主要只是多了排序之后的分组操作。当然,如果在分组的时候还使用了其他的一些聚合函数,那么还需要一些聚合函数的计算。
所以,在GROUP BY 的实现过程中,与 ORDER BY 一样也可以利用到索引。如果查询包含 group by 但是用户想要避免排序结果的消耗, 则可以执行order by null 禁止排序。
4.4 优化嵌套查询
Mysql4.1版本之后,开始支持SQL的子查询。这个技术可以使用SELECT语句来创建一个单列的查询结果,然后把这个结果作为过滤条件用在另一个查询中。使用子查询可以一次性的完成很多逻辑上需要多个步骤才能完成的SQL操作,同时也可以避免事务或者表锁死,并且写起来也很容易。但有些情况下,子查询是可以被更高效的连接(JOIN)替代。
4.5 优化OR条件
对于包含OR的查询子句,如果要利用索引,则OR之间的每个条件列都必须用到索引 , 而且不能使用到复合索引; 如果没有索引,则应该考虑增加索引。
4.6 优化分页查询
一般分页查询时,通过创建覆盖索引能够比较好地提高性能。一个常见又非常头疼的问题就是 limit 2000000,10 ,此时需要MySQL排序前2000010 记录,仅仅返回2000000 - 2000010 的记录,其他记录丢弃,查询排序的代价非常大 。优化的思路有:
- 在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容
- 对于主键自增的表,可以把Limit 查询转换成某个位置的查询
4.7 批量插入数据
当使用load 命令导入数据的时候,适当的设置可以提高导入的效率。对于 InnoDB 类型的表,有以下几种方式可以提高导入的效率:
- 主键顺序插入:InnoDB类型的表是按照主键的顺序保存的,所以将导入的数据按照主键的顺序排列,可以有效的提高导入数据的效率。如果InnoDB表没有主键,那么系统会自动默认创建一个内部列作为主键,所以如果可以给表创建一个主键,将可以利用这点,来提高导入数据的效率
- 关闭唯一性校验:在导入数据前执行
SET UNIQUE_CHECKS=0
,关闭唯一性校验,在导入结束后执行SET UNIQUE_CHECKS=1
,恢复唯一性校验,可以提高导入的效率 - 手动提交事务:如果应用使用自动提交的方式,建议在导入前执行
SET AUTOCOMMIT=0
,关闭自动提交,导入结束后再执行SET AUTOCOMMIT=1
,打开自动提交,也可以提高导入的效率