一个sql问题的排查

一.现象

线上一台任务机对一张mysql表进行全表扫描,当扫描过程中有一行记录读出来做数据转换,发现有很多行被多次读出来,并且多次尝试去做数据转换,同时数据转换也就失败了,因此也就打出了error级别的日志。

二.分析

由于这个任务是在遍历表,因此我们直接找到相应的sql语句看看,下面就是找到的sql语句

1
2
3
4
5
6
7
8
<![CDATA[
	select xxx,xxxx
	from xxxxyyyyy
	where gmt_create >=  #beginTime#
    and gmt_create <  #endTime#
    and id >= #idBegin#
    limit #pageSize#     
]]>

上面的sql在一个循环中会不断被调用,下面我们先来看看调用这个sql的java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 找到数据库中最大的主键ID
long maxId = findCompanyMaxId(TABLE_NAME);
// 循环次数
long loopTime = 1;
do {
    // 计算开始id
    long idBegin = maxId - loopTime * SCOPE_OF_EACH_LOOP + 1;
    long idEnd = maxId - (loopTime - 1) * SCOPE_OF_EACH_LOOP;

    List<Xxxx> currentList = null;

    if (idBegin > 0) {
        // 调用上面的sql语句获取数据,beginTime和endTime在这个方法中设置,因为这个两个值可以系统当前时间推算出来
        currentList = xxDAO.getListFromMaxId(days, TABLE_NAME, TABLE_PK, idBegin, idEnd);
    } else {
        // 退出循环
        break;
    }
    
    if (currentList == null || currentList.size() == 0) {
        // 退出循环
        break;
    }
} while (true);

上面的代码很简单,先找出数据库中表主键的最大ID,select max(id) from …即可,然后每次把maxId向前推进SCOPE_OF_EACH_LOOP,这样就能不断遍历这张表了。我们假设第一次获取到的maxId为9000,每次向前推进1000,pageSize=1000,这样第一调用sql时的idBegin=9000 - 1000 + 1 = 80001,这时候我们取出满足条件的limit 1000条记录,但是最终结果可能不足1000条,因为有beginTime和endTime的限制,好了第一次调用这个sql没啥问题的,后面我们考虑一次sql的调用,idBegin = 9000 - 2 * 1000 + 1 = 7001,这时候我们指定的idBegin=7001,limit大小为1000,其实这个sql的本质期望是扫描[7001,8000]之间的记录,但是[7001,8000]之间符合条件的记录又不足1000条,这样就会从[80001,9000]之间再去取一些行来补足1000行,谁让你写了1000呢?但是[8001,9000]之间的记录明显已经被扫描过了,以此类推数据的重复扫描就会发生了,悲剧由此而产生,只能说这种sql写的太没水平了。

三.解决办法

修改sql,删掉limit,增加idEnd的限制,修改后的sql如下:

1
2
3
4
5
6
7
8
<![CDATA[
	select xxx,xxxx
	from xxxxyyyyy
	where gmt_create >=  #beginTime#
    and gmt_create <  #endTime#
    and id >= #idBegin#
    and id <= #idEnd#
]]>

注意:

  • 当sql语句中出现了limit并且有比较关系的时候,注意考虑sql语句会不会出现重复扫描数据的逻辑。