在上一篇文章中学习了如何使用pt-query-digest分析RDS慢查询日志,而在本文中我们将进一步学习如何来优化慢查询
一、什么是慢查询?
慢查询是指在数据库中执行的查询操作,其执行时间超过了一定的阈值(这个阈值通常是根据实际情况设定的在MySQL中受long_query_time控制)。
二、慢查询会有什么现象?
监控层面:
慢查询现象出现的时候,一般伴随着多个监控指标同时飙升,例如:CPU 使用率、慢查询数量、innodb行读。
业务层面:
- 系统响应迟缓:在执行相关查询时,整个系统的响应变得很慢,其他操作也受到影响。
- 资源占用异常:可能会导致 CPU、内存等资源被过度占用,影响其他进程的正常运行。
- 页面加载缓慢:如果涉及到数据库查询的网页或应用界面,会出现加载缓慢甚至长时间无响应的情况。
- 查询执行时间过长:明显超出正常预期的时间来完成查询操作,导致用户等待时间增加。
- 大量并发时性能急剧下降:在高并发场景下,慢查询会更加突出,导致整体性能大幅下滑。
三、慢查询相关的参数
slow_query_log
用于开启或关闭慢查询日志。
slow_query_log_file
指定慢查询日志的存储文件路径。
long_query_time
慢查询阈值,超过这个时间的查询将被记录为慢查询,默认值通常是 10 秒。
log_queries_not_using_indexes
如果设置为 ON,则会将没有使用索引的查询记录到慢查询日志中,有助于提前规避一些隐患。
innodb_buffer_pool_size
缓冲池大小,该部分缓存是 InnoDB 引擎最重要的缓存区域,是通过内存来弥补物理数据文件的重要手段,在云数据库 MySQL 上会采用实例规格配置的25%作为该部分大小。其中主要包含数据页、索引页、undo 页、insert buffer、自适应哈希索引、锁信息以及数据字典等信息。在进行 SQL 读和写的操作时,首先并不是对物理数据文件操作,而是先对 buffer_pool 进行操作,再通过 checkpoint 等机制写回数据文件。该空间的优点是可以提升数据库的性能、加快 SQL 运行速度,缺点是故障恢复速度较慢。对查询性能有较大影响,具体说明可以查下MySQL官方文档,根据业务情况一般建议设置在65%左右。
query_cache_size
主要用于控制查询缓存的大小,当相同的查询再次执行时,如果结果在查询缓存中,就可以直接从缓存中获取结果,避免了重复执行查询的开销,从而可以提高查询性能。这个参数限制也是有的,比如对于查询SQL的匹配度要完全相同才会命中,其次对于经常需要更新的表也不适用。当查询缓存变得过大时,管理和维护它也会消耗一定资源。不是所有类型的查询都适合缓存,一些复杂的、动态性强的查询可能不太适合放入查询缓存。它的工作流程如下:-
- 当一个SELECT语句执行时,MySQL会先检查查询缓存,是否有该查询的结果。
- 如果缓存中有该查询的结果,则直接返回结果给客户端。
- 如果缓存中没有该查询的结果,则执行查询语句,将结果存储到缓存中,并返回结果给客户端。
thread_cache_size
该参数用于指定线程缓存的大小。优点就是可以减少频繁创建和销毁连接线程的开销,提高连接处理的效率。当有新的连接请求时,首先会尝试从线程缓存中获取可用的线程,而不是立即创建新的线程。需要注意的是需要根据系统的实际负载和连接情况来合理设置。如果设置得太小,可能无法充分利用线程缓存的优势;如果设置得太大,可能会浪费系统资源。要结合服务器的并发连接数、连接的活跃程度等因素来综合考虑。通常需要通过实际测试和观察来确定一个较为合适的值,以在性能和资源利用之间达到较好的平衡。结合物理内存设置:根据系统的物理内存大小来调整 thread_cache_size。可以参考以下则:
1G 内存:thread_cache_size=8
2G 内存:thread_cache_size=16
3G 内存:thread_cache_size=32
大于 3G 内存:thread_cache_size=60+4
四、慢查询通常情况下是 SQL 语句的执行效率不够高,导致大量的请求堆积在云数据库 MySQL 中,常见原因有两个
- 原因1:SQL 设计不合理,语句没有利用索引或者没有用较佳的索引,简单举例一下走索引和不走索引的区别。
1.准备一张t1的测试表
CREATE TABLE "t1" (
"id" int(11) NOT NULL,
"name" varchar(100) DEFAULT NULL,
"insert_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY ("id")
) ;
2.通过explain分析慢SQL,explain字段详细说明
mysql> explain select * from t1;
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------+
| 1 | SIMPLE | t1 | NULL | ALL | NULL | NULL | NULL | NULL | 10161 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------+
1 row in set, 1 warning (0.00 sec)
id | 表示查询中每个子查询或操作的编号 |
select_type | 描述查询的类型,如简单查询、子查询等 |
table | 涉及的表名 |
partitions | 相关的分区信息 |
type | 表示索引的访问类型,如全表扫描、索引范围扫描等,它反映了查询的效率 |
possible_keys | 可能用到的索引 |
key | 实际使用的索引 |
key_len | 索引键的长度 |
ref | 表示与索引进行比较的对象 |
rows | 估计需要扫描的行数 |
filtered | 表示过滤后剩余结果的比例 |
Extra | 包含一些额外的执行信息,如是否使用了索引等 |
2.用id字段和name字段作为条件,分别查看他们的执行计划有什么区别。
mysql> explain select * from t1 where id =2;
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| 1 | SIMPLE | t1 | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
mysql> explain select * from t1 where name ='dengmeng';
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------------+
| 1 | SIMPLE | t1 | NULL | ALL | NULL | NULL | NULL | NULL | 10161 | 10.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------------+
1 row in set, 1 warning (0.01 sec)
3.通过对比我们发现以id为条件,走了主键索引,扫描行数是1行执行效率很不错的,而通过name为条件时进行了全表扫描,没有走到索引。那么业务就需要通过name条件查询时,该如何优化呢?我们可以给name列也加上一个索引。
alter table t1 add index index_name (name);
4.在查看执行计划发现效果很明显,从全表扫描降低到只扫描1行数据,大大提升了SQL执行效率。对于大表查询没有走索引是很容易造成慢查询,从而导致业务响应慢。还有一种情况是走了索引缺还是查询慢,这种就需要根据业务情况具体分析,是索引失效或是索引区分度不高,再或者是表的数据量非常之大本身就很慢。
mysql> explain select * from t1 where name='dengmeng';
+----+-------------+-------+------------+------+---------------+------------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------------+---------+-------+------+----------+-------+
| 1 | SIMPLE | t1 | NULL | ref | index_name | index_name | 303 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
5.索引优化参考官方文档:
优化建议:
- 单表的索引数量不应超过5个,单个索引中的字段数不应超过5个,以避免索引过多的过滤作用和资源消耗。
- 确保索引字段长度固定且不宜过长。过长的索引字段会占用更多磁盘空间,并影响索引的性能。
- 避免冗余索引,即存在两个索引 (a,b) 和 (a) 的情况,若查询条件为a列,只需建立 (a,b) 索引即可,不需要额外建立 (a) 索引。
- 对于高过滤性的字段,考虑在其上加索引。高过滤性字段的索引可以提高查询效率。
- 注意选择性和数据类型。选择性高的字段和合适的数据类型可以提高索引效果和查询性能。
- 合理利用覆盖索引来减少IO开销,通过创建包含所需列的复合索引,避免回表操作。
注意事项:
- 禁止在更新频繁且区分度低的列上建立索引,这会降低数据库性能。
- 在使用LIKE关键字时,避免在搜索模式字符串之前使用前置%符号。这样做会导致索引失效,影响查询性能。
- 避免在具有空值的列上创建索引。一般情况下,索引不会建立在含有空值的列上。
- 在使用OR关键字时,确保OR操作符的左右字段都有索引。如果一个字段有索引而另一个字段没有索引,有索引的字段也会失效。
- 避免使用!=操作符。范围的不确定性导致使用索引效率低下,数据库引擎会自动将其改为全表扫描。
- 不要在索引字段上进行运算。这会导致索引失效,降低查询性能。
- 使用复合索引时,应该遵循最左前缀原则,并确保查询中使用索引的第一个字段,否则索引将失效。为了最大化索引的效率,应该尽量保持查询中字段的顺序与索引的顺序一致。
- 原因2:QPS 压力超过当前实例的承载上限
处理措施:提升云数据库 MySQL 配置。
1.登录管理控制台。
2.单击管理控制台左上角的资源池,选择区域和项目。
3.选择“数据库 > 关系数据库MySQL版”,进入关系数据库MySQL版控制台。
4.在“实例管理”页面, 选择指定的实例,单击“更多”选择规格扩容操作,进入规格扩容页面,对实例进行扩容。