JDBC 分页

Posted

技术标签:

【中文标题】JDBC 分页【英文标题】:JDBC Pagination 【发布时间】:2011-02-15 19:39:07 【问题描述】:

我想使用 JDBC 实现分页。我想知道的实际事情是“我怎样才能分别从数据库中获取第 1 页和第 2 页的前 50 条和后 50 条记录”

我的查询是Select * from data [数据表包含 20,000 行]

对于第 1 页,我获得 50 条记录,对于第 2 页,我想获得接下来的 50 条记录。如何在 JDBC 中高效实现?

我已经搜索并发现rs.absolute(row) 是跳过首页记录的方法,但是在大型结果集上需要一些时间,我不想承受这么多时间。另外,我不想在查询中使用rownumlimit + offset,因为这些在查询中不好用,我不知道为什么,我仍然不想在查询中使用它。

任何人都可以帮助我如何获得有限的ResultSet 进行分页,或者 JDBC 有什么办法给我们?

【问题讨论】:

【参考方案1】:

简单地使用 JDBC 没有有效的方法来做到这一点。您必须将 限制为 n 行从第 i 个项目开始 子句直接到 SQL 以使其高效。根据数据库的不同,这实际上可能很容易(参见 mysql 的 LIMIT -keyword),但在 Oracle 等其他数据库上,它可能有点棘手(涉及子查询和使用 rownum 伪列)。

请参阅此 JDBC 分页教程: http://java.avdiel.com/Tutorials/JDBCPaging.html

【讨论】:

【参考方案2】:

您应该只查询实际需要在当前页面上显示的数据。不要将整个数据集拖到 Java 的内存中,然后在那里过滤。它只会让事情变得不必要地慢。

如果您确实很难正确实现这一点和/或为特定数据库计算 SQL 查询,请查看my answerhere。

更新:由于您使用的是 Oracle,以下是上述答案中针对 Oracle 的摘录:

在 Oracle 中,您需要一个带有 rownum 子句的子查询,它应该如下所示:

private static final String SQL_SUBLIST = "SELECT id, username, job, place FROM"
    + " (SELECT id, username, job, place FROM contact ORDER BY id)"
    + " WHERE ROWNUM BETWEEN %d AND %d";

public List<Contact> list(int firstrow, int rowcount) 
    String sql = String.format(SQL_SUBLIST, firstrow, firstrow + rowcount);

    // Implement JDBC.
    return contacts;

【讨论】:

如果在选择标准结果集之后会变大会怎样?这就是为什么我要分析极端情况。并且结果集可能包含 100,000 行 :) 感谢您的回复。 将这 100.000 行保留在数据库中。 查询您需要在每页显示的内容。 Google 也不会一次查询这无数行,只在结果页面上显示前十行。 如何查询?我不想查询特定供应商? 你必须这样做。它不是 ANSI SQL 标准的一部分。您也可以只为所有五个主要的 RDBMS 供应商编写查询,并添加配置设置或 JDBC URL 自动检测器来确定您想要打开哪个 DB 供应商特定的查询。另一种选择是使用 Hibernate/JPA,它已经为您抽象了数据库供应商的东西。最后一个选择是用纯 Java 完成这项工作。但你真的不想那样做。【参考方案3】:

免责声明:This blog post on SQL pagination & JDBC pagination is posted by me.

不考虑Hibernate分页,我们可以使用SQL分页/JDBC分页

SQL 分页

有两种基本方法:

    对零碎的结果集进行操作(每个页面的新查询) 对完整结果集进行操作

方法是特定于 SQL 的

对于 MySQL / 许多其他 SQL,可以使用 limit 和 offset 来完成

Postgresql:http://microjet.ath.cx/WebWiki/ResultPaginationWithPostgresql.html

在 Oracle 中,它使用与处理“Top-N 查询”相同的形式,例如谁是薪酬最高的 5 名员工,这是经过优化的

select *   from ( select a.*, rownum rnum

from ( YOUR_QUERY_GOES_HERE -- including the order by ) a

where rownum <= MAX_ROWS )

where rnum >= MIN_ROWS

Here is a very detailed explanation on ROW-NUM

Similar SO Thread

JDBC 分页

想到的问题是:当我执行SQL时,结果是如何加载的?立即或应要求?和这个 SO 线程一样

首先我们需要了解一些basics of JDBC, as from Oracle

根据 javadoc:statement.execute()

execute: Returns true if the first object that the query returns is a ResultSet object. Use this method if the query could return one or more ResultSet objects. Retrieve the ResultSet objects returned from the query by repeatedly calling Statement.getResutSet.

我们通过游标访问结果集中的数据。注意这个游标不同于DB,它是一个最初定位在第一行数据之前的指针。

数据是根据请求获取的。而当您执行 execute() 时,您是第一次获取。

那么,加载了多少数据?它是可配置的。 可以使用 ResultSet 上的 java API setFetchSize() 方法来控制驱动程序一次从 DB 中获取多少行,一次检索的块有多大。

例如假设总结果为 1000。如果提取大小为 100,则提取第 1 行将从 DB 加载 100 行,从本地内存加载第 2 到第 100 行。查询第 101 行将加载另外 100 行进入记忆。

来自 JavaDoc

Gives the JDBC driver a hint as to the number of rows that should be fetched from the database when more rows are needed for ResultSet objects genrated by this Statement. If the value specified is zero, then the hint is ignored. The default value is zero.

注意“提示”这个词 - 它可以被驱动程序特定的实现覆盖。

这也是 SQL 开发者等客户端的“限制行数为 100”功能的基础。

完成整个解决方案,滚动结果,需要考虑API中的ResultSet Types和ScrollableCursor

可以从 oracle 中的这篇文章中找到示例实现

来自《Oracle Toplink 开发人员指南》一书 Example 112 JDBC Driver Fetch Size

ReadAllQuery query = new ReadAllQuery();

query.setReferenceClass(Employee.class);

query.setSelectionCriteria(new ExpressionBuilder.get("id").greaterThan(100));

// Set the JDBC fetch size

query.setFetchSize(50);

// Configure the query to return results as a ScrollableCursor

query.useScrollableCursor();

// Execute the query

ScrollableCursor cursor = (ScrollableCursor) session.executeQuery(query);

// Iterate over the results

while (cursor.hasNext()) 

System.out.println(cursor.next().toString());



cursor.close();

.....................

毕竟,问题归结为

哪种方法更好地进行分页?

注意 SQL 应该是 ORDER by 以便在 SQL 方法中有意义,

否则可能会在下一页再次显示一些行。

以下是 Postgresql 关于 JDBC 驱动程序和其他 SO 答案的文档中的一些要点

首先,原始查询需要有一个 ORDER BY 子句才能使分页解决方案合理工作。否则,Oracle 为第一页、第二页和第 N 页返回相同的 500 行是完全有效的

主要区别在于 JDBC 方式,在获取过程中需要保持连接。例如,这可能不适用于无状态 Web 应用程序。

SQL方式

语法是特定于 SQL 的,可能不容易维护。 对于 JDBC 方式

与服务器的连接必须使用 V3 协议。这是 服务器版本 7.4 的默认值(并且仅受其支持)和 之后。 连接不得处于自动提交模式。后端 在事务结束时关闭游标,因此在自动提交模式下 后端将在获取任何内容之前关闭游标 来自它。 必须使用 ResultSet 类型创建语句 结果集.TYPE_FORWARD_ONLY。这是默认设置,所以没有代码 需要重写以利用这一点,但这也意味着 您不能向后滚动或以其他方式在 结果集。 给定的查询必须是单个语句,而不是多个 用分号串在一起的语句。

进一步阅读

This post is about performance tuning with optical fetch size

【讨论】:

【参考方案4】:

我知道这个问题很老,但这就是我实现分页的方式,我希望它对某人有所帮助

int pageNo = ....;    
String query = "SELECT * FROM data LIMIT ";

switch (pageNo) 
case 0: // no pagination, get all rows
    query += Integer.MAX_VALUE; // a big value to get all rows
    break;
case 1: // get 1st page i.e 50 rows
    query += 50;
    break;
default:
    query += String.valueOf((pageNo-1)*50 + 1) ", " + String.valueOf(50);
    break;


PreparedStatement ps = connection.prepareStatement(query);
....

将值 50 设为一个名为 pageSize 的常量,以便将其更改为任意数字

【讨论】:

注意:这个答案适用于 MySQL。对于 Postgres,使用 LIMIT X OFFSET (pageNo - 1) * pageSize 其中 OFFSET 将跳过前 n 条记录 postgresql.org/docs/9.3/queries-limit.html【参考方案5】:

您是在使用某种 ORM 框架,例如 hibernate 甚至 Java Persistence API,还是只是简单的 SQL?

那么我的回答是: 使用 LIMIT 和 OFFSET http://www.petefreitag.com/item/451.cfm

或通过 ROWNUM 运算符 那么你需要一个包装器来围绕你的 SQL,但基本上它是

  select * from (select bla.*, ROWNUM rn from (
  <your sql here>
  ) bla where rownum < 200) where rn >= 150'

【讨论】:

我在 oracle 10g 中使用标准 JDBC API【参考方案6】:

如果您使用 MySQL 或 PostgreSQL,limitoffset 是您的关键字。 MSSqlServer 和 Oracle 有类似的功能,但我似乎更痛苦一些。

MySQLPostgreSQL 看这里:

http://www.petefreitag.com/item/451.cfm

Oracle请看这里:

http://www.oracle-base.com/forums/viewtopic.php?f=2&t=8635

【讨论】:

感谢您的回复。有没有什么方法可以在不使用sql关键字的情况下实现这个功能? @Zeeshan - 不使用标准 JDBC API。 我在 oracle 10g 中使用标准 JDBC API 我在答案中添加了另一个链接,您可以在其中找到有关 Oracle 数据库中分页的更多信息。【参考方案7】:

这是一个用于分页结果的休眠解决方案的链接: HQL - row identifier for pagination

【讨论】:

【参考方案8】:

我含蓄地理解,您不希望 JDBC 连接有一个巨大的结果集,您可以长时间保持打开并在需要时导航。

通常的方法是添加仅获取完整请求的子集所需的 SQL,不幸的是,这因数据库而异,并且会使您的 SQL 语句特定于供应商。如果我没记错的话,LIMIT 与 MySQL 一起使用。为每个请求询问适当的范围。

我也相信 Hibernate 包含允许您为 HQL 执行此操作的功能,但我不熟悉它。

【讨论】:

【参考方案9】:

Oracle 从 8i 开始支持标准 ROW_NUMBER() 窗口函数,因此您可以使用它。您可以将其作为参数化查询来执行,因此您只需要设置开始行号和结束行号。例如

SELECT * 
   FROM ( SELECT *, ROW_NUMBER() ORDER BY (sort key) AS rowNumber FROM <your table name> ) AS data
WHERE 
   rowNumber>=:start AND
   rowNumber<:end

(如果您不使用命名参数,请将 :start/:end 替换为位置参数占位符 '?')

参见***上的SELECT SQL, Window Functions。 文章还列出了支持 ROW_NUMBER() 标准窗口函数的其他 DB。

【讨论】:

【参考方案10】:
PreparedStatement pStmt = // ... however you make it
pStmt.setFetchSize( /* desired number of records to get into memory */ ); 

注意,setFetchSize(int) is only a hint - 例如,上次我将它与 MySQL 一起使用时,它不受支持。浏览一下 Oracle 文档,看起来他们的 JDBC 确实支持它。我不会引用我的话,但至少值得一试;是的,这个答案很脆弱,但与实施强大的解决方案相比,它可能不会让人头疼。

基本上,您可以对所有内容发出请求,并且一次只能将 fetch 大小放入内存中(前提是您没有保留以前的结果)。因此,您将获取大小设置为 50,进行连接/查询,显示前 50 个结果(导致下一次查询的另一个获取)等等。

【讨论】:

另外,我认为 LIMIT + OFFSET 解决方案更好,因为稳健性值得温和的额外争论,但我想指出一个替代方案。 尝试依赖 setFetchSize(int) 的问题在于,您在进行提取时需要保持连接。分页可能需要几分钟(当用户向后导航时)。长时间保持连接是不合理的。使用(特定于供应商的)SQL 语法来限制/确定结果范围更具可扩展性。从池中拉出一个连接并执行一个分页 SQL 语句(参数化为 where 它在分页中),将连接返回到池并显示结果。如果用户离开,重新参数化 SQL 语句并重复。【参考方案11】:

输入:

    订单信息示例(A2 或 D3)(A/D 升序/降序)+ 列 订单信息示例(A2 或 D3)(A/D 升序/降序)+ 列 过滤值 开始行 开始行 最大行数

结果:

选定的值 所选页面 此排序中行的索引 计算可用数据。 (保存第二个查询)

仅优势查询:

使用此过滤器的可用列的总和 仅从 db 传输所选页面 在没有动态 sql 的情况下正确排序

缺点:

Oracle 依赖

选择 x.* from ( 选择 c.pk_field、c.numeric_a、c.char_b、c.char_c ROW_NUMBER( ) 超过(ORDER BY decode(?,'A1',to_char(c.numeric_a,'FM00000000'),'A2',c.char_b,'A3',c.char_c,'A') asc , decode(?,'D1',to_char(c.numeric_a,'FM00000000'),'D2',c.char_b,'D3',c.char_c,'A') desc, c.pk_field asc ) 作为 "idx", 从 myTable c 计数 (*) OVER (ORDER BY 1) "cnt" c.haystack=? ) x where x."idx" between maximum(nvl(?,1),1) 和 nvl(?,1)-1+?

【讨论】:

【参考方案12】:

您可以使用以下方法。

    这里 rowNo 是您想要的起始行的编号 获取 n 条记录。 pageSize 是您要获取的记录总数
公共 CachedRowSet getScrollableList(PreparedStatement prestmt,int rowNo) 获取页面大小(); 结果集 rs = null; CachedRowSet crs = null; 尝试 rs = prestmt.executeQuery(); crs = new CachedRowSetImpl(); crs.setPageSize(pageSize); crs.populate(rs, rowNo); 捕捉(SQLException ex) logger.error("getScrollableList() 方法中 DatabaseInterface 异常:"+ ex.getMessage(),ex); 最后 //关闭rs //关闭pstmt 返回crs;

【讨论】:

【参考方案13】:

以下java代码运行良好:

package paginationSample;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;

/**
 *
 * @ Nwaeze Emmanuel (FUNAI, 2016)
 */
public class PaginationSample extends javax.swing.JFrame   
public void getRows() 
Connection con2;
Statement stmt2;
ResultSet rs2;
int j=0;
String sz="";

  try 
// Load MS accces driver class
 Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); 
String url = "jdbc:odbc:Driver=Microsoft Access Driver (*.mdb, 
  *.accdb);DBQ=" + "C:\\Database2.mdb"; 
try
con2 = DriverManager.getConnection(url, "user", "");
System.out.println("Connection Succesfull");

 try
   stmt2=con2.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, 
   ResultSet.CONCUR_UPDATABLE );
   String sql="";

   if (txtpage.getText().trim().equals("") || 
      txtpagesize.getText().trim().equals(""))

    else
     int pagesize=Integer.parseInt(txtpagesize.getText());   
     int page=Integer.parseInt(txtpage.getText());
     sql="SELECT * FROM FSRegistration100L WHERE SN >= " +  
    (pagesize*page-pagesize+1) + " AND " + "SN <= " + (pagesize*page);
     rs2=stmt2.executeQuery(sql);
     if (rs2.wasNull())

     else
       while ( rs2.next())
         sz=sz + rs2.getString("RegNo") + "\n";

          j++;  

        
       txta.setText(sz);
       txta.append(" Total rows =" + Integer.toString(j));
     
    
      stmt2.close();
      con2.close();
  
  catch (NullPointerException s)
  System.err.println("Got an exception166! ");
  System.out.println(s.getMessage());
   
  catch (SQLException e1) 
System.err.println("Got an exception1! ");
System.err.println(e1.getMessage());

 catch (ClassNotFoundException e2) 
System.err.println("Got an exception2! ");
System.err.println(e2.getMessage());

 




    private void jButton1ActionPerformed(java.awt.event.ActionEvent 
       evt)                                              
       // TODO add your handling code here:
        getRows();
                                            

    // Variables declaration - do not modify                     
    private javax.swing.JButton jButton1;
    private javax.swing.JLabel jLabel1;
    private javax.swing.JLabel jLabel2;
    private javax.swing.JScrollPane jScrollPane1;
    private javax.swing.JTextArea txta;
    private javax.swing.JTextField txtpage;
    private javax.swing.JTextField txtpagesize;
    // End of variables declaration 

【讨论】:

以上是关于JDBC 分页的主要内容,如果未能解决你的问题,请参考以下文章

单页分页问题中的多个角度材料表

梦内容页分页标题提取

一个视图中的 CI 多页分页,

以多页分页打印所有数据

CakePHP 2中带有分页分页类的大小为f数组的问题

当我在基于类的视图中应用过滤器时,如何在 django 中使用分页分页。网址总是不断变化我如何跟踪网址