1. 数据库调优原理
1.1 什么影响数据库性能?
- 服务器:OS、CPU、memory、network
- Mysql:
- 数据库表机构【对性能影响最大】
- 低效率的SQL语句
- 超大的表
- 大事务
- 数据库配置
- 数据库整体架构
- 。。。
1.2 数据库调优到底调什么?
- 调SQL语句:根据需求创建结构良好的SQL语句
- 调索引:索引创建原则
- 调数据库表结构
- 调Mysql配置:最大连接数、连接超时、线程缓存、查询缓存、排序缓存、连接查询缓存。。。
- 调Mysql宿主机OS:TCP连接数、打开文件数、线程栈大小。。。
- 调服务器硬件:更多核CPU、更大内存
- Mysql客户端:连接池(MaxActive, MaxWait),连接属性
2. 数据库压力测试
2.1 JMeter
Apache JMeter是Apache组织开发的基于Java的压力测试工具。用于对软件做压力测试,它最初被设计 用于Web应用测试,但后来扩展到其他测试领域。 它可以用于测试静态和动态资源 ,例如静态文件、Java 小服务程序、CGI 脚本、Java 对象、数据库、FTP 服务器, 等等。
2.2 驱动下载
在测试计划中,需要添加JDBC驱动。这里我用的MySQL-5.7版本,JDBC驱动选择5.x版本。JDBC驱动在 MySQL官网下载,具体地址是:downloads.mysql.com/archives/c-... 选择对应版本进行下载
2.3 测试过程
sql
select id from tb_seckill_goods where id=1;
2.3.1 配置数据库驱动
下载后解压文件夹,把文件夹中的mysql-connector-java-5.1.49.jar 复制到JMeter安装目录的bin文件下
2.3.2 配置线程组
2.3.3 配置JDBC连接池
添加JDBC Connection Configuration
需要设置jdbc线程池名称,这个变量在JDBC Request中要使用,还有要设置Database URL,格式为:
sql
jdbc:mysql://localhost:3306/dbname?serverTimezone=UTC&characterEncoding=utf-8
重要配置说明:
- Variable Name: 数据库连接池的名称
- JDBC Connection Configuration:算是一个数据库连接池配置
- Variable Name for created pool: 连接池唯一标识,后面JDBC Request需要用到
- Max Number of Connections: 池中允许的最大连接数,可以设置为20,也可以设置为0,意味着没有连接池
- Max Wait: 标示从连接池获取连接的超时等待时间,单位毫秒
- Database URL: 数据库连接URL
- JDBC Driver class: 数据库驱动
- Username: 数据库登陆用户名
- Password: 数据库登陆密码
注意:一个测试计划可以有多个JDBC Connection Configuration配置,只要名称不重复即可。
思考: 1. 是不是连接数越多服务性能越好?
2. 从连接池获取连接的等待时间越短效率越高呢?
其他基本保持默认就行,也可根据需要进行修改,下面是所有参数详解:
- 连接池参数配置
- Transaction Isolation: 事务间隔级别设置,主要有如下几个选项:(对JMX加解密)
- TRANSACTION_NODE 事务节点
- TRANSACTION_READ_UNCOMMITTED 事务未提交读
- TRANSACTION_READ_COMMITTED 事务已提交读
- TRANSACTION_SERIALIZABLE 事务序列化
- DEFAULT 默认
- TRANSACTION_REPEATABLE_READ 事务重复读
- 校验连接池
- 配置数据库连接
- 常见数据库的连接 URL和驱动:
2.3.4 添加JDBC请求
再添加一个采样器:JDBC request,在JMeter中request可以编辑select和insert等不同的采样器类别。即通过不同的类别添加配置我们需要的对MySQL不同的操作。
参数详解:
- Variable Name:数据库连接池的名字,需要与JDBC Connection Configuration的Variable NameBound Pool名字保持一致
- Query Type:此处支持方式多样,可以用于添加或者筛选数据,根据需要和Query配合使用
- select statement 查询
- update statement 更新
- prepared select statement 预处理参数查询
- prepared update statement 预处理参数更新
- Query: 填写的SQL语句末尾可以不加';'
- Parameter values:参数值,顺序替代Query中的?
- 此处对应query中的'?':有几个?,则此处要填写几个值,以','分隔
- Parameter types: 参数类型
- 可参考:javadoc for java.sql.Types
- Parameter types则必须和Parameter values一一对应,且类型必须正确;
- Variable names:保存sql语句返回结果的变量名 ,用于作为参数供调用
- Result variable name:创建一个对象变量,保存所有返回的结果 ,供调用;
- Query timeout:查询超时时间
- Handle result set:定义如何处理由callable statements语句返回的结果。
2.3.5 添加结果监听器
- 聚合报告
- 查看结果树
- 活动线程数Active Threads Over Time
- 每秒事务数TPS
- 平均响应时间RT
- 服务端:内存、网络、CPU、磁盘io、网络io【单位mb】
2.3.6 查看测试结果
测试结论:连接数为20,数据库750+的TPS(本地会更多,云上有带宽影响)
3. 客户端-连接池
使用Druid作为数据源,连接池相关参数配置如下:
yml
# 连接池配置
# 初始化连接数
spring.datasource.druid.initial-size=1
# 最小空闲连接数,一般设置和initial-size一致
spring.datasource.druid.min-idle=1
# 最大活动连接数,一个数据库能够支撑最大的连接数是多少呢?
spring.datasource.druid.max-active=20
# 从连接池获取连接超时时间
spring.datasource.druid.max-wait=60000
# 配置间隔多久启动一次销毁线程,对连接池内的空闲的connection进行检测,单位是毫秒。
# 1.如果连接空闲并且超过minIdle以外的连接,如果空闲时间超过minEvictableIdleTimeMillis设置的连接物理关闭。
# 2.在minIdle以内的不处理。
spring.datasource.druid.time-between-eviction-runs-millis=60000
# 配置一个连接在池中连接最小可清理的空闲时间,单位是毫秒
spring.datasource.druid.min-evictable-idle-time-millis=300000
# 打开后,增强timeBetweenEvictionRunsMillis的周期性连接检查,minIdle内的空闲连接
# 设置从连接池获取连接时是否检查连接有效性,true时,每次都检查;false时,不检查
spring.datasource.druid.test-on-borrow=false
# 设置往连接池归还连接时是否检查连接有效性,true时,每次都检查;false时,不检查
spring.datasource.druid.test-on-return=false
# 设置从连接池获取连接时是否检查连接有效性
# 为true时,如果连接空闲时间超过minEvictableIdleTimeMillis进行检查,否则不检查
# 为false时,不检查
spring.datasource.druid.test-while-idle=true
# 检验连接是否有效的查询语句
# 如果数据库Driver支持ping()方法,则优先使用ping()方法进行检查,否则使用
validationQuery查询进行检查
spring.datasource.druid.validation-query=select 1 from dual
# 每次检查强制验证连接有效性
spring.datasource.druid.keep-alive=true
3.1 不使用连接池行不行
3.2 连接池参数设置
3.2.1 MaxWait
参数表示从连接池获取连接的超时等待时间,单位毫秒
应用程序从客户端连接池获取连接的超时等待时间。
注意:这个参数只管理获取连接的超时。获取连接等待的直接原因是池里没有可用连接,具体包括四种情况:
diff
- 连接池未初始化
- 连接长久未使用已被释放
- 连接使用中需要新建连接
- 连接池已耗尽需等待连接用完后归还
这个有一个很关键的点是MaxWait未配置或者配置为0时,表示不设置超时时间。
如果不配置maxWait,后果会怎么样?
可能有些应用就这么干,来做个案例:
yml
maxWait=0,
maxActive=5
正常流量下业务没有发现任何问题,但突发大流量涌入时,造成连接池耗尽,所有新增的DB请求处于等待获取连接的状态中。由于 maxWait=0 表示无限等待,在请求速度大于处理速度的情况下等待队列会越排越长,最终业务上的表现就是业务接口大量超时,流量越大造成实际吞吐量反而越低。
并发5:
并发10:
并发20:
结论:配置建议,如果内网状态良好,获取连接等待时间800,网络状况不佳,推荐设置为1200。原因是TCP重连的时间一般是1秒。
3.2.2 MaxActive
yml
线程数:20
ramp-up:1
循环次数:5000
最大连接池数量,允许的最大同时使用中的连接数。
最大连接10:
最大连接20:
最大连接30:
注意:配置maxActive千万不要好大喜多。
虽然配置大了看起来业务流量飙升后还能处理更多的请求,但切换到DB视角会发现其实连接数的增多在很多场景下反而会减少吞吐量。
举个例子:缓存刷新,在更新热点数据时DB查询耗时如果很高,这时再让更多的连接操作DB,就只会给DB添堵,因为DB处理能力有限,开辟更多的连接并不能提升DB处理的效率。
结论:大多数场景下,20连接足够使用了,当然这个参数需要结合业务场景的特点与配置。一般标准是配置成为正常使用连接数的3-4倍即可。
sql
# 查看数据库中的最大连接数
show variables like 'max_connections';
20个连接connection,可以产生多大的TPS呢?
- RT-5ms,TPS=1000/5 x 20个=4000
- RT-10ms,TPS=1000/10 x 20个=2000
- RT-20ms,TPS=1000/20 x 20个=1000
为什么最大连接数设置的过多并不是一件好事?
- 首先,20个连接可以产生足够的吞吐量,只要SQL执行不耗时,20个连接足以产生2000以上TPS
- 其次,如果设置过大,多个服务连接数据库超过数据库最大的连接数,会出现资源争抢踩踏,导致服务器报错。反而会造成服务器性能下降。
- 大多数业务场景及应用中,设置为10、20、30均为合适的值,判断标准主要是应用的数量,及数据库最大连接数的值。如果只有一个数据库配置一个应用可以设置为0。
3.2.3 连接属性设置
之前在配置JDBC连接池的时候讲过2个参数:serverTimezone=UTC&characterEncode=utf-8。接下来再说这两个参数在网络方面有很大的作用,主要应对在网络异常模式下,数据库无法释放连接的问题。
- ConnectTimeout: 表示等待和Mysql数据库建立socket连接的超时时间。如果与服务器(这里指数据库)请求建立连接的时间超过了ConnectTimeout,就会抛出连接超时异常,即服务器连接超时。
- socketTimeout: 表示客户端和Mysql数据库建立socket后,读写socket时的等待超时时间。如果与服务器连接成功,就开始数据传输了。如果服务器处理数据等待时间过长,超过了socketTimeout,就会抛出socketTimeOutException,即服务器响应超时,服务器没有在规定的时间内返给客户端数据。
sql
jdbc:mysql://172.26.233.200:3306/hero_all?serverTimezone=UTC&characterEncoding=utf-8&connectionTimeout=3000&socketTimeout=1200
小结:
- 一个请求包含三个完整的阶段:1.建立连接 2.数据传输 3.断开连接
- connectionTimeout默认是0,表示不会连接超时,单位是毫秒
- socketTimeout可以不设置,默认是30分钟,在linux中设置,单位毫秒
- 推荐配置:connectionTimeout3000, socketTimeout1200
- 主要解决的问题:在网络异常的情况下,网络连接被耗尽,却无法及时失效,从而导致后续请求不能正常进入。
4. SQL语句优化【开发人员】
4.1 查看SQL执行计划【explain】
MySQL 提供了一个 Explain 命令, 它可以对 SELECT 语句的执行计划进行分析,并输出 SELECT 执行的详细信息,以供开发人员针对性优化。使用explain命令来查看该SQL语句有没有使用上了索引,有没有做全表扫描等等。
Explain 命令用法很简单, 在 SELECT 语句前加上 explain 就可以了。
sql
EXPLAIN SELECT * FROM tb_seckill_goods
- id: select标识符,这时select查询序列号。
- select_type: 表示单位查询的查询类型,比如:普通查询、联合查询(union、union all)、子查询等复杂查询。
- table:表示查询的表
- partitions: 使用的那些分区(对于非分区表值为null)
- type(重要):表示表的连接类型。
- possible_keys: 此次查询中可能选用的索引
- key: 查询真正使用的索引
- key_len: 显示Mysql决定使用的索引size
- ref: 哪个字段或常数与key一起被使用
- rows: 显示此查询一共扫描了多少行,这是一个估计值,不是准确的值
- filtered:表示此查询条件所过滤的数据的百分比
- Extra: 额外信息
4.2 关键结果说明
4.2.1 select_type
单位查询的查询类型,比如:普通查询、联合查询(union、union all)、子查询等复杂询。
-
simple:普通查询。 表示不需要union操作或不包含子查询的简单的select查询。有连接查询的时候,外层的查询为simple.
- primary: 查询的主要部分 一个需要union操作或者包含子查询的select位于最外面的单位查询的select_type即为primary.
sql# simple: 表示不需要union操作或者不包含子查询的简单select查询。有连接查询时,外层的查询为simple。 explain select * from org_financial_detail;
2. union:连接查询
- derived, 在from列表中包含的自查询被标记为derived(衍生),Mysql会递归执行这些自查询,把结果放在临时表中
- union, 若第二个select出现在union之后,则被标记为union: 若union包含在from子句的子查询中,外层select将被标记为derived
- union result: 从union表获取结果的select
- dependent union:依赖连接查询,与union一样,出现在union或union all语句中,但是这个查询要受到外部查询的影响。
- subquery:子查询, 除了from子句中包含的子查询外,其他地方出现的子查询都可能是subquery
- dependent subquery:依赖子查询,与dependent union类似,表示这个subquery的查询要受到外部表查询的影响
- derived:派生表, from子句中出现的子查询,也叫派生表,其他数据库中可能叫做内联视图或嵌套select
示例:
sql
# UNION: 如果第二个select出现在union之后,就会标记为union
explain select * from org_financial_detail a union select * from org_financial_detail b;
sql
# 如果UNION包含在FROM子句的子查询中,外层select会被标记为:DRIVED
explain select * from ( select * from org_financial_detail a union select * from org_financial_detail b) c;
# union:union连接的两个select查询,第一个查询是derived派生表,除了第一个表外,第二个以后的表的select_type都是union
# derived:在from列表中包含的子查询被标记为derived(衍生),Mysql会递归执行这些子查询,把结果放到临时表中
sql
# dependent union:与union一样,出现在union或union all语句中,但是这个查询要受到外部查询的影响
explain select * from org_financial_detail a WHERE
a.id in (SELECT id from org_financial_detail b union select id from org_financial_detail c);
# union result: 从union表获取结果的select
mysql
# subquery 除了from字句中包含的子查询外,其他地方出现的子查询都可能是subquery
explain select (select id from org_financial_detail where group_code = '') from org_r_group_financial;
mysql
# dependent subquery 与 dependent union类似,表示这个subquery的查询要受到外部表查询的影响
explain select (select id from org_financial_detail a where a.group_code = b.group_code) from org_r_group_financial b;
4.2.2 type
显示的是单位查询的 连接类型 或者理解为 访问类型 , 访问性能依次从好到差
mysql
system
const
eq_ref
ref
fulltext
ref_or_null
unique_subquery
index_subquery
range
index_merge
index
ALL
- system: 表里只有一行数据或者空表。 等于系统表,const类别的特例,不用看
- const(重要): 使用唯一索引或者主键 返回记录一定是1行记录的等值where条件时,通常type是const。其他数据库也叫唯一索引扫描。
- eq_ref(重要):唯一性索引扫描,对于每个索引健。表中只有一条记录与之匹配
- ref(重要):非唯一性索引扫描 ,返回匹配某个单独值的所有行,本质上也是一种索引访问,它返回所有匹配某个单独值的行,然而,他可能会找到多个符合条件的行,所以它应该属于查找和扫描的混合体
- 组合索引
- 非唯一性索引
- fulltext: 全文索引检索,要注意,全文索引的优先级很高,如果全文索引和普通索引同时存在时,mysql不管代价,优先使用全文索引。
- ref_or_null: 与ref类似,只是增加了null的比较,实际用到不多。
- unique_subquery: 用于where中的in形式子查询,子查询返回不重复唯一值
- index_subquery: 用于in形式子查询使用到了辅助索引或者in常数列表,子查询可能返回重复值,可以使用索引将子查询去重。
- range(重要):索引范围扫描,常见于>,<,is null,between,in,like等运算符的查询中。
- index_merge: 表示查询使用了两个以上的索引,最后取交集或者并集,常见and,or的条件使用了不同的索引,官方排序这个在ref_or_null之后,实际上由于要读取所有索引,性能可能大部分时间都不如range.
- index(重要) :select结果列中使用到了索引,type会显示为index。全部索引扫描,把索引从头到尾扫一遍,常见于使用索引列就可以处理,不需要读取数据文件的查询,可以使用索引排序或者分组的查询。
- ALL(重要) :全表扫描数据文件,然后在server层进行过滤返回符合要求的记录。
详解(重要的):
const:唯一索引或主键
使用唯一索引或者主键,返回记录一定是1行记录的等值where条件时,通常type是const。
explain select * from t_multiple_index WHERE id = 1;
eq_ref:唯一性索引
连接字段主键或者唯一性索引
此类型通常出现在多表的join查询,表示对于前表的每一个结果,都只能匹配到后表的一行结果。查询的比较操作通常是'=',查询效率较高
explain select * from t_multiple_index a LEFT JOIN t_multiple_index b on a.id = b.id;
sql
select * from a,b where a.id=b.id (等值连接)
select * from a where name='zs' (条件查询)
ref:非唯一性索引
非唯一性索引扫描,返回匹配某个单独值的所有行,本质上也是一种索引访问,它返回所有匹配某个单独值的行,然而,他可能会找到多个符合条件的行,所以它应该属于查找和扫描的混合体。
组合索引
sql
# a,b,c为组合索引
CREATE TABLE `t_multiple_index` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
`c` varchar(10) DEFAULT NULL,
`d` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_abc` (`a`,`b`,`c`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4;
explain select * from t_multiple_index where a = 13 and b=16;
explain select * from t_multiple_index a left join t_multiple_index b on a.a = b.a;
非唯一索引
sql
# ref 非唯一索引
explain select * from org_financial_detail WHERE group_code = '50091847';
range:范围查询
索引范围查询,常见于使用>,<,is null,between ,in ,like等运算符的查询中。
sql
explain select * from org_financial_detail WHERE group_code like '500918%';
index:查询结果列中使用了索引
select结果列中使用了索引,type会显示index。全部索引扫描,把索引从头到尾全部扫一遍,常见于使用索引列就可以处理,不需要读取数据文件的查询、可以使用索引排序或者分组的查询。
sql
explain select group_code from org_financial_detail
ALL:全表扫描
这个就是全表扫描数据文件,然后再在server层进行过滤返回符合要求的记录。
sql
explain select * from org_financial_detail
注意事项:
- 除了ALL之外,其他的type都可以使用到索引
- 最少要使用到range级别
4.2.3 Extra
这个列包含不适合在其他列展示的,但十分重要的额外的信息,这个列可以显示的信息非常多,有几十种,这里解释几个经常遇到的。
01-Using filesort
- 使用了文件排序,说明mysql会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取。Mysql中无法利用索引完成的排序操作称之为'文件排序',这种情况需要优化SQL。
sql
# group_code是非唯一性索引
explain select * from org_financial_detail WHERE group_code like '500918%' order by group_name;
explain select * from org_financial_detail WHERE group_code like '500918%' order by group_code;
02-Using index
表示相应的select查询中使用了索引,避免访问表的数据行,这种查询的效率很高!
- 如果同时出现Using Where,索引在where之后,用作查询条件
- 如果没有同时出现Using Where,索引在where之前,用作查询结果读取
sql
-- 使用where,索引在where之后,用作查询条件
explain select id,title,price from tb_seckill_goods where price>100;
-- 没有使用where,索引在where之前,用作查询结果读取
explain select id,title,price from tb_seckill_goods;
03-Using where
- 表示mysql将对innodb提取的结果在SQL layer层进行过滤,过滤条件字段无索引
sql
explain select * from org_financial_detail WHERE status > 1;
04-Using join buffer
- 表示使用了连接缓存,比如说查询的时候,多表join次数非常多,那么将配置文件中缓冲区的join buffer调大一些
4.3 索引优化【理论可以去看专栏的索引篇,这里就直接说结论了】
4.3.1 哪些情况需要创建索引?
- 频繁出现在where条件字段,order排序,group by分组字段
- select频繁查询的列,考虑是否需要创建联合索引(覆盖索引,不回表)
- 多表join关联查询,on两边的字段都要创建索引
4.3.2 哪些情况需要创建索引?
- 表记录很少不需要创建索引:索引是要有存储的开销
- 一个表的索引个数不能过多 :
- 空间:浪费空间。每个索引都是一个索引树,占据大量的磁盘空间
- 时间:更新(插入、删除、更新)变慢。需要更新所有的索引树,太多的索引树也会增加优化器的选择时间。
- 所以索引虽然能提高查询效率,但是并不是越多越好,只为需要的列创建就可以了
- 频繁更新的字段不建议作为索引 :频繁更新的字段会引发频繁的页分裂和页合并,性能消耗比较高。
- 区分度低的字段,不建议建索引: 比如性别、状态。区分度太低,会导致扫描行数过多,再加上回表的消耗。有时候使用索引比全表扫描的性能还差。可以将这些字段用在组合索引中
- 在InnoDB存储引擎中,主键索引建议使用自增的长整型,避免使用很长的字段:主键索引树一个页节点是16K,主键字段越长,一个页可存储的数据量就会越少,比较臃肿。查询时尤其是区间查询时磁盘IO次数会增多。辅助索引树上叶子节点存储的数据是主键值,主键值越长,一个页可存储的数据越少,查询时磁盘IO次数就会增多,查询效率会降低
- 不建议使用无序的值作为索引:比如UUID、身份证。更新数据时会发生频繁的页分裂,页内数据不紧凑,浪费磁盘空间。
- 尽量选择组合索引,而不是单列索引 :一个组合索引等于多个索引效果,节省空间。可以使用覆盖索引和索引下行ICP。
- 创建原则:组合索引应该把频繁用到的列、区分度高的值放在前面。频繁用到代表索引的利用率高,区分度高代表筛选颗粒度大,这样做可最大限度的利用索引价值,缩小筛选范围。
4.3.3 索引失效-组合索引心法口诀:
- 全值匹配我最爱,最左前缀要遵守
- 带头大哥不能死,中间兄弟不能断
- 索引列上不计算,范围之后全失效
- Like百分写最右,覆盖索引不写星
- 不等空值还有OR,索引失效要少用
4.4 LIMIT优化
如果预计SELECT语句的查询结果是一条,最好使用 LIMIT 1,可以停止全表扫描。
处理分页会使用到 LIMIT ,当翻页到非常靠后的页面的时候,偏移量会非常大,这时LIMIT的效率会非常差。 LIMIT OFFSET , SIZE;
Limit的优化问题,其实是offset的问题,他会导致mysql扫描大量不需要的行然后再抛弃掉。
解决方案:单表分页时,使用自增主键排序之后,先使用where条件id>offset值,limit后面只写rows
mysql
select * from (select * from tuser2 where id > 1000000 and id < 1000500 order by id )t limit 0,20;
4.5 子查询优化
MySQL从4.1版本开始支持子查询,使用子查询进行SELECT语句嵌套查询,可以一次完成很多逻辑上需要多个步骤才能完成的SQL操作。子查询虽然很灵活,但是执行效率并不高。
那么问题又来了啊? 为什么它效率不高?
把内层查询结果当作外层查询的比较条件的
mysql
select goods_id,goods_name from goods where goods_id = (select max(goods_id) from goods);
执行子查询时,mysql需要创建临时表 ,查询完毕后再删除这些临时表,所以子查询的速度会受到一定的影响。这多了一个创建临时表和销毁临时表的过程。
优化方式:可以使用连接查询(JOIN)代替子查询,连接查询不需要建立临时表,其速度比子查询快。
4.6 其他查询优化
- 小表驱动大表:建议使用left join时,以小表关联大表,因为使用join的话,第一张表必须全扫描,以少关联多就可以减少这个扫描次数
- JOIN两张表的关联字段最好都建立索引,而且最好字段类型一致
- 避免全表扫描:注意索引失效口诀,避免索引失效导致的全表扫描
- 避免Mysql放弃索引 :如果Mysql估计使用全表扫描要比使用索引快,则不适用索引
- 最典型场景:查询数据量到一定阈值的时候出现的索引失效,数据量到达一定阈值使用索引不如全表扫描来的更快!
- where条件中尽量不要使用not in语句,建议使用 not exists
- 利用慢查询日志、explain执行计划查询、show profile查看SQL执行时的资源使用情况
SQL优化实战三剑客:慢查询日志 -> explain -> show profile
4.7 SQL语句性能分析
4.7.1 什么是profile?
Query Profiler是Mysql自带的一种query诊断分析工具 ,通过它可以分析出一条SQL语句的硬件性能瓶颈 在什么地方。通常我们使用的explain,以及show query log都无法做到精确分析,但是query profiler却可以定位出一条SQL语句执行的各种资源消耗情况,比如CPU、IO等,以及该SQL执行所耗费的时间等。
不过这个工具只有在Mysql 5.0.37以及以上版本才有实现。默认情况下,Mysql的功能没有打开,需要自己手动开启。
4.7.2 开启profile功能
- profile功能由mysql会话变量:profiling控制,默认是OFF关闭状态
- 查看是否开启了profile功能
mysql
select @@profiling;
或者
show variables like '%profil%';
- 开启profile功能
mysql
# 1是开启、0是关闭
set profiling = 1;
4.7.3 基本使用
语法
css
SHOW PROFILE [type [, type] ... ]
[FOR QUERY n]
[LIMIT row_count [OFFSET offset]]
type: {
ALL
| BLOCK IO
| CONTEXT SWITCHES
| CPU
| IPC
| MEMORY
| PAGE FAULTS
| SOURCE
| SWAPS
}
- show profile 和 show profiles 语句可以展示当前会话(退出session后,profiling重置为0)中执行语句的资源使用情况。
- show profiles : 以列表形式显示最近发送到服务器上执行的语句的资源使用情况,显示的记录数由变量:profiling_history_size控制,默认15条。
- show profile : 展示最近一条语句执行的详细资源占用信息,默认展示status 和Duration两列
- show profile 还可以根据show profiles列表的Query ID,选择显示某条记录的性能分析信息
mysql
# 查看某条SQL的性能分析信息
show profile for query 1;
# 查看某条SQL的具体某个指标的性能分析
show profile cpu for query 1;
type是可选的,取值范围如下:
- ALL 显示所有性能信息
- BLOCK IO 显示块IO操作的次数
- CONTEXT SWITCHES 显示上下文切换次数,不管是主动还是被动
- CPU 显示用户CPU时间、系统CPU时间
- IPC 显示发送和接收的消息数量
- MEMORY【暂未实现】
- PAGE FAULTS 显示页错误数量
- SOURCE 显示源码中的函数名称与位置
- SWAPS 显示swaps的次数
4.7.4 分析案例
查看是否打开了性能分析功能 select @@profiling;
打开profiling功能 set profiling = 1;
执行sql语句
mysql
select * from org_financial_detail WHERE dr = 0;
执行show profiles查看分析列表
查看id为76的执行情况
mysql
show profile for query 76;
可以指定资源类型查询
mysql
show profile cpu,swaps for query 76;
5. 数据库优化
如何发现复杂的SQL有问题?
5.1 慢查询日志
数据库的性能问题,80%以上都是由于慢SQL产生的。
数据库查询快慢是影响项目性能的一大因素,对于数据库,我们除了要优化SQL,更重要的是得先找到需要优化的SQL。
Mysql数据库"慢查询日志"功能,用来记录查询时间超过某个设定值的SQL语句,这将极大程度帮助我们快速定位到症结所在,以便对症下药。至于查询时间的多少才算慢,每个项目、业务都有不同的要求。
Mysql慢查询日志功能默认是关闭的,需要手动开启。
5.1.1 开启慢查询日志
查看是否开启慢查询日志
sql
# 查看是否开启慢查询日志
show variables like '%low_query%';
show variables like '%long_query_time%';
- slow_query_log: 是否开启慢查询日志,1为开启,0为关闭
- log-slow-queries: 旧版(5.6以下)Mysql数据库慢查询日志存储路径
- slow-query-log-file: 新版(5.6级以上)Mysql数据库慢查询日志存储路径
- 不设置改参数:系统会默认给一个文件host_name-slow.log
- long_query_time: 慢查询阈值,当查询时间多于设定的阈值时,记录日志,单位秒
开启慢查询功能
注意:打开慢查询日志可能会对系统性能有一点点影响,如果你的MySQL是主从结构,可以考虑打开其中一台从服务器的慢查询日志,这样既可以监控慢查询,对系统性能影响又小。
sql
# 开启慢查询日志
set global slow_query_log=on;
# 大于1秒钟的数据记录到慢日志中,如果设置为默认0,则会有大量的信息存储在磁盘中,磁盘很容易满掉
# 如果设置不生效,建议配置在my.cnf配置文件中
set global long_query_time=1;
# 记录没有索引的查询。
set global log_queries_not_using_indexes=on;
5.1.2 慢查询日志格式
sql
# Time: 2021-07-27T08:32:44.023309Z
# User@Host: root[root] @ [172.26.233.201] Id: 1243
# Query_time: 218.295526 Lock_time: 0.000126 Rows_sent: 10959
Rows_examined: 10929597
use hero_all;
SET timestamp=1627374764;
# 慢查询SQL语句
select tk.id,ts.* from tb_seckill_goods ts LEFT JOIN tb_sku tk ON
tk.id=ts.id where ts.id>100 order by ts.price;
日志解析:
- 第一行:SQL查询执行的具体时间
- 第二行:执行SQL查询的连接信息,用户和连接IP
- 第三行:记录了一些我们比较有用的信息,如下解析
Query_time: 这条SQL的执行的时间,越长则越慢 Lock_time: 在Mysql服务器阶段(不是在存储引擎阶段)等待表锁的时间 Rows_sent: 查询返回的行数 Rows_examined: 查询检查的行数,越长越浪费时间
- 第四行:设置时间戳,没有实际意义,只有和第一行对应执行时间
- 第五行及后面所有行是执行的SQL语句,SQL可能会很长。截止到下一个#Time之前
5.1.3 分析慢查询日志工具
使用mysqldumpslow功能,mysqldumpslow是mysql自带的慢查询日志工具。可以使用mysqldumpslow工具搜索慢查询日志中的SQL语句。
得到按照时间排序的前10条里面包含左连接的查询语句:
sql
mysqldumpslow -s t -t 10 -g "left join" /var/lib/mysql/slow.log
常用参数说明:
- -s: 表示按照何种方式排序
- al 平均锁定时间
- ar 平均返回记录时间
- at 平均查询时间(默认)
- c 计数
- l 锁定时间
- r 返回记录
- t 查询时间
- -t: 是top n 的意思,即为返回前面多少条的数据
- -g: 后面可以写一个正则匹配模式,大小写不敏感的
sql
[root@localhost ~]# mysqldumpslow -s t /var/lib/mysql/localhost-slow.log
Reading mysql slow query log from /var/lib/mysql/localhost-slow.log
Count: 1 Time=77.12s (77s) Lock=0.00s (0s) Rows=0.0 (0),
root[root]@[192.168.200.1]
select tk.id,ts.* from tb_seckill_goods ts LEFT JOIN tb_sku tk ON
tk.id=ts.id where ts.id>N order by ts.price
Count: 1 Time=2.00s (2s) Lock=0.00s (0s) Rows=1.0 (1),
root[root]@[192.168.200.1]
select sleep(N)
5.2 连接数max_connections
同时连接客户端的最大数量,默认151,最小值1
连接数导致问题:ERROR 1040, Too Many Connections原因如下:
- 第一:mysql的max_connections配置少了
- 第二:访问确实太高,Mysql有点扛不住了,考虑扩容
- 第三:你的连接池配置有误,MaxAvtive为0
sql
# 查看max_connections
show global varibales like 'max_connections';
# 设置max_connections(立即生效重启后失效)
set global max_connections = 800;
# 查询服务器使用过的最大连接数
show global status like 'Max_used_connections';
比较理想的设置是:Max_used_connections / max_connections * 100% ≈ 85%
最大连接数占上限连接数的85%左右,如果发现比例在10%以下,MySQL服务器连接数上限设置的过高了。
Max_connections可以无限大吗?
mysql支持的最大连接数取决于如下几个重要因素
- 可使用内存
- 每个连接占用的内存
- 连接响应时间
- 。。。
Max_connections大了会影响系统的吞吐能力?
一般情况下,Linux操作系统支持最大连接数范围在500-1000之间,最大连接数上限10W。如果想设置为最大,要么你有足够的资源,要么就是你可以接受很长的响应时间。
建议设置:最大连接数占上限连接数的85%左右,如果发现比例在10%以下,Mysql服务器连接数上限设置的过高了。
比较理想的设置是:Max_used_connections / max_connections * 100% ≈ 85%
5.3 线程使用情况
如果我们在Mysql服务器配置文件中设置了thread_cache_size,当服务端断开之后,服务器处理此客户端的线程将被缓存起来以响应下一个客户,而不是销毁。(前提是缓存数未达上限)
服务器线程缓存thread_cache_size没有进行设置或者设置的过小,mysql服务器一直在创建线程销毁线程。增加这个值可以改善系统性能(可以使RT更趋于平稳)
通过比较connections 和 Threads_created 状态的变量
Threads_created表示创建过的线程数,如果发现Threads_created值过大的话,表明服务器一直在创建线程,比较耗资源,可以适当增加配置文件中threads_cache_size值,查询thread_cache_size配置:
sql
# 查询线程使用情况
show global status like 'Thread%';
# 查询线程缓存
show variables like 'thread_cache_size';
# 增加thread_cache_size的值
set global thread_cache_size = 64;
根据物理内存建议设置规则如下:
- 1G -》8
- 2G -》16
- 3G -》32
- 大于3G -》64
5.4 数据库优化-结构优化
一个好的数据库设计方案对于数据库的性能往往会起到事半功倍的效果,要考虑数据冗余、查询和更新速度、字段的数据类型是否合理等多方面的内容。
5.4.1 将字段很多的表分解成多个表(分表)
对于字段很多的表,如果有些字段的使用频率很低,可以讲这些字段分离出来形成新表。因为当一个表的数据量很大的时候,会由于使用频率低的字段而变慢。
项目实战的时候会将一个完全信息的表里面的数据拆分出来 形成多个新表 每个新表负责那一块的数据查询。
5.4.2 增加中间表
对于需要经常联合查询的表,可以建立中间表以提高查询效率。通过建立中间表,将需要通过联合查询的数据插入到中间表中,然后将原来的联合查询改为对中间表的查询。
通常都是在统计当中使用,每次统计报表的时候都是离线统计,后台有一个线程对你这统计结果放入一个中间表,然后你对这个中间表查询。
举个例子:比如我们需要五张表联查,left join每次要查询5张表,如果我们做了一个中间表,把这五张表的查询结果放在这里面,直接查询这个表,是不是就变成了单表查询
5.4.3 增加冗余字段
设计数据表时应尽量遵循关系数据库范式 的规约,尽可能的减少冗余字段,让数据库设计看起来精致、优雅。但是合理的加入冗余字段可以提高查询速度。
表的规范化程度越高,表和表之间的关系越多,需要连接查询的情况也就越多,性能也就越差。
注意:冗余字段的值在一个表中修改了,就要想办法在其他表中更新,否则就会导致数据不一致的问题。
6. 服务器层面优化
6.1 缓冲区优化
将数据保存在内存中,保证从内存读取数据
- 设置足够大的innodb_buffer_pool_size,将数据读取到内存中,推荐设置为物理内存的50%~80%
- 怎么确定足够大?
sql
show global status like 'innodb_buffer_pool_pages%';
6.2 降低磁盘写入次数
- 对于生产环境,很多日志是不需要开启的,比如:通用查询日志
- 使用足够大的写入缓存
- 设置合适的日志落盘策略
6.3 Mysql数据库配置优化
- 缓冲池字节大小。推荐为物理内存的50%-80%
yml
innodb_buffer_pool_size
- 日志组(Redo)中每个日志文件的大小,默认48MB,日志文件越大越节省磁盘IO,但需要注意日志文件变大会增加崩溃恢复时间
yml
innodb_log_file_size=48
- 用来控制redo log刷新到磁盘的策略
yml
innodb_flush_log_at_trx_commit=1
- 每提交一次事务同步写到二进制日志到磁盘中,可以设置为n
yml
sync_binlog=1
- 脏页占innodb_buffer_pool_size的比例,会触发刷脏页到磁盘。推荐值为25%-50%
yml
innodb_max_dirty_pages_pct=30
- 后台进程最大IO性能指标。默认200,如果SSD,调整为5000-20000
yml
innodb_io_capacity=200
在MySQL5.1.X版本中,由于代码写死,因此最多只会刷新100个脏页到磁盘、合并20个插入缓 冲,即使磁盘有能力处理更多的请求,也只会处理这么多,这样在更新量较大(比如大批量 Insert)的时候,脏页刷新可能就会跟不上,导致性能下降。
而在MySQL5.5.X版本之后,innodb_io_capacity参数可以动态调整刷新脏页的数量,这在一定程度上解决了这一问题。
innodb_io_capacity参数默认是200,单位是页。该参数设置的大小取决于硬盘的IOPS,即每秒的输入输出量(或读写次数)。 至于什么样的磁盘配置应该设置innodb_io_capacity参数的值是多少,大家可参考下表。
- 慢查询日志的阈值设置,单位秒
yml
long_query_time=3
- Mysql的binlog复制的形式,Mysql8.0默认为row
yml
binlog_format=10
- 同时连接客户端的最大数量
yml
max_connections=200
- 全量日志建议关闭。默认关闭
yml
general_log=0
MySQL的配置参数都在my.conf或者my.ini文件的[mysqld]组中,常用参数汇总如下:
yml
# 01-缓冲区,将数据保存在内存中,保证从内存读取数据。推荐值为总物理内存的50%~80%。
innodb_buffer_pool_size=
# 02-日志组(Redo)中每个日志文件的大小,默认48MB,日志文件越大越节省磁盘IO,但需要注意日志文件变大增加崩溃恢复时间
innodb_log_file_size=48
# 03-用来控制Redo日志刷新到磁盘的策略。
innodb_flush_log_at_trx_commit=1
# 04-每提交1次事务同步写到磁盘中,可以设置为n。
sync_binlog=1
# 05-脏页占innodb_buffer_pool_size的比例时,触发刷脏页到磁盘。推荐值为25%~50%。
innodb_max_dirty_pages_pct=30
# 06-后台进程最大IO性能指标。默认200,如果SSD,调整为5000~20000
innodb_io_capacity=200
# 07-指定innodb共享表空间文件及大小。
innodb_data_file_path=
# 08-慢查询日志的阈值设置,单位秒。
long_qurey_time=3
# 09-MySQL的binlog复制的形式,MySQL8.0默认为row
binlog_format=row
# 10-同时连接客户端的最大数量
max_connections=200
# 11-全量日志建议关闭。默认关闭。
general_log=0
6.4 服务器硬件优化
提升硬件设备,例如选择尽量高频率的内存 、提升网络带宽 ,使用SSD高速硬盘、提升CPU性能等
CPU的选择:
- 对于数据库并发比较高的场景,CPU的数量比频率重要
- 对于CPU密集型场景和频繁执行复杂SQL的场景,CPU的频率越高越好