mysql limit 优化和流查询

遇到一个需求是需要将整个表的数据逐行读取出来进行处理。

使用 limit

一开始实现是使用limit来做,每个循环结束后将offset+= 100,如下:

PreparedStatement pStatement = dm.prepareStatement("SELECT start_time,input_params FROM execution_jobs limit ? , ? ";
pStatement.setInt(offset, 100);
ResultSet rs = pStatement.executeQuery();

这个方法在offset变得越来越大之后,查询会非常缓慢。因为 select * from XXX limit 10000,10; 相当于扫描了满足条件的前10000行之后,丢掉,然后再读取10行。所以性能会非常差。

针对limit 查询性能慢的优化办法有很多。下面详细介绍:

  • 子查询法
     先找出第一条数据,然后大于等于这条数据的id就是要获取的数据
     缺点:数据必须是连续的,可以说不能有where条件,where条件会筛选数据,导致数据失去连续性
mysql> set profiling=1;                             # 开启profile
Query OK, 0 rows affected (0.00 sec)

mysql> pager grep !~-                              #关闭stdout
PAGER set to 'grep !~-'
mysql> select exec_id ,project_id from execution_jobs limit 100000,100;
100 rows in set (2.65 sec)

mysql> select exec_id ,project_id from execution_jobs where exec_id >= (select exec_id from execution_jobs  limit 100000,1) limit 100;
100 rows in set (0.52 sec)

mysql> nopager                                   #打开stdout
PAGER set to stdout
mysql> show profiles;                          #查看耗时
+----------+------------+---------------+----------------+--------------------------------------------------------------------------------------------------------------------------------+
| Query_ID | Duration   | Logical_reads | Physical_reads | Query                                                                                                                          |
+----------+------------+---------------+----------------+--------------------------------------------------------------------------------------------------------------------------------+
|        1 | 2.67572900 |         13337 |            410 | select exec_id ,project_id from execution_jobs limit 100000,100                                                                |
|        2 | 0.53610725 |         13026 |            264 | select exec_id ,project_id from execution_jobs where exec_id >= (select exec_id from execution_jobs  limit 100000,1) limit 100 |                                                                                                       |
+----------+------------+---------------+----------------+--------------------------------------------------------------------------------------------------------------------------------+
3 rows in set (0.01 sec)

  • 倒排表优化法

    倒排表法类似建立索引,用一张表来维护页数,然后通过高效的连接得到数据
     缺点:只适合数据数固定的情况,数据不能删除,维护页表困难

  • 反向查找优化法
     当偏移超过一半记录数的时候,先用排序,这样偏移就反转了
     缺点:order by优化比较麻烦,要增加索引,索引影响数据的修改效率,并且要知道总记录数,偏移大于数据的一半

  • limit限制优化法
    把limit偏移量限制低于某个数,超过这个数等于没数据。

使用Mysql的流式查询

用上边的各种办法优化limit固然可行,但使用mysql的流查询是更加优越的实现这个需求的办法。

默认情况下,JDBC去操作mysql的时候,select语句都是将所有的结果缓存到内存中,但是采用流式查询的话,可以设置一个抓取数值,每次只读取一小部分数据。实现办法是:

PreparedStatement pStatement = dm.prepareStatement("SELECT exec_id,project_id  FROM execution_jobs", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
pStatement.setFetchSize(Integer.MIN_VALUE);   
ResultSet rs = pStatement.executeQuery();

while (rs.next()) {
// do something
}

这里 pStatement.setFetchSize(Integer.MIN_VALUE) 会让人困惑。还是直接看mysql的源码:

/**
 * We only stream result sets when they are forward-only, read-only, and the
 * fetch size has been set to Integer.MIN_VALUE
 *
 * @return true if this result set should be streamed row at-a-time, rather
 * than read all at once.
 */
protected boolean createStreamingResultSet() {
    try {
        synchronized(checkClosed().getConnectionMutex()) {
            return ((this.resultSetType == java.sql.ResultSet.TYPE_FORWARD_ONLY)
                 && (this.resultSetConcurrency == java.sql.ResultSet.CONCUR_READ_ONLY) && (this.fetchSize == Integer.MIN_VALUE));
        }
    } catch (SQLException e) {
        // we can't break the interface, having this be no-op in case of error is ok

        return false;
    }
}

从源码我们可以看到,只有forward-only, read-only, fetchsize为Integer.MIN_VALUE 三者同时成立,才会开启流查询方式。可以参考这里的回答:
http://stackoverflow.com/questions/20899977/what-and-when-should-i-specify-setfetchsize

更多精彩:
Duval的GithubIO

你可能感兴趣的:(mysql limit 优化和流查询)