我想从两个时间戳之间的表中获取数百万行,然后对其进行处理。触发单个查询并一次检索所有记录看起来是一个糟糕的策略,因为它可能超出了我的JVM程序的内存功能。
我读过这篇文章:
http://oracle.readthedocs.io/en/latest/sql/indexes/top-n-pagination.html
因此,我计划分批进行分页,并提出以下策略:
我们说Start_Date = X and End_Date = Y
select * from table where CREATE_TIMESTAMP > X AND CREATE_TIMESTAMP < Y ORDER BY CREATE_TIMESTAMP FETCH NEXT 1000 ROWS ONLY.
如果我只获得少于1000行,则表示所有记录都已完成。如果我得到1000行,那意味着可能会有更多记录。
set X = CREATE_TIMESTAMP of 1000th record
select * from table where CREATE_TIMESTAMP > X AND CREATE_TIMESTAMP < Y ORDER BY CREATE_TIMESTAMP FETCH NEXT 1000 ROWS ONLY
重复此操作直到我记录的记录少于1000条。
有没有人发现这种方法存在任何问题?
答案 0 :(得分:2)
Pagination pattern是为了网站呈现而发明的(与滚动导航相反),并且在那里效果最好。简而言之,实时用户实际上无法一次查看数千/数百万条记录,因此信息被分成短页(50~200条记录),其中一个查询通常被发送到每个页面的数据库。用户通常只点击几个页面,但不会浏览所有页面,此外用户需要一些时间来浏览页面,因此查询不是逐个发送到数据库,而是长时间间隔。检索大量数据的时间比检索数百万条记录要短得多,因此用户很高兴,因为他不必等待后续页面,整个系统负载较小。
但问题似乎是,您的应用程序的性质面向批处理,而不是 Web演示。应用程序必须获取所有记录并对每个记录执行一些操作/转换(计算)。在这种情况下,使用完全不同的设计模式(流/流水线处理,步骤顺序,并行步骤/操作等),并且分页将不起作用,如果你这样做你将杀死你的系统性能。强>
让我们看一下简单实用的例子,而不是花哨的理论,它会告诉你我们在这里谈论的速度差异
假设有一个表PAGINATION
,其中有大约7百万条记录:
create table pagination as
select sysdate - 200 * dbms_random.value As my_date, t.*
from (
select o.* from all_objects o
cross join (select * from dual connect by level <= 100)
fetch first 10000000 rows only
) t;
select count(*) from pagination;
COUNT(*)
----------
7369600
假设在MY_DATE
列上创建了索引,索引统计信息是新鲜的:
create index PAGINATION_IX on pagination( my_date );
BEGIN dbms_stats.gather_table_stats( 'TEST', 'PAGINATION', method_opt => 'FOR ALL COLUMNS' ); END;
/
假设我们将在以下日期之间处理约10%的记录:
select count(*) from pagination
where my_date between date '2017-10-01' and '2017-10-21';
COUNT(*)
----------
736341
最后说出我们的&#34;处理&#34;为简单起见,将包括对一个字段的长度的简单求和。
这是一个简单的分页实现:
public class Pagination {
public static class RecordPojo {
Date myDate;
String objectName;
public Date getMyDate() {
return myDate;
}
public RecordPojo setMyDate(Date myDate) {
this.myDate = myDate;
return this;
}
public String getObjectName() {
return objectName;
}
public RecordPojo setObjectName(String objectName) {
this.objectName = objectName;
return this;
}
};
static class MyPaginator{
private Connection conn;
private int pageSize;
private int currentPage = 0;
public MyPaginator( Connection conn, int pageSize ) {
this.conn = conn;
this.pageSize = pageSize;
}
static final String QUERY = ""
+ "SELECT my_date, object_name FROM pagination "
+ "WHERE my_date between date '2017-10-01' and '2017-10-21' "
+ "ORDER BY my_date "
+ "OFFSET ? ROWS FETCH NEXT ? ROWS ONLY";
List<RecordPojo> getNextPage() {
List<RecordPojo> list = new ArrayList<>();
ResultSet rs = null;
try( PreparedStatement ps = conn.prepareStatement(QUERY);) {
ps.setInt(1, pageSize * currentPage++ );
ps.setInt(2, pageSize);
rs = ps.executeQuery();
while( rs.next()) {
list.add( new RecordPojo().setMyDate(rs.getDate(1)).setObjectName(rs.getString(2)));
}
} catch (SQLException e) {
e.printStackTrace();
}finally {
try{rs.close();}catch(Exception e) {}
}
return list;
}
public int getCurrentPage() {
return currentPage;
}
}
public static void main(String ...x) throws SQLException {
OracleDataSource ds = new OracleDataSource();
ds.setURL("jdbc:oracle:thin:test/test@//localhost:1521/orcl");
long startTime = System.currentTimeMillis();
long value = 0;
int pageSize = 1000;
try( Connection conn = ds.getConnection();){
MyPaginator p = new MyPaginator(conn, pageSize);
List<RecordPojo> list;
while( ( list = p.getNextPage()).size() > 0 ) {
value += list.stream().map( y -> y.getObjectName().length()).mapToLong(Integer::longValue).sum();
System.out.println("Page: " + p.getCurrentPage());
}
System.out.format("==================\nValue = %d, Pages = %d, time = %d seconds", value, p.getCurrentPage(), (System.currentTimeMillis() - startTime)/1000);
}
}
}
结果是:
Value = 18312338, Pages = 738, time = 2216 seconds
现在让我们测试一个非常简单的基于流的解决方案 - 只需要一个记录,处理它,丢弃它(释放内存),然后选择下一个记录。
public class NoPagination {
static final String QUERY = ""
+ "SELECT my_date, object_name FROM pagination "
+ "WHERE my_date between date '2017-10-01' and '2017-10-21' "
+ "ORDER BY my_date ";
public static void main(String[] args) throws SQLException {
OracleDataSource ds = new OracleDataSource();
ds.setURL("jdbc:oracle:thin:test/test@//localhost:1521/orcl");
long startTime = System.currentTimeMillis();
long count = 0;
ResultSet rs = null;
PreparedStatement ps = null;
try( Connection conn = ds.getConnection();){
ps = conn.prepareStatement(QUERY);
rs = ps.executeQuery();
while( rs.next()) {
// processing
RecordPojo r = new RecordPojo().setMyDate(rs.getDate(1)).setObjectName(rs.getString(2));
count+=r.getObjectName().length();
}
System.out.format("==================\nValue = %d, time = %d seconds", count, (System.currentTimeMillis() - startTime)/1000);
}finally {
try { rs.close();}catch(Exception e) {}
try { ps.close();}catch(Exception e) {}
}
}
结果是:
Value = 18312328, time = 11 seconds
是 - 2216秒/ 11秒=快201倍 - 20 100%更快!!!
难以置信的 ?你可以自己测试一下。
此示例显示了选择正确的解决方案(正确的设计模式)来解决问题的重要性。
答案 1 :(得分:1)
你没有说你是否打算调整&#34; X&#34;和&#34; Y&#34;每次你做分页。如果您不接受,那么只有在您确信数据是相当静态的情况下才可能有效。
考虑以下示例:
我的表T有&#34;今天&#34;的100行日期时间戳,ID = 1到100,我想要第一页的最后20行。所以我这样做:
select *
from T
where date_col = trunc(sysdate)
order by id desc
fetch first 20 rows only
我运行我的查询并将ID = 100降低到80.到目前为止一切都很好 - 它全部在用户的页面上,并且他们需要30秒的时间来读取数据。在此期间,该表中又增加了17条记录(ID = 101至117)。
现在用户按下&#34;下一页&#34;
现在我再次运行查询以获取下一组
select *
from T
where date_col = trunc(sysdate)
order by id desc
offset 20 fetch next 20 rows only
他们不会看到80行到60行,这是他们的期望,因为数据已经改变了。他们会
a)将行ID = 117降至97,并由于OFFSET而跳过它们 b)然后将行ID = 97降至77以显示在屏幕上
他们会感到困惑,因为他们看到的行数几乎与第一页上的行数相同。
对于更改数据的分页,您通常希望远离偏移条款,并使用您的应用程序记录您的目标,即
第1页
select *
from T
where date_col = trunc(sysdate)
order by id desc
fetch first 20 rows only
我将ID = 100提取到80 ...我记下80的。我的下一个查询将是
select *
from T
where date_col = trunc(sysdate)
AND ID<80
order by id desc
fetch first 20 rows only
我的下一个查询是
select *
from T
where date_col = trunc(sysdate)
AND ID<60
order by id desc
fetch first 20 rows only
等等。