数据库优化
昨天遇到一个问题, 200万的表里查询9万条数据, 耗时达63秒. 200万数据不算多, 查询9万也还好. 怎么用了这么长的时间呢? 问题是一句非常简单的sql.
select * from tk_template_product t WHERE t.product_id=1135
前提: product_id已经添加了索引, 可依然慢的无法接受.
目标: 优化sql, 至少要在5秒以内完成
针对这个问题开始优化. 优化过程如下:
1. sql语句优化
2. 使用explain分析
3. 使用profile分析SQL执行状态
4. MySQL Sending data导致查询很慢的问题详细分析
5. mysql查询慢Sending data耗时问题
6. 启用MySQL查询缓存,
7. 最终的解决方案
1. sql语句优化. 将 * 改为具体查询某个字段,
select t.id from tk_template_product t WHERE t.product_id=1136 AND t.deleted=0
只是查询了一个字段, 数据量还是200万查询9万, 耗时6-7秒. 多一个字段, 时间翻一倍.
网上还有其他sql语句优化的点, 但是, 我这个语句用不上呀, 这已经是一个最简单的sql语句了
2. 使用explain对sql语句进行分析, 看看是否使用了索引
首先怀疑索引没有建好,于是使用explain查看查询计划
explain select product_id, t.`template_id` from `tk_template_product` t where t.product_id=1136 and t.`deleted` = 0;
查询结果:
从explain的结果来看,整个语句的索引设计是没有问题的,
下面来看看这几个字段你的含义:
- id
我的理解是SQL执行的顺序的标识,SQL从大到小的执行
1. id相同时,执行顺序由上至下
2. 如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行
3.id如果相同,可以认为是一组,从上往下顺序执行;在所有组中,id值越大,优先级越高,越先执行
- select_type
示查询中每个select子句的类型
(1) SIMPLE(简单SELECT,不使用UNION或子查询等)
(2) PRIMARY(查询中若包含任何复杂的子部分,最外层的select被标记为PRIMARY)
(3) UNION(UNION中的第二个或后面的SELECT语句)
(4) DEPENDENT UNION(UNION中的第二个或后面的SELECT语句,取决于外面的查询)
(5) UNION RESULT(UNION的结果)
(6) SUBQUERY(子查询中的第一个SELECT)
(7) DEPENDENT SUBQUERY(子查询中的第一个SELECT,取决于外面的查询)
(8) DERIVED(派生表的SELECT, FROM子句的子查询)
(9) UNCACHEABLE SUBQUERY(一个子查询的结果不能被缓存,必须重新评估外链接的第一行)
- table
显示这一行的数据是关于哪张表的,有时不是真实的表名字,看到的是derivedx(x是个数字,我的理解是第几步执行的结果)
我这里显示的是表的别名
- type
表示MySQL在表中找到所需行的方式,又称“访问类型”。
常用的类型有: ALL, index, range, ref, eq_ref, const, system, NULL(从左到右,性能从差到好)
ALL:Full Table Scan, MySQL将遍历全表以找到匹配的行
index: Full Index Scan,index与ALL区别为index类型只遍历索引树
range:只检索给定范围的行,使用一个索引来选择行
ref: 表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值(我们这里显示的这一项, 表示使用了索引)
eq_ref: 类似ref,区别就在使用的索引是唯一索引,对于每个索引键值,表中只有一条记录匹配,简单来说,就是多表连接中使用primary key或者 unique key作为关联条件
const、system: 当MySQL对查询某部分进行优化,并转换为一个常量时,使用这些类型访问。如将主键置于where列表中,MySQL就能将该查询转换为一个常量,system是const类型的特例,当查询的表只有一行的情况下,使用system
NULL: MySQL在优化过程中分解语句,执行时甚至不用访问表或索引,例如从一个索引列里选取最小值可以通过单独索引查找完成。
- possible_keys
指出MySQL能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用
该列完全独立于EXPLAIN输出所示的表的次序。这意味着在possible_keys中的某些键实际上不能按生成的表次序使用。
如果该列是NULL,则没有相关的索引。在这种情况下,可以通过检查WHERE子句看是否它引用某些列或适合索引的列来提高你的查询性能。如果是这样,创造一个适当的索引并且再次用EXPLAIN检查查询
- Key
key列显示MySQL实际决定使用的键(索引)
如果没有选择索引,键是NULL。要想强制MySQL使用或忽视possible_keys列中的索引,在查询中使用FORCE INDEX、USE INDEX或者IGNORE INDEX。
- key_len
表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度(key_len显示的值为索引字段的最大可能长度,并非实际使用长度,即key_len是根据表定义计算而得,不是通过表内检索出的)
不损失精确性的情况下,长度越短越好
- ref
表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值
- rows
表示MySQL根据表统计信息及索引选用情况,估算的找到所需的记录所需要读取的行数
- Extra
该列包含MySQL解决查询的详细信息,有以下几种情况:
Using where:列数据是从仅仅使用了索引中的信息而没有读取实际的行动的表返回的,这发生在对表的全部的请求列都是同一个索引的部分的时候,表示mysql服务器将在存储引擎检索行后再进行过滤
Using temporary:表示MySQL需要使用临时表来存储结果集,常见于排序和分组查询
Using filesort:MySQL中无法利用索引完成的排序操作称为“文件排序”
Using join buffer:改值强调了在获取连接条件时没有使用索引,并且需要连接缓冲区来存储中间结果。如果出现了这个值,那应该注意,根据查询的具体情况可能需要添加索引来改进能。
Impossible where:这个值强调了where语句会导致没有符合条件的行。
Select tables optimized away:这个值意味着仅通过使用索引,优化器可能仅从聚合函数结果中返回一行
总结:
• EXPLAIN不会告诉你关于触发器、存储过程的信息或用户自定义函数对查询的影响情况
• EXPLAIN不考虑各种Cache
• EXPLAIN不能显示MySQL在执行查询时所作的优化工作
• 部分统计信息是估算的,并非精确值
• EXPALIN只能解释SELECT操作,其他操作要重写为SELECT后查看执行计划。
这个步骤, 能够得出的结论是, 我的这个sql语句使用了缓存, 缓存字段是product_id, 但是并没有显示出为什么会这么慢
3. 使用status查看mysql运行状态
show table status
可以查看到表中的行数, 每一行的容量大小, 以及总容量大小. 表可存储数据, 剩余存储数据数等信息
4. show processlist : 这时再通过show processlist命令来查看当前正在运行的SQL,从中找出运行慢的SQL语句,找到执行慢的语句后,再用explain命令查看这些语句的执行计划。
这里可以看出是哪个ip发出的请求, 访问的哪个数据库, 命令是否已经执行完成, 耗时等信息. 通过这些可以看到正在执行的慢查询. 然后再用profile进行分析, 看看到底慢在哪里了
5. 使用profile分析SQL执行状态
打开profile, mysql可以通过profiling命令查看到执行查询SQL消耗的时间。默认情况下,mysql是关闭profiling的
select @@have_profiling;
select @@profiling; 第一次执行语句, 返回结果可能是0.
0:表示profiling功能是关闭;
1:表示打开的。
设置profiling打开. set session profiling=1;
select @@profiling;
以上表示profiling已经打开了
下面, 使用profile分析SQL,可以看到执行两次后,Send data和sending cached result to clien执行效率的变化
执行一个sql语句
select count(*) from dtc_mntk.tk_template_product;
执行show profiles; 查询所有 sql执行的时间
show profiles;
找到刚刚我们执行sql语句的第query_id, 通过下面的sql查询具体分析
show profile for query 2;
通过分析, 可以看出Sending data耗时最多,
下面是我要查询的数据的内容
select t.id from dtc_mntk.`tk_template_product` t where t.product_id=1136 and t.`deleted` = 0;
耗时: 11秒, 只是查询了一个字段
执行show profiles; 查询所有 sql执行的时间
show profile for query 4;
可以看到sending data耗时10秒, 也就是基本上所有的耗时都在这里了, 接下来就要分析, 如何优化sending data
5. mysql查询慢Sending data耗时问题
查询一下“Sending data”状态的含义,原来这个状态的名称很具有误导性,所谓的“Sending data”并不是单纯的发送数据,而是包括“收集 + 发送 数据”。
这里的关键是为什么要收集数据,原因在于:mysql使用“索引”完成查询结束后,mysql得到了一堆的行id,如果有的列并不在索引中,mysql需要重新到“数据行”上将需要返回的数据读取出来返回个客户端。
那么sending data为什么会耗时这么长呢?
有几种原因
- 字段的长度过大, 可以通过show table status来查看表的状态
红色圈出的部分就是我们查询的相关表的信息. Avg_row_length: 表示平均每一行的长度. 这里大约0.05K, 这个还不是很大. 但像下面那个8192, 表示大约8K, 这就很恐怖了, 每行大约8M, 这可能不是数据量真的有这么大, 而是, 设置的字段长度设置的很大. 需要优化.
如果字段设置就要求这么大, 那么查询出来的时候, 最好不查询这一列
【解决方法】
找到了问题的根本原因,解决方法也就不难了。有几种方法:
1)查询时去掉列长度较大的查询,但这受限于业务的实现,可能需要业务做较大调整
2)表结构优化,将列长度较大的表进行拆分到另外的表,这个改动较大,需要已有业务配合修改,且如果业务还是要继续查询这个description的信息,则优化后的性能也不会有很大提升。
6. 启用MySQL查询缓存
https://www.cnblogs.com/mengfanrong/p/5335724.html
参考这篇文章, 可以设置mysql缓存, 但并不是所有设置了的缓存都会生效. 比如我查询的这个9万条数据, 缓存是不生效的, 因为数据量很大
7. 最后解决方案
比较滑稽的事, 上面做了这么多工作, 最后的解决方案是修改sql语句. 换一个写法. 为什么呢? 这次让我对jpa的用法有了一个更深刻的认识
JPA的用法总结:
1. 使用起来非常方便, 内部定义了很多配合方法, 简化sql.
2. 使用的是单表查询, 单表查询比连表要快很多, 查询出来以后, 将业务逻辑在代码里拼接, io消耗比与数据库交互少很多.
JPA使用注意事项:
1. 不可使用FindAll(), 尽量不要使用这个方法, 把所有数据查询出来, 然后在代码里处理逻辑. 不带limit, 这个方法就有坑, 数据量小还可以, 数据量大传输会特别耗时
2. 如果只是要获取记录条数, 而不需要获取内容的时候, 直接sql查询使用count(). 不要Find出来再size(). 这个坑也是在数据量大的时候.
我这个问题就是, 查询出来了所有的数据, 9万条啊, 然后使用stream分组, 分组后去size, 这个操作在数据库一个sql就完事, 用时不到1秒. 可把所有数据查出来, 竟然用了68秒.
![](https://img2020.cnblogs.com/blog/1187916/202106/1187916-20210627082053046-596566365.jpg)