遇到一个需求是需要将整个表的数据逐行读取出来进行处理。
使用 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