使用带有 JDBC 和 SQLServer 的数据库 API 游标来选择批处理结果

Posted

技术标签:

【中文标题】使用带有 JDBC 和 SQLServer 的数据库 API 游标来选择批处理结果【英文标题】:Using a database API cursor with JDBC and SQLServer to select batch results 【发布时间】:2014-11-21 21:04:07 【问题描述】:

已解决(请参阅下面的答案。)

我没有在适当的上下文中理解我的问题。真正的问题是我的查询返回了多个 ResultSet 对象,而我以前从未遇到过。我在下面发布了解决问题的代码。


问题

我有一个包含数千行的 SQL Server 数据库表。我的目标是从源数据库中提取数据并将其写入第二个数据库。由于应用程序内存限制,我将无法一次将数据全部拉回​​。此外,由于这个特定表的架构(我无法控制),我没有好的方法可以使用某种 ID 列来勾选行。

Database Administrators StackExchange 的一位先生帮助我整理了一个称为数据库 API 游标的东西,并且基本上编写了这个复杂的查询,我只需要将我的语句放入其中。当我在 SQL Management Studio (SSMS) 中运行查询时,它运行良好。我一次取回所有数据,一千行。

不幸的是,当我尝试将其转换为 JDBC 代码时,我只取回了前一千行。

问题

是否可以使用 JDBC 检索数据库 API 游标,从中拉出第一组行,让游标前进,然后一次拉出后续组? (在这种情况下,一次有一千行。)

SQL 代码

这很复杂,所以我要分解它。

实际的查询可以是简单的也可以是复杂的。没关系。我在实验过程中尝试了几个不同的查询,它们都有效。您只需将其放入适当位置的 SQL 代码中即可。所以,让我们把这个简单的语句作为我们的查询:

SELECT MyColumn FROM MyTable; 

实际的 SQL 数据库 API 游标要复杂得多。我将在下面打印出来。你可以看到上面的查询隐藏在其中:

-- http://dba.stackexchange.com/a/82806
DECLARE @cur INTEGER
    ,
    -- FAST_FORWARD | AUTO_FETCH | AUTO_CLOSE
    @scrollopt INTEGER = 16 | 8192 | 16384
    ,
    -- READ_ONLY, CHECK_ACCEPTED_OPTS, READ_ONLY_ACCEPTABLE
    @ccopt INTEGER = 1 | 32768 | 65536
    ,@rowcount INTEGER = 1000
    ,@rc INTEGER;

-- Open the cursor and return the first 1,000 rows
EXECUTE @rc = sys.sp_cursoropen @cur OUTPUT
    ,'SELECT MyColumn FROM MyTable'
    ,@scrollopt OUTPUT
    ,@ccopt OUTPUT
    ,@rowcount OUTPUT;

IF @rc <> 16 -- FastForward cursor automatically closed
BEGIN
    -- Name the cursor so we can use CURSOR_STATUS
    EXECUTE sys.sp_cursoroption @cur
        ,2
        ,'MyCursorName';

    -- Until the cursor auto-closes
    WHILE CURSOR_STATUS('global', 'MyCursorName') = 1
    BEGIN
        EXECUTE sys.sp_cursorfetch @cur
            ,2
            ,0
            ,1000;
    END;
END;

正如我所说,上面在数据库中创建了一个游标,并要求数据库执行该语句,(在内部)跟踪它返回的数据,并一次返回一千行数据。效果很好。

JDBC 代码

这就是我遇到问题的地方。我的 Java 代码没有编译问题或运行时问题。我遇到的问题是它只返回前一千行。我不明白如何正确使用数据库游标。我已经尝试过 Java 基础的变体:

// Hoping to get all of the data, but I only get the first thousand.
ResultSet rs = stmt.executeQuery(fq.getQuery());
while (rs.next()) 
    System.out.println(rs.getString("MyColumn"));

我对结果并不感到惊讶,但我尝试过的所有变体都会产生相同的结果。

根据我的研究,当数据库是 Oracle 时,JDBC 似乎对数据库游标做了一些事情,但是您必须将结果集中返回的数据类型设置为 Oracle 游标对象。我猜 SQL Server 也有类似的东西,但我一直找不到任何东西。

有人知道方法吗?

我包含完整的示例 Java 代码(尽可能丑)。

// FancyQuery.java

import java.sql.*;

public class FancyQuery 

    // Adapted from http://dba.stackexchange.com/a/82806
    String query = "DECLARE @cur INTEGER\n"
                 + "    ,\n"
                 + "    -- FAST_FORWARD | AUTO_FETCH | AUTO_CLOSE\n"
                 + "    @scrollopt INTEGER = 16 | 8192 | 16384\n"
                 + "    ,\n"
                 + "    -- READ_ONLY, CHECK_ACCEPTED_OPTS, READ_ONLY_ACCEPTABLE\n"
                 + "    @ccopt INTEGER = 1 | 32768 | 65536\n"
                 + "    ,@rowcount INTEGER = 1000\n"
                 + "    ,@rc INTEGER;\n"
                 + "\n"
                 + "-- Open the cursor and return the first 1,000 rows\n"
                 + "EXECUTE @rc = sys.sp_cursoropen @cur OUTPUT\n"
                 + "    ,'SELECT MyColumn FROM MyTable;'\n"
                 + "    ,@scrollopt OUTPUT\n"
                 + "    ,@ccopt OUTPUT\n"
                 + "    ,@rowcount OUTPUT;\n"
                 + "    \n"
                 + "IF @rc <> 16 -- FastForward cursor automatically closed\n"
                 + "BEGIN\n"
                 + "    -- Name the cursor so we can use CURSOR_STATUS\n"
                 + "    EXECUTE sys.sp_cursoroption @cur\n"
                 + "        ,2\n"
                 + "        ,'MyCursorName';\n"
                 + "\n"
                 + "    -- Until the cursor auto-closes\n"
                 + "    WHILE CURSOR_STATUS('global', 'MyCursorName') = 1\n"
                 + "    BEGIN\n"
                 + "        EXECUTE sys.sp_cursorfetch @cur\n"
                 + "            ,2\n"
                 + "            ,0\n"
                 + "            ,1000;\n"
                 + "    END;\n"
                 + "END;\n";

    public String getQuery() 
        return this.query;
    

    public static void main(String[ ] args) throws Exception 

        String dbUrl = "jdbc:sqlserver://tc-sqlserver:1433;database=MyBigDatabase";
        String user = "mario";
        String password = "p@ssw0rd";
        String driver = "com.microsoft.sqlserver.jdbc.SQLServerDriver";

        FancyQuery fq = new FancyQuery();

        Class.forName(driver);

        Connection conn = DriverManager.getConnection(dbUrl, user, password);
        Statement stmt = conn.createStatement();

        // We expect to get 1,000 rows at a time.
        ResultSet rs = stmt.executeQuery(fq.getQuery());
        while (rs.next()) 
            System.out.println(rs.getString("MyColumn"));
        

        // Alas, we've only gotten 1,000 rows, total.

        rs.close();
        stmt.close();
        conn.close();
    

【问题讨论】:

如果您从单个表中提取,为什么不在 JDBC 中使用“select * from tableName”。然后使用 JDBC 批量更新获取每一行并插入到目标数据库中。每隔一千行左右,刷新输出批次。但是,不需要对输入做任何事情:只需选择 * 也许,我不理解您问题的某些方面? 我发现了以下内容:tutorials.jenkov.com/jdbc/batchupdate.html。上面写着:“您可以批处理 SQL 插入、更新和删除。批处理选择语句没有意义。”我相信批量更新假设您正在批量发送数据。我希望数据库批量发送数据给我。如果我没记错的话,我相信这就是数据库世界中所谓的“分页”。 (我使用的光标查询的链接来自更详细地解释了这个问题。)我相信我已经解决了这个问题。谢谢。 【参考方案1】:

我想通了。

stmt.execute(fq.getQuery());

ResultSet rs = null;

for (;;) 
    rs = stmt.getResultSet();
    while (rs.next()) 
        System.out.println(rs.getString("MyColumn"));
    
    if ((stmt.getMoreResults() == false) && (stmt.getUpdateCount() == -1)) 
        break;
    


if (rs != null) 
    rs.close();

经过一些额外的谷歌搜索,我发现了一些 2004 年发布的代码:

http://www.coderanch.com/t/300865/JDBC/databases/SQL-Server-JDBC-Registering-cursor

发布我认为有帮助的 sn-p 的绅士(朱利安·肯尼迪)建议:“阅读 getUpdateCount() 和 getMoreResults() 的 Javadoc 以获得清晰的理解。”我能够把它拼凑起来。

基本上,我认为我一开始并没有很好地理解我的问题,无法正确表达它。归结为我的查询将返回多个ResultSet 实例中的数据。我需要的是一种方法,不仅可以遍历 ResultSet 中的每一行,还可以遍历整个 ResultSet 集。上面的代码就是这样做的。

【讨论】:

【参考方案2】:

如果您想要表中的所有记录,只需执行“从表中选择 *”。

分块检索的唯一原因是数据是否存在某个中间位置:例如如果您将其显示在屏幕上,或将其存储在内存中。

如果您只是从一个读取并插入另一个,只需从第一个读取所有内容。尝试批量检索不会获得更好的性能。如果有差异,则为负。以带回所有内容的方式构建您的查询。 JDBC 软件将处理您需要的所有其他分解和重组。

但是,您应该批量更新/插入事物。

该设置将在两个连接上创建两个语句:

Statement stmt = null;
ResultSet rs = null;
PreparedStatement insStmt = null;

stmt = conDb1.createStatement();
insStmt = conDb2.prepareStament("insert into tgt_db2_table (?,?,?,?,?......etc. ?,?) ");
rs = stmt.executeQuery("select * from src_db1_table");

然后,像往常一样循环选择,但在目标上使用批处理。

    int batchedRecordCount = 0;
    while (rs.next()) 
        System.out.println(rs.getString("MyColumn"));

        //Here you read values from the cursor and set them to the insStmt ...
        String field1 = rs.getString(1);
        String field2 = rs.getString(2);
        int field3 = rs.getInt(3);
        //--- etc. 

        insStmt.setString(1, field1);
        insStmt.setString(2, field2);
        insStmt.setInt(3, field3);

        //----- etc. for all the fields

        batchedRecordCount++;
        insStmt.addBatch();
        if (batchRecordCount > 1000) 
          insStmt.executeBatch();
        
    
    if (batchRecordCount > 0) 
       //Finish of the final (partial) set of records
       insStmt.executeBatch();
    

    //Close resources...

【讨论】:

我意识到我最初的描述有点冗长,但我注意到问题不在于性能,而在于“应用程序内存限制”。在我从中提取的这个特定表中,一些数据库可能有数千万行。客户要求应用程序在“正常”内存和网络使用情况下工作。因此,我必须分批提取。不过,谢谢。 您最了解您的环境,但如果您只是简单地拉取和插入,即使您有数十亿行,我也看不到内存在哪里使用。您的意思是数据库进程本身会使用太多内存来处理所有内容的查询吗?还是你的意思是你的程序? 源数据库在一台服务器上,目标数据库在另一台服务器上。此外,源是 SQL Server,目标是 mysql。我不知道如何在不将数据临时存储在(Java)应用程序内存中的情况下从一个到另一个获取数据。 我假设您正在打开两个 JDBC 连接。因此,当您从一个中获取时,您将插入到另一个中。在任何时候,您的应用程序中都有一条记录。如果您将其更改为使用批量插入,那么您将拥有 1000 条记录(或其他批量大小)......在您的应用程序中并不完全,但在它链接到的 JDBC 代码中......所以同样的事情。将源中的数据插入目标后,您无需保留它。 是的,这就是我正在做的。但是,如果我尝试一次提取所有内容,那么在提取和插入之间,我会在内存中保存数百万行数据,而这正是我要避免的。

以上是关于使用带有 JDBC 和 SQLServer 的数据库 API 游标来选择批处理结果的主要内容,如果未能解决你的问题,请参考以下文章

Java:如何使用用于 Sql Server 的 java jdbc 执行带有标识列的批量插入

带有实例名称和域的 JDBC 连接字符串

jdbc向sqlserver插入数据时报错,SQLServerException: 不支持从 UNKNOWN 到 UNKNOWN 的转换

带有 JSP 的 JDBC 连接字符串 SQL Server 导致“非法转义字符”错误

我无法使用带有 Android Studio 的 JDBC 连接到 SQL Server Express

带有 OpenQuery 和参数的 NativeQuery