获取慢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.log

Druid连接池

可以使用Druid连接池来监控慢SQL

PMM / Grafana / ELK / Loki 统一收集

可以从平台侧来建立针对慢SQL的监控

优化慢SQL

统计出来后使用explain来查看执行计划定位问题。

索引优化

参考索引失效场景来进行优化

下面在写几种:

连接查询优化

  1. 遵循小表驱动大表

  2. 连接的两个表字符集需要一致

  3. 外连接时优先给被驱动表字段加索引

子查询优化

优先使用JOIN来替代子查询,多次查询代替子查询。子查询慢的原因:

  1. 外层每条数据都会走子查询,成本高

  2. JOIN更容易参与优化器的优化操作

  3. 可能产生临时表增加额外的排序或者去重操作

排序和分组优化

  1. 尽量进行索引排序

  2. 符合索引的最左匹配原则

  3. 要么全升序要么全降序

排序分组都比较耗费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执行的逻辑:

  1. 走二级索引 idx_update_time 做范围扫描,定位到 update_time > '2020-09-19' 的起点往后扫100010行

  2. Id回表100010行获取行数据(这里会回表100010次,OFFSET 是在 SQL 层丢弃,不是在存储引擎层提前跳过行数据

  3. 抛弃前面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万的表。

拆分原则:

数据量增长情况

数据表类型

优化核心思想

数据量为千万级,是一个相对稳定的数据量

状态表

能不拆就不拆读需求水平扩展

数据量为千万级,可能达到亿级或者更高

流水表

业务拆分,面向分布式存储设计

数据量为千万级,可能达到亿级或者更高

流水表

设计数据统计需求存储的分布式扩展

数据量为千万级,不应该有这么多的数据

配置表

小而简,避免大一统