【问题标题】:Efficient way to do batch INSERTS with JDBC使用 JDBC 进行批量插入的有效方法
【发布时间】:2011-04-16 14:23:33
【问题描述】:

在我的应用中,我需要做很多 INSERTS。它是一个 Java 应用程序,我使用普通的 JDBC 来执行查询。数据库是 Oracle。我已经启用了批处理,所以它可以节省我执行查询的网络延迟。但是查询作为单独的 INSERT 串行执行:

insert into some_table (col1, col2) values (val1, val2)
insert into some_table (col1, col2) values (val3, val4)
insert into some_table (col1, col2) values (val5, val6)

我想知道以下形式的 INSERT 是否更有效:

insert into some_table (col1, col2) values (val1, val2), (val3, val4), (val5, val6)

即将多个 INSERT 合并为一个。

还有其他加快批量 INSERT 的技巧吗?

【问题讨论】:

  • 哇!我在插入 SQL Server 时测试了您的“将多个插入合并为一个”,我从每秒 107 行变为每秒 3333 行!
  • 增长了惊人的 31 倍。

标签: java sql performance jdbc


【解决方案1】:

这是前面两个答案的混合:

  PreparedStatement ps = c.prepareStatement("INSERT INTO employees VALUES (?, ?)");

  ps.setString(1, "John");
  ps.setString(2,"Doe");
  ps.addBatch();

  ps.clearParameters();
  ps.setString(1, "Dave");
  ps.setString(2,"Smith");
  ps.addBatch();

  ps.clearParameters();
  int[] results = ps.executeBatch();

【讨论】:

  • 这是完美的解决方案,因为语句只准备(解析)一次。
  • ps.clearParameters(); 在这种特殊情况下是不必要的。
  • 一定要量一下。根据 JDBC 驱动程序的实现,这可能是预期的每批一次往返,但也可能最终成为每条语句一次往返。
  • prepareStatement/setXXX - 应该是这样的!
  • 对于 mysql 还要在 url 中添加以下内容:"&useServerPrepStmts=false&rewriteBatchedStatements=true"
【解决方案2】:

虽然问题询问使用 JDBC 有效地插入 Oracle,但我目前正在使用 DB2(在 IBM 大型机上),从概念上讲,插入将是相似的,因此认为查看我的指标可能会有所帮助

  • 一次插入一条记录

  • 插入一批记录(效率很高)

这里是指标

1) 一次插入一条记录

public void writeWithCompileQuery(int records) {
    PreparedStatement statement;

    try {
        Connection connection = getDatabaseConnection();
        connection.setAutoCommit(true);

        String compiledQuery = "INSERT INTO TESTDB.EMPLOYEE(EMPNO, EMPNM, DEPT, RANK, USERNAME)" +
                " VALUES" + "(?, ?, ?, ?, ?)";
        statement = connection.prepareStatement(compiledQuery);

        long start = System.currentTimeMillis();

        for(int index = 1; index < records; index++) {
            statement.setInt(1, index);
            statement.setString(2, "emp number-"+index);
            statement.setInt(3, index);
            statement.setInt(4, index);
            statement.setString(5, "username");

            long startInternal = System.currentTimeMillis();
            statement.executeUpdate();
            System.out.println("each transaction time taken = " + (System.currentTimeMillis() - startInternal) + " ms");
        }

        long end = System.currentTimeMillis();
        System.out.println("total time taken = " + (end - start) + " ms");
        System.out.println("avg total time taken = " + (end - start)/ records + " ms");

        statement.close();
        connection.close();

    } catch (SQLException ex) {
        System.err.println("SQLException information");
        while (ex != null) {
            System.err.println("Error msg: " + ex.getMessage());
            ex = ex.getNextException();
        }
    }
}

100 笔交易的指标:

each transaction time taken = 123 ms
each transaction time taken = 53 ms
each transaction time taken = 48 ms
each transaction time taken = 48 ms
each transaction time taken = 49 ms
each transaction time taken = 49 ms
...
..
.
each transaction time taken = 49 ms
each transaction time taken = 49 ms
total time taken = 4935 ms
avg total time taken = 49 ms

第一个事务在120-150ms 周围进行,这是the query parse 然后执行,后续事务仅在50ms 周围进行。 (仍然很高,但我的数据库在不同的服务器上(我需要对网络进行故障排除))

2) 批量插入(高效) - 由preparedStatement.executeBatch() 实现

public int[] writeInABatchWithCompiledQuery(int records) {
    PreparedStatement preparedStatement;

    try {
        Connection connection = getDatabaseConnection();
        connection.setAutoCommit(true);

        String compiledQuery = "INSERT INTO TESTDB.EMPLOYEE(EMPNO, EMPNM, DEPT, RANK, USERNAME)" +
                " VALUES" + "(?, ?, ?, ?, ?)";
        preparedStatement = connection.prepareStatement(compiledQuery);

        for(int index = 1; index <= records; index++) {
            preparedStatement.setInt(1, index);
            preparedStatement.setString(2, "empo number-"+index);
            preparedStatement.setInt(3, index+100);
            preparedStatement.setInt(4, index+200);
            preparedStatement.setString(5, "usernames");
            preparedStatement.addBatch();
        }

        long start = System.currentTimeMillis();
        int[] inserted = preparedStatement.executeBatch();
        long end = System.currentTimeMillis();

        System.out.println("total time taken to insert the batch = " + (end - start) + " ms");
        System.out.println("total time taken = " + (end - start)/records + " s");

        preparedStatement.close();
        connection.close();

        return inserted;

    } catch (SQLException ex) {
        System.err.println("SQLException information");
        while (ex != null) {
            System.err.println("Error msg: " + ex.getMessage());
            ex = ex.getNextException();
        }
        throw new RuntimeException("Error");
    }
}

一批 100 笔交易的指标是

total time taken to insert the batch = 127 ms

1000 笔交易

total time taken to insert the batch = 341 ms

因此,在 ~5000ms 中进行 100 次交易(一次只有一个 trxn)减少到 ~150ms(一批 100 条记录)。

注意 - 忽略我的超级慢的网络,但指标值是相对的。

【讨论】:

  • 嗨。记录的长度是否在插入时间中起作用?我有 3 个 Varchar 列,它们的值是 URI,并且将 8555 作为批处理插入仍然需要 3.5 分钟才能插入!!
  • 据我了解,在将数据从应用程序服务器传输到数据库服务器期间,记录大小可能很重要,但插入时间影响不大。我尝试在本地 oracle 数据库中使用 3 列大小为 125 字节的列,批量处理 10,000 条记录大约需要(145 到 300)毫秒。代码here。而multiple transactions for 10,000 records takes 20seconds.
  • 批处理可以处理的插入数量是否有限制?我有一些分析跟踪代码可以跟踪内存中的分析条目,然后单个线程在常规内部运行并插入记录。 60 秒内可能有数千条记录。
  • 对于任何好奇的人,我在一个具有 1,000、10,000、100,000 和 1,000,000 条记录的 Oracle DB 上测试了这种批处理方法,而且时间非常短。无论批次中的插入总数如何,每个插入的平均插入时间约为 0.2 毫秒。我使用 System.nanoTime() 来获得更准确的时间。
【解决方案3】:

Statement 为您提供以下选项:

Statement stmt = con.createStatement();

stmt.addBatch("INSERT INTO employees VALUES (1000, 'Joe Jones')");
stmt.addBatch("INSERT INTO departments VALUES (260, 'Shoe')");
stmt.addBatch("INSERT INTO emp_dept VALUES (1000, 260)");

// submit a batch of update commands for execution
int[] updateCounts = stmt.executeBatch();

【讨论】:

  • 虽然最终结果是一样的,但是在这个方法中,会解析多条语句,这对于批量来说速度要慢得多,实际上并没有单独执行每个语句效率高。此外,请尽可能使用 PreparedStatement 进行重复查询,因为它们的性能要好得多..
  • @AshishPatil:有没有使用 PreparedStatement 和没有 PreparedStatement 的测试基准?
  • 哇! 8年后。尽管如此,@prayagupd 在他最近的回答中给出了详细的统计数据。 stackoverflow.com/a/42756134/372055
  • 非常感谢您。这在动态插入数据并且您没有时间检查参数的数据类型时非常有用。
  • "另外,请尽可能使用 PreparedStatement 进行重复查询,因为它们的性能要好得多。"如果您不想先在循环中解析每个该死的列名和值怎么办?这似乎非常重 Java。如果我将此代码放入单个 DB 类中,则需要 DB 代码了解有关数据的一些信息。非常糟糕的耦合。我宁愿发送一个已经格式化的插入字符串列表以及它们在 HashMap(或其他东西)中的值字符串,然后让 DB 代码插入所有内容。
【解决方案4】:

显然,您必须进行基准测试,但是如果您使用 PreparedStatement 而不是 Statement,则通过 JDBC 发出多个插入会快得多。

【讨论】:

    【解决方案5】:

    您可以使用此rewriteBatchedStatements 参数使批量插入更快。

    您可以在此处阅读有关参数的信息:MySQL and JDBC with rewriteBatchedStatements=true

    【讨论】:

      【解决方案6】:

      SQLite:以上答案都是正确的。对于 SQLite,它有点不同。没有什么真正有帮助的,即使将它放在一个批次中(有时)也不会提高性能。在这种情况下,请尝试禁用自动提交并在完成后手动提交(警告!当多个连接同时写入时,您可能会与这些操作发生冲突)

      // connect(), yourList and compiledQuery you have to implement/define beforehand
      try (Connection conn = connect()) {
           conn.setAutoCommit(false);
           preparedStatement pstmt = conn.prepareStatement(compiledQuery);
           for(Object o : yourList){
              pstmt.setString(o.toString());
              pstmt.executeUpdate();
              pstmt.getGeneratedKeys(); //if you need the generated keys
           }
           pstmt.close();
           conn.commit();
      
      }
      

      【讨论】:

      • 当您使用 try-with-resources 编写代码时,您还应该尝试使用 pstmt,这样您就不会忘记关闭 pstmt(比如出现异常时)被抛出,例如并发修改)。我也喜欢环绕连接自动提交/提交,但这是因为我有点偏执。 SQLite 有一个很长的记录(自古以来),除非您为它提供事务,否则数据库中的每个更新都会创建自己的事务,但总是一个很好的剩余
      【解决方案7】:

      如何使用 INSERT ALL 语句?

      INSERT ALL
      
      INTO table_name VALUES ()
      
      INTO table_name VALUES ()
      
      ...
      
      SELECT Statement;
      

      我记得最后一个 select 语句是强制性的,才能使这个请求成功。不过不记得为什么了。 您也可以考虑使用 PreparedStatement。很多优点!

      法里德

      【讨论】:

        【解决方案8】:

        您可以在 java 中使用 addBatch 和 executeBatch 进行批量插入参见示例:Batch Insert In Java

        【讨论】:

          【解决方案9】:

          在我的代码中,我无法直接访问“preparedStatement”,因此无法使用批处理,我只是将查询和参数列表传递给它。然而,诀窍是创建一个可变长度的插入语句和一个 LinkedList 参数。效果和上面的例子一样,可变参数输入长度。见下文(省略错误检查)。 假设“myTable”有 3 个可更新字段:f1、f2 和 f3

          String []args={"A","B","C", "X","Y","Z" }; // etc, input list of triplets
          final String QUERY="INSERT INTO [myTable] (f1,f2,f3) values ";
          LinkedList params=new LinkedList();
          String comma="";
          StringBuilder q=QUERY;
          for(int nl=0; nl< args.length; nl+=3 ) { // args is a list of triplets values
              params.add(args[nl]);
              params.add(args[nl+1]);
              params.add(args[nl+2]);
              q.append(comma+"(?,?,?)");
              comma=",";
          }      
          int nr=insertIntoDB(q, params);
          

          在我的 DBInterface 类中,我有:

          int insertIntoDB(String query, LinkedList <String>params) {
              preparedUPDStmt = connectionSQL.prepareStatement(query);
              int n=1;
              for(String x:params) {
                  preparedUPDStmt.setString(n++, x);
              }
              int updates=preparedUPDStmt.executeUpdate();
              return updates;
          }
          

          【讨论】:

            【解决方案10】:

            如果你使用 jdbcTemplate 那么:

            import org.springframework.jdbc.core.JdbcTemplate;
            import org.springframework.jdbc.core.BatchPreparedStatementSetter;
            
                public int[] batchInsert(List<Book> books) {
            
                    return this.jdbcTemplate.batchUpdate(
                        "insert into books (name, price) values(?,?)",
                        new BatchPreparedStatementSetter() {
            
                            public void setValues(PreparedStatement ps, int i) throws SQLException {
                                ps.setString(1, books.get(i).getName());
                                ps.setBigDecimal(2, books.get(i).getPrice());
                            }
            
                            public int getBatchSize() {
                                return books.size();
                            }
            
                        });
                }
            

            或更高级的配置

              import org.springframework.jdbc.core.JdbcTemplate;
              import org.springframework.jdbc.core.ParameterizedPreparedStatementSetter;
            
                public int[][] batchInsert(List<Book> books, int batchSize) {
            
                    int[][] updateCounts = jdbcTemplate.batchUpdate(
                            "insert into books (name, price) values(?,?)",
                            books,
                            batchSize,
                            new ParameterizedPreparedStatementSetter<Book>() {
                                public void setValues(PreparedStatement ps, Book argument) 
                                    throws SQLException {
                                    ps.setString(1, argument.getName());
                                    ps.setBigDecimal(2, argument.getPrice());
                                }
                            });
                    return updateCounts;
            
                }
            

            链接到source

            【讨论】:

              【解决方案11】:

              如果迭代次数少,使用 PreparedStatements 将比 Statements 慢得多。要通过在语句上使用 PrepareStatement 来获得性能优势,您需要在迭代次数至少为 50 次或更高的循环中使用它。

              【讨论】:

              • 不,永远不会。一个普通的 Statement(不是 PrepareStatement)对象必须做所有与 PreparedStatement 相同的事情,实际上它是 PreparedStatement 的一个包装器,它实际上也做了准备好的部分。两者的区别在于,Statement 对象静默地准备语句并在每次执行时对其进行验证,而准备好的语句只执行一次,然后可以多次执行以处理批处理中的每个项目。
              • 这个答案是否有效??
              猜你喜欢
              • 2016-10-12
              • 2011-12-14
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-01-14
              • 1970-01-01
              • 2015-05-05
              • 1970-01-01
              相关资源
              最近更新 更多