Oracle分页策略

时间:2018-02-08 11:10:20

标签: sql oracle pagination oracle12c

我想从两个时间戳之间的表中获取数百万行,然后对其进行处理。触发单个查询并一次检索所有记录看起来是一个糟糕的策略,因为它可能超出了我的JVM程序的内存功能。

我读过这篇文章:

http://oracle.readthedocs.io/en/latest/sql/indexes/top-n-pagination.html

因此,我计划分批进行分页,并提出以下策略: 我们说Start_Date = X and End_Date = Y

  1. 触发查询,
  2. select * from table where CREATE_TIMESTAMP > X AND CREATE_TIMESTAMP < Y ORDER BY CREATE_TIMESTAMP FETCH NEXT 1000 ROWS ONLY.

    1. 如果我只获得少于1000行,则表示所有记录都已完成。如果我得到1000行,那意味着可能会有更多记录。

    2. 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

    3. 重复此操作直到我记录的记录少于1000条。

      有没有人发现这种方法存在任何问题?

2 个答案:

答案 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

等等。