最近在处理一个历史遗留问题的时候,踩到了一个坑。估计很多老程序员平时也很容易忽略。
背景
有一个业务需求,需要重刷数据,数据量大概有1000多万,存在MySQL中。
解决思路
无脑用分页查询,每页大概500条数据。结果当处理到百万条数据的时候,异常的卡顿。以下是我的代码逻辑。
public void process(){
@Autowired
CustomerMapper customerMapper;
int pageNo = 0;
int pageSize = 500;
List<Customer> customerList = customerMapper.page(pageNo, pageSize);
while(ObjectsUtils.isEmpty(customerList)){
for(Customer customer : customerList){
// 核心处理在doProcess()方法中,此处忽略。
doProcess(customer);
}
pageNo += 1;
customerList = customerMapper.page(pageNo, pageSize)
}
}
以下是customerMapper.page执行的SQL。
SELECT * FROM customer LIMIT #{pageNo}, #{pageSize};
问题分析
我们首先来看下以上SQL的执行时间。
explain
SELECT * FROM customer LIMIT 20,500
[2025-02-26 21:18:50] 500 rows retrieved starting from 1 in 775 ms (execution: 98 ms, fetching: 677 ms)
查询第20页的时候,耗费了677ms。
接下来用explain命令看看SQL的执行情况。
由此可以看出,该SQL进行了全表扫描,即会把所有数据查出来,然后集中选中选择第21行到第520行的数据(跳过前20条数据),并且该查询没有使用索引。同时,MySQL估计需要检查的行数为5262行,只是一个简单的查询,不涉及到多表联查。
解决思路
分析了SQL的执行情况,我们避免全表扫描,同时查询的时候,使用索引就可以减少查询的时间,提高查询效率。
我们每次查询的时候,记录拿到数据的最大ID, 记为maxId。
long maxId = customerList.stream().mapToLong(Customer::getId).max().orElse(0);
然后,执行customerMapper.page()方法时,传下maxId。
以下是最终的SQL。
SELECT * FROM customer where id > #{maxId} LIMIT #{pageNo}, #{pageSize};
再来看下该SQL的执行时间。
explain
SELECT * FROM customer where id >= 1000 LIMIT 20,500
[2025-02-26 21:36:25] 500 rows retrieved starting from 1 in 389 ms (execution: 117 ms, fetching: 272 ms)
减少了288ms。
最后用explain命令看下SQL的执行情况。
由此可以看出,该SQL避免了全表扫描,使用了范围查询,同时使用了主键索引 。MySQL估计需要检查的行数也从5262行减少到2631行,减少了将近50%。
至此,SQL优化完成。