最近在做项目的时候,发现接口的请求处理速度不理想,虽然使用了缓存可以保证后来用户的体验,但是当第一位用户访问时,需要生产数据并写入缓存,这就有一点影响用户体验了。于是考虑对接口进行调优,这里主要说慢SQL方面的调优。
我的项目比较简单,先说结果,只要加一个索引就好了。后面会记录一下从发现原因到思考,再到出解决方案的整个流程,我觉得更重要的是后面的部分
在使用JProfiler进行JDBC性能监控时,发现了一条慢SQL:
出现了一个长达4s的查询,通过找日志,发现是这一条语句
2022-03-09 14:49:49.897 DEBUG 61663 --- [nio-8081-exec-1] c.e.h.x.dao.SearchMapper.getRecords : ==> Preparing: SELECT origin, dest, velocity FROM xm_bus.bus_velocity WHERE YEAR(time) = ? AND MONTH(time) = ? AND DAY(time) = ? AND HOUR(time) = ? AND MINUTE(time) = ?
2022-03-09 14:49:49.898 DEBUG 61663 --- [nio-8081-exec-1] c.e.h.x.dao.SearchMapper.getRecords : ==> Parameters: 2021(Integer), 6(Integer), 29(Integer), 16(Integer), 39(Integer)
2022-03-09 14:49:54.055 DEBUG 61663 --- [nio-8081-exec-1] c.e.h.x.dao.SearchMapper.getRecords : <== Total: 773
上述语句的作用是将某个数据采集点的所有数据取出来,因为不单单是拥塞检测,搜索算法也会用到,因此考虑开启Mybatis二级缓存(作用在Mapper域),也可以考虑是否可以加索引提升单次性能。
这里就设及到一个概念,慢查询,到底怎么定义这个“慢”?SQL执行超过多少秒算慢?
首先看MySQL的默认值为10秒mysql> show variables like 'long_query_time'; +-----------------+-----------+ | Variable_name | Value | +-----------------+-----------+ | long_query_time | 10.000000 | +-----------------+-----------+ 1 row in set (0.00 sec)
但是10秒确实有点长,在一些公司,查询超过100ms就算慢查询
在确定优化SQL之前,还需要考虑,这个慢查询是由网络原因导致的,还是SQL本身导致的,JProfiler所分析的项目是通过互联网连接远程库的,于是在docker容器内执行相同的查询
根据结果,会好一些,用了2秒多,但是还是属于慢SQL范围,因此可以确定需要优化的是SQL本身,也就是如何在大数据量下能较快地进行查询
注:表数据量在300w条左右
综上,如果单纯开启Mybatis二级缓存治标不治本,需要对SQL本身下手,或者考虑其他解决方案
使用explain分析慢SQL
所以,考虑加索引加速。
索引一般是加在字段上,我们的SQL的Where查询使用的是函数,这时候就可以使用函数索引,该特性为MySQL 5.7以上支持。
函数索引的主要思想是:构造一个虚拟列,该列的值都是通过函数计算生成的,然后再把索引加在这上面
这里就使用二级索引,也就是把(年、月、日、时、分)作为key建立索引,顺序不能变,要满足最左前缀原则
建立索引的时候要考虑到索引的区分度问题,还有MySQL的索引选择问题:
这里要使用Generated Column构造虚拟列,它有两种模式:
下面开始创建虚拟列:
mysql> alter table bus_velocity add column index_year int generated always as (YEAR(bus_velocity.time)) stored;
Query OK, 3752534 rows affected (39.08 sec)
Records: 3752534 Duplicates: 0 Warnings: 0
mysql> alter table bus_velocity add column index_month int generated always as (MONTH(bus_velocity.time)) stored;
Query OK, 3752534 rows affected (34.04 sec)
Records: 3752534 Duplicates: 0 Warnings: 0
mysql> alter table bus_velocity add column index_day int generated always as (DAY(bus_velocity.time)) stored;
Query OK, 3752534 rows affected (42.19 sec)
Records: 3752534 Duplicates: 0 Warnings: 0
mysql> alter table bus_velocity add column index_hour int generated always as (HOUR(bus_velocity.time)) stored;
Query OK, 3752534 rows affected (36.50 sec)
Records: 3752534 Duplicates: 0 Warnings: 0
mysql> alter table bus_velocity add column index_min int generated always as (MINUTE(bus_velocity.time)) stored;
Query OK, 3752534 rows affected (42.76 sec)
Records: 3752534 Duplicates: 0 Warnings: 0
mysql> select * from bus_velocity limit 1 \G;
*************************** 1. row ***************************
id: 1
time: 2021-05-24 00:01:38
origin: ?????
origin_lon: 118.126207
origin_lat: 24.487075
dest: ??????
dest_lon: 118.123850
dest_lat: 24.484735
velocity: 17.54609
travel_time: 231
weather: ?
index_year: 2021
index_month: 5
index_day: 24
index_hour: 0
index_min: 1
1 row in set (0.00 sec)
通过show create table
命令查看,发现数据表的结构已经改变
mysql> show create table bus_velocity \G;
*************************** 1. row ***************************
Table: bus_velocity
Create Table: CREATE TABLE `bus_velocity` (
`id` bigint NOT NULL AUTO_INCREMENT,
`time` datetime DEFAULT NULL COMMENT '????',
`origin` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '???????',
`origin_lon` decimal(20,6) DEFAULT NULL COMMENT '????',
`origin_lat` decimal(20,6) DEFAULT NULL COMMENT '????',
`dest` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '???????',
`dest_lon` decimal(20,6) DEFAULT NULL COMMENT '????',
`dest_lat` decimal(20,6) DEFAULT NULL COMMENT '????',
`velocity` decimal(20,5) DEFAULT NULL COMMENT '?????????',
`travel_time` int DEFAULT NULL COMMENT '???????????',
`weather` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '???????????',
`index_year` int GENERATED ALWAYS AS (year(`time`)) STORED,
`index_month` int GENERATED ALWAYS AS (month(`time`)) STORED,
`index_day` int GENERATED ALWAYS AS (dayofmonth(`time`)) STORED,
`index_hour` int GENERATED ALWAYS AS (hour(`time`)) STORED,
`index_min` int GENERATED ALWAYS AS (minute(`time`)) STORED,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3910014 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin
1 row in set (0.02 sec)
所以以后插入新数据的时候要注意!
下面开始创建索引
mysql> create index IX_YEAR_MONTH_DAY_HOUR_MIN on bus_velocity(index_year, index_month, index_day, index_hour, index_min);
Query OK, 0 rows affected (34.69 sec)
Records: 0 Duplicates: 0 Warnings: 0
索引创建完成后,重新运行查询