MySQL 性能优化技巧

原文地址:MySQL 性能优化技巧
博客地址:www.extlight.comhtml

1、背景

最近公司项目添加新功能,上线后发现有些功能的列表查询时间好久。缘由是新功能用到旧功能的接口,而这些旧接口的 SQL 查询语句关联5,6张表且编写不够规范,致使 MySQL 在执行 SQL 语句时索引失效,进行全表扫描。本来负责优化的同事有事请假回家,所以优化查询数据的问题落在笔者手中。笔者在查阅网上 SQL 优化的资料后成功解决了问题,在此从==全局角度==记录和总结 MySQL 查询优化相关技巧。mysql

2、优化思路

数据查询慢,不表明 SQL 语句写法有问题。 首先,咱们须要找到问题的源头才能“对症下药”。笔者用一张流程图展现 MySQL 优化的思路:程序员

image

无需更多言语,从图中能够清楚地看出,致使数据查询慢的缘由有多种,如:缓存失效,在此一段时间内因为高并发访问致使 MySQL 服务器崩溃;SQL 语句编写问题;MySQL 服务器参数问题;硬件配置限制 MySQL 服务性能问题等。正则表达式

3、查看 MySQL 服务器运行的状态值

若是系统的并发请求数不高,且查询速度慢,能够忽略该步骤直接进行 SQL 语句调优步骤。sql

执行命令:数据库

show status复制代码

因为返回结果太多,此处不贴出结果。其中,再返回的结果中,咱们主要关注 “Queries”、“Threads_connected” 和 “Threads_running” 的值,即查询次数、线程链接数和线程运行数。vim

咱们能够经过执行以下脚本监控 MySQL 服务器运行的状态值segmentfault

#!/bin/bash
while true
do
mysqladmin -uroot -p"密码" ext | awk '/Queries/{q=$4}/Threads_connected/{c=$4}/Threads_running/{r=$4}END{printf("%d %d %d\n",q,c,r)}' >> status.txt
sleep 1
done复制代码

执行该脚本 24 小时,获取 status.txt 里的内容,再次经过 awk 计算==每秒请求 MySQL 服务的次数==缓存

awk '{q=$1-last;last=$1}{printf("%d %d %d\n",q,$2,$3)}' status.txt复制代码

复制计算好的内容到 Excel 中生成图表观察数据周期性。安全

若是观察的数据有周期性的变化,如上图的解释,须要修改缓存失效策略。

例如:

经过随机数在[3,6,9] 区间获取其中一个值做为缓存失效时间,这样分散了缓存失效时间,从而节省了一部份内存的消耗。

当访问高峰期时,一部分请求分流到未失效的缓存,另外一部分则访问 MySQL 数据库,这样减小了 MySQL 服务器的压力。

4、获取须要优化的 SQL 语句

4.1 方式一:查看运行的线程

执行命令:

show processlist复制代码

返回结果:

mysql> show processlist;
+----+------+-----------+------+---------+------+----------+------------------+
| Id | User | Host      | db   | Command | Time | State    | Info             |
+----+------+-----------+------+---------+------+----------+------------------+
|  9 | root | localhost | test | Query   |    0 | starting | show processlist |
+----+------+-----------+------+---------+------+----------+------------------+
1 row in set (0.00 sec)复制代码

从返回结果中咱们能够了解该线程执行了什么命令/SQL 语句以及执行的时间。实际应用中,查询的返回结果会有 N 条记录。

其中,返回的 State 的值是咱们判断性能好坏的关键,其值出现以下内容,则该行记录的 SQL 语句须要优化:

Converting HEAP to MyISAM # 查询结果太大时,把结果放到磁盘,严重
Create tmp table #建立临时表,严重
Copying to tmp table on disk  #把内存临时表复制到磁盘,严重
locked #被其余查询锁住,严重
loggin slow query #记录慢查询
Sorting result #排序复制代码

State 字段有不少值,如需了解更多,能够参看文章末尾提供的连接。

4.2 方式二:开启慢查询日志

在配置文件 my.cnf 中的 [mysqld] 一行下边添加两个参数:

slow_query_log = 1
slow_query_log_file=/var/lib/mysql/slow-query.log
long_query_time = 2

log_queries_not_using_indexes = 1复制代码

其中,slow_query_log = 1 表示开启慢查询;slow_query_log_file 表示慢查询日志存放的位置;
long_query_time = 2 表示查询 >=2 秒才记录日志;log_queries_not_using_indexes = 1 记录没有使用索引的 SQL 语句。

注意:slow_query_log_file 的路径不能随便写,不然 MySQL 服务器可能没有权限将日志文件写到指定的目录中。建议直接复制上文的路径。

修改保存文件后,重启 MySQL 服务。在 /var/lib/mysql/ 目录下会建立 slow-query.log 日志文件。链接 MySQL 服务端执行以下命令能够查看配置状况。

show variables like 'slow_query%';

show variables like 'long_query_time';复制代码

测试慢查询日志:

mysql> select sleep(2);
+----------+
| sleep(2) |
+----------+
|        0 |
+----------+
1 row in set (2.00 sec)复制代码

打开慢查询日志文件

[root@localhost mysql]# vim /var/lib/mysql/slow-query.log
/usr/sbin/mysqld, Version: 5.7.19-log (MySQL Community Server (GPL)). started with:
Tcp port: 0  Unix socket: /var/lib/mysql/mysql.sock
Time                 Id Command    Argument
# Time: 2017-10-05T04:39:11.408964Z
# User@Host: root[root] @ localhost [] Id: 3
# Query_time: 2.001395 Lock_time: 0.000000 Rows_sent: 1 Rows_examined: 0
use test;
SET timestamp=1507178351;
select sleep(2);复制代码

咱们能够看到刚才执行了 2 秒的 SQL 语句被记录下来了。

虽然在慢查询日志中记录查询慢的 SQL 信息,可是日志记录的内容密集且不易查阅。所以,咱们须要经过工具将 SQL 筛选出来。

MySQL 提供 mysqldumpslow 工具对日志进行分析。咱们可使用 mysqldumpslow --help 查看命令相关用法。

经常使用参数以下:

-s:排序方式,后边接着以下参数
        c:访问次数
        l:锁定时间
        r:返回记录
        t:查询时间
    al:平均锁定时间
    ar:平均返回记录书
    at:平均查询时间
    -t:返回前面多少条的数据
    -g:翻遍搭配一个正则表达式,大小写不敏感复制代码

案例:

获取返回记录集最多的10个sql
mysqldumpslow -s r -t 10 /var/lib/mysql/slow-query.log

获取访问次数最多的10个sql
mysqldumpslow -s c -t 10 /var/lib/mysql/slow-query.log

获取按照时间排序的前10条里面含有左链接的查询语句
mysqldumpslow -s t -t 10 -g "left join" /var/lib/mysql/slow-query.log复制代码

5、分析 SQL 语句

5.1 方式一:explain

筛选出有问题的 SQL,咱们可使用 MySQL 提供的 explain 查看 SQL 执行计划状况(关联表,表查询顺序、索引使用状况等)。

用法:

explain select * from category;复制代码

返回结果:

mysql> explain select * from category;
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table    | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra |
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------+
|  1 | SIMPLE      | category | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    1 |   100.00 | NULL  |
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)复制代码

字段解释:
1) id:select 查询序列号。id相同,执行顺序由上至下;id不一样,id值越大优先级越高,越先被执行

2) select_type:查询数据的操做类型,其值以下:

simple:简单查询,不包含子查询或 union
primary:包含复杂的子查询,最外层查询标记为该值
subquery:在 select 或 where 包含子查询,被标记为该值
derived:在 from 列表中包含的子查询被标记为该值,MySQL 会递归执行这些子查询,把结果放在临时表
union:若第二个 select 出如今 union 以后,则被标记为该值。若 union 包含在 from 的子查询中,外层 select 被标记为 derived    
union result:从 union 表获取结果的 select复制代码

3) table:显示该行数据是关于哪张表

4) partitions:匹配的分区

5) type:表的链接类型,其值,性能由高到底排列以下:

system:表只有一行记录,至关于系统表
const:经过索引一次就找到,只匹配一行数据
eq_ref:惟一性索引扫描,对于每一个索引键,表中只有一条记录与之匹配。经常使用于主键或惟一索引扫描
ref:非惟一性索引扫描,返回匹配某个单独值的全部行。用于=、< 或 > 操做符带索引的列
range:只检索给定范围的行,使用一个索引来选择行。通常使用between、>、<状况
index:只遍历索引树
ALL:全表扫描,性能最差复制代码

注:前5种状况都是理想状况的索引使用状况。一般优化至少到range级别,最好能优化到 ref

6) possible_keys:指出 MySQL 使用哪一个索引在该表找到行记录。若是该值为 NULL,说明没有使用索引,能够创建索引提升性能

7) key:显示 MySQL 实际使用的索引。若是为 NULL,则没有使用索引查询

8) key_len:表示索引中使用的字节数,经过该列计算查询中使用的索引的长度。在不损失精确性的状况下,长度越短越好
显示的是索引字段的最大长度,并不是实际使用长度

9) ref:显示该表的索引字段关联了哪张表的哪一个字段

10) rows:根据表统计信息及选用状况,大体估算出找到所需的记录或所需读取的行数,数值越小越好

11) filtered:返回结果的行数占读取行数的百分比,值越大越好

12) extra: 包含不合适在其余列中显示但十分重要的额外信息,常见的值以下:

using filesort:说明 MySQL 会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取。出现该值,应该优化 SQL
using temporary:使用了临时表保存中间结果,MySQL 在对查询结果排序时使用临时表。常见于排序 order by 和分组查询 group by。出现该值,应该优化 SQL 
using index:表示相应的 select 操做使用了覆盖索引,避免了访问表的数据行,效率不错
using wherewhere 子句用于限制哪一行
using join buffer:使用链接缓存
distinct:发现第一个匹配后,中止为当前的行组合搜索更多的行复制代码

注意:出现前 2 个值,SQL 语句必需要优化。

5.2 方式二:profiling

使用 profiling 命令能够了解 SQL 语句消耗资源的详细信息(每一个执行步骤的开销)。

5.2.1 查看 profile 开启状况

select @@profiling;复制代码

返回结果:

mysql> select @@profiling;
+-------------+
| @@profiling |
+-------------+
|           0 |
+-------------+
1 row in set, 1 warning (0.00 sec)复制代码

0 表示关闭状态,1 表示开启

5.2.2 启用 profile

set profiling = 1;复制代码

返回结果:

mysql> set profiling = 1;  
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> select @@profiling;
+-------------+
| @@profiling |
+-------------+
|           1 |
+-------------+
1 row in set, 1 warning (0.00 sec)复制代码

在链接关闭后,profiling 状态自动设置为关闭状态。

5.2.3 查看执行的 SQL 列表

show profiles;复制代码

返回结果:

mysql> show profiles;
+----------+------------+------------------------------+
| Query_ID | Duration   | Query                        |
+----------+------------+------------------------------+
|        1 | 0.00062925 | select @@profiling           |
|        2 | 0.00094150 | show tables                  |
|        3 | 0.00119125 | show databases               |
|        4 | 0.00029750 | SELECT DATABASE()            |
|        5 | 0.00025975 | show databases               |
|        6 | 0.00023050 | show tables                  |
|        7 | 0.00042000 | show tables                  |
|        8 | 0.00260675 | desc role                    |
|        9 | 0.00074900 | select name,is_key from role |
+----------+------------+------------------------------+
9 rows in set, 1 warning (0.00 sec)复制代码

该命令执行以前,须要执行其余 SQL 语句才有记录。

5.2.4 查询指定 ID 的执行详细信息

show profile for query Query_ID;复制代码

返回结果:

mysql> show profile for query 9;
+----------------------+----------+
| Status               | Duration |
+----------------------+----------+
| starting             | 0.000207 |
| checking permissions | 0.000010 |
| Opening tables       | 0.000042 |
| init                 | 0.000050 |
| System lock          | 0.000012 |
| optimizing           | 0.000003 |
| statistics           | 0.000011 |
| preparing            | 0.000011 |
| executing            | 0.000002 |
| Sending data         | 0.000362 |
| end                  | 0.000006 |
| query end            | 0.000006 |
| closing tables       | 0.000006 |
| freeing items        | 0.000011 |
| cleaning up          | 0.000013 |
+----------------------+----------+
15 rows in set, 1 warning (0.00 sec)复制代码

每行都是状态变化的过程以及它们持续的时间。Status 这一列和 show processlist 的 State 是一致的。所以,须要优化的注意点与上文描述的同样。

其中,Status 字段的值一样能够参考末尾连接。

5.2.5 获取 CPU、 Block IO 等信息

show profile block io,cpu for query Query_ID;

show profile cpu,block io,memory,swaps,context switches,source for query Query_ID;

show profile all for query Query_ID;复制代码

6、优化手段

主要以查询优化、索引使用和表结构设计方面进行讲解。

6.1 查询优化

1) 避免 SELECT *,须要什么数据,就查询对应的字段。

2) 小表驱动大表,即小的数据集驱动大的数据集。如:以 A,B 两表为例,两表经过 id 字段进行关联。

当 B 表的数据集小于 A 表时,用 in 优化 exist;使用 in ,两表执行顺序是先查 B 表,再查 A 表
select * from A where id in (select id from B)

当 A 表的数据集小于 B 表时,用 exist 优化 in;使用 exists,两表执行顺序是先查 A 表,再查 B 表
select * from A where exists (select 1 from B where B.id = A.id)复制代码

3) 一些状况下,可使用链接代替子查询,由于使用 join,MySQL 不会在内存中建立临时表。

4) 适当添加冗余字段,减小表关联。

5) 合理使用索引(下文介绍)。如:为排序、分组字段创建索引,避免 filesort 的出现。

6.2 索引使用

6.2.1 适合使用索引的场景

1) 主键自动建立惟一索引

2) 频繁做为查询条件的字段

3) 查询中与其余表关联的字段

4) 查询中排序的字段

5) 查询中统计或分组字段

6.2.2 不适合使用索引的场景

1) 频繁更新的字段

2) where 条件中用不到的字段

3) 表记录太少

4) 常常增删改的表

5) 字段的值的差别性不大或重复性高

6.2.3 索引建立和使用原则

1) 单表查询:哪一个列做查询条件,就在该列建立索引

2) 多表查询:left join 时,索引添加到右表关联字段;right join 时,索引添加到左表关联字段

3) 不要对索引列进行任何操做(计算、函数、类型转换)

4) 索引列中不要使用 !=,<> 非等于

5) 索引列不要为空,且不要使用 is null 或 is not null 判断

6) 索引字段是字符串类型,查询条件的值要加''单引号,避免底层类型自动转换

违背上述原则可能会致使索引失效,具体状况须要使用 explain 命令进行查看

6.2.4 索引失效状况

除了违背索引建立和使用原则外,以下状况也会致使索引失效:

1) 模糊查询时,以 % 开头

2) 使用 or 时,如:字段1(非索引)or 字段2(索引)会致使索引失效。

3) 使用复合索引时,不使用第一个索引列。

index(a,b,c) ,以字段 a,b,c 做为复合索引为例:

语句 索引是否生效
where a = 1 是,字段 a 索引生效
where a = 1 and b = 2 是,字段 a 和 b 索引生效
where a = 1 and b = 2 and c = 3 是,所有生效
where b = 2 或 where c = 3
where a = 1 and c = 3 字段 a 生效,字段 c 失效
where a = 1 and b > 2 and c = 3 字段 a,b 生效,字段 c 失效
where a = 1 and b like 'xxx%' and c = 3 字段 a,b 生效,字段 c 失效

6.3 数据库表结构设计

6.3.1 选择合适的数据类型

1) 使用能够存下数据最小的数据类型

2) 使用简单的数据类型。int 要比 varchar 类型在mysql处理简单

3) 尽可能使用 tinyint、smallint、mediumint 做为整数类型而非 int

4) 尽量使用 not null 定义字段,由于 null 占用4字节空间

5) 尽可能少用 text 类型,非用不可时最好考虑分表

6) 尽可能使用 timestamp 而非 datetime

7) 单表不要有太多字段,建议在 20 之内

6.3.2 表的拆分

当数据库中的数据很是大时,查询优化方案也不能解决查询速度慢的问题时,咱们能够考虑拆分表,让每张表的数据量变小,从而提升查询效率。

1) 垂直拆分:将表中多个列分开放到不一样的表中。例如用户表中一些字段常常被访问,将这些字段放在一张表中,另一些不经常使用的字段放在另外一张表中。
插入数据时,使用事务确保两张表的数据一致性。

2) 水平拆分:按照行进行拆分。例如用户表中,使用用户ID,对用户ID取10的余数,将用户数据均匀的分配到0~9的10个用户表中。查找时也按照这个规则查询数据。

6.3.3 读写分离

通常状况下对数据库而言都是“读多写少”。换言之,数据库的压力多数是由于大量的读取数据的操做形成的。咱们能够采用数据库集群的方案,使用一个库做为主库,负责写入数据;其余库为从库,负责读取数据。这样能够缓解对数据库的访问压力。

7、服务器参数调优

7.1 内存相关

sort_buffer_size 排序缓冲区内存大小

join_buffer_size 使用链接缓冲区大小

read_buffer_size 全表扫描时分配的缓冲区大小

7.2 IO 相关

Innodb_log_file_size 事务日志大小

Innodb_log_files_in_group 事务日志个数

Innodb_log_buffer_size 事务日志缓冲区大小

Innodb_flush_log_at_trx_commit 事务日志刷新策略 ,其值以下:

0:每秒进行一次 log 写入 cache,并 flush log 到磁盘

1:在每次事务提交执行 log 写入 cache,并 flush log 到磁盘

2:每次事务提交,执行 log 数据写到 cache,每秒执行一次 flush log 到磁盘

7.3 安全相关

expire_logs_days 指定自动清理 binlog 的天数

max_allowed_packet 控制 MySQL 能够接收的包的大小

skip_name_resolve 禁用 DNS 查找

read_only 禁止非 super 权限用户写权限

skip_slave_start 级你用 slave 自动恢复

7.4 其余

max_connections 控制容许的最大链接数

tmp_table_size 临时表大小

max_heap_table_size 最大内存表大小

笔者并无使用这些参数对 MySQL 服务器进行调优,具体详情介绍和性能效果请参考文章末尾的资料或另行百度。

8、硬件选购和参数优化

硬件的性能直接决定 MySQL 数据库的性能。硬件的性能瓶颈,直接决定 MySQL 数据库的运行数据和效率。

做为软件开发程序员,咱们主要关注软件方面的优化内容,如下硬件方面的优化做为了解便可

8.1 内存相关

内存的 IO 比硬盘的速度快不少,能够增长系统的缓冲区容量,使数据在内存停留的时间更长,以减小磁盘的 IO

8.2 磁盘 I/O 相关

1) 使用 SSD 或 PCle SSD 设备,至少得到数百倍甚至万倍的 IOPS 提高

2) 购置阵列卡同时配备 CACHE 及 BBU 模块,能够明显提高 IOPS

3) 尽量选用 RAID-10,而非 RAID-5

8.3 配置 CUP 相关

在服务器的 BIOS 设置中,调整以下配置:

1) 选择 Performance Per Watt Optimized(DAPC)模式,发挥 CPU 最大性能

2) 关闭 C1E 和 C States 等选项,提高 CPU 效率

3) Memory Frequency(内存频率)选择 Maximum Performance

9、参考资料