【问题标题】:Oracle Pagination strategyOracle 分页策略
【发布时间】:2018-07-18 22:34:47
【问题描述】:

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

我读过这篇文章:

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

因此,我计划以 1000 个为单位进行分页,并提出以下策略: 假设Start_Date = X and End_Date = Y

  1. 触发查询,

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

这会重复,直到我得到少于 1000 条记录。

有人发现这种方法有什么问题吗?

【问题讨论】:

  • 提到的文章是指Oracle 12
  • 已更新 .. 仅适用于 oracle 12...
  • 如果您有多个时间戳为 X 的记录会怎样?
  • 是的,这是一种情况......如果我确保我的处理是幂等的,那应该不是问题。 ..对吗?
  • 你想对数据做什么样的处理。使用 SQL 或 PL/SQL 操作数据的最有效、最具执行力的方法是数据库。 Oracle 拥有如此丰富的功能 - 数学、分析、基于文本等 - 确实没有什么是它不能做的。

标签: sql oracle pagination oracle12c


【解决方案1】:

您没有说您是否计划在每次进行分页时调整“X”和“Y”。如果你不这样做,那么这种方法可能只有在你高度确信数据是相当静态的情况下才有效。

考虑以下示例:

我的表 T 有 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)。

现在用户按下“下一页”

现在我再次运行查询以获取下一组

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,并由于偏移量而跳过它们 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

等等。

【讨论】:

    【解决方案2】:

    Pagination pattern 是为了网站展示而发明的(与滚动导航相反),在那里效果最好。简而言之,实时用户实际上无法一次查看数千/百万条记录,因此将信息分为短页(50~200 条记录),通常每页向数据库发送一次查询。用户通常只点击几个页面,并没有浏览所有页面,另外用户浏览页面需要一点时间,所以查询不是一一发送到数据库,而是间隔很长。检索一块数据的时间比检索所有数百万条记录的时间要短得多,因此用户很高兴,因为他不必等待很长时间才能等待后续页面,并且整体系统负载更小。


    但从问题来看,您的应用程序的性质似乎是面向批处理而不是网络演示。应用程序必须获取所有记录并对每条记录执行一些操作/转换(计算)。在这种情况下,使用了完全不同的设计模式(流/流水线处理、步骤顺序、并行步骤/操作等),并且分页将不起作用,如果您这样做会降低系统性能。强>


    让我们看一个简单实用的例子,而不是花哨的理论,它会告诉你我们在这里讨论的速度差异

    假设有一个表PAGINATION,大约有700万条记录:

    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
    

    最后,为了简单起见,我们的“处理”将包括对字段之一的长度进行简单求和。
    这是一个简单的分页实现:

    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 % !!!
    逆天 ?您可以自己测试。
    这个例子说明了选择正确的解决方案(正确的设计模式)来解决问题的重要性。

    【讨论】:

    • 理论预测分页应该使索引读取访问量增加 350 倍,因为第一个查询读取前 1K 行,但最后一个查询读取所有 700K 行并跳过前 699K 行。 DB 缓存机制使得经过时间的增加不那么显着。很好地说明了*如果打算处理所有行,则不恰当地使用分页模式。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-03-30
    • 1970-01-01
    • 2015-12-19
    • 2023-03-18
    • 2020-10-25
    • 2018-10-10
    • 2018-07-23
    相关资源
    最近更新 更多