使用JOOQ seek语句及动态SQL重写MySQL分页逻辑

众所周知,MySQL在数据量很大的时候查询的效率是很低的,因为假如你需要OFFSET 100000 LIMIT 5这样的数据,数据库就需要跳过前100000条数据,才能返回给你你需要的5条数据。由于数据在磁盘上面不一定是相等长度的,所以没法在跳过这100000条数据上面进行优化,只能一条一条地查找数据、找到结尾处并查找下一条,这就导致了数据库很差的性能。解决的办法就是使用seek这种方法,可以参考这篇文章。
假如你有这样的数据,

|   ID | VALUE | PAGE_BOUNDARY |
|------|-------|---------------|
|  ... |   ... |           ... |
|  474 |     2 |             0 |
|  533 |     2 |             1 | <-- Last on page 5
|  640 |     2 |             0 |
|  776 |     2 |             0 |
|  815 |     2 |             0 |
|  947 |     2 |             0 |
|   37 |     3 |             1 | <-- Last on page 6
|  287 |     3 |             0 |
|  450 |     3 |             0 |
|  ... |   ... |           ... |

你需要将第6页的数据返回给用户,这时不需要用OFFSET和LIMIT语句,取而代之的是这种方法——找到第5页的最后一个数据的标识(在这里是id和value),并传递给SQL语句,用于定位从哪里开始第6页的数据,这样即使前面有100000条数据,数据库也不用管这些,只需一行where id > ?这样的语句便可略过这100000条数据直接找到你需要的那5条。

用SQL表示为:
SELECT id, value
FROM t
WHERE (value, id) > (2, 533)
ORDER BY value, id
LIMIT 5

用JOOQ则表示为:
DSL.using(configuration)
   .select(T.ID, T.VALUE)
   .from(T)
   .orderBy(T.VALUE, T.ID)
   .seek(2, 533)
   .limit(5)
   .fetch();

返回的结果是

|  ID | VALUE |
|-----|-------|
| 640 |     2 |
| 776 |     2 |
| 815 |     2 |
| 947 |     2 |
|  37 |     3 |

和用OFFSET、LIMIT语句查询的结果一样,但是性能方面显然这个更好。

在使用的时候,一般需要配合JOOQ的动态SQL来使用,以便区分各种筛选条件以及究竟是第一次查询还是需要查询更靠后面的数据,代码如下所示:

    //动态生成SQL语句
    public Condition whereCondition(List<Integer> ids,
                                    List<String> wordList,
                                    String time,
                                    boolean own) {
        Condition condition = trueCondition();

        if (ids != null) {
            if (own) condition = condition.and(SELF_SITE.URL_ID.in(ids));
            else condition = condition.and(SELF_SITE.URL_ID.notIn(ids));
        }

        //加上关键字的条件
        Condition wordCondition = falseCondition();
        for (String word : wordList) {
            wordCondition = wordCondition.or(SELF_SITE.NAME.contains(word));
        }
        condition = condition.and(wordCondition);

        //加上时间的条件
        if (time != null) {
            Instant instant = Instant.ofEpochMilli(Long.parseLong(time));
            LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneOffset.ofHours(8));
            condition = condition.and(SELF_SITE.FETCH_TIME.greaterOrEqual(localDateTime));
        }

        return condition;
    }

    @Override
    public List<SelfSite> findData(List<Integer> ids,
                                   int self_site_id,
                                   List<String> wordList,
                                   String time,
                                   boolean own) {
        SelectQuery query = dsl.selectQuery();
        query.addFrom(SELF_SITE);
        query.addConditions(whereCondition(ids, wordList, time, own));
        query.addOrderBy(SELF_SITE.ID.desc());
        if (self_site_id != -1) query.addSeekAfter(DSL.val(self_site_id));
        query.addLimit(NUM_PER_PAGE);
        return query.fetch().into(SelfSite.class);
    }

从上述代码可以看出JOOQ对动态SQL的支持非常强大,既可以使用Condition对象创建动态SQL,也可以使用SelectQuery来创建动态SQL。

总结一下,这样既使服务器端的代码变得简单了一些,而且使得数据库的性能提高了很多,更重要的是它大大简化了前端和移动端的分页逻辑,让你的前端开发人员能少写很多代码(我同学说修改了代码之后他可以少写至少200行代码==)。

你可能感兴趣的:(Spring)