获取慢SQL
首先确认这么几点:
服务器占用资源,通过监控来服务器资源有没有用尽。查看MYSQL进程占用的资源,看看CPU资源或者硬盘有没有到达瓶颈。
确认设置了正确的InnoDB Buffer Pool缓存池大小需要通过innodb_buffer_pool_size来设置,一般设置为服务器本机的%25~%75
调整最大连接数:max_connections和线程池缓存线程数:thread_cache_size
这里的缓存池指的是大小
首先统计慢SQL,有这么几种方式:
MYSQL
执行:
show variables like '%query%';查看结果中的
slow_query_log:是否记录慢SQL,默认是OFF,可以设置为ON。
slow_query_log_file:慢SQL文件位置
slow_query_time:慢SQL时间阈值,默认是10秒,可以设置为2秒
想永久生效,要改 MySQL 配置文件 my.cnf 或 my.ini
我本地设置了
# 开启慢查询日志
slow_query_log=ON
# 慢查询阈值(秒)
long_query_time=2后执行了:SELECT SLEEP(2.5);,看到慢SQL日志内容是:
/* ApplicationName=DataGrip 2025.1.3 */ SELECT SLEEP(2.5);可以看到确实统计到了。
mysqldumpslow
可以使用mysqldumpslow来统计慢SQL:
#命令行,按照查询时间排序,查看前五条 慢查询SQL 语句
mysqldumpslow -s t -t 5 /var/lib/mysql/xxx-slow.logDruid连接池
可以使用Druid连接池来监控慢SQL
PMM / Grafana / ELK / Loki 统一收集
可以从平台侧来建立针对慢SQL的监控
优化慢SQL
统计出来后使用explain来查看执行计划定位问题。
索引优化
参考索引失效场景来进行优化
下面在写几种:
连接查询优化
遵循小表驱动大表
连接的两个表字符集需要一致
外连接时优先给被驱动表字段加索引
子查询优化
优先使用JOIN来替代子查询,多次查询代替子查询。子查询慢的原因:
外层每条数据都会走子查询,成本高
JOIN更容易参与优化器的优化操作
可能产生临时表增加额外的排序或者去重操作
排序和分组优化
尽量进行索引排序
符合索引的最左匹配原则
要么全升序要么全降序
排序分组都比较耗费cpu,能不用就不用。
where效率高于having。where是分组前过滤,having是分组后过滤。
深分页优化
先看下效果,我有表,有200w数据
CREATE TABLE `account` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键Id',
`name` varchar(255) DEFAULT NULL COMMENT '账户名',
`balance` int DEFAULT NULL COMMENT '余额',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_name` (`name`),
KEY `idx_update_time` (`update_time`)
) ENGINE=InnoDB AUTO_INCREMENT=2031586 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=REDUNDANT COMMENT='账户表'
我执行下面的例子,为了走索引我加了排序,并且本地测试数据不一定准确,只是为了看差别
EXPLAIN ANALYZE SELECT id,name,balance FROM account WHERE update_time > '2020-09-19' ORDER BY update_time, id LIMIT 0,10;返回总用时2毫秒,然后我再执行:
EXPLAIN ANALYZE SELECT id,name,balance FROM account WHERE update_time > '2020-09-19' ORDER BY update_time, id LIMIT 100000,10;用时124毫秒
首先看第二个SQL执行的逻辑:
走二级索引 idx_update_time 做范围扫描,定位到 update_time > '2020-09-19' 的起点往后扫100010行
Id回表100010行获取行数据(这里会回表100010次,OFFSET 是在 SQL 层丢弃,不是在存储引擎层提前跳过行数据)
抛弃前面100000条记录,返回最终的10条记录
现在问题在于第二条SQL扫描了100010行,而第一条只扫描了10行,这就是性能差异点。
解决方案是使用
延迟关联
explain analyze SELECT a.id, a.name, a.balance
FROM account a
JOIN (SELECT id
FROM account
WHERE update_time > '2020-09-19'
ORDER BY update_time, id
LIMIT 100000,10) s ON s.id = a.id
ORDER BY a.update_time, a.id;延迟关联(也叫“先取主键再回表”)本质是把“大量回表”变成“少量回表”
子查询只查询了id,拿到了10条id,外层再用id回表查只有10条回表操作。
Keyset Pagination(游标分页/seek 分页)
假设这里是ID自增的,上一页返回的最后一条记录id是100000,那可以这么写:
-- 不使用OFFSET,而是利用上一页的id进行定位
SELECT * FROM products
WHERE id > 100000
ORDER BY id ASC
LIMIT 10;优点:查询性能恒定,不受分页深度影响,速度极快。
缺点:无法直接跳转到指定页码,仅适用于“上一页/下一页”或无限滚动场景。需要一个唯一且有序的排序列。
这种需要前端请求时将最后一个id传到后端
业务上限制
业务上限制比如最多只能查100页。
系统层面
使用合适的存储引擎
读写分离
分库分表
数据量级到达千万级以上后,垂直拆分(分库)、水平拆分(分表)、垂直+水平拆分(分库分表)。
概念:
只分表:单表数据量大,读写出现瓶颈,这个表所在的库还可以支撑未来几年的增长。
只分库:整个数据库读写出现性能瓶颈,将整个库拆开。
分库分表:单表数据量大,所在库也出现性能瓶颈,就要既分库又分表。
垂直拆分:把字段分开。例如spu表的pic字段特别长,建议把这个pic字段拆到另一个表(同库或不同库)。
水平拆分:把记录分开。例如表数据量到达百万,我们拆成四张20万的表。
拆分原则: